From c34b8a898f8991c75122870145b25a4ad9f446e7 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Tue, 14 Oct 2025 17:11:20 +0200 Subject: [PATCH 1/3] feat: Added smartscapeNode command for finding entities --- CHANGELOG.md | 8 ++++ README.md | 1 + .../find-monitored-entity-by-name.ts | 33 ++++++++++++-- src/index.ts | 45 ++++++++++++++++--- 4 files changed, 77 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9dc84a..a47bbc8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## Unreleased Changes +### Tools + +- `find_entities_by_name` now uses `smartscapeNode` DQL command under the hood, and will fallback to `fetch dt.entity.${entityType}`. + +### Scopes + +- Added OAuth scope `storage:smartscape:read` + ## 0.9.2 - Improved error handling when initializing the connection for the first time diff --git a/README.md b/README.md index 601a6dc..45796e0 100644 --- a/README.md +++ b/README.md @@ -321,6 +321,7 @@ Depending on the features you are using, the following scopes are needed: - `storage:system:read` - needed for `execute_dql` tool to read System Data from Grail - `storage:user.events:read` - needed for `execute_dql` tool to read User events from Grail - `storage:user.sessions:read` - needed for `execute_dql` tool to read User sessions from Grail +- `storage:smartscape:read` - needed for `execute_dql` tool to read Smartscape Data - `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot) - `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill - `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill diff --git a/src/capabilities/find-monitored-entity-by-name.ts b/src/capabilities/find-monitored-entity-by-name.ts index 9574472..69fc6dc 100644 --- a/src/capabilities/find-monitored-entity-by-name.ts +++ b/src/capabilities/find-monitored-entity-by-name.ts @@ -28,10 +28,37 @@ export const generateDqlSearchEntityCommand = (entityNames: string[], extendedSe }; /** - * Find a monitored entity by name via DQL + * Find a monitored entity via "smartscapeNodes" by name via DQL * @param dtClient - * @param entityName - * @returns A string with the entity details like id, name and type, or an error message if no entity was found + * @param entityNames Array of entitiy names to search for + * @returns An array with the entity details like id, name and type + */ +export const findMonitoredEntityViaSmartscapeByName = async (dtClient: HttpClient, entityNames: string[]) => { + const dql = `smartscapeNodes "*" | search "*${entityNames.join('*" OR "*')}*" | fields id, name, type`; + console.error(`Executing DQL: ${dql}`); + + try { + const smartscapeResult = await executeDql(dtClient, { query: dql }); + + if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) { + // return smartscape results if we found something + return smartscapeResult; + } + } catch (error) { + // ignore errors here, as smartscapeNodes may not be ready for all environments/users + console.error('Error while querying smartscapeNodes:', error); + } + + console.error('No results from smartscapeNodes'); + return null; +}; + +/** + * Find a monitored entity via "dt.entity.${entityType}" by name via DQL + * @param dtClient + * @param entityNames Array of entitiy names to search for + * @param extendedSearch If true, search over all entity types, otherwise only basic ones + * @returns An array with the entity details like id, name and type */ export const findMonitoredEntitiesByName = async ( dtClient: HttpClient, diff --git a/src/index.ts b/src/index.ts index e9b1f58..d40c7ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,7 +31,10 @@ import { updateWorkflow } from './capabilities/update-workflow'; import { executeDql, verifyDqlStatement } from './capabilities/execute-dql'; import { sendSlackMessage } from './capabilities/send-slack-message'; import { sendEmail } from './capabilities/send-email'; -import { findMonitoredEntitiesByName } from './capabilities/find-monitored-entity-by-name'; +import { + findMonitoredEntitiesByName, + findMonitoredEntityViaSmartscapeByName, +} from './capabilities/find-monitored-entity-by-name'; import { chatWithDavisCopilot, explainDqlInNaturalLanguage, @@ -464,8 +467,35 @@ const main = async () => { readOnlyHint: true, }, async ({ entityNames, maxEntitiesToDisplay, extendedSearch }) => { - const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:entities:read')); + const dtClient = await createAuthenticatedHttpClient( + scopesBase.concat('storage:entities:read', 'storage:smartscape:read'), + ); + + const smartscapeResult = await findMonitoredEntityViaSmartscapeByName(dtClient, entityNames); + + if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) { + let resp = `Found ${smartscapeResult.records.length} monitored entities via Smartscape! Displaying the first ${maxEntitiesToDisplay} entities:\n`; + + // iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems + smartscapeResult.records.slice(0, maxEntitiesToDisplay).forEach((entity) => { + if (entity && entity.id && entity.type && entity.name) { + 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`; + } + }); + + // ToDo: Refine next-steps, this is not working properly yet. + + resp += + '\n\n**Next Steps:**\n' + + '1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statement: "smartscapeNodes \"\" | filter id == "\n' + + '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' + + '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. == | limit 20"\n' + + '4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n'; + + return resp; + } + // If no result from Smartscape, try the classic entities API const result = await findMonitoredEntitiesByName(dtClient, entityNames, extendedSearch); if (result && result.records && result.records.length > 0) { @@ -475,15 +505,15 @@ const main = async () => { result.records.slice(0, maxEntitiesToDisplay).forEach((entity) => { if (entity && entity.id) { const entityType = getEntityTypeFromId(String(entity.id)); - 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`; + 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`; } }); resp += '\n\n**Next Steps:**\n' + - '1. Try to fetch more details about the entity, using the `execute_dql` tool with "describe(dt.entity.)", and "fetch dt.entity. | filter id == | fieldsAdd , , ..."\n' + + '1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statements: "describe(dt.entity.)", and "fetch dt.entity. | filter id == | fieldsAdd , , ..."\n' + '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' + - '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. == "\n' + + '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. == | limit 20"\n' + '4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n'; return resp; @@ -558,14 +588,14 @@ const main = async () => { tool( 'execute_dql', - 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' + + 'Get data like Logs, Metrics, Spans, Events, or Entity Data from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' + 'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' + 'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', { dqlStatement: z .string() .describe( - 'DQL Statement (Ex: "fetch [logs, spans, events], from: now()-4h, to: now() | filter | summarize count(), by:{some-fields}.", or for metrics: "timeseries { avg(), value.A = avg(, scalar: true) }"). ' + + 'DQL Statement (Ex: "fetch [logs, spans, events, metric.series, ...], from: now()-4h, to: now() [| filter ] [| summarize count(), by:{some-fields}]", or for metrics: "timeseries { avg(), value.A = avg(, scalar: true) }", or for entities via smartscape: "smartscapeNodes \"[*, HOST, PROCESS, ...]\" [| filter id == ""]"). ' + '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. ', ), }, @@ -591,6 +621,7 @@ const main = async () => { 'storage:user.events:read', // Read User events from Grail 'storage:user.sessions:read', // Read User sessions from Grail 'storage:security.events:read', // Read Security events from Grail + 'storage:smartscape:read', // Read Smartscape Entities from Grail ), ); const response = await executeDql(dtClient, { query: dqlStatement }, grailBudgetGB); From 8854972dde840a4cae3b61e876ba6f42dce5f347 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Thu, 16 Oct 2025 08:41:43 +0200 Subject: [PATCH 2/3] Update CHANGELOG.md Co-authored-by: Manuel Warum --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a47bbc8..71ee760 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Tools -- `find_entities_by_name` now uses `smartscapeNode` DQL command under the hood, and will fallback to `fetch dt.entity.${entityType}`. +- `find_entities_by_name` now uses `smartscapeNode` DQL command under the hood, and will fall back to `fetch dt.entity.${entityType}`. ### Scopes From 86f18e3be58a74c8a5cd63f8bc5e51816e6e4bc2 Mon Sep 17 00:00:00 2001 From: Christian Kreuzberger Date: Thu, 16 Oct 2025 08:55:10 +0200 Subject: [PATCH 3/3] chore: Refactor the slice filter for entity search --- src/index.ts | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index d40c7ff..38de4c0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -474,23 +474,25 @@ const main = async () => { const smartscapeResult = await findMonitoredEntityViaSmartscapeByName(dtClient, entityNames); if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) { - let resp = `Found ${smartscapeResult.records.length} monitored entities via Smartscape! Displaying the first ${maxEntitiesToDisplay} entities:\n`; + // Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities + const validSmartscapeEntities = smartscapeResult.records.filter( + (entity): entity is { id: string; type: string; name: string; [key: string]: any } => + !!(entity && entity.id && entity.type && entity.name), + ); - // iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems - smartscapeResult.records.slice(0, maxEntitiesToDisplay).forEach((entity) => { - if (entity && entity.id && entity.type && entity.name) { - 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`; - } - }); + let resp = `Found ${validSmartscapeEntities.length} monitored entities via Smartscape! Displaying the first ${Math.min(maxEntitiesToDisplay, validSmartscapeEntities.length)} valid entities:\n`; - // ToDo: Refine next-steps, this is not working properly yet. + validSmartscapeEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => { + 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`; + }); resp += '\n\n**Next Steps:**\n' + '1. Fetch more details about the entity, using the `execute_dql` tool with the following DQL Statement: "smartscapeNodes \"\" | filter id == "\n' + '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' + '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. == | limit 20"\n' + - '4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n'; + '4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n' + + '5. Explore dependency & relationships with: "smartscapeEdges \"*\" | filter source_id == or target_id == " to list inbound/outbound edges (depends_on, dependency_of, owned_by, part_of) for graph context\n'; return resp; } @@ -499,10 +501,16 @@ const main = async () => { const result = await findMonitoredEntitiesByName(dtClient, entityNames, extendedSearch); if (result && result.records && result.records.length > 0) { - let resp = `Found ${result.records.length} monitored entities! Displaying the first ${maxEntitiesToDisplay} entities:\n`; + // Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities + const validClassicEntities = result.records.filter( + (entity): entity is { id: string; type: string; name: string; [key: string]: any } => + !!(entity && entity.id && entity.type && entity.name), + ); + + let resp = `Found ${validClassicEntities.length} monitored entities! Displaying the first ${Math.min(maxEntitiesToDisplay, validClassicEntities.length)} entities:\n`; // iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems - result.records.slice(0, maxEntitiesToDisplay).forEach((entity) => { + validClassicEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => { if (entity && entity.id) { const entityType = getEntityTypeFromId(String(entity.id)); 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`;