Skip to content

Commit 27bd9a8

Browse files
patch: Added Telemetry via Dynatrace OpenKit (#129)
* patch: Added Telemetry via Dynatrace OpenKit * Apply suggestions from code review Co-authored-by: Manuel Warum <[email protected]> * patch: Switch to the proper Dt telemetry endpoint Signed-off-by: Christian Kreuzberger <[email protected]> * chore: Refactor package.json version retrieval function into a dedicated file * chore: Refactor sigterm/sigint handler * chore: Rename telemetry track functions * chore: Added code docs for return statements --------- Signed-off-by: Christian Kreuzberger <[email protected]> Co-authored-by: Manuel Warum <[email protected]>
1 parent cf403ff commit 27bd9a8

File tree

9 files changed

+328
-21
lines changed

9 files changed

+328
-21
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
- Added metadata output which includes scanned bytes (for cost tracking) to `execute_dql`
66
- Added next-steps for `get_entity_details` to find out about metrics, problems and logs
7+
- Added Telemetry via Dynatrace OpenKit to improve the product with anonymous usage statistics and error information (can be disabled via `DT_MCP_DISABLE_TELEMETRY` environment variable)
78

89
## 0.5.0
910

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,34 @@ curl -X GET https://abc12345.apps.dynatrace.com/platform/management/v1/environme
553553

554554
Grail has a dedicated section about permissions in the Dynatrace Docs. Please refer to https://docs.dynatrace.com/docs/discover-dynatrace/platform/grail/data-model/assign-permissions-in-grail for more details.
555555

556+
## Telemetry
557+
558+
The Dynatrace MCP Server includes sending Telemetry Data via Dynatrace OpenKit to help improve the product. This includes:
559+
560+
- Server start events
561+
- Tool usage (which tools are called, success/failure, execution duration)
562+
- Error tracking for debugging and improvement
563+
564+
**Privacy and Opt-out:**
565+
566+
- Telemetry is **enabled by default** but can be disabled by setting `DT_MCP_DISABLE_TELEMETRY=true`
567+
- No sensitive data from your Dynatrace environment is tracked
568+
- Only anonymous usage statistics and error information are collected
569+
- Usage statistics and error data are transmitted to Dynatrace’s analytics endpoint
570+
571+
**Configuration options:**
572+
573+
- `DT_MCP_DISABLE_TELEMETRY` (boolean, default: `false`) - Disable Telemetry
574+
- `DT_MCP_TELEMETRY_APPLICATION_ID` (string, default: `dynatrace-mcp-server`) - Application ID for tracking
575+
- `DT_MCP_TELEMETRY_ENDPOINT_URL` (string, default: Dynatrace endpoint) - OpenKit endpoint URL
576+
- `DT_MCP_TELEMETRY_DEVICE_ID` (string, default: auto-generated) - Device identifier for tracking
577+
578+
To disable usage tracking, add this to your environment:
579+
580+
```bash
581+
DT_MCP_DISABLE_TELEMETRY=true
582+
```
583+
556584
## Development
557585

558586
For local development purposes, you can use VSCode and GitHub Copilot.

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"@dynatrace-sdk/client-platform-management-service": "^1.6.3",
5151
"@dynatrace-sdk/client-query": "^1.18.1",
5252
"@dynatrace-sdk/shared-errors": "^1.0.0",
53+
"@dynatrace/openkit-js": "^4.1.0",
5354
"@modelcontextprotocol/sdk": "^1.8.0",
5455
"commander": "^14.0.0",
5556
"dotenv": "^17.2.1",

src/authentication/dynatrace-clients.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { OAuthTokenResponse } from './types';
99
* @param clientSecret - Oauth Client Secret for Dynatrace
1010
* @param ssoAuthUrl - SSO Authentication URL
1111
* @param scopes - List of requested scopes
12-
* @returns
12+
* @returns Response of the OAuth Endpoint (which, in the best case includes a token)
1313
*/
1414
const requestToken = async (
1515
clientId: string,
@@ -45,7 +45,7 @@ const requestToken = async (
4545
* @param clientId
4646
* @param clientSecret
4747
* @param dtPlatformToken
48-
* @returns
48+
* @returns an authenticated HttpClient
4949
*/
5050
export const createDtHttpClient = async (
5151
environmentUrl: string,

src/index.ts

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { randomUUID } from 'node:crypto';
2323
import { Command } from 'commander';
2424
import { z, ZodRawShape, ZodTypeAny } from 'zod';
2525

26-
import { version as VERSION } from '../package.json';
26+
import { getPackageJsonVersion } from './utils/version';
2727
import { createDtHttpClient } from './authentication/dynatrace-clients';
2828
import { listVulnerabilities } from './capabilities/list-vulnerabilities';
2929
import { listProblems } from './capabilities/list-problems';
@@ -41,7 +41,9 @@ import {
4141
generateDqlFromNaturalLanguage,
4242
} from './capabilities/davis-copilot';
4343
import { DynatraceEnv, getDynatraceEnv } from './getDynatraceEnv';
44+
import { createTelemetry, Telemetry } from './utils/telemetry-openkit';
4445
import { getEntityTypeFromId } from './utils/dynatrace-entity-types';
46+
import { Http2ServerRequest } from 'node:http2';
4547

4648
config();
4749

@@ -90,7 +92,7 @@ const main = async () => {
9092
console.error((err as Error).message);
9193
process.exit(1);
9294
}
93-
console.error(`Initializing Dynatrace MCP Server v${VERSION}...`);
95+
console.error(`Initializing Dynatrace MCP Server v${getPackageJsonVersion()}...`);
9496
const { oauthClientId, oauthClientSecret, dtEnvironment, dtPlatformToken, slackConnectionId } = dynatraceEnv;
9597

9698
// Test connection on startup
@@ -122,11 +124,28 @@ const main = async () => {
122124
}
123125
}
124126

125-
console.error(`Starting Dynatrace MCP Server v${VERSION}...`);
127+
console.error(`Starting Dynatrace MCP Server v${getPackageJsonVersion()}...`);
128+
129+
// Initialize usage tracking
130+
const telemetry = createTelemetry();
131+
await telemetry.trackMcpServerStart();
132+
133+
// Create a shutdown handler that takes shutdown operations as parameters
134+
const shutdownHandler = (...shutdownOps: Array<() => void | Promise<void>>) => {
135+
return async () => {
136+
console.error('Shutting down MCP server...');
137+
for (const op of shutdownOps) {
138+
await op();
139+
}
140+
process.exit(0);
141+
};
142+
};
143+
144+
// Initialize Metadata for MCP Server
126145
const server = new McpServer(
127146
{
128147
name: 'Dynatrace MCP Server',
129-
version: VERSION,
148+
version: getPackageJsonVersion(),
130149
},
131150
{
132151
capabilities: {
@@ -143,12 +162,22 @@ const main = async () => {
143162
cb: (args: z.objectOutputType<ZodRawShape, ZodTypeAny>) => Promise<string>,
144163
) => {
145164
const wrappedCb = async (args: ZodRawShape): Promise<CallToolResult> => {
165+
// track starttime for telemetry
166+
const startTime = Date.now();
167+
// track toolcall for telemetry
168+
let toolCallSuccessful = false;
169+
146170
try {
171+
// call the tool
147172
const response = await cb(args);
173+
toolCallSuccessful = true;
148174
return {
149175
content: [{ type: 'text', text: response }],
150176
};
151177
} catch (error: any) {
178+
// Track error
179+
telemetry.trackError(error, `tool_${name}`).catch((e) => console.warn('Failed to track error:', e));
180+
152181
// check if it's an error originating from the Dynatrace SDK / API Gateway and provide an appropriate message to the user
153182
if (isClientRequestError(error)) {
154183
return {
@@ -162,12 +191,20 @@ const main = async () => {
162191
content: [{ type: 'text', text: `Error: ${error.message}` }],
163192
isError: true,
164193
};
194+
} finally {
195+
// Track tool usage
196+
const duration = Date.now() - startTime;
197+
telemetry
198+
.trackMcpToolUsage(name, toolCallSuccessful, duration)
199+
.catch((e) => console.warn('Failed to track tool usage:', e));
165200
}
166201
};
167202

168203
server.tool(name, description, paramsSchema, (args) => wrappedCb(args));
169204
};
170205

206+
/** Tool Definitions below */
207+
171208
tool(
172209
'get_environment_info',
173210
'Get information about the connected Dynatrace Environment (Tenant) and verify the connection and authentication.',
@@ -778,7 +815,7 @@ const main = async () => {
778815
program
779816
.name('dynatrace-mcp-server')
780817
.description('Dynatrace Model Context Protocol (MCP) Server')
781-
.version(VERSION)
818+
.version(getPackageJsonVersion())
782819
.option('--http', 'enable HTTP server mode instead of stdio')
783820
.option('--server', 'enable HTTP server mode (alias for --http)')
784821
.option('-p, --port <number>', 'port for HTTP server', '3000')
@@ -825,13 +862,14 @@ const main = async () => {
825862
console.error(`Dynatrace MCP Server running on HTTP at http://${host}:${httpPort}`);
826863
});
827864

828-
// Handle graceful shutdown
829-
process.on('SIGINT', () => {
830-
console.error('Shutting down HTTP server...');
831-
httpServer.close(() => {
832-
process.exit(0);
833-
});
834-
});
865+
// Handle graceful shutdown for http server mode
866+
process.on(
867+
'SIGINT',
868+
shutdownHandler(
869+
async () => await telemetry.shutdown(),
870+
() => new Promise<void>((resolve) => httpServer.close(() => resolve())),
871+
),
872+
);
835873
} else {
836874
// Default stdio mode
837875
const transport = new StdioServerTransport();
@@ -840,10 +878,28 @@ const main = async () => {
840878
await server.connect(transport);
841879

842880
console.error('Dynatrace MCP Server running on stdio');
881+
882+
// Handle graceful shutdown for stdio mode
883+
process.on(
884+
'SIGINT',
885+
shutdownHandler(async () => await telemetry.shutdown()),
886+
);
887+
process.on(
888+
'SIGTERM',
889+
shutdownHandler(async () => await telemetry.shutdown()),
890+
);
843891
}
844892
};
845893

846-
main().catch((error) => {
894+
main().catch(async (error) => {
847895
console.error('Fatal error in main():', error);
896+
try {
897+
// report error in main
898+
const telemetry = createTelemetry();
899+
await telemetry.trackError(error, 'main_error');
900+
await telemetry.shutdown();
901+
} catch (e) {
902+
console.warn('Failed to track fatal error:', e);
903+
}
848904
process.exit(1);
849905
});

0 commit comments

Comments
 (0)