@dynatrace-oss/dynatrace-mcp-server 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/capabilities/davis-copilot.js +22 -1
- package/dist/capabilities/find-monitored-entity-by-name.js +10 -25
- package/dist/capabilities/find-monitored-entity-by-name.test.js +26 -10
- package/dist/getDynatraceEnv.js +7 -3
- package/dist/getDynatraceEnv.test.js +32 -0
- package/dist/index.js +132 -55
- package/dist/utils/dynatrace-entity-types.js +17 -13
- package/dist/utils/dynatrace-entity-types.test.js +4 -4
- package/package.json +1 -1
- package/dist/capabilities/get-monitored-entity-details.js +0 -47
package/README.md
CHANGED
|
@@ -277,6 +277,43 @@ The [Amazon Q Developer CLI](https://docs.aws.amazon.com/amazonq/latest/qdevelop
|
|
|
277
277
|
|
|
278
278
|
This configuration should be stored in `<your-repo>/.amazonq/mcp.json`.
|
|
279
279
|
|
|
280
|
+
**Google Gemini CLI**
|
|
281
|
+
|
|
282
|
+
The [Google Gemini CLI](https://github.com/google-gemini/gemini-cli) is Google's official command-line AI assistant that supports MCP server integration. You can add the Dynatrace MCP server using either the built-in management commands or manual configuration.
|
|
283
|
+
|
|
284
|
+
Using `gemini` CLI directly (recommended):
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
gemini extensions install https://github.com/dynatrace-oss/dynatrace-mcp
|
|
288
|
+
export DT_PLATFORM_TOKEN=...
|
|
289
|
+
export DT_ENVIRONMENT=https://...
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
and verify that the server is running via
|
|
293
|
+
|
|
294
|
+
```bash
|
|
295
|
+
gemini mcp list
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Or manually in your `~/.gemini/settings.json` or `.gemini/settings.json`:
|
|
299
|
+
|
|
300
|
+
```json
|
|
301
|
+
{
|
|
302
|
+
"mcpServers": {
|
|
303
|
+
"dynatrace": {
|
|
304
|
+
"command": "npx",
|
|
305
|
+
"args": ["@dynatrace-oss/dynatrace-mcp-server@latest"],
|
|
306
|
+
"env": {
|
|
307
|
+
"DT_PLATFORM_TOKEN": "",
|
|
308
|
+
"DT_ENVIRONMENT": ""
|
|
309
|
+
},
|
|
310
|
+
"timeout": 30000,
|
|
311
|
+
"trust": false
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
280
317
|
### HTTP Server Mode (Alternative)
|
|
281
318
|
|
|
282
319
|
For scenarios where you need to run the MCP server as an HTTP service instead of using stdio (e.g., for stateful sessions, load balancing, or integration with web clients), you can use the HTTP server mode:
|
|
@@ -16,8 +16,29 @@
|
|
|
16
16
|
* in Dynatrace, including problem events, security issues, logs, metrics, and spans.
|
|
17
17
|
*/
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.chatWithDavisCopilot = exports.explainDqlInNaturalLanguage = exports.generateDqlFromNaturalLanguage = void 0;
|
|
19
|
+
exports.chatWithDavisCopilot = exports.explainDqlInNaturalLanguage = exports.generateDqlFromNaturalLanguage = exports.isDavisCopilotSkillAvailable = exports.DAVIS_COPILOT_DOCS = void 0;
|
|
20
20
|
const client_davis_copilot_1 = require("@dynatrace-sdk/client-davis-copilot");
|
|
21
|
+
// Documentation links for Davis Copilot
|
|
22
|
+
exports.DAVIS_COPILOT_DOCS = {
|
|
23
|
+
ENABLE_COPILOT: 'https://docs.dynatrace.com/docs/discover-dynatrace/platform/davis-ai/copilot/copilot-getting-started#enable-davis-copilot',
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Check if a specific Davis Copilot skill is available
|
|
27
|
+
* Returns true if the skill is available, false otherwise
|
|
28
|
+
*/
|
|
29
|
+
const isDavisCopilotSkillAvailable = async (dtClient, skill) => {
|
|
30
|
+
try {
|
|
31
|
+
const client = new client_davis_copilot_1.PublicClient(dtClient);
|
|
32
|
+
const response = await client.listAvailableSkills();
|
|
33
|
+
const availableSkills = response.skills || [];
|
|
34
|
+
return availableSkills.includes(skill);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
// If Davis Copilot is not enabled or any other error occurs, return false
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
exports.isDavisCopilotSkillAvailable = isDavisCopilotSkillAvailable;
|
|
21
42
|
/**
|
|
22
43
|
* Generate DQL from natural language
|
|
23
44
|
* Converts plain English descriptions into powerful Dynatrace Query Language (DQL) statements.
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.findMonitoredEntitiesByName = exports.generateDqlSearchEntityCommand = void 0;
|
|
4
4
|
const execute_dql_1 = require("./execute-dql");
|
|
5
5
|
const dynatrace_entity_types_1 = require("../utils/dynatrace-entity-types");
|
|
6
6
|
/**
|
|
7
|
-
* Construct a DQL statement like "fetch <entityType> | search "*<
|
|
7
|
+
* Construct a DQL statement like "fetch <entityType> | search "*<entityName1>*" OR "*<entityName2>*" | fieldsAdd entity.type" for each entity type,
|
|
8
8
|
* and join them with " | append [ ... ]"
|
|
9
9
|
* @param entityName
|
|
10
10
|
* @returns DQL Statement for searching all entity types
|
|
11
11
|
*/
|
|
12
|
-
const generateDqlSearchEntityCommand = (
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
const generateDqlSearchEntityCommand = (entityNames, extendedSearch) => {
|
|
13
|
+
// If extendedSearch is true, use all entity types, otherwise use only basic ones
|
|
14
|
+
const fetchDqlCommands = (extendedSearch ? dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL : dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_BASICS).map((entityType, index) => {
|
|
15
|
+
const dql = `fetch ${entityType} | search "*${entityNames.join('*" OR "*')}*" | fieldsAdd entity.type | expand tags`;
|
|
15
16
|
if (index === 0) {
|
|
16
17
|
return dql;
|
|
17
18
|
}
|
|
@@ -26,27 +27,11 @@ exports.generateDqlSearchEntityCommand = generateDqlSearchEntityCommand;
|
|
|
26
27
|
* @param entityName
|
|
27
28
|
* @returns A string with the entity details like id, name and type, or an error message if no entity was found
|
|
28
29
|
*/
|
|
29
|
-
const
|
|
30
|
-
if (!entityName) {
|
|
31
|
-
return 'You need to provide an entity name to search for.';
|
|
32
|
-
}
|
|
30
|
+
const findMonitoredEntitiesByName = async (dtClient, entityNames, extendedSearch) => {
|
|
33
31
|
// construct a DQL statement for searching the entityName over all entity types
|
|
34
|
-
const dql = (0, exports.generateDqlSearchEntityCommand)(
|
|
32
|
+
const dql = (0, exports.generateDqlSearchEntityCommand)(entityNames, extendedSearch);
|
|
35
33
|
// Get response from API
|
|
36
34
|
// Note: This may be slow, as we are appending multiple entity types above
|
|
37
|
-
|
|
38
|
-
if (dqlResponse && dqlResponse.records && dqlResponse.records.length > 0) {
|
|
39
|
-
let resp = 'The following monitored entities were found:\n';
|
|
40
|
-
// iterate over dqlResponse and create a string with the entity names
|
|
41
|
-
dqlResponse.records.forEach((entity) => {
|
|
42
|
-
if (entity) {
|
|
43
|
-
resp += `- Entity '${entity['entity.name']}' of type '${entity['entity.type']} has entity id '${entity.id}'\n`;
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
return resp;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
return 'No monitored entity found with the specified name.';
|
|
50
|
-
}
|
|
35
|
+
return await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
|
|
51
36
|
};
|
|
52
|
-
exports.
|
|
37
|
+
exports.findMonitoredEntitiesByName = findMonitoredEntitiesByName;
|
|
@@ -5,27 +5,43 @@ const find_monitored_entity_by_name_1 = require("./find-monitored-entity-by-name
|
|
|
5
5
|
describe('generateDqlSearchCommand', () => {
|
|
6
6
|
beforeEach(() => {
|
|
7
7
|
// Ensure we have at least some entity types for testing
|
|
8
|
-
expect(dynatrace_entity_types_1.
|
|
8
|
+
expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.length).toBeGreaterThan(0);
|
|
9
9
|
});
|
|
10
|
-
it('should include all entity types from
|
|
10
|
+
it('should include all entity types from DYNATRACE_ENTITY_TYPES_ALL', () => {
|
|
11
11
|
const entityName = 'test';
|
|
12
|
-
const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)(entityName);
|
|
12
|
+
const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)([entityName], true);
|
|
13
13
|
console.log(result);
|
|
14
14
|
// Check that all entity types are included in the DQL
|
|
15
|
-
dynatrace_entity_types_1.
|
|
15
|
+
dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.forEach((entityType) => {
|
|
16
|
+
expect(result).toContain(`fetch ${entityType}`);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
it('should include entity types from DYNATRACE_ENTITY_TYPES_BASICS', () => {
|
|
20
|
+
const entityName = 'test';
|
|
21
|
+
const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)([entityName], false);
|
|
22
|
+
console.log(result);
|
|
23
|
+
// Check that all entity types are included in the DQL
|
|
24
|
+
dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_BASICS.forEach((entityType) => {
|
|
16
25
|
expect(result).toContain(`fetch ${entityType}`);
|
|
17
26
|
});
|
|
18
27
|
});
|
|
19
28
|
it('should structure the DQL correctly with first fetch and subsequent appends', () => {
|
|
20
29
|
const entityName = 'test';
|
|
21
|
-
const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)(entityName);
|
|
30
|
+
const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)([entityName], true);
|
|
22
31
|
// First entity type should not have append prefix
|
|
23
|
-
const firstEntityType = dynatrace_entity_types_1.
|
|
24
|
-
expect(result).toContain(`fetch ${firstEntityType} | search "*${entityName}*" | fieldsAdd entity.type`);
|
|
32
|
+
const firstEntityType = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL[0];
|
|
33
|
+
expect(result).toContain(`fetch ${firstEntityType} | search "*${entityName}*" | fieldsAdd entity.type | expand tags`);
|
|
25
34
|
// Subsequent entity types should have append prefix (if there are more than 1)
|
|
26
|
-
if (dynatrace_entity_types_1.
|
|
27
|
-
const secondEntityType = dynatrace_entity_types_1.
|
|
28
|
-
expect(result).toContain(` | append [ fetch ${secondEntityType} | search "*${entityName}*" | fieldsAdd entity.type ]`);
|
|
35
|
+
if (dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.length > 1) {
|
|
36
|
+
const secondEntityType = dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL[1];
|
|
37
|
+
expect(result).toContain(` | append [ fetch ${secondEntityType} | search "*${entityName}*" | fieldsAdd entity.type | expand tags ]`);
|
|
29
38
|
}
|
|
30
39
|
});
|
|
40
|
+
it('should handle multiple entityNames correctly', () => {
|
|
41
|
+
const entityNames = ['test1', 'test2', 'example'];
|
|
42
|
+
const result = (0, find_monitored_entity_by_name_1.generateDqlSearchEntityCommand)(entityNames, true);
|
|
43
|
+
// Check that the search part includes all entity names joined by OR
|
|
44
|
+
const searchPart = `search "*test1*" OR "*test2*" OR "*example*"`;
|
|
45
|
+
expect(result).toContain(searchPart);
|
|
46
|
+
});
|
|
31
47
|
});
|
package/dist/getDynatraceEnv.js
CHANGED
|
@@ -11,16 +11,20 @@ function getDynatraceEnv(env = process.env) {
|
|
|
11
11
|
const dtPlatformToken = env.DT_PLATFORM_TOKEN;
|
|
12
12
|
const dtEnvironment = env.DT_ENVIRONMENT;
|
|
13
13
|
const slackConnectionId = env.SLACK_CONNECTION_ID || 'fake-slack-connection-id';
|
|
14
|
-
|
|
14
|
+
let grailBudgetGB = parseFloat(env.DT_GRAIL_QUERY_BUDGET_GB || '1000'); // Default to 1000 GB
|
|
15
15
|
if (!dtEnvironment) {
|
|
16
16
|
throw new Error('Please set DT_ENVIRONMENT environment variable to your Dynatrace Platform Environment');
|
|
17
17
|
}
|
|
18
18
|
if (!oauthClientId && !oauthClientSecret && !dtPlatformToken) {
|
|
19
19
|
throw new Error('Please set either OAUTH_CLIENT_ID and OAUTH_CLIENT_SECRET, or DT_PLATFORM_TOKEN environment variables');
|
|
20
20
|
}
|
|
21
|
+
// For dev and hardening stages, set unlimited budget (-1) unless explicitly overridden
|
|
22
|
+
if (dtEnvironment.includes('apps.dynatracelabs.com') && !env.DT_GRAIL_QUERY_BUDGET_GB) {
|
|
23
|
+
grailBudgetGB = -1;
|
|
24
|
+
}
|
|
21
25
|
// ToDo: Allow the case of -1 for unlimited Budget
|
|
22
|
-
if (isNaN(grailBudgetGB) || (grailBudgetGB
|
|
23
|
-
throw new Error('DT_GRAIL_QUERY_BUDGET_GB must be a positive number representing GB budget for Grail queries');
|
|
26
|
+
if (isNaN(grailBudgetGB) || (grailBudgetGB < 0 && grailBudgetGB !== -1)) {
|
|
27
|
+
throw new Error('DT_GRAIL_QUERY_BUDGET_GB must be a positive number or -1 (for unlimited) representing GB budget for Grail queries');
|
|
24
28
|
}
|
|
25
29
|
if (!dtEnvironment.startsWith('https://')) {
|
|
26
30
|
throw new Error('Please set DT_ENVIRONMENT to a valid Dynatrace Environment URL (e.g., https://<environment-id>.apps.dynatrace.com)');
|
|
@@ -71,4 +71,36 @@ describe('getDynatraceEnv', () => {
|
|
|
71
71
|
};
|
|
72
72
|
expect(() => (0, getDynatraceEnv_1.getDynatraceEnv)(env)).not.toThrow();
|
|
73
73
|
});
|
|
74
|
+
it('Defaults the Grail Budget to 1000', () => {
|
|
75
|
+
const env = {
|
|
76
|
+
...baseEnv,
|
|
77
|
+
GRAIL_BUDGET_GB: undefined,
|
|
78
|
+
DT_ENVIRONMENT: 'https://abc123.apps.dynatrace.com',
|
|
79
|
+
};
|
|
80
|
+
const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
|
|
81
|
+
expect(result).toEqual({
|
|
82
|
+
oauthClientId: env.OAUTH_CLIENT_ID,
|
|
83
|
+
oauthClientSecret: env.OAUTH_CLIENT_SECRET,
|
|
84
|
+
dtEnvironment: env.DT_ENVIRONMENT,
|
|
85
|
+
dtPlatformToken: env.DT_PLATFORM_TOKEN,
|
|
86
|
+
slackConnectionId: env.SLACK_CONNECTION_ID,
|
|
87
|
+
grailBudgetGB: 1000, // Default value
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
it('Resets the Grail Budget if a dev/sprint URL is used', () => {
|
|
91
|
+
const env = {
|
|
92
|
+
...baseEnv,
|
|
93
|
+
GRAIL_BUDGET_GB: undefined,
|
|
94
|
+
DT_ENVIRONMENT: 'https://abc123.dev.apps.dynatracelabs.com',
|
|
95
|
+
};
|
|
96
|
+
const result = (0, getDynatraceEnv_1.getDynatraceEnv)(env);
|
|
97
|
+
expect(result).toEqual({
|
|
98
|
+
oauthClientId: env.OAUTH_CLIENT_ID,
|
|
99
|
+
oauthClientSecret: env.OAUTH_CLIENT_SECRET,
|
|
100
|
+
dtEnvironment: env.DT_ENVIRONMENT,
|
|
101
|
+
dtPlatformToken: env.DT_PLATFORM_TOKEN,
|
|
102
|
+
slackConnectionId: env.SLACK_CONNECTION_ID,
|
|
103
|
+
grailBudgetGB: -1, // Default value for dynatracelabs.com
|
|
104
|
+
});
|
|
105
|
+
});
|
|
74
106
|
});
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,6 @@ const version_1 = require("./utils/version");
|
|
|
14
14
|
const dynatrace_clients_1 = require("./authentication/dynatrace-clients");
|
|
15
15
|
const list_vulnerabilities_1 = require("./capabilities/list-vulnerabilities");
|
|
16
16
|
const list_problems_1 = require("./capabilities/list-problems");
|
|
17
|
-
const get_monitored_entity_details_1 = require("./capabilities/get-monitored-entity-details");
|
|
18
17
|
const get_ownership_information_1 = require("./capabilities/get-ownership-information");
|
|
19
18
|
const get_events_for_cluster_1 = require("./capabilities/get-events-for-cluster");
|
|
20
19
|
const create_workflow_for_problem_notification_1 = require("./capabilities/create-workflow-for-problem-notification");
|
|
@@ -26,8 +25,22 @@ const find_monitored_entity_by_name_1 = require("./capabilities/find-monitored-e
|
|
|
26
25
|
const davis_copilot_1 = require("./capabilities/davis-copilot");
|
|
27
26
|
const getDynatraceEnv_1 = require("./getDynatraceEnv");
|
|
28
27
|
const telemetry_openkit_1 = require("./utils/telemetry-openkit");
|
|
28
|
+
const dynatrace_entity_types_1 = require("./utils/dynatrace-entity-types");
|
|
29
29
|
const grail_budget_tracker_1 = require("./utils/grail-budget-tracker");
|
|
30
|
-
|
|
30
|
+
// Load environment variables from .env file if available, and suppress warnings/logging to stdio
|
|
31
|
+
// as it breaks MCP communication when using stdio transport
|
|
32
|
+
const dotEnvOutput = (0, dotenv_1.config)({ quiet: true });
|
|
33
|
+
if (dotEnvOutput.error) {
|
|
34
|
+
// Only log error if it's not about missing .env file
|
|
35
|
+
if (dotEnvOutput.error.code !== 'ENOENT') {
|
|
36
|
+
console.error('Error loading .env file:', dotEnvOutput.error);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
// Successfully loaded .env file
|
|
42
|
+
console.error(`.env file loaded successfully - loaded ${dotEnvOutput.parsed ? Object.keys(dotEnvOutput.parsed).length : 0} environment variables: ${Object.keys(dotEnvOutput.parsed || {}).join(', ')}`);
|
|
43
|
+
}
|
|
31
44
|
let scopesBase = [
|
|
32
45
|
'app-engine:apps:run', // needed for environmentInformationClient
|
|
33
46
|
'app-engine:functions:run', // needed for environmentInformationClient
|
|
@@ -126,6 +139,7 @@ const main = async () => {
|
|
|
126
139
|
}, {
|
|
127
140
|
capabilities: {
|
|
128
141
|
tools: {},
|
|
142
|
+
elicitation: {},
|
|
129
143
|
},
|
|
130
144
|
});
|
|
131
145
|
// quick abstraction/wrapper to make it easier for tools to reply text instead of JSON
|
|
@@ -170,6 +184,38 @@ const main = async () => {
|
|
|
170
184
|
};
|
|
171
185
|
server.tool(name, description, paramsSchema, annotations, (args) => wrappedCb(args));
|
|
172
186
|
};
|
|
187
|
+
/**
|
|
188
|
+
* Helper function to request human approval for potentially sensitive operations
|
|
189
|
+
* @param operation - Description of the operation requiring approval
|
|
190
|
+
* @returns Promise<boolean> - true if approved, false if declined or cancelled
|
|
191
|
+
*/
|
|
192
|
+
const requestHumanApproval = async (operation) => {
|
|
193
|
+
try {
|
|
194
|
+
const result = await server.server.elicitInput({
|
|
195
|
+
message: `Please review: ${operation}`,
|
|
196
|
+
requestedSchema: {
|
|
197
|
+
type: 'object',
|
|
198
|
+
properties: {
|
|
199
|
+
approval: {
|
|
200
|
+
type: 'boolean',
|
|
201
|
+
title: 'Approve this operation?',
|
|
202
|
+
description: 'Select true to approve this operation, or false to decline.',
|
|
203
|
+
default: false,
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
required: ['approval'],
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
if (result.action === 'accept' && result.content?.approval === true) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error('Failed to elicit human approval:', error);
|
|
216
|
+
return false; // Default to deny if elicitation fails
|
|
217
|
+
}
|
|
218
|
+
};
|
|
173
219
|
/** Tool Definitions below */
|
|
174
220
|
tool('get_environment_info', 'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.', {}, {
|
|
175
221
|
readOnlyHint: true,
|
|
@@ -183,7 +229,7 @@ const main = async () => {
|
|
|
183
229
|
resp += `You can reach it via ${dtEnvironment}\n`;
|
|
184
230
|
return resp;
|
|
185
231
|
});
|
|
186
|
-
tool('list_vulnerabilities', '
|
|
232
|
+
tool('list_vulnerabilities', 'Retrieve all active (non-muted) vulnerabilities from Dynatrace for the last 30 days. An additional filter can be provided using DQL filter (filter for a specific entity type and id).', {
|
|
187
233
|
riskScore: zod_1.z
|
|
188
234
|
.number()
|
|
189
235
|
.optional()
|
|
@@ -192,7 +238,8 @@ const main = async () => {
|
|
|
192
238
|
additionalFilter: zod_1.z
|
|
193
239
|
.string()
|
|
194
240
|
.optional()
|
|
195
|
-
.describe('Additional filter for
|
|
241
|
+
.describe('Additional DQL-based filter for accessing vulnerabilities, e.g., by entity type (preferred), like \'dt.entity.<service|host|application|$type> == "<entity-id>"\', by entity name (not recommended) \'affected_entity.name contains "<entity-name>"\' , or by tags \'entity_tags == array("dt.owner:team-foobar", "tag:tag")\'. ' +
|
|
242
|
+
'You can also filter by vulnerability details like \'vulnerability.stack == "CODE_LIBRARY"\' or \'vulnerability.risk.level == "CRITICAL"\' or \'vulnerability.davis_assessment.exposure_status == "PUBLIC_NETWORK"\''),
|
|
196
243
|
maxVulnerabilitiesToDisplay: zod_1.z
|
|
197
244
|
.number()
|
|
198
245
|
.default(25)
|
|
@@ -234,11 +281,11 @@ const main = async () => {
|
|
|
234
281
|
`\n3. Last but not least, tell the user to visit ${dtEnvironment}/ui/apps/dynatrace.security.vulnerabilities/vulnerabilities/<vulnerability-id> for full details.`;
|
|
235
282
|
return resp;
|
|
236
283
|
});
|
|
237
|
-
tool('list_problems', 'List all problems (dt.davis.problems) known on Dynatrace, sorted by their recency, for the last 12h. An additional filter can be provided
|
|
284
|
+
tool('list_problems', 'List all problems (dt.davis.problems) known on Dynatrace, sorted by their recency, for the last 12h. An additional DQL based filter, like filtering for specific entities, can be provided.', {
|
|
238
285
|
additionalFilter: zod_1.z
|
|
239
286
|
.string()
|
|
240
287
|
.optional()
|
|
241
|
-
.describe('Additional
|
|
288
|
+
.describe('Additional DQL filter for dt.davis.problems - filter by entity type (preferred), like \'dt.entity.<service|host|application|$type> == "<entity-id>"\', or by entity tags \'entity_tags == array("dt.owner:team-foobar", "tag:tag")\''),
|
|
242
289
|
maxProblemsToDisplay: zod_1.z.number().default(10).describe('Maximum number of problems to display in the response.'),
|
|
243
290
|
}, {
|
|
244
291
|
readOnlyHint: true,
|
|
@@ -274,65 +321,61 @@ const main = async () => {
|
|
|
274
321
|
return 'No problems found';
|
|
275
322
|
}
|
|
276
323
|
});
|
|
277
|
-
tool('find_entity_by_name', '
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
entityId: zod_1.z.string().optional(),
|
|
324
|
+
tool('find_entity_by_name', 'Find the entityId and type of a monitored entity (service, host, process-group, application, kubernetes-node, custom-app, ...) within the topology on Dynatrace, based on the name of the entity. Run this before querying data like logs, metrics, problems, events. If no entity name is known, make an educated guess with common identifiers like package.json `id`/`name`, helm chart names, kubernetes manfiest names, and alike.', {
|
|
325
|
+
entityNames: zod_1.z
|
|
326
|
+
.array(zod_1.z.string())
|
|
327
|
+
.describe('Names of the entities to search for - try with one name at first (identifiers like package.json id), and only try with multiple names if the first search was unsuccessful'),
|
|
328
|
+
maxEntitiesToDisplay: zod_1.z.number().default(10).describe('Maximum number of entities to display in the response.'),
|
|
329
|
+
extendedSearch: zod_1.z
|
|
330
|
+
.boolean()
|
|
331
|
+
.optional()
|
|
332
|
+
.default(false)
|
|
333
|
+
.describe('Set this to true if you want a comprehensive search over all available entity types.'),
|
|
288
334
|
}, {
|
|
289
335
|
readOnlyHint: true,
|
|
290
|
-
}, async ({
|
|
336
|
+
}, async ({ entityNames, maxEntitiesToDisplay, extendedSearch }) => {
|
|
291
337
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('storage:entities:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
292
|
-
const
|
|
293
|
-
if (
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
resp +=
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
resp
|
|
309
|
-
}
|
|
310
|
-
resp += `\n\n**Filter**:`;
|
|
311
|
-
// Use entityTypeTable as the filter (e.g., fetch logs | filter dt.entity.service == "SERVICE-1234")
|
|
312
|
-
if (entityDetails.entityTypeTable) {
|
|
313
|
-
resp += ` You can use the following filter to get relevant information from other tools: \`| filter ${entityDetails.entityTypeTable} == "${entityDetails.entityId}"\`. `;
|
|
338
|
+
const result = await (0, find_monitored_entity_by_name_1.findMonitoredEntitiesByName)(dtClient, entityNames, extendedSearch);
|
|
339
|
+
if (result && result.records && result.records.length > 0) {
|
|
340
|
+
let resp = `Found ${result.records.length} monitored entities! Displaying the first ${maxEntitiesToDisplay} entities:\n`;
|
|
341
|
+
// iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems
|
|
342
|
+
result.records.slice(0, maxEntitiesToDisplay).forEach((entity) => {
|
|
343
|
+
if (entity && entity.id) {
|
|
344
|
+
const entityType = (0, dynatrace_entity_types_1.getEntityTypeFromId)(String(entity.id));
|
|
345
|
+
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`;
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
resp +=
|
|
349
|
+
'\n\n**Next Steps:**\n' +
|
|
350
|
+
'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' +
|
|
351
|
+
'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' +
|
|
352
|
+
'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' +
|
|
353
|
+
'4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n';
|
|
354
|
+
return resp;
|
|
314
355
|
}
|
|
315
356
|
else {
|
|
316
|
-
|
|
357
|
+
return 'No monitored entity found with the specified name. Try to broaden your search term or check for typos.';
|
|
317
358
|
}
|
|
318
|
-
resp += `\n\n**Next Steps**\n\n`;
|
|
319
|
-
resp += `1. Find available metrics for this entity, by using execute_dql tool with the following DQL statement: "fetch metric.series" and the filter defined above\n`;
|
|
320
|
-
resp += `2. Find out whether any problems exist for this entity using the list_problems tool\n`;
|
|
321
|
-
resp += `3. Explore logs for this entity by using execute_dql with "fetch logs" and applying the filter mentioned above'\n`;
|
|
322
|
-
return resp;
|
|
323
359
|
});
|
|
324
360
|
tool('send_slack_message', 'Sends a Slack message to a dedicated Slack Channel via Slack Connector on Dynatrace', {
|
|
325
|
-
channel: zod_1.z.string()
|
|
326
|
-
message: zod_1.z
|
|
361
|
+
channel: zod_1.z.string(),
|
|
362
|
+
message: zod_1.z
|
|
363
|
+
.string()
|
|
364
|
+
.describe('Slack markdown supported. Avoid sending sensitive data like log lines. Focus on context, insights, links, and summaries.'),
|
|
327
365
|
}, {
|
|
328
366
|
// not read-only, not open-world, not destructive
|
|
329
367
|
readOnlyHint: false,
|
|
330
368
|
}, async ({ channel, message }) => {
|
|
369
|
+
// Request human approval before sending the message
|
|
370
|
+
const approved = await requestHumanApproval(`Send information via Slack to ${channel}`);
|
|
371
|
+
if (!approved) {
|
|
372
|
+
return 'Operation cancelled: Human approval was not granted for sending this Slack message.';
|
|
373
|
+
}
|
|
331
374
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('app-settings:objects:read'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
332
375
|
const response = await (0, send_slack_message_1.sendSlackMessage)(dtClient, slackConnectionId, channel, message);
|
|
333
376
|
return `Message sent to Slack channel: ${JSON.stringify(response)}`;
|
|
334
377
|
});
|
|
335
|
-
tool('verify_dql', '
|
|
378
|
+
tool('verify_dql', 'Syntactically verify a Dynatrace Query Language (DQL) statement on Dynatrace GRAIL before executing it. Recommended for generated DQL statements. Skip for statements created by `generate_dql_from_natural_language` tool, as well as from documentation.', {
|
|
336
379
|
dqlStatement: zod_1.z.string(),
|
|
337
380
|
}, {
|
|
338
381
|
readOnlyHint: true,
|
|
@@ -351,16 +394,17 @@ const main = async () => {
|
|
|
351
394
|
resp += `The DQL statement is valid - you can use the "execute_dql" tool.\n`;
|
|
352
395
|
}
|
|
353
396
|
else {
|
|
354
|
-
resp += `The DQL statement is invalid. Please adapt your statement.\n`;
|
|
397
|
+
resp += `The DQL statement is invalid. Please adapt your statement. Consider using "generate_dql_from_natural_language" tool for help.\n`;
|
|
355
398
|
}
|
|
356
399
|
return resp;
|
|
357
400
|
});
|
|
358
401
|
tool('execute_dql', 'Get Logs, Metrics, Spans or Events from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
|
|
359
|
-
'
|
|
360
|
-
'
|
|
402
|
+
'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
|
|
403
|
+
'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
|
|
361
404
|
dqlStatement: zod_1.z
|
|
362
405
|
.string()
|
|
363
|
-
.describe('DQL Statement (Ex: "fetch [logs, spans, events] | filter <some-filter> | summarize count(), by:{some-fields}.", or for metrics: "timeseries { avg(<metric-name>), value.A = avg(<metric-name>, scalar: true) }")'
|
|
406
|
+
.describe('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) }"). ' +
|
|
407
|
+
'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. '),
|
|
364
408
|
}, {
|
|
365
409
|
// not readonly (DQL statements may modify things), not idempotent (may change over time)
|
|
366
410
|
readOnlyHint: false,
|
|
@@ -431,6 +475,11 @@ const main = async () => {
|
|
|
431
475
|
idempotentHint: true,
|
|
432
476
|
}, async ({ text }) => {
|
|
433
477
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:nl2dql:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
478
|
+
// Check if the nl2dql skill is available
|
|
479
|
+
const isAvailable = await (0, davis_copilot_1.isDavisCopilotSkillAvailable)(dtClient, 'nl2dql');
|
|
480
|
+
if (!isAvailable) {
|
|
481
|
+
return `❌ The DQL generation skill is not available. Please visit: ${davis_copilot_1.DAVIS_COPILOT_DOCS.ENABLE_COPILOT}`;
|
|
482
|
+
}
|
|
434
483
|
const response = await (0, davis_copilot_1.generateDqlFromNaturalLanguage)(dtClient, text);
|
|
435
484
|
let resp = `🔤 Natural Language to DQL:\n\n`;
|
|
436
485
|
resp += `**Query:** "${text}"\n\n`;
|
|
@@ -460,6 +509,11 @@ const main = async () => {
|
|
|
460
509
|
idempotentHint: true,
|
|
461
510
|
}, async ({ dql }) => {
|
|
462
511
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:dql2nl:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
512
|
+
// Check if the dql2nl skill is available
|
|
513
|
+
const isAvailable = await (0, davis_copilot_1.isDavisCopilotSkillAvailable)(dtClient, 'dql2nl');
|
|
514
|
+
if (!isAvailable) {
|
|
515
|
+
return `❌ The DQL explanation skill is not available. Please visit: ${davis_copilot_1.DAVIS_COPILOT_DOCS.ENABLE_COPILOT}`;
|
|
516
|
+
}
|
|
463
517
|
const response = await (0, davis_copilot_1.explainDqlInNaturalLanguage)(dtClient, dql);
|
|
464
518
|
let resp = `📝 DQL to Natural Language:\n\n`;
|
|
465
519
|
resp += `**DQL Query:**\n\`\`\`\n${dql}\n\`\`\`\n\n`;
|
|
@@ -475,7 +529,7 @@ const main = async () => {
|
|
|
475
529
|
}
|
|
476
530
|
return resp;
|
|
477
531
|
});
|
|
478
|
-
tool('chat_with_davis_copilot', 'Use this tool
|
|
532
|
+
tool('chat_with_davis_copilot', 'Use this tool to ask any Dynatrace related question, in case no other more specific tool is available.', {
|
|
479
533
|
text: zod_1.z.string().describe('Your question or request for Davis CoPilot'),
|
|
480
534
|
context: zod_1.z.string().optional().describe('Optional context to provide additional information'),
|
|
481
535
|
instruction: zod_1.z.string().optional().describe('Optional instruction for how to format the response'),
|
|
@@ -485,6 +539,11 @@ const main = async () => {
|
|
|
485
539
|
openWorldHint: true, // web-search like characteristics
|
|
486
540
|
}, async ({ text, context, instruction }) => {
|
|
487
541
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('davis-copilot:conversations:execute'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
542
|
+
// Check if the conversation skill is available
|
|
543
|
+
const isAvailable = await (0, davis_copilot_1.isDavisCopilotSkillAvailable)(dtClient, 'conversation');
|
|
544
|
+
if (!isAvailable) {
|
|
545
|
+
return `❌ The conversation skill is not available. Please visit: ${davis_copilot_1.DAVIS_COPILOT_DOCS.ENABLE_COPILOT}`;
|
|
546
|
+
}
|
|
488
547
|
const conversationContext = [];
|
|
489
548
|
if (context) {
|
|
490
549
|
conversationContext.push({
|
|
@@ -537,6 +596,11 @@ const main = async () => {
|
|
|
537
596
|
readOnlyHint: false,
|
|
538
597
|
idempotentHint: false, // creating the same workflow multiple times is possible
|
|
539
598
|
}, async ({ problemType, teamName, channel, isPrivate }) => {
|
|
599
|
+
// ask for human approval
|
|
600
|
+
const approved = await requestHumanApproval(`Create a workflow for notifying team ${teamName} via ${channel} about ${problemType} problems`);
|
|
601
|
+
if (!approved) {
|
|
602
|
+
return 'Operation cancelled: Human approval was not granted for creating this workflow.';
|
|
603
|
+
}
|
|
540
604
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
541
605
|
const response = await (0, create_workflow_for_problem_notification_1.createWorkflowForProblemNotification)(dtClient, teamName, channel, problemType, isPrivate);
|
|
542
606
|
let resp = `Workflow Created: ${response?.id} with name ${response?.title}.\nYou can access the Workflow via the following link: ${dtEnvironment}/ui/apps/dynatrace.automations/workflows/${response?.id}.\nTell the user to inspect the Workflow by visiting the link.\n`;
|
|
@@ -558,6 +622,11 @@ const main = async () => {
|
|
|
558
622
|
readOnlyHint: false,
|
|
559
623
|
idempotentHint: true, // making the same workflow public multiple times yields the same result
|
|
560
624
|
}, async ({ workflowId }) => {
|
|
625
|
+
// ask for human approval
|
|
626
|
+
const approved = await requestHumanApproval(`Make workflow ${workflowId} publicly available to everyone on the Dynatrace Environment`);
|
|
627
|
+
if (!approved) {
|
|
628
|
+
return 'Operation cancelled: Human approval was not granted for making this workflow public.';
|
|
629
|
+
}
|
|
561
630
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('automation:workflows:write', 'automation:workflows:read', 'automation:workflows:run'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
562
631
|
const response = await (0, update_workflow_1.updateWorkflow)(dtClient, workflowId, {
|
|
563
632
|
isPrivate: false,
|
|
@@ -618,7 +687,9 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
|
|
|
618
687
|
ccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for CC recipients'),
|
|
619
688
|
bccRecipients: zod_1.z.array(zod_1.z.string().email()).optional().describe('Array of email addresses for BCC recipients'),
|
|
620
689
|
subject: zod_1.z.string().describe('Subject line of the email'),
|
|
621
|
-
body: zod_1.z
|
|
690
|
+
body: zod_1.z
|
|
691
|
+
.string()
|
|
692
|
+
.describe('Body content of the email (plain text only). Avoid sending sensitive data like log lines. Focus on context, insights, links, and summaries.'),
|
|
622
693
|
}, {
|
|
623
694
|
openWorldHint: true, // email is as close to the open-world as we can get with our system
|
|
624
695
|
}, async ({ toRecipients, ccRecipients, bccRecipients, subject, body }) => {
|
|
@@ -627,6 +698,12 @@ You can now execute new Grail queries (DQL, etc.) again. If this happens more of
|
|
|
627
698
|
if (totalRecipients > 10) {
|
|
628
699
|
throw new Error(`Total recipients (${totalRecipients}) exceeds maximum limit of 10 across TO, CC, and BCC fields`);
|
|
629
700
|
}
|
|
701
|
+
// Request human approval before sending the email
|
|
702
|
+
const allRecipients = [...toRecipients, ...(ccRecipients || []), ...(bccRecipients || [])];
|
|
703
|
+
const approved = await requestHumanApproval(`Send information via Email to ${allRecipients.join(', ')}`);
|
|
704
|
+
if (!approved) {
|
|
705
|
+
return 'Operation cancelled: Human approval was not granted for sending this email.';
|
|
706
|
+
}
|
|
630
707
|
const dtClient = await (0, dynatrace_clients_1.createDtHttpClient)(dtEnvironment, scopesBase.concat('email:emails:send'), oauthClientId, oauthClientSecret, dtPlatformToken);
|
|
631
708
|
const emailRequest = {
|
|
632
709
|
toRecipients: { emailAddresses: toRecipients },
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @see https://docs.dynatrace.com/docs/discover-dynatrace/references/semantic-dictionary/model/dt-entities
|
|
9
9
|
*/
|
|
10
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
-
exports.
|
|
11
|
+
exports.DYNATRACE_ENTITY_TYPES_ALL = exports.DYNATRACE_ENTITY_TYPES_BASICS = void 0;
|
|
12
12
|
exports.getEntityTypeFromId = getEntityTypeFromId;
|
|
13
13
|
/**
|
|
14
14
|
* Entity ID prefixes mapped to their corresponding Dynatrace entity types
|
|
@@ -19,28 +19,23 @@ exports.getEntityTypeFromId = getEntityTypeFromId;
|
|
|
19
19
|
*
|
|
20
20
|
* Recommendation: Use `verify_dql` and/or `execute_dql` to ensure that the entity type can be queried correctly.
|
|
21
21
|
*/
|
|
22
|
-
const
|
|
22
|
+
const ENTITY_ID_PREFIX_TO_TYPE_MAP_BASICS = {
|
|
23
23
|
// Core Applications and Services
|
|
24
24
|
APPLICATION: 'dt.entity.application', // Verified!
|
|
25
25
|
SERVICE: 'dt.entity.service', // Verified!
|
|
26
|
-
SERVICE_INSTANCE: 'dt.entity.service_instance', // Verified!
|
|
27
26
|
MOBILE_APPLICATION: 'dt.entity.mobile_application', // Verified! (0 rows found, manually verified that entity exists)
|
|
28
27
|
CUSTOM_APPLICATION: 'dt.entity.custom_application', // Verified!
|
|
29
28
|
// Infrastructure
|
|
30
29
|
HOST: 'dt.entity.host', // Verified!
|
|
31
30
|
HOST_GROUP: 'dt.entity.host_group', // Verified!
|
|
32
31
|
PROCESS_GROUP: 'dt.entity.process_group', // Verified!
|
|
33
|
-
PROCESS_GROUP_INSTANCE: 'dt.entity.process_group_instance', // Verified!
|
|
34
32
|
DISK: 'dt.entity.disk', // Verified!
|
|
35
33
|
NETWORK_INTERFACE: 'dt.entity.network_interface', // Verified!
|
|
36
34
|
// Cloud Services
|
|
37
35
|
CLOUD_APPLICATION: 'dt.entity.cloud_application', // Verified!
|
|
38
|
-
CLOUD_APPLICATION_INSTANCE: 'dt.entity.cloud_application_instance', // Verified!
|
|
39
36
|
CLOUD_APPLICATION_NAMESPACE: 'dt.entity.cloud_application_namespace', // Verified!
|
|
40
37
|
// Containers and Container Groups
|
|
41
38
|
CONTAINER_GROUP: 'dt.entity.container_group', // Verified!
|
|
42
|
-
CONTAINER_GROUP_INSTANCE: 'dt.entity.container_group_instance', // Verified!
|
|
43
|
-
DCG_INSTANCE: 'dt.entity.docker_container_group_instance', // Verified! (0 rows found, manually verified that entity exists, but this might be deprecated / old)
|
|
44
39
|
// Environment
|
|
45
40
|
ENVIRONMENT: 'dt.entity.environment', // Verified!
|
|
46
41
|
// Operating System
|
|
@@ -55,6 +50,18 @@ const ENTITY_ID_PREFIX_TO_TYPE_MAP = {
|
|
|
55
50
|
GEOLOCATION: 'dt.entity.geolocation', // Verified!
|
|
56
51
|
// Database Services
|
|
57
52
|
RELATIONAL_DATABASE_SERVICE: 'dt.entity.relational_database_service', // Verified - might need an additional integration/config to work properly though
|
|
53
|
+
// Kubernetes Entities
|
|
54
|
+
KUBERNETES_NODE: 'dt.entity.kubernetes_node', // Verified!
|
|
55
|
+
KUBERNETES_CLUSTER: 'dt.entity.kubernetes_cluster', // Verified!
|
|
56
|
+
KUBERNETES_SERVICE: 'dt.entity.kubernetes_service', // Verified!
|
|
57
|
+
};
|
|
58
|
+
const ENTITY_ID_PREFIX_TO_TYPE_MAP_ALL = {
|
|
59
|
+
...ENTITY_ID_PREFIX_TO_TYPE_MAP_BASICS,
|
|
60
|
+
SERVICE_INSTANCE: 'dt.entity.service_instance', // Verified!
|
|
61
|
+
PROCESS_GROUP_INSTANCE: 'dt.entity.process_group_instance', // Verified!
|
|
62
|
+
CLOUD_APPLICATION_INSTANCE: 'dt.entity.cloud_application_instance', // Verified!
|
|
63
|
+
DCG_INSTANCE: 'dt.entity.docker_container_group_instance', // Verified! (0 rows found, manually verified that entity exists, but this might be deprecated / old)
|
|
64
|
+
CONTAINER_GROUP_INSTANCE: 'dt.entity.container_group_instance', // Verified!
|
|
58
65
|
// AWS Services
|
|
59
66
|
EC2_INSTANCE: 'dt.entity.ec2_instance', // Verified!
|
|
60
67
|
AWS_LAMBDA_FUNCTION: 'dt.entity.aws_lambda_function', // Verified!
|
|
@@ -66,12 +73,9 @@ const ENTITY_ID_PREFIX_TO_TYPE_MAP = {
|
|
|
66
73
|
// Virtual Machines
|
|
67
74
|
AZURE_VM: 'dt.entity.azure_vm', // Verified
|
|
68
75
|
OPENSTACK_VM: 'dt.entity.openstack_vm', // Needs manual verification - available only if OpenStack integration is configured
|
|
69
|
-
// Kubernetes Entities
|
|
70
|
-
KUBERNETES_NODE: 'dt.entity.kubernetes_node', // Verified!
|
|
71
|
-
KUBERNETES_CLUSTER: 'dt.entity.kubernetes_cluster', // Verified!
|
|
72
|
-
KUBERNETES_SERVICE: 'dt.entity.kubernetes_service', // Verified!
|
|
73
76
|
};
|
|
74
|
-
exports.
|
|
77
|
+
exports.DYNATRACE_ENTITY_TYPES_BASICS = Object.values(ENTITY_ID_PREFIX_TO_TYPE_MAP_BASICS).sort();
|
|
78
|
+
exports.DYNATRACE_ENTITY_TYPES_ALL = Object.values(ENTITY_ID_PREFIX_TO_TYPE_MAP_ALL).sort();
|
|
75
79
|
/**
|
|
76
80
|
* Maps a Dynatrace entity ID to its corresponding entity type.
|
|
77
81
|
*
|
|
@@ -97,5 +101,5 @@ function getEntityTypeFromId(entityId) {
|
|
|
97
101
|
}
|
|
98
102
|
const prefix = entityId.substring(0, hyphenIndex);
|
|
99
103
|
// Look up the entity type in our mapping
|
|
100
|
-
return
|
|
104
|
+
return ENTITY_ID_PREFIX_TO_TYPE_MAP_ALL[prefix] || null;
|
|
101
105
|
}
|
|
@@ -3,12 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const dynatrace_entity_types_1 = require("./dynatrace-entity-types");
|
|
4
4
|
describe('DYNATRACE_ENTITY_TYPES', () => {
|
|
5
5
|
it('should be sorted alphabetically', () => {
|
|
6
|
-
const sortedTypes = [...dynatrace_entity_types_1.
|
|
7
|
-
expect(dynatrace_entity_types_1.
|
|
6
|
+
const sortedTypes = [...dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL].sort();
|
|
7
|
+
expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL).toEqual(sortedTypes);
|
|
8
8
|
});
|
|
9
9
|
it('should have unique values', () => {
|
|
10
|
-
const uniqueTypes = [...new Set(dynatrace_entity_types_1.
|
|
11
|
-
expect(dynatrace_entity_types_1.
|
|
10
|
+
const uniqueTypes = [...new Set(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL)];
|
|
11
|
+
expect(dynatrace_entity_types_1.DYNATRACE_ENTITY_TYPES_ALL.length).toBe(uniqueTypes.length);
|
|
12
12
|
});
|
|
13
13
|
});
|
|
14
14
|
describe('getEntityTypeFromId', () => {
|
package/package.json
CHANGED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.getMonitoredEntityDetails = void 0;
|
|
4
|
-
const execute_dql_1 = require("./execute-dql");
|
|
5
|
-
const dynatrace_entity_types_1 = require("../utils/dynatrace-entity-types");
|
|
6
|
-
/**
|
|
7
|
-
* Get monitored entity details by entity ID via DQL
|
|
8
|
-
* @param dtClient
|
|
9
|
-
* @param entityId
|
|
10
|
-
* @returns Details about the monitored entity, or undefined in case we couldn't find it
|
|
11
|
-
*/
|
|
12
|
-
const getMonitoredEntityDetails = async (dtClient, entityId) => {
|
|
13
|
-
// Try to determine the entity type directly from the entity ID (e.g., PROCESS_GROUP-F84E4759809ADA84 -> dt.entity.process_group)
|
|
14
|
-
const entityType = (0, dynatrace_entity_types_1.getEntityTypeFromId)(entityId);
|
|
15
|
-
if (!entityType) {
|
|
16
|
-
console.error(`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.`);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
// construct DQL statement like `fetch dt.entity.hosts | filter id == "HOST-1234"`
|
|
20
|
-
const dql = `fetch ${entityType} | filter id == "${entityId}" | expand tags | fieldsAdd entity.type`;
|
|
21
|
-
// Get response from API
|
|
22
|
-
const dqlResponse = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
|
|
23
|
-
// verify response and length
|
|
24
|
-
if (!dqlResponse || !dqlResponse.records || dqlResponse.records.length === 0) {
|
|
25
|
-
console.error(`No entity found for ID: ${entityId}`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
// in case we have more than one entity -> log it
|
|
29
|
-
if (dqlResponse.records.length > 1) {
|
|
30
|
-
console.error(`Multiple entities (${dqlResponse.records.length}) found for entity ID: ${entityId}. Returning the first one.`);
|
|
31
|
-
}
|
|
32
|
-
const entity = dqlResponse.records[0];
|
|
33
|
-
// make typescript happy; entity should never be null though
|
|
34
|
-
if (!entity) {
|
|
35
|
-
console.error(`No entity found for ID: ${entityId}`);
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
// return entity details
|
|
39
|
-
return {
|
|
40
|
-
entityId: String(entity.id),
|
|
41
|
-
entityTypeTable: entityType,
|
|
42
|
-
displayName: String(entity['entity.name']),
|
|
43
|
-
type: String(entity['entity.type']),
|
|
44
|
-
allProperties: entity || undefined,
|
|
45
|
-
};
|
|
46
|
-
};
|
|
47
|
-
exports.getMonitoredEntityDetails = getMonitoredEntityDetails;
|