@dynatrace-oss/dynatrace-mcp-server 0.9.2 → 0.11.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
CHANGED
|
@@ -298,6 +298,24 @@ In addition, depending on the features you use, the following variables can be c
|
|
|
298
298
|
|
|
299
299
|
- `SLACK_CONNECTION_ID` (string) - connection ID of a [Slack Connection](https://docs.dynatrace.com/docs/analyze-explore-automate/workflows/actions/slack)
|
|
300
300
|
|
|
301
|
+
### Proxy Configuration
|
|
302
|
+
|
|
303
|
+
The MCP server honors system proxy settings for corporate environments:
|
|
304
|
+
|
|
305
|
+
- `https_proxy` or `HTTPS_PROXY` (optional, string, e.g., `http://proxy.example.com:8080`) - Proxy server URL for HTTPS requests
|
|
306
|
+
- `http_proxy` or `HTTP_PROXY` (optional, string, e.g., `http://proxy.example.com:8080`) - Proxy server URL for HTTP requests
|
|
307
|
+
- `no_proxy` or `NO_PROXY` (optional, string, e.g., `localhost,127.0.0.1,.local`) - Comma-separated list of hostnames or domains that should bypass the proxy
|
|
308
|
+
|
|
309
|
+
**Note:** The `no_proxy` environment variable is currently logged for informational purposes but not fully enforced by the underlying HTTP client. If you need to bypass the proxy for specific hosts, consider configuring your proxy server to handle these exclusions.
|
|
310
|
+
|
|
311
|
+
Example configuration with proxy:
|
|
312
|
+
|
|
313
|
+
```bash
|
|
314
|
+
export HTTPS_PROXY=http://proxy.company.com:8080
|
|
315
|
+
export NO_PROXY=localhost,127.0.0.1,.company.local
|
|
316
|
+
export DT_ENVIRONMENT=https://abc12345.apps.dynatrace.com
|
|
317
|
+
```
|
|
318
|
+
|
|
301
319
|
### Scopes for Authentication
|
|
302
320
|
|
|
303
321
|
Depending on the features you are using, the following scopes are needed:
|
|
@@ -321,6 +339,7 @@ Depending on the features you are using, the following scopes are needed:
|
|
|
321
339
|
- `storage:system:read` - needed for `execute_dql` tool to read System Data from Grail
|
|
322
340
|
- `storage:user.events:read` - needed for `execute_dql` tool to read User events from Grail
|
|
323
341
|
- `storage:user.sessions:read` - needed for `execute_dql` tool to read User sessions from Grail
|
|
342
|
+
- `storage:smartscape:read` - needed for `execute_dql` tool to read Smartscape Data
|
|
324
343
|
- `davis-copilot:conversations:execute` - execute conversational skill (chat with Copilot)
|
|
325
344
|
- `davis-copilot:nl2dql:execute` - execute Davis Copilot Natural Language (NL) to DQL skill
|
|
326
345
|
- `davis-copilot:dql2nl:execute` - execute DQL to Natural Language (NL) skill
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.findMonitoredEntitiesByName = exports.generateDqlSearchEntityCommand = void 0;
|
|
3
|
+
exports.findMonitoredEntitiesByName = exports.findMonitoredEntityViaSmartscapeByName = 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
|
/**
|
|
@@ -22,10 +22,35 @@ const generateDqlSearchEntityCommand = (entityNames, extendedSearch) => {
|
|
|
22
22
|
};
|
|
23
23
|
exports.generateDqlSearchEntityCommand = generateDqlSearchEntityCommand;
|
|
24
24
|
/**
|
|
25
|
-
* Find a monitored entity by name via DQL
|
|
25
|
+
* Find a monitored entity via "smartscapeNodes" by name via DQL
|
|
26
26
|
* @param dtClient
|
|
27
|
-
* @param
|
|
28
|
-
* @returns
|
|
27
|
+
* @param entityNames Array of entitiy names to search for
|
|
28
|
+
* @returns An array with the entity details like id, name and type
|
|
29
|
+
*/
|
|
30
|
+
const findMonitoredEntityViaSmartscapeByName = async (dtClient, entityNames) => {
|
|
31
|
+
const dql = `smartscapeNodes "*" | search "*${entityNames.join('*" OR "*')}*" | fields id, name, type`;
|
|
32
|
+
console.error(`Executing DQL: ${dql}`);
|
|
33
|
+
try {
|
|
34
|
+
const smartscapeResult = await (0, execute_dql_1.executeDql)(dtClient, { query: dql });
|
|
35
|
+
if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) {
|
|
36
|
+
// return smartscape results if we found something
|
|
37
|
+
return smartscapeResult;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
// ignore errors here, as smartscapeNodes may not be ready for all environments/users
|
|
42
|
+
console.error('Error while querying smartscapeNodes:', error);
|
|
43
|
+
}
|
|
44
|
+
console.error('No results from smartscapeNodes');
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
exports.findMonitoredEntityViaSmartscapeByName = findMonitoredEntityViaSmartscapeByName;
|
|
48
|
+
/**
|
|
49
|
+
* Find a monitored entity via "dt.entity.${entityType}" by name via DQL
|
|
50
|
+
* @param dtClient
|
|
51
|
+
* @param entityNames Array of entitiy names to search for
|
|
52
|
+
* @param extendedSearch If true, search over all entity types, otherwise only basic ones
|
|
53
|
+
* @returns An array with the entity details like id, name and type
|
|
29
54
|
*/
|
|
30
55
|
const findMonitoredEntitiesByName = async (dtClient, entityNames, extendedSearch) => {
|
|
31
56
|
// construct a DQL statement for searching the entityName over all entity types
|
package/dist/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const telemetry_openkit_1 = require("./utils/telemetry-openkit");
|
|
|
28
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
|
const dynatrace_connection_utils_1 = require("./utils/dynatrace-connection-utils");
|
|
31
|
+
const proxy_config_1 = require("./utils/proxy-config");
|
|
31
32
|
// Load environment variables from .env file if available, and suppress warnings/logging to stdio
|
|
32
33
|
// as it breaks MCP communication when using stdio transport
|
|
33
34
|
const dotEnvOutput = (0, dotenv_1.config)({ quiet: true });
|
|
@@ -79,6 +80,8 @@ const allRequiredScopes = scopesBase.concat([
|
|
|
79
80
|
]);
|
|
80
81
|
const main = async () => {
|
|
81
82
|
console.error(`Initializing Dynatrace MCP Server v${(0, version_1.getPackageJsonVersion)()}...`);
|
|
83
|
+
// Configure proxy from environment variables early in the startup process
|
|
84
|
+
(0, proxy_config_1.configureProxyFromEnvironment)();
|
|
82
85
|
// read Environment variables
|
|
83
86
|
let dynatraceEnv;
|
|
84
87
|
try {
|
|
@@ -91,7 +94,6 @@ const main = async () => {
|
|
|
91
94
|
// Unpack environment variables
|
|
92
95
|
let { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId, grailBudgetGB } = dynatraceEnv;
|
|
93
96
|
// Infer OAuth auth code flow if no OAuth Client credentials are provided
|
|
94
|
-
// -> configure default OAuth client ID for auth code flow
|
|
95
97
|
if (!oauthClientId && !oauthClientSecret && !dtPlatformToken) {
|
|
96
98
|
console.error('No OAuth credentials or platform token provided - switching to OAuth authorization code flow.');
|
|
97
99
|
oauthClientId = DT_MCP_AUTH_CODE_FLOW_OAUTH_CLIENT_ID; // Default OAuth client ID for auth code flow
|
|
@@ -364,22 +366,40 @@ const main = async () => {
|
|
|
364
366
|
}, {
|
|
365
367
|
readOnlyHint: true,
|
|
366
368
|
}, async ({ entityNames, maxEntitiesToDisplay, extendedSearch }) => {
|
|
367
|
-
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:entities:read'));
|
|
369
|
+
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:entities:read', 'storage:smartscape:read'));
|
|
370
|
+
const smartscapeResult = await (0, find_monitored_entity_by_name_1.findMonitoredEntityViaSmartscapeByName)(dtClient, entityNames);
|
|
371
|
+
if (smartscapeResult && smartscapeResult.records && smartscapeResult.records.length > 0) {
|
|
372
|
+
// Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities
|
|
373
|
+
const validSmartscapeEntities = smartscapeResult.records.filter((entity) => !!(entity && entity.id && entity.type && entity.name));
|
|
374
|
+
let resp = `Found ${validSmartscapeEntities.length} monitored entities via Smartscape! Displaying the first ${Math.min(maxEntitiesToDisplay, validSmartscapeEntities.length)} valid entities:\n`;
|
|
375
|
+
validSmartscapeEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
|
|
376
|
+
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`;
|
|
377
|
+
});
|
|
378
|
+
resp +=
|
|
379
|
+
'\n\n**Next Steps:**\n' +
|
|
380
|
+
'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' +
|
|
381
|
+
'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' +
|
|
382
|
+
'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' +
|
|
383
|
+
'4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n' +
|
|
384
|
+
'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';
|
|
385
|
+
return resp;
|
|
386
|
+
}
|
|
387
|
+
// If no result from Smartscape, try the classic entities API
|
|
368
388
|
const result = await (0, find_monitored_entity_by_name_1.findMonitoredEntitiesByName)(dtClient, entityNames, extendedSearch);
|
|
369
389
|
if (result && result.records && result.records.length > 0) {
|
|
370
|
-
|
|
390
|
+
// Filter valid entities first, to ensure we display up to maxEntitiesToDisplay entities
|
|
391
|
+
const validClassicEntities = result.records.filter((entity) => !!(entity && entity.id && entity['entity.type'] && entity['entity.name']));
|
|
392
|
+
let resp = `Found ${validClassicEntities.length} monitored entities! Displaying the first ${Math.min(maxEntitiesToDisplay, validClassicEntities.length)} entities:\n`;
|
|
371
393
|
// iterate over dqlResponse and create a string with the problem details, but only show the top maxEntitiesToDisplay problems
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
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`;
|
|
376
|
-
}
|
|
394
|
+
validClassicEntities.slice(0, maxEntitiesToDisplay).forEach((entity) => {
|
|
395
|
+
const entityType = (0, dynatrace_entity_types_1.getEntityTypeFromId)(String(entity.id));
|
|
396
|
+
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`;
|
|
377
397
|
});
|
|
378
398
|
resp +=
|
|
379
399
|
'\n\n**Next Steps:**\n' +
|
|
380
|
-
'1.
|
|
400
|
+
'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' +
|
|
381
401
|
'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' +
|
|
382
|
-
'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' +
|
|
402
|
+
'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' +
|
|
383
403
|
'4. Find out whether any problems exist for this entity using the `list_problems` or `list_vulnerabilities` tool, and the provided DQL-Filter\n';
|
|
384
404
|
return resp;
|
|
385
405
|
}
|
|
@@ -428,20 +448,26 @@ const main = async () => {
|
|
|
428
448
|
}
|
|
429
449
|
return resp;
|
|
430
450
|
});
|
|
431
|
-
tool('execute_dql', 'Get Logs, Metrics, Spans or
|
|
451
|
+
tool('execute_dql', 'Get data like Logs, Metrics, Spans, Events, or Entity Data from Dynatrace GRAIL by executing a Dynatrace Query Language (DQL) statement. ' +
|
|
432
452
|
'Use the "generate_dql_from_natural_language" tool upfront to generate or refine a DQL statement based on your request. ' +
|
|
433
453
|
'To learn about possible fields available for filtering, use the query "fetch dt.semantic_dictionary.models | filter data_object == \"logs\""', {
|
|
434
454
|
dqlStatement: zod_1.z
|
|
435
455
|
.string()
|
|
436
|
-
.describe('DQL Statement (Ex: "fetch [logs, spans, events], from: now()-4h, to: now() | filter <some-filter> | summarize count(), by:{some-fields}
|
|
456
|
+
.describe('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>"]"). ' +
|
|
437
457
|
'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. '),
|
|
458
|
+
recordLimit: zod_1.z.number().optional().default(100).describe('Maximum number of records to return (default: 100)'),
|
|
459
|
+
recordSizeLimitMB: zod_1.z
|
|
460
|
+
.number()
|
|
461
|
+
.optional()
|
|
462
|
+
.default(1)
|
|
463
|
+
.describe('Maximum size of the returned records in MB (default: 1MB)'),
|
|
438
464
|
}, {
|
|
439
465
|
// not readonly (DQL statements may modify things), not idempotent (may change over time)
|
|
440
466
|
readOnlyHint: false,
|
|
441
467
|
idempotentHint: false,
|
|
442
468
|
// while we are not strictly talking to the open world here, the response from execute DQL could interpreted as a web-search, which often is referred to open-world
|
|
443
469
|
openWorldHint: true,
|
|
444
|
-
}, async ({ dqlStatement }) => {
|
|
470
|
+
}, async ({ dqlStatement, recordLimit = 100, recordSizeLimitMB = 1 }) => {
|
|
445
471
|
// Create a HTTP Client that has all storage:*:read scopes
|
|
446
472
|
const dtClient = await createAuthenticatedHttpClient(scopesBase.concat('storage:buckets:read', // Read all system data stored on Grail
|
|
447
473
|
'storage:logs:read', // Read logs for reliability guardian validations
|
|
@@ -453,8 +479,9 @@ const main = async () => {
|
|
|
453
479
|
'storage:system:read', // Read System Data from Grail
|
|
454
480
|
'storage:user.events:read', // Read User events from Grail
|
|
455
481
|
'storage:user.sessions:read', // Read User sessions from Grail
|
|
456
|
-
'storage:security.events:read'
|
|
457
|
-
|
|
482
|
+
'storage:security.events:read', // Read Security events from Grail
|
|
483
|
+
'storage:smartscape:read'));
|
|
484
|
+
const response = await (0, execute_dql_1.executeDql)(dtClient, { query: dqlStatement, maxResultRecords: recordLimit, maxResultBytes: recordSizeLimitMB * 1024 * 1024 }, grailBudgetGB);
|
|
458
485
|
if (!response) {
|
|
459
486
|
return 'DQL execution failed or returned no result.';
|
|
460
487
|
}
|
|
@@ -472,8 +499,15 @@ const main = async () => {
|
|
|
472
499
|
result += `- **Scanned Bytes:** ${scannedGB.toFixed(2)} GB`;
|
|
473
500
|
// Show budget status if available
|
|
474
501
|
if (response.budgetState) {
|
|
475
|
-
const
|
|
476
|
-
|
|
502
|
+
const totalScannedGB = (response.budgetState.totalBytesScanned / (1000 * 1000 * 1000)).toFixed(2);
|
|
503
|
+
if (response.budgetState.budgetLimitGB > 0) {
|
|
504
|
+
const usagePercentage = ((response.budgetState.totalBytesScanned / response.budgetState.budgetLimitBytes) *
|
|
505
|
+
100).toFixed(1);
|
|
506
|
+
result += ` (Session total: ${totalScannedGB} GB / ${response.budgetState.budgetLimitGB} GB budget, ${usagePercentage}% used)`;
|
|
507
|
+
}
|
|
508
|
+
else {
|
|
509
|
+
result += ` (Session total: ${totalScannedGB} GB)`;
|
|
510
|
+
}
|
|
477
511
|
}
|
|
478
512
|
result += '\n';
|
|
479
513
|
if (scannedGB > 500) {
|
|
@@ -492,6 +526,9 @@ const main = async () => {
|
|
|
492
526
|
if (response.sampled !== undefined && response.sampled) {
|
|
493
527
|
result += `- **⚠️ Sampling Used:** Yes (results may be approximate)\n`;
|
|
494
528
|
}
|
|
529
|
+
if (response.records.length === recordLimit) {
|
|
530
|
+
result += `- **⚠️ Record Limit Reached:** The result set was limited to ${recordLimit} records. Consider changing your query with a smaller timeframe, an aggregation or a more concise filter. Alternatively, increase the recordLimit if you expect more results.\n`;
|
|
531
|
+
}
|
|
495
532
|
result += `\n📋 **Query Results**: (${response.records?.length || 0} records):\n\n`;
|
|
496
533
|
result += `\`\`\`json\n${JSON.stringify(response.records, null, 2)}\n\`\`\``;
|
|
497
534
|
return result;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.configureProxyFromEnvironment = configureProxyFromEnvironment;
|
|
4
|
+
const undici_1 = require("undici");
|
|
5
|
+
/**
|
|
6
|
+
* Parse and configure system proxy settings from environment variables.
|
|
7
|
+
* Supports https_proxy, HTTPS_PROXY, http_proxy, HTTP_PROXY, no_proxy, and NO_PROXY.
|
|
8
|
+
*
|
|
9
|
+
* This function should be called early in the application lifecycle to ensure
|
|
10
|
+
* all HTTP requests honor the system proxy settings.
|
|
11
|
+
*/
|
|
12
|
+
function configureProxyFromEnvironment() {
|
|
13
|
+
// Check for proxy environment variables (case-insensitive)
|
|
14
|
+
const httpsProxy = process.env.https_proxy || process.env.HTTPS_PROXY;
|
|
15
|
+
const httpProxy = process.env.http_proxy || process.env.HTTP_PROXY;
|
|
16
|
+
const noProxy = process.env.no_proxy || process.env.NO_PROXY;
|
|
17
|
+
// Determine which proxy to use (prefer HTTPS proxy for HTTPS requests)
|
|
18
|
+
const proxyUrl = httpsProxy || httpProxy;
|
|
19
|
+
if (!proxyUrl) {
|
|
20
|
+
// No proxy configured, nothing to do
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
console.error(`Configuring proxy from environment: ${proxyUrl}`);
|
|
25
|
+
// Parse no_proxy list if provided
|
|
26
|
+
let noProxyHosts = [];
|
|
27
|
+
if (noProxy) {
|
|
28
|
+
// Split by comma and trim whitespace
|
|
29
|
+
noProxyHosts = noProxy
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((host) => host.trim())
|
|
32
|
+
.filter((host) => host.length > 0);
|
|
33
|
+
console.error(`No proxy hosts configured: ${noProxyHosts.join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
// Create ProxyAgent with the configured proxy URL
|
|
36
|
+
// Note: undici's ProxyAgent doesn't have built-in no_proxy support.
|
|
37
|
+
// The no_proxy environment variable is logged for informational purposes,
|
|
38
|
+
// but the ProxyAgent will route all requests through the proxy.
|
|
39
|
+
// If no_proxy support is critical for your use case, you may need to
|
|
40
|
+
// configure your proxy server to handle no_proxy exclusions.
|
|
41
|
+
const proxyAgent = new undici_1.ProxyAgent({
|
|
42
|
+
uri: proxyUrl,
|
|
43
|
+
});
|
|
44
|
+
// Set the global dispatcher for undici (affects global fetch)
|
|
45
|
+
(0, undici_1.setGlobalDispatcher)(proxyAgent);
|
|
46
|
+
console.error(`✅ Proxy configured successfully: ${proxyUrl}`);
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
console.error(`⚠️ Failed to configure proxy: ${error instanceof Error ? error.message : String(error)}`);
|
|
50
|
+
console.error('Continuing without proxy configuration.');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const proxy_config_1 = require("./proxy-config");
|
|
4
|
+
// Mock undici
|
|
5
|
+
jest.mock('undici', () => ({
|
|
6
|
+
ProxyAgent: jest.fn(),
|
|
7
|
+
setGlobalDispatcher: jest.fn(),
|
|
8
|
+
getGlobalDispatcher: jest.fn(),
|
|
9
|
+
}));
|
|
10
|
+
const undici_1 = require("undici");
|
|
11
|
+
describe('proxy-config', () => {
|
|
12
|
+
let originalEnv;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
// Save original environment
|
|
15
|
+
originalEnv = { ...process.env };
|
|
16
|
+
// Clear all proxy-related env vars
|
|
17
|
+
delete process.env.https_proxy;
|
|
18
|
+
delete process.env.HTTPS_PROXY;
|
|
19
|
+
delete process.env.http_proxy;
|
|
20
|
+
delete process.env.HTTP_PROXY;
|
|
21
|
+
delete process.env.no_proxy;
|
|
22
|
+
delete process.env.NO_PROXY;
|
|
23
|
+
// Clear mocks
|
|
24
|
+
jest.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
// Restore original environment
|
|
28
|
+
process.env = originalEnv;
|
|
29
|
+
});
|
|
30
|
+
describe('configureProxyFromEnvironment', () => {
|
|
31
|
+
it('should configure proxy when HTTPS_PROXY is set', () => {
|
|
32
|
+
process.env.HTTPS_PROXY = 'http://proxy.example.com:8080';
|
|
33
|
+
(0, proxy_config_1.configureProxyFromEnvironment)();
|
|
34
|
+
expect(undici_1.ProxyAgent).toHaveBeenCalledWith({
|
|
35
|
+
uri: 'http://proxy.example.com:8080',
|
|
36
|
+
});
|
|
37
|
+
expect(undici_1.setGlobalDispatcher).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
it('should configure proxy when https_proxy is set (lowercase)', () => {
|
|
40
|
+
process.env.https_proxy = 'http://proxy.example.com:8080';
|
|
41
|
+
(0, proxy_config_1.configureProxyFromEnvironment)();
|
|
42
|
+
expect(undici_1.ProxyAgent).toHaveBeenCalledWith({
|
|
43
|
+
uri: 'http://proxy.example.com:8080',
|
|
44
|
+
});
|
|
45
|
+
expect(undici_1.setGlobalDispatcher).toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
it('should prefer HTTPS_PROXY over HTTP_PROXY', () => {
|
|
48
|
+
process.env.HTTPS_PROXY = 'http://https-proxy.example.com:8443';
|
|
49
|
+
process.env.HTTP_PROXY = 'http://http-proxy.example.com:8080';
|
|
50
|
+
(0, proxy_config_1.configureProxyFromEnvironment)();
|
|
51
|
+
expect(undici_1.ProxyAgent).toHaveBeenCalledWith({
|
|
52
|
+
uri: 'http://https-proxy.example.com:8443',
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
it('should fall back to HTTP_PROXY if HTTPS_PROXY is not set', () => {
|
|
56
|
+
process.env.HTTP_PROXY = 'http://proxy.example.com:8080';
|
|
57
|
+
(0, proxy_config_1.configureProxyFromEnvironment)();
|
|
58
|
+
expect(undici_1.ProxyAgent).toHaveBeenCalledWith({
|
|
59
|
+
uri: 'http://proxy.example.com:8080',
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
it('should not configure proxy when no proxy env vars are set', () => {
|
|
63
|
+
(0, proxy_config_1.configureProxyFromEnvironment)();
|
|
64
|
+
expect(undici_1.ProxyAgent).not.toHaveBeenCalled();
|
|
65
|
+
expect(undici_1.setGlobalDispatcher).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
it('should handle errors gracefully', () => {
|
|
68
|
+
process.env.HTTPS_PROXY = 'http://proxy.example.com:8080';
|
|
69
|
+
const mockProxyAgent = undici_1.ProxyAgent;
|
|
70
|
+
mockProxyAgent.mockImplementation(() => {
|
|
71
|
+
throw new Error('ProxyAgent error');
|
|
72
|
+
});
|
|
73
|
+
// Should not throw
|
|
74
|
+
expect(() => (0, proxy_config_1.configureProxyFromEnvironment)()).not.toThrow();
|
|
75
|
+
// Should still attempt to create ProxyAgent
|
|
76
|
+
expect(undici_1.ProxyAgent).toHaveBeenCalled();
|
|
77
|
+
// Should not set dispatcher if ProxyAgent creation fails
|
|
78
|
+
expect(undici_1.setGlobalDispatcher).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -215,8 +215,8 @@ function createTelemetry() {
|
|
|
215
215
|
return new DynatraceMcpTelemetry();
|
|
216
216
|
}
|
|
217
217
|
catch (e) {
|
|
218
|
-
// Failed to initialize
|
|
219
|
-
console.error(e);
|
|
218
|
+
// Failed to initialize (unexpected). Log concise message without stack trace spam.
|
|
219
|
+
console.error('Dynatrace Telemetry initialization failed:', e.message);
|
|
220
220
|
// fallback to NoOp Telemetry
|
|
221
221
|
return new NoOpTelemetry();
|
|
222
222
|
}
|