Skip to content

Commit 24f2a17

Browse files
patch: Support all entities of dynatrace-topology for find_entity_by_name and get_entity_details (#92)
* patch: Support all entities of dynatrace-topology for get_entity_by_name * patch: Improve get entity details to use a dql statement * patch: Update entity types to include a more comprehensive list * chore: fix integration-test * chore: Added Relational Database Service * chore: Refactor unit test into a table-test * chore: Added some code comments * patch: Remove client-classic-environment-v2 dependency, as it's no longer needed * Fix Code Example: Kubernetes Pod to Service Co-authored-by: Copilot <[email protected]> * Remove time import Co-authored-by: Copilot <[email protected]> * Updated tool description for search entity by name * Added GitHub Issue link to response of get-monitored-entity-details * Updated response for find_entity_by_name in case the name provided is empty * fixup! Updated response for find_entity_by_name in case the name provided is empty * chore: refactor dqlSearchEntityCommand and added unit test --------- Co-authored-by: Copilot <[email protected]>
1 parent bd94e3f commit 24f2a17

11 files changed

+538
-30
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
- Removed unneeded scopes `environment-api:slo:read` (no tool is using this) and `environment-api:metrics:read` (anyway handled via execute DQL tool)
2121
- Removed `metrics` from `execute_dql` example with `fetch`.
2222
- Clarified usage of `verify_dql` to avoid unnecessary tool calls.
23+
- Adapted `find_entity_by_name` tool to include all entities from the smartscape topology.
24+
- Optimized `get_monitored_entity_details` tool to use direct entity type lookup for better performance
2325

2426
## 0.5.0 (Release Candidate 2)
2527

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,7 @@ Depending on the features you are using, the following scopes are needed:
306306

307307
- `app-engine:apps:run` - needed for almost all tools
308308
- `app-engine:functions:run` - needed for for almost all tools
309-
- `environment-api:entities:read` - read monitored entities (_currently not available for Platform Tokens_)
309+
- `environment-api:entities:read` - for retrieving ownership details from monitored entities (_currently not available for Platform Tokens_)
310310
- `automation:workflows:read` - read Workflows
311311
- `automation:workflows:write` - create and update Workflows
312312
- `automation:workflows:run` - run Workflows
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/**
2+
* Integration test for find monitored entity by name functionality
3+
*
4+
* This test verifies the entity finding functionality by making actual API calls
5+
* to the Dynatrace environment. These tests require valid authentication credentials.
6+
*/
7+
8+
import { config } from 'dotenv';
9+
import { createDtHttpClient } from '../src/authentication/dynatrace-clients';
10+
import { findMonitoredEntityByName } from '../src/capabilities/find-monitored-entity-by-name';
11+
import { getDynatraceEnv, DynatraceEnv } from '../src/getDynatraceEnv';
12+
13+
// Load environment variables
14+
config();
15+
16+
const API_RATE_LIMIT_DELAY = 100; // Delay in milliseconds to avoid hitting API rate limits
17+
18+
const scopesBase = [
19+
'app-engine:apps:run', // needed for environmentInformationClient
20+
'app-engine:functions:run', // needed for environmentInformationClient
21+
];
22+
23+
const scopesEntitySearch = [
24+
'storage:entities:read', // Read entities from Grail
25+
];
26+
27+
describe('Find Monitored Entity by Name Integration Tests', () => {
28+
let dynatraceEnv: DynatraceEnv;
29+
30+
// Setup that runs once before all tests
31+
beforeAll(async () => {
32+
try {
33+
dynatraceEnv = getDynatraceEnv();
34+
console.log(`Testing against environment: ${dynatraceEnv.dtEnvironment}`);
35+
} catch (err) {
36+
throw new Error(`Environment configuration error: ${(err as Error).message}`);
37+
}
38+
});
39+
40+
afterEach(async () => {
41+
// sleep after every call to avoid hitting API Rate limits
42+
await new Promise((resolve) => setTimeout(resolve, API_RATE_LIMIT_DELAY)); // Delay to avoid hitting API rate limits
43+
});
44+
45+
// Helper function to create HTTP client for entity search
46+
const createHttpClient = async () => {
47+
const { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken } = dynatraceEnv;
48+
49+
return await createDtHttpClient(
50+
dtEnvironment,
51+
scopesBase.concat(scopesEntitySearch),
52+
oauthClientId,
53+
oauthClientSecret,
54+
dtPlatformToken,
55+
);
56+
};
57+
58+
test('should handle search for non-existent entity gracefully', async () => {
59+
const dtClient = await createHttpClient();
60+
61+
// Search for an entity name that is very unlikely to exist
62+
const searchTerm = 'this-entity-definitely-does-not-exist-12345';
63+
64+
const response = await findMonitoredEntityByName(dtClient, searchTerm);
65+
66+
expect(response).toBeDefined();
67+
expect(typeof response).toBe('string');
68+
expect(response).toBe('No monitored entity found with the specified name.');
69+
}, 30_000); // Increased timeout for API calls
70+
71+
test('should handle search with empty string', async () => {
72+
const dtClient = await createHttpClient();
73+
74+
// Test with empty string
75+
const searchTerm = '';
76+
77+
const response = await findMonitoredEntityByName(dtClient, searchTerm);
78+
79+
expect(response).toBeDefined();
80+
expect(typeof response).toBe('string');
81+
82+
// Should handle gracefully - likely will return many results or handle empty search
83+
expect(response).toContain('You need to provide an entity name to search for');
84+
});
85+
86+
test('should return properly formatted response when entities are found', async () => {
87+
const dtClient = await createHttpClient();
88+
89+
// Search for a pattern that is likely to find at least one entity
90+
// "host" is common in most Dynatrace environments
91+
const searchTerm = 'host';
92+
93+
const response = await findMonitoredEntityByName(dtClient, searchTerm);
94+
95+
expect(response).toBeDefined();
96+
expect(typeof response).toBe('string');
97+
98+
// If entities are found, check the format
99+
if (response.includes('The following monitored entities were found:')) {
100+
// Each line should follow the expected format
101+
const lines = response.split('\n').filter((line) => line.startsWith('- Entity'));
102+
103+
lines.forEach((line) => {
104+
expect(line).toMatch(/^- Entity '.*' of type '.* has entity id '.*'$/);
105+
});
106+
}
107+
});
108+
});

package-lock.json

Lines changed: 0 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@
4747
"license": "MIT",
4848
"dependencies": {
4949
"@dynatrace-sdk/client-automation": "^5.3.0",
50-
"@dynatrace-sdk/client-classic-environment-v2": "^3.6.8",
5150
"@dynatrace-sdk/client-platform-management-service": "^1.6.3",
5251
"@dynatrace-sdk/client-query": "^1.18.1",
5352
"@dynatrace-sdk/shared-errors": "^1.0.0",
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { DYNATRACE_ENTITY_TYPES } from '../utils/dynatrace-entity-types';
2+
import { generateDqlSearchEntityCommand } from './find-monitored-entity-by-name';
3+
4+
describe('generateDqlSearchCommand', () => {
5+
beforeEach(() => {
6+
// Ensure we have at least some entity types for testing
7+
expect(DYNATRACE_ENTITY_TYPES.length).toBeGreaterThan(0);
8+
});
9+
10+
it('should include all entity types from DYNATRACE_ENTITY_TYPES', () => {
11+
const entityName = 'test';
12+
const result = generateDqlSearchEntityCommand(entityName);
13+
14+
console.log(result);
15+
16+
// Check that all entity types are included in the DQL
17+
DYNATRACE_ENTITY_TYPES.forEach((entityType) => {
18+
expect(result).toContain(`fetch ${entityType}`);
19+
});
20+
});
21+
22+
it('should structure the DQL correctly with first fetch and subsequent appends', () => {
23+
const entityName = 'test';
24+
const result = generateDqlSearchEntityCommand(entityName);
25+
26+
// First entity type should not have append prefix
27+
const firstEntityType = DYNATRACE_ENTITY_TYPES[0];
28+
expect(result).toContain(`fetch ${firstEntityType} | search "*${entityName}*" | fieldsAdd entity.type`);
29+
30+
// Subsequent entity types should have append prefix (if there are more than 1)
31+
if (DYNATRACE_ENTITY_TYPES.length > 1) {
32+
const secondEntityType = DYNATRACE_ENTITY_TYPES[1];
33+
expect(result).toContain(
34+
` | append [ fetch ${secondEntityType} | search "*${entityName}*" | fieldsAdd entity.type ]`,
35+
);
36+
}
37+
});
38+
});

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

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
import { HttpClient } from '@dynatrace-sdk/http-client';
22
import { executeDql } from './execute-dql';
3+
import { DYNATRACE_ENTITY_TYPES } from '../utils/dynatrace-entity-types';
34

5+
/**
6+
* Construct a DQL statement like "fetch <entityType> | search "*<entityName>*" | fieldsAdd entity.type" for each entity type,
7+
* and join them with " | append [ ... ]"
8+
* @param entityName
9+
* @returns DQL Statement for searching all entity types
10+
*/
11+
export const generateDqlSearchEntityCommand = (entityName: string): string => {
12+
const fetchDqlCommands = DYNATRACE_ENTITY_TYPES.map((entityType, index) => {
13+
const dql = `fetch ${entityType} | search "*${entityName}*" | fieldsAdd entity.type`;
14+
if (index === 0) {
15+
return dql;
16+
}
17+
return ` | append [ ${dql} ]\n`;
18+
});
19+
20+
return fetchDqlCommands.join('');
21+
};
22+
23+
/**
24+
* Find a monitored entity by name via DQL
25+
* @param dtClient
26+
* @param entityName
27+
* @returns A string with the entity details like id, name and type, or an error message if no entity was found
28+
*/
429
export const findMonitoredEntityByName = async (dtClient: HttpClient, entityName: string) => {
5-
const dql = `fetch dt.entity.application | search "*${entityName}*" | fieldsAdd entity.type
6-
| append [fetch dt.entity.service | search "*${entityName}*" | fieldsAdd entity.type]
7-
| append [fetch dt.entity.host | search "*${entityName}*" | fieldsAdd entity.type]
8-
| append [fetch dt.entity.process_group | search "*${entityName}*" | fieldsAdd entity.type]
9-
| append [fetch dt.entity.cloud_application | search "*${entityName}*" | fieldsAdd entity.type]`;
30+
if (!entityName) {
31+
return 'You need to provide an entity name to search for.';
32+
}
33+
34+
// construct a DQL statement for searching the entityName over all entity types
35+
const dql = generateDqlSearchEntityCommand(entityName);
1036

37+
// Get response from API
38+
// Note: This may be slow, as we are appending multiple entity types above
1139
const dqlResponse = await executeDql(dtClient, { query: dql });
1240

1341
if (dqlResponse && dqlResponse.length > 0) {
Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,60 @@
11
import { HttpClient } from '@dynatrace-sdk/http-client';
2-
import { MonitoredEntitiesClient } from '@dynatrace-sdk/client-classic-environment-v2';
2+
import { executeDql } from './execute-dql';
3+
import { getEntityTypeFromId } from '../utils/dynatrace-entity-types';
34

4-
export const getMonitoredEntityDetails = async (dtClient: HttpClient, entityId: string) => {
5-
const monitoredEntitiesClient = new MonitoredEntitiesClient(dtClient);
5+
type MonitoredEntityDetails = { entityId: string; displayName: string; type: string; allProperties: any };
66

7-
const entityDetails = await monitoredEntitiesClient.getEntity({
8-
entityId: entityId,
9-
});
7+
/**
8+
* Get monitored entity details by entity ID via DQL
9+
* @param dtClient
10+
* @param entityId
11+
* @returns Details about the monitored entity, or undefined in case we couldn't find it
12+
*/
13+
export const getMonitoredEntityDetails = async (
14+
dtClient: HttpClient,
15+
entityId: string,
16+
): Promise<MonitoredEntityDetails | undefined> => {
17+
// Try to determine the entity type directly from the entity ID (e.g., PROCESS_GROUP-F84E4759809ADA84 -> dt.entity.process_group)
18+
const entityType = getEntityTypeFromId(entityId);
1019

11-
return entityDetails;
20+
if (!entityType) {
21+
console.error(
22+
`Couldn't determine entity type for ID: ${entityId}. Please raise an issue at https://github.com/dynatrace-oss/dynatrace-mcp/issues if you believe this is a bug.`,
23+
);
24+
return;
25+
}
26+
27+
// construct DQL statement like `fetch dt.entity.hosts | filter id == "HOST-1234"`
28+
const dql = `fetch ${entityType} | filter id == "${entityId}" | expand tags | fieldsAdd entity.type`;
29+
30+
// Get response from API
31+
const dqlResponse = await executeDql(dtClient, { query: dql });
32+
33+
// verify response and length
34+
if (!dqlResponse || dqlResponse.length === 0) {
35+
console.error(`No entity found for ID: ${entityId}`);
36+
return;
37+
}
38+
39+
// in case we have more than one entity -> log it
40+
if (dqlResponse.length > 1) {
41+
console.error(
42+
`Multiple entities (${dqlResponse.length}) found for entity ID: ${entityId}. Returning the first one.`,
43+
);
44+
}
45+
46+
const entity = dqlResponse[0];
47+
// make typescript happy; entity should never be null though
48+
if (!entity) {
49+
console.error(`No entity found for ID: ${entityId}`);
50+
return;
51+
}
52+
53+
// return entity details
54+
return {
55+
entityId: String(entity.id),
56+
displayName: String(entity['entity.name']),
57+
type: String(entity['entity.type']),
58+
allProperties: entity || undefined,
59+
};
1260
};

src/index.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -317,14 +317,14 @@ const main = async () => {
317317

318318
tool(
319319
'find_entity_by_name',
320-
'Get the entityId of a monitored entity based on the name of the entity on Dynatrace',
320+
'Get the entityId of a monitored entity (service, host, process-group, application, kubernetes-node, ...) within the topology based on the name of the entity on Dynatrace',
321321
{
322-
entityName: z.string(),
322+
entityName: z.string().describe('Name of the entity to search for, e.g., "my-service" or "my-host"'),
323323
},
324324
async ({ entityName }) => {
325325
const dtClient = await createDtHttpClient(
326326
dtEnvironment,
327-
scopesBase.concat('environment-api:entities:read', 'storage:entities:read'),
327+
scopesBase.concat('storage:entities:read'),
328328
oauthClientId,
329329
oauthClientSecret,
330330
dtPlatformToken,
@@ -343,16 +343,20 @@ const main = async () => {
343343
async ({ entityId }) => {
344344
const dtClient = await createDtHttpClient(
345345
dtEnvironment,
346-
scopesBase.concat('environment-api:entities:read'),
346+
scopesBase.concat('storage:entities:read'),
347347
oauthClientId,
348348
oauthClientSecret,
349349
dtPlatformToken,
350350
);
351351
const entityDetails = await getMonitoredEntityDetails(dtClient, entityId);
352352

353+
if (!entityDetails) {
354+
return `No entity found with entityId: ${entityId}`;
355+
}
356+
353357
let resp =
354358
`Entity ${entityDetails.displayName} of type ${entityDetails.type} with \`entityId\` ${entityDetails.entityId}\n` +
355-
`Properties: ${JSON.stringify(entityDetails.properties)}\n`;
359+
`Properties: ${JSON.stringify(entityDetails.allProperties)}\n`;
356360

357361
if (entityDetails.type == 'SERVICE') {
358362
resp += `You can find more information about the service at ${dtEnvironment}/ui/apps/dynatrace.services/explorer?detailsId=${entityDetails.entityId}&sidebarOpen=false`;

0 commit comments

Comments
 (0)