Skip to content

Commit bed341f

Browse files
feat: Added smartscapeNode command for finding entities (#204)
* feat: Added smartscapeNode command for finding entities * Update CHANGELOG.md Co-authored-by: Manuel Warum <[email protected]> * chore: Refactor the slice filter for entity search --------- Co-authored-by: Manuel Warum <[email protected]>
1 parent f2a79e5 commit bed341f

File tree

4 files changed

+87
-12
lines changed

4 files changed

+87
-12
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,14 @@
22

33
## Unreleased Changes
44

5+
### Tools
6+
7+
- `find_entities_by_name` now uses `smartscapeNode` DQL command under the hood, and will fall back to `fetch dt.entity.${entityType}`.
8+
9+
### Scopes
10+
11+
- Added OAuth scope `storage:smartscape:read`
12+
513
## 0.9.2
614

715
- Improved error handling when initializing the connection for the first time

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ Depending on the features you are using, the following scopes are needed:
321321
- `storage:system:read` - needed for `execute_dql` tool to read System Data from Grail
322322
- `storage:user.events:read` - needed for `execute_dql` tool to read User events from Grail
323323
- `storage:user.sessions:read` - needed for `execute_dql` tool to read User sessions from Grail
324+
- `storage:smartscape:read` - needed for `execute_dql` tool to read Smartscape Data
324325
- `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot)
325326
- `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill
326327
- `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill

src/capabilities/find-monitored-entity-by-name.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,37 @@ export const generateDqlSearchEntityCommand = (entityNames: string[], extendedSe
2828
};
2929

3030
/**
31-
* Find a monitored entity by name via DQL
31+
* Find a monitored entity via "smartscapeNodes" by name via DQL
3232
* @param dtClient
33-
* @param entityName
34-
* @returns A string with the entity details like id, name and type, or an error message if no entity was found
33+
* @param entityNames Array of entitiy names to search for
34+
* @returns An array with the entity details like id, name and type
35+
*/
36+
export const findMonitoredEntityViaSmartscapeByName = async (dtClient: HttpClient, entityNames: string[]) => {
37+
const dql = `smartscapeNodes "*" | search "*${entityNames.join('*" OR "*')}*" | fields id, name, type`;
38+
console.error(`Executing DQL: ${dql}`);
39+
40+
try {
41+
const smartscapeResult = await executeDql(dtClient, { query: dql });
42+
43+
if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) {
44+
// return smartscape results if we found something
45+
return smartscapeResult;
46+
}
47+
} catch (error) {
48+
// ignore errors here, as smartscapeNodes may not be ready for all environments/users
49+
console.error('Error while querying smartscapeNodes:', error);
50+
}
51+
52+
console.error('No results from smartscapeNodes');
53+
return null;
54+
};
55+
56+
/**
57+
* Find a monitored entity via "dt.entity.${entityType}" by name via DQL
58+
* @param dtClient
59+
* @param entityNames Array of entitiy names to search for
60+
* @param extendedSearch If true, search over all entity types, otherwise only basic ones
61+
* @returns An array with the entity details like id, name and type
3562
*/
3663
export const findMonitoredEntitiesByName = async (
3764
dtClient: HttpClient,

src/index.ts

Lines changed: 48 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ import { updateWorkflow } from './capabilities/update-workflow';
3131
import { executeDql, verifyDqlStatement } from './capabilities/execute-dql';
3232
import { sendSlackMessage } from './capabilities/send-slack-message';
3333
import { sendEmail } from './capabilities/send-email';
34-
import { findMonitoredEntitiesByName } from './capabilities/find-monitored-entity-by-name';
34+
import {
35+
findMonitoredEntitiesByName,
36+
findMonitoredEntityViaSmartscapeByName,
37+
} from './capabilities/find-monitored-entity-by-name';
3538
import {
3639
chatWithDavisCopilot,
3740
explainDqlInNaturalLanguage,
@@ -464,26 +467,61 @@ const main = async () => {
464467
readOnlyHint: true,
465468
},
466469
async ({ entityNames, maxEntitiesToDisplay, extendedSearch }) => {
467-
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:entities:read'));
470+
const dtClient = await createAuthenticatedHttpClient(
471+
scopesBase.concat('storage:entities:read', 'storage:smartscape:read'),
472+
);
473+
474+
const smartscapeResult = await findMonitoredEntityViaSmartscapeByName(dtClient, entityNames);
475+
476+
if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) {
477+
// Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities
478+
const validSmartscapeEntities = smartscapeResult.records.filter(
479+
(entity): entity is { id: string; type: string; name: string; [key: string]: any } =>
480+
!!(entity && entity.id && entity.type && entity.name),
481+
);
482+
483+
let resp = `Found ${validSmartscapeEntities.length} monitored entities via Smartscape! Displaying the first ${Math.min(maxEntitiesToDisplay, validSmartscapeEntities.length)} valid entities:\n`;
484+
485+
validSmartscapeEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
486+
resp += `- Entity '${entity.name}' of entity-type '${entity.type}' has entity id '${entity.id}' and tags ${entity['tags'] ? JSON.stringify(entity['tags']) : 'none'} - DQL Filter: '| filter dt.smartscape.${String(entity.type).toLowerCase()} == "${entity.id}"'\n`;
487+
});
468488

489+
resp +=
490+
'\n\n**Next Steps:**\n' +
491+
'1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statement: "smartscapeNodes \"<entity-type>\" | filter id == <entity-id>"\n' +
492+
'2. Perform a sanity check that found entities are actually the ones you are looking for, by comparing name and by type (hosts vs. containers vs. apps vs. functions) and technology (Java, TypeScript, .NET) with what is available in the local source code repo.\n' +
493+
'3. Find and investigate available metrics for relevant entities, by using the `execute_dql` tool with the following DQL statement: "fetch metric.series | filter dt.smartscape.<entity-type> == <entity-id> | limit 20"\n' +
494+
'4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n' +
495+
'5. Explore dependency & relationships with: "smartscapeEdges \"*\" | filter source_id == <entity-id> or target_id == <entity-id>" to list inbound/outbound edges (depends_on, dependency_of, owned_by, part_of) for graph context\n';
496+
497+
return resp;
498+
}
499+
500+
// If no result from Smartscape, try the classic entities API
469501
const result = await findMonitoredEntitiesByName(dtClient, entityNames, extendedSearch);
470502

471503
if (result && result.records && result.records.length > 0) {
472-
let resp = `Found ${result.records.length} monitored entities! Displaying the first ${maxEntitiesToDisplay} entities:\n`;
504+
// Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities
505+
const validClassicEntities = result.records.filter(
506+
(entity): entity is { id: string; type: string; name: string; [key: string]: any } =>
507+
!!(entity && entity.id && entity.type && entity.name),
508+
);
509+
510+
let resp = `Found ${validClassicEntities.length} monitored entities! Displaying the first ${Math.min(maxEntitiesToDisplay, validClassicEntities.length)} entities:\n`;
473511

474512
// iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems
475-
result.records.slice(0, maxEntitiesToDisplay).forEach((entity) => {
513+
validClassicEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
476514
if (entity && entity.id) {
477515
const entityType = getEntityTypeFromId(String(entity.id));
478-
resp += `- Entity '${entity['entity.name']}' of type '${entity['entity.type']}' has entity id '${entity.id}' and tags ${entity['tags'] ? entity['tags'] : 'none'} - Use the DQL Filter: '| filter ${entityType} == "${entity.id}"'\n`;
516+
resp += `- Entity '${entity['entity.name']}' of entity-type '${entity['entity.type']}' has entity id '${entity.id}' and tags ${entity['tags'] ? entity['tags'] : 'none'} - DQL Filter: '| filter ${entityType} == "${entity.id}"'\n`;
479517
}
480518
});
481519

482520
resp +=
483521
'\n\n**Next Steps:**\n' +
484-
'1. Try to fetch more details about the entity, using the `execute_dql` tool with "describe(dt.entity.<entity-type>)", and "fetch dt.entity.<entity-type> | filter id == <entity-id> | fieldsAdd <field-1>, <field2>, ..."\n' +
522+
'1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statements: "describe(dt.entity.<entity-type>)", and "fetch dt.entity.<entity-type> | filter id == <entity-id> | fieldsAdd <field-1>, <field-2>, ..."\n' +
485523
'2. Perform a sanity check that found entities are actually the ones you are looking for, by comparing name and by type (hosts vs. containers vs. apps vs. functions) and technology (Java, TypeScript, .NET) with what is available in the local source code repo.\n' +
486-
'3. Find and investigate available metrics for relevant entities, by using the `execute_dql` tool with the following DQL statement: "fetch metric.series | filter dt.entity.<entity-type> == <entity-id>"\n' +
524+
'3. Find and investigate available metrics for relevant entities, by using the `execute_dql` tool with the following DQL statement: "fetch metric.series | filter dt.entity.<entity-type> == <entity-id> | limit 20"\n' +
487525
'4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n';
488526

489527
return resp;
@@ -558,14 +596,14 @@ const main = async () => {
558596

559597
tool(
560598
'execute_dql',
561-
'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
599+
'Get data like Logs, Metrics, Spans, Events, or Entity Data from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
562600
'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
563601
'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""',
564602
{
565603
dqlStatement: z
566604
.string()
567605
.describe(
568-
'DQL Statement (Ex: "fetch [logs, spans, events], from: now()-4h, to: now() | filter <some-filter> | summarize count(), by:{some-fields}.", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }"). ' +
606+
'DQL Statement (Ex: "fetch [logs, spans, events, metric.series, ...], from: now()-4h, to: now() [| filter <some-filter>] [| summarize count(), by:{some-fields}]", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }", or for entities via smartscape: "smartscapeNodes \"[*, HOST, PROCESS, ...]\" [| filter id == "<ENTITY-ID>"]"). ' +
569607
'When querying data for a specific entity, call the `find_entity_by_name` tool first to get an appropriate filter like `dt.entity.service == "SERVICE-1234"` or `dt.entity.host == "HOST-1234"` to be used in the DQL statement. ',
570608
),
571609
},
@@ -591,6 +629,7 @@ const main = async () => {
591629
'storage:user.events:read', // Read User events from Grail
592630
'storage:user.sessions:read', // Read User sessions from Grail
593631
'storage:security.events:read', // Read Security events from Grail
632+
'storage:smartscape:read', // Read Smartscape Entities from Grail
594633
),
595634
);
596635
const response = await executeDql(dtClient, { query: dqlStatement }, grailBudgetGB);

0 commit comments

Comments
 (0)