From 47522574340e6f7f05d9ea0c7b98dd9f4b2aa0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?katsumata=EF=BC=88TK=EF=BC=89?= <12413150+winor30@users.noreply.github.com> Date: Sun, 2 Mar 2025 18:33:49 +0900 Subject: [PATCH] refactor: update Datadog tools to use dynamic configuration factory (#10) * refactor: update Datadog tools to use dynamic configuration factory functions This commit introduces a significant refactoring of Datadog tool handlers: - Replace static API instances with dynamic configuration factory functions - Remove direct import of datadogConfig from utils/datadog - Update index files to export new factory functions instead of static handlers - Improve flexibility by allowing dynamic Datadog client configuration - Standardize tool handler creation across different modules The changes enable more flexible and configurable Datadog tool integrations while maintaining the existing tool interfaces. * test: add comprehensive tests for Datadog configuration utilities This commit introduces a new test suite for Datadog configuration functions: - Create tests for `createDatadogConfig` with various configuration scenarios - Add tests for `getDatadogSite` to verify site retrieval - Validate configuration creation with and without custom sites - Ensure proper error handling for missing API and APP keys * feat: integrate Datadog configuration with tool handlers This commit updates the main index file to: - Validate Datadog API and APP key environment variables - Create a dynamic Datadog configuration using environment settings - Refactor tool handlers to use the new configuration factory functions - Ensure consistent configuration across all Datadog tool handlers * chore: bump package version to 1.1.0 This version bump reflects the recent feature additions and improvements to the Datadog server integration, including: - Dynamic Datadog configuration - Enhanced tool handlers - New host management functionality * test: remove commented-out test configuration in Datadog test suite --- package.json | 2 +- src/index.ts | 42 ++-- src/tools/dashboards/index.ts | 2 +- src/tools/dashboards/tool.ts | 88 ++++---- src/tools/hosts/index.ts | 4 +- src/tools/hosts/tool.ts | 311 ++++++++++++++-------------- src/tools/incident/index.ts | 2 +- src/tools/incident/tool.ts | 82 ++++---- src/tools/logs/index.ts | 2 +- src/tools/logs/tool.ts | 78 +++---- src/tools/metrics/index.ts | 2 +- src/tools/metrics/tool.ts | 50 ++--- src/tools/monitors/index.ts | 2 +- src/tools/monitors/tool.ts | 170 +++++++-------- src/tools/traces/index.ts | 2 +- src/tools/traces/tool.ts | 106 +++++----- src/utils/__tests__/datadog.test.ts | 75 +++++++ src/utils/datadog.ts | 55 +++-- 18 files changed, 591 insertions(+), 484 deletions(-) create mode 100644 src/utils/__tests__/datadog.test.ts diff --git a/package.json b/package.json index 849e544..bc5e637 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@winor30/mcp-server-datadog", - "version": "1.0.1", + "version": "1.1.0", "description": "MCP server for interacting with Datadog API", "repository": { "type": "git", diff --git a/src/index.ts b/src/index.ts index 9e0f6ef..07cebda 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,14 +14,18 @@ import { ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js' import { log, mcpDatadogVersion } from './utils/helper' -import { INCIDENT_HANDLERS, INCIDENT_TOOLS } from './tools/incident' -import { METRICS_TOOLS, METRICS_HANDLERS } from './tools/metrics' -import { LOGS_TOOLS, LOGS_HANDLERS } from './tools/logs' -import { MONITORS_TOOLS, MONITORS_HANDLERS } from './tools/monitors' -import { DASHBOARDS_TOOLS, DASHBOARDS_HANDLERS } from './tools/dashboards' -import { TRACES_TOOLS, TRACES_HANDLERS } from './tools/traces' -import { HOSTS_TOOLS, HOSTS_HANDLERS } from './tools/hosts' +import { INCIDENT_TOOLS, createIncidentToolHandlers } from './tools/incident' +import { METRICS_TOOLS, createMetricsToolHandlers } from './tools/metrics' +import { LOGS_TOOLS, createLogsToolHandlers } from './tools/logs' +import { MONITORS_TOOLS, createMonitorsToolHandlers } from './tools/monitors' +import { + DASHBOARDS_TOOLS, + createDashboardsToolHandlers, +} from './tools/dashboards' +import { TRACES_TOOLS, createTracesToolHandlers } from './tools/traces' +import { HOSTS_TOOLS, createHostsToolHandlers } from './tools/hosts' import { ToolHandlers } from './utils/types' +import { createDatadogConfig } from './utils/datadog' const server = new Server( { @@ -57,14 +61,24 @@ server.setRequestHandler(ListToolsRequestSchema, async () => { } }) +if (!process.env.DATADOG_API_KEY || !process.env.DATADOG_APP_KEY) { + throw new Error('DATADOG_API_KEY and DATADOG_APP_KEY must be set') +} + +const datadogConfig = createDatadogConfig({ + apiKeyAuth: process.env.DATADOG_API_KEY, + appKeyAuth: process.env.DATADOG_APP_KEY, + site: process.env.DATADOG_SITE, +}) + const TOOL_HANDLERS: ToolHandlers = { - ...INCIDENT_HANDLERS, - ...METRICS_HANDLERS, - ...LOGS_HANDLERS, - ...MONITORS_HANDLERS, - ...DASHBOARDS_HANDLERS, - ...TRACES_HANDLERS, - ...HOSTS_HANDLERS, + ...createIncidentToolHandlers(datadogConfig), + ...createMetricsToolHandlers(datadogConfig), + ...createLogsToolHandlers(datadogConfig), + ...createMonitorsToolHandlers(datadogConfig), + ...createDashboardsToolHandlers(datadogConfig), + ...createTracesToolHandlers(datadogConfig), + ...createHostsToolHandlers(datadogConfig), } /** * Handler for invoking Datadog-related tools in the mcp-server-datadog. diff --git a/src/tools/dashboards/index.ts b/src/tools/dashboards/index.ts index 14b2b97..c78b468 100644 --- a/src/tools/dashboards/index.ts +++ b/src/tools/dashboards/index.ts @@ -1 +1 @@ -export { DASHBOARDS_TOOLS, DASHBOARDS_HANDLERS } from './tool' +export { DASHBOARDS_TOOLS, createDashboardsToolHandlers } from './tool' diff --git a/src/tools/dashboards/tool.ts b/src/tools/dashboards/tool.ts index 5a8b353..7c6aaf8 100644 --- a/src/tools/dashboards/tool.ts +++ b/src/tools/dashboards/tool.ts @@ -1,8 +1,7 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types' -import { v1 } from '@datadog/datadog-api-client' +import { client, v1 } from '@datadog/datadog-api-client' import { createToolSchema } from '../../utils/tool' import { ListDashboardsZodSchema } from './schema' -import { datadogConfig } from '../../utils/datadog' type DashboardsToolName = 'list_dashboards' type DashboardsTool = ExtendedTool @@ -15,51 +14,54 @@ export const DASHBOARDS_TOOLS: DashboardsTool[] = [ ), ] as const -const API_INSTANCE = new v1.DashboardsApi(datadogConfig) - type DashboardsToolHandlers = ToolHandlers -export const DASHBOARDS_HANDLERS: DashboardsToolHandlers = { - list_dashboards: async (request) => { - const { name, tags } = ListDashboardsZodSchema.parse( - request.params.arguments, - ) +export const createDashboardsToolHandlers = ( + config: client.Configuration, +): DashboardsToolHandlers => { + const apiInstance = new v1.DashboardsApi(config) + return { + list_dashboards: async (request) => { + const { name, tags } = ListDashboardsZodSchema.parse( + request.params.arguments, + ) - const response = await API_INSTANCE.listDashboards({ - filterShared: false, - }) + const response = await apiInstance.listDashboards({ + filterShared: false, + }) - if (!response.dashboards) { - throw new Error('No dashboards data returned') - } + if (!response.dashboards) { + throw new Error('No dashboards data returned') + } - // Filter dashboards based on name and tags if provided - let filteredDashboards = response.dashboards - if (name) { - const searchTerm = name.toLowerCase() - filteredDashboards = filteredDashboards.filter((dashboard) => - dashboard.title?.toLowerCase().includes(searchTerm), - ) - } - if (tags && tags.length > 0) { - filteredDashboards = filteredDashboards.filter((dashboard) => { - const dashboardTags = dashboard.description?.split(',') || [] - return tags.every((tag) => dashboardTags.includes(tag)) - }) - } + // Filter dashboards based on name and tags if provided + let filteredDashboards = response.dashboards + if (name) { + const searchTerm = name.toLowerCase() + filteredDashboards = filteredDashboards.filter((dashboard) => + dashboard.title?.toLowerCase().includes(searchTerm), + ) + } + if (tags && tags.length > 0) { + filteredDashboards = filteredDashboards.filter((dashboard) => { + const dashboardTags = dashboard.description?.split(',') || [] + return tags.every((tag) => dashboardTags.includes(tag)) + }) + } - const dashboards = filteredDashboards.map((dashboard) => ({ - ...dashboard, - url: `https://app.datadoghq.com/dashboard/${dashboard.id}`, - })) + const dashboards = filteredDashboards.map((dashboard) => ({ + ...dashboard, + url: `https://app.datadoghq.com/dashboard/${dashboard.id}`, + })) - return { - content: [ - { - type: 'text', - text: `Dashboards: ${JSON.stringify(dashboards)}`, - }, - ], - } - }, -} as const + return { + content: [ + { + type: 'text', + text: `Dashboards: ${JSON.stringify(dashboards)}`, + }, + ], + } + }, + } +} diff --git a/src/tools/hosts/index.ts b/src/tools/hosts/index.ts index 942b70f..016a83d 100644 --- a/src/tools/hosts/index.ts +++ b/src/tools/hosts/index.ts @@ -3,6 +3,6 @@ * Re-exports the tools and their handlers from the implementation file. * * HOSTS_TOOLS: Array of tool schemas defining the available host management operations - * HOSTS_HANDLERS: Object containing the implementation of each host management operation + * createHostsToolHandlers: Function that creates host management operation handlers */ -export { HOSTS_TOOLS, HOSTS_HANDLERS } from './tool' +export { HOSTS_TOOLS, createHostsToolHandlers } from './tool' diff --git a/src/tools/hosts/tool.ts b/src/tools/hosts/tool.ts index 2fa4ffe..f38c5b2 100644 --- a/src/tools/hosts/tool.ts +++ b/src/tools/hosts/tool.ts @@ -1,5 +1,5 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types' -import { v1 } from '@datadog/datadog-api-client' +import { client, v1 } from '@datadog/datadog-api-client' import { createToolSchema } from '../../utils/tool' import { ListHostsZodSchema, @@ -7,7 +7,6 @@ import { MuteHostZodSchema, UnmuteHostZodSchema, } from './schema' -import { datadogConfig } from '../../utils/datadog' /** * This module implements Datadog host management tools for muting, unmuting, @@ -46,9 +45,6 @@ export const HOSTS_TOOLS: HostsTool[] = [ ), ] as const -/** Datadog API client instance for host operations */ -const API_INSTANCE = new v1.HostsApi(datadogConfig) - /** Type definition for host management tool implementations */ type HostsToolHandlers = ToolHandlers @@ -56,154 +52,159 @@ type HostsToolHandlers = ToolHandlers * Implementation of host management tool handlers. * Each handler validates inputs using Zod schemas and interacts with the Datadog API. */ -export const HOSTS_HANDLERS: HostsToolHandlers = { - /** - * Mutes a specified host in Datadog. - * Silences alerts and notifications for the host until unmuted or until the specified end time. - */ - mute_host: async (request) => { - const { hostname, message, end, override } = MuteHostZodSchema.parse( - request.params.arguments, - ) - - await API_INSTANCE.muteHost({ - hostName: hostname, - body: { - message, - end, - override, - }, - }) - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - status: 'success', - message: `Host ${hostname} has been muted successfully${message ? ` with message: ${message}` : ''}${end ? ` until ${new Date(end * 1000).toISOString()}` : ''}`, - }, - null, - 2, - ), - }, - ], - } - }, - - /** - * Unmutes a previously muted host in Datadog. - * Re-enables alerts and notifications for the specified host. - */ - unmute_host: async (request) => { - const { hostname } = UnmuteHostZodSchema.parse(request.params.arguments) - - await API_INSTANCE.unmuteHost({ - hostName: hostname, - }) - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - status: 'success', - message: `Host ${hostname} has been unmuted successfully`, - }, - null, - 2, - ), - }, - ], - } - }, - - /** - * Retrieves counts of active and up hosts in Datadog. - * Provides total counts of hosts that are reporting and operational. - */ - get_active_hosts_count: async (request) => { - const { from } = GetActiveHostsCountZodSchema.parse( - request.params.arguments, - ) - - const response = await API_INSTANCE.getHostTotals({ - from, - }) - - return { - content: [ - { - type: 'text', - text: JSON.stringify( - { - total_active: response.totalActive || 0, // Total number of active hosts (UP and reporting) to Datadog - total_up: response.totalUp || 0, // Number of hosts that are UP and reporting to Datadog - }, - null, - 2, - ), - }, - ], - } - }, - - /** - * Lists and filters hosts monitored by Datadog. - * Supports comprehensive querying with filtering, sorting, and pagination. - * Returns detailed host information including status, metadata, and monitoring data. - */ - list_hosts: async (request) => { - const { - filter, - sort_field, - sort_dir, - start, - count, - from, - include_muted_hosts_data, - include_hosts_metadata, - } = ListHostsZodSchema.parse(request.params.arguments) - - const response = await API_INSTANCE.listHosts({ - filter, - sortField: sort_field, - sortDir: sort_dir, - start, - count, - from, - includeMutedHostsData: include_muted_hosts_data, - includeHostsMetadata: include_hosts_metadata, - }) - - if (!response.hostList) { - throw new Error('No hosts data returned') - } - - // Transform API response into a more convenient format - const hosts = response.hostList.map((host) => ({ - name: host.name, - id: host.id, - aliases: host.aliases, - apps: host.apps, - mute: host.isMuted, - last_reported: host.lastReportedTime, - meta: host.meta, - metrics: host.metrics, - sources: host.sources, - up: host.up, - url: `https://${datadogConfig.site}/infrastructure?host=${host.name}`, - })) - - return { - content: [ - { - type: 'text', - text: `Hosts: ${JSON.stringify(hosts)}`, +export const createHostsToolHandlers = ( + config: client.Configuration, +): HostsToolHandlers => { + const apiInstance = new v1.HostsApi(config) + return { + /** + * Mutes a specified host in Datadog. + * Silences alerts and notifications for the host until unmuted or until the specified end time. + */ + mute_host: async (request) => { + const { hostname, message, end, override } = MuteHostZodSchema.parse( + request.params.arguments, + ) + + await apiInstance.muteHost({ + hostName: hostname, + body: { + message, + end, + override, }, - ], - } - }, -} as const + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + status: 'success', + message: `Host ${hostname} has been muted successfully${message ? ` with message: ${message}` : ''}${end ? ` until ${new Date(end * 1000).toISOString()}` : ''}`, + }, + null, + 2, + ), + }, + ], + } + }, + + /** + * Unmutes a previously muted host in Datadog. + * Re-enables alerts and notifications for the specified host. + */ + unmute_host: async (request) => { + const { hostname } = UnmuteHostZodSchema.parse(request.params.arguments) + + await apiInstance.unmuteHost({ + hostName: hostname, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + status: 'success', + message: `Host ${hostname} has been unmuted successfully`, + }, + null, + 2, + ), + }, + ], + } + }, + + /** + * Retrieves counts of active and up hosts in Datadog. + * Provides total counts of hosts that are reporting and operational. + */ + get_active_hosts_count: async (request) => { + const { from } = GetActiveHostsCountZodSchema.parse( + request.params.arguments, + ) + + const response = await apiInstance.getHostTotals({ + from, + }) + + return { + content: [ + { + type: 'text', + text: JSON.stringify( + { + total_active: response.totalActive || 0, // Total number of active hosts (UP and reporting) to Datadog + total_up: response.totalUp || 0, // Number of hosts that are UP and reporting to Datadog + }, + null, + 2, + ), + }, + ], + } + }, + + /** + * Lists and filters hosts monitored by Datadog. + * Supports comprehensive querying with filtering, sorting, and pagination. + * Returns detailed host information including status, metadata, and monitoring data. + */ + list_hosts: async (request) => { + const { + filter, + sort_field, + sort_dir, + start, + count, + from, + include_muted_hosts_data, + include_hosts_metadata, + } = ListHostsZodSchema.parse(request.params.arguments) + + const response = await apiInstance.listHosts({ + filter, + sortField: sort_field, + sortDir: sort_dir, + start, + count, + from, + includeMutedHostsData: include_muted_hosts_data, + includeHostsMetadata: include_hosts_metadata, + }) + + if (!response.hostList) { + throw new Error('No hosts data returned') + } + + // Transform API response into a more convenient format + const hosts = response.hostList.map((host) => ({ + name: host.name, + id: host.id, + aliases: host.aliases, + apps: host.apps, + mute: host.isMuted, + last_reported: host.lastReportedTime, + meta: host.meta, + metrics: host.metrics, + sources: host.sources, + up: host.up, + url: `https://app.datadoghq.com/infrastructure?host=${host.name}`, + })) + + return { + content: [ + { + type: 'text', + text: `Hosts: ${JSON.stringify(hosts)}`, + }, + ], + } + }, + } +} diff --git a/src/tools/incident/index.ts b/src/tools/incident/index.ts index 4167c57..a0e0fd5 100644 --- a/src/tools/incident/index.ts +++ b/src/tools/incident/index.ts @@ -1 +1 @@ -export { INCIDENT_TOOLS, INCIDENT_HANDLERS } from './tool' +export { INCIDENT_TOOLS, createIncidentToolHandlers } from './tool' diff --git a/src/tools/incident/tool.ts b/src/tools/incident/tool.ts index bfceeab..9e35713 100644 --- a/src/tools/incident/tool.ts +++ b/src/tools/incident/tool.ts @@ -1,8 +1,7 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types' -import { v2 } from '@datadog/datadog-api-client' +import { client, v2 } from '@datadog/datadog-api-client' import { createToolSchema } from '../../utils/tool' import { GetIncidentZodSchema, ListIncidentsZodSchema } from './schema' -import { datadogConfig } from '../../utils/datadog' type IncidentToolName = 'list_incidents' | 'get_incident' type IncidentTool = ExtendedTool @@ -20,43 +19,48 @@ export const INCIDENT_TOOLS: IncidentTool[] = [ ), ] as const -const API_INSTANCE = new v2.IncidentsApi(datadogConfig) - type IncidentToolHandlers = ToolHandlers -export const INCIDENT_HANDLERS: IncidentToolHandlers = { - list_incidents: async (request) => { - const { pageSize, pageOffset } = ListIncidentsZodSchema.parse( - request.params.arguments, - ) +export const createIncidentToolHandlers = ( + config: client.Configuration, +): IncidentToolHandlers => { + const apiInstance = new v2.IncidentsApi(config) + return { + list_incidents: async (request) => { + const { pageSize, pageOffset } = ListIncidentsZodSchema.parse( + request.params.arguments, + ) - const res = await API_INSTANCE.listIncidents({ - pageSize: pageSize, - pageOffset: pageOffset, - }) - return { - content: [ - { - type: 'text', - text: `Listed incidents:\n${res.data - .map((d) => JSON.stringify(d)) - .join('\n')}`, - }, - ], - } - }, - get_incident: async (request) => { - const { incidentId } = GetIncidentZodSchema.parse(request.params.arguments) - const res = await API_INSTANCE.getIncident({ - incidentId: incidentId, - }) - return { - content: [ - { - type: 'text', - text: `Incident: ${JSON.stringify(res)}`, - }, - ], - } - }, -} as const + const res = await apiInstance.listIncidents({ + pageSize: pageSize, + pageOffset: pageOffset, + }) + return { + content: [ + { + type: 'text', + text: `Listed incidents:\n${res.data + .map((d) => JSON.stringify(d)) + .join('\n')}`, + }, + ], + } + }, + get_incident: async (request) => { + const { incidentId } = GetIncidentZodSchema.parse( + request.params.arguments, + ) + const res = await apiInstance.getIncident({ + incidentId: incidentId, + }) + return { + content: [ + { + type: 'text', + text: `Incident: ${JSON.stringify(res)}`, + }, + ], + } + }, + } +} diff --git a/src/tools/logs/index.ts b/src/tools/logs/index.ts index 732b7e0..0583e41 100644 --- a/src/tools/logs/index.ts +++ b/src/tools/logs/index.ts @@ -1 +1 @@ -export { LOGS_TOOLS, LOGS_HANDLERS } from './tool' +export { LOGS_TOOLS, createLogsToolHandlers } from './tool' diff --git a/src/tools/logs/tool.ts b/src/tools/logs/tool.ts index 2c9d363..0fd38d6 100644 --- a/src/tools/logs/tool.ts +++ b/src/tools/logs/tool.ts @@ -1,8 +1,7 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types' -import { v2 } from '@datadog/datadog-api-client' +import { client, v2 } from '@datadog/datadog-api-client' import { createToolSchema } from '../../utils/tool' import { GetLogsZodSchema } from './schema' -import { datadogConfig } from '../../utils/datadog' type LogsToolName = 'get_logs' type LogsTool = ExtendedTool @@ -15,42 +14,45 @@ export const LOGS_TOOLS: LogsTool[] = [ ), ] as const -const API_INSTANCE = new v2.LogsApi(datadogConfig) - type LogsToolHandlers = ToolHandlers -export const LOGS_HANDLERS: LogsToolHandlers = { - get_logs: async (request) => { - const { query, from, to, limit } = GetLogsZodSchema.parse( - request.params.arguments, - ) - - const response = await API_INSTANCE.listLogs({ - body: { - filter: { - query, - // `from` and `to` are in epoch seconds, but the Datadog API expects milliseconds - from: `${from * 1000}`, - to: `${to * 1000}`, - }, - page: { - limit, - }, - sort: '-timestamp', - }, - }) - - if (response.data == null) { - throw new Error('No logs data returned') - } - - return { - content: [ - { - type: 'text', - text: `Logs data: ${JSON.stringify(response.data)}`, +export const createLogsToolHandlers = ( + config: client.Configuration, +): LogsToolHandlers => { + const apiInstance = new v2.LogsApi(config) + return { + get_logs: async (request) => { + const { query, from, to, limit } = GetLogsZodSchema.parse( + request.params.arguments, + ) + + const response = await apiInstance.listLogs({ + body: { + filter: { + query, + // `from` and `to` are in epoch seconds, but the Datadog API expects milliseconds + from: `${from * 1000}`, + to: `${to * 1000}`, + }, + page: { + limit, + }, + sort: '-timestamp', }, - ], - } - }, -} as const + }) + + if (response.data == null) { + throw new Error('No logs data returned') + } + + return { + content: [ + { + type: 'text', + text: `Logs data: ${JSON.stringify(response.data)}`, + }, + ], + } + }, + } +} diff --git a/src/tools/metrics/index.ts b/src/tools/metrics/index.ts index 4abff1e..f23d2eb 100644 --- a/src/tools/metrics/index.ts +++ b/src/tools/metrics/index.ts @@ -1 +1 @@ -export { METRICS_TOOLS, METRICS_HANDLERS } from './tool' +export { METRICS_TOOLS, createMetricsToolHandlers } from './tool' diff --git a/src/tools/metrics/tool.ts b/src/tools/metrics/tool.ts index a3f8669..783f14c 100644 --- a/src/tools/metrics/tool.ts +++ b/src/tools/metrics/tool.ts @@ -1,8 +1,7 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types' -import { v1 } from '@datadog/datadog-api-client' +import { client, v1 } from '@datadog/datadog-api-client' import { createToolSchema } from '../../utils/tool' import { GetMetricsZodSchema } from './schema' -import { datadogConfig } from '../../utils/datadog' type MetricsToolName = 'get_metrics' type MetricsTool = ExtendedTool @@ -15,29 +14,32 @@ export const METRICS_TOOLS: MetricsTool[] = [ ), ] as const -const API_INSTANCE = new v1.MetricsApi(datadogConfig) - type MetricsToolHandlers = ToolHandlers -export const METRICS_HANDLERS: MetricsToolHandlers = { - get_metrics: async (request) => { - const { from, to, query } = GetMetricsZodSchema.parse( - request.params.arguments, - ) +export const createMetricsToolHandlers = ( + config: client.Configuration, +): MetricsToolHandlers => { + const apiInstance = new v1.MetricsApi(config) + return { + get_metrics: async (request) => { + const { from, to, query } = GetMetricsZodSchema.parse( + request.params.arguments, + ) - const response = await API_INSTANCE.queryMetrics({ - from, - to, - query, - }) + const response = await apiInstance.queryMetrics({ + from, + to, + query, + }) - return { - content: [ - { - type: 'text', - text: `Metrics data: ${JSON.stringify({ response })}`, - }, - ], - } - }, -} as const + return { + content: [ + { + type: 'text', + text: `Metrics data: ${JSON.stringify({ response })}`, + }, + ], + } + }, + } +} diff --git a/src/tools/monitors/index.ts b/src/tools/monitors/index.ts index d3103ec..a93a8a8 100644 --- a/src/tools/monitors/index.ts +++ b/src/tools/monitors/index.ts @@ -1 +1 @@ -export { MONITORS_TOOLS, MONITORS_HANDLERS } from './tool' +export { MONITORS_TOOLS, createMonitorsToolHandlers } from './tool' diff --git a/src/tools/monitors/tool.ts b/src/tools/monitors/tool.ts index 51e50a3..5336e05 100644 --- a/src/tools/monitors/tool.ts +++ b/src/tools/monitors/tool.ts @@ -1,8 +1,7 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types' -import { v1 } from '@datadog/datadog-api-client' +import { client, v1 } from '@datadog/datadog-api-client' import { createToolSchema } from '../../utils/tool' import { GetMonitorsZodSchema } from './schema' -import { datadogConfig } from '../../utils/datadog' import { unreachable } from '../../utils/helper' import { UnparsedObject } from '@datadog/datadog-api-client/dist/packages/datadog-api-client-common/util.js' @@ -17,95 +16,98 @@ export const MONITORS_TOOLS: MonitorsTool[] = [ ), ] as const -const API_INSTANCE = new v1.MonitorsApi(datadogConfig) - type MonitorsToolHandlers = ToolHandlers -export const MONITORS_HANDLERS: MonitorsToolHandlers = { - get_monitors: async (request) => { - const { groupStates, name, tags } = GetMonitorsZodSchema.parse( - request.params.arguments, - ) - - const response = await API_INSTANCE.listMonitors({ - groupStates: groupStates?.join(','), - name, - tags: tags?.join(','), - }) +export const createMonitorsToolHandlers = ( + config: client.Configuration, +): MonitorsToolHandlers => { + const apiInstance = new v1.MonitorsApi(config) + return { + get_monitors: async (request) => { + const { groupStates, name, tags } = GetMonitorsZodSchema.parse( + request.params.arguments, + ) - if (response == null) { - throw new Error('No monitors data returned') - } + const response = await apiInstance.listMonitors({ + groupStates: groupStates?.join(','), + name, + tags: tags?.join(','), + }) - const monitors = response.map((monitor) => ({ - name: monitor.name || '', - id: monitor.id || 0, - status: (monitor.overallState as string) || 'unknown', - message: monitor.message, - tags: monitor.tags || [], - query: monitor.query || '', - lastUpdatedTs: monitor.modified - ? Math.floor(new Date(monitor.modified).getTime() / 1000) - : undefined, - })) + if (response == null) { + throw new Error('No monitors data returned') + } - // Calculate summary - const summary = response.reduce( - (acc, monitor) => { - const status = monitor.overallState - if (status == null || status instanceof UnparsedObject) { - return acc - } + const monitors = response.map((monitor) => ({ + name: monitor.name || '', + id: monitor.id || 0, + status: (monitor.overallState as string) || 'unknown', + message: monitor.message, + tags: monitor.tags || [], + query: monitor.query || '', + lastUpdatedTs: monitor.modified + ? Math.floor(new Date(monitor.modified).getTime() / 1000) + : undefined, + })) - switch (status) { - case 'Alert': - acc.alert++ - break - case 'Warn': - acc.warn++ - break - case 'No Data': - acc.noData++ - break - case 'OK': - acc.ok++ - break - case 'Ignored': - acc.ignored++ - break - case 'Skipped': - acc.skipped++ - break - case 'Unknown': - acc.unknown++ - break - default: - unreachable(status) - } - return acc - }, - { - alert: 0, - warn: 0, - noData: 0, - ok: 0, - ignored: 0, - skipped: 0, - unknown: 0, - }, - ) + // Calculate summary + const summary = response.reduce( + (acc, monitor) => { + const status = monitor.overallState + if (status == null || status instanceof UnparsedObject) { + return acc + } - return { - content: [ - { - type: 'text', - text: `Monitors: ${JSON.stringify(monitors)}`, + switch (status) { + case 'Alert': + acc.alert++ + break + case 'Warn': + acc.warn++ + break + case 'No Data': + acc.noData++ + break + case 'OK': + acc.ok++ + break + case 'Ignored': + acc.ignored++ + break + case 'Skipped': + acc.skipped++ + break + case 'Unknown': + acc.unknown++ + break + default: + unreachable(status) + } + return acc }, { - type: 'text', - text: `Summary of monitors: ${JSON.stringify(summary)}`, + alert: 0, + warn: 0, + noData: 0, + ok: 0, + ignored: 0, + skipped: 0, + unknown: 0, }, - ], - } - }, -} as const + ) + + return { + content: [ + { + type: 'text', + text: `Monitors: ${JSON.stringify(monitors)}`, + }, + { + type: 'text', + text: `Summary of monitors: ${JSON.stringify(summary)}`, + }, + ], + } + }, + } +} diff --git a/src/tools/traces/index.ts b/src/tools/traces/index.ts index 9b818eb..61c6047 100644 --- a/src/tools/traces/index.ts +++ b/src/tools/traces/index.ts @@ -1 +1 @@ -export { TRACES_TOOLS, TRACES_HANDLERS } from './tool' +export { TRACES_TOOLS, createTracesToolHandlers } from './tool' diff --git a/src/tools/traces/tool.ts b/src/tools/traces/tool.ts index 439868f..23d3d49 100644 --- a/src/tools/traces/tool.ts +++ b/src/tools/traces/tool.ts @@ -1,7 +1,6 @@ import { ExtendedTool, ToolHandlers } from '../../utils/types' -import { v2 } from '@datadog/datadog-api-client' +import { client, v2 } from '@datadog/datadog-api-client' import { createToolSchema } from '../../utils/tool' -import { datadogConfig as config } from '../../utils/datadog' import { ListTracesZodSchema } from './schema' type TracesToolName = 'list_traces' @@ -15,65 +14,60 @@ export const TRACES_TOOLS: TracesTool[] = [ ), ] as const -const API_INSTANCE = new v2.SpansApi(config) - type TracesToolHandlers = ToolHandlers -export const TRACES_HANDLERS: TracesToolHandlers = { - list_traces: async (request) => { - const { - query, - from, - to, - limit = 100, - sort = '-timestamp', - service, - operation, - } = request.params.arguments as { - query: string - from: number - to: number - limit?: number - sort?: string - service?: string - operation?: string - } +export const createTracesToolHandlers = ( + config: client.Configuration, +): TracesToolHandlers => { + const apiInstance = new v2.SpansApi(config) + return { + list_traces: async (request) => { + const { + query, + from, + to, + limit = 100, + sort = '-timestamp', + service, + operation, + } = ListTracesZodSchema.parse(request.params.arguments) - const response = await API_INSTANCE.listSpans({ - body: { - data: { - attributes: { - filter: { - query: [ - query, - ...(service ? [`service:${service}`] : []), - ...(operation ? [`operation:${operation}`] : []), - ].join(' '), - from: new Date(from * 1000).toISOString(), - to: new Date(to * 1000).toISOString(), + const response = await apiInstance.listSpans({ + body: { + data: { + attributes: { + filter: { + query: [ + query, + ...(service ? [`service:${service}`] : []), + ...(operation ? [`operation:${operation}`] : []), + ].join(' '), + from: new Date(from * 1000).toISOString(), + to: new Date(to * 1000).toISOString(), + }, + sort: sort as 'timestamp' | '-timestamp', + page: { limit }, }, - sort: sort as 'timestamp' | '-timestamp', - page: { limit }, + type: 'search_request', }, - type: 'search_request', }, - }, - }) + }) - if (!response.data) { - throw new Error('No traces data returned') - } + if (!response.data) { + throw new Error('No traces data returned') + } - return { - content: [ - { - type: 'text', - text: `Traces: ${JSON.stringify({ - traces: response.data, - count: response.data.length, - })}`, - }, - ], - } - }, -} as const + return { + content: [ + { + type: 'text', + text: `Traces: ${JSON.stringify({ + traces: response.data, + count: response.data.length, + })}`, + }, + ], + } + }, + } +} diff --git a/src/utils/__tests__/datadog.test.ts b/src/utils/__tests__/datadog.test.ts new file mode 100644 index 0000000..9b08b3d --- /dev/null +++ b/src/utils/__tests__/datadog.test.ts @@ -0,0 +1,75 @@ +import { + ApiKeyAuthAuthentication, + AppKeyAuthAuthentication, +} from '@datadog/datadog-api-client/dist/packages/datadog-api-client-common' +import { createDatadogConfig, getDatadogSite } from '../datadog' + +describe('createDatadogConfig', () => { + it('should create a datadog config with custom site when DATADOG_SITE is configured', () => { + const datadogConfig = createDatadogConfig({ + apiKeyAuth: 'test-api-key', + appKeyAuth: 'test-app-key', + site: 'us3.datadoghq.com', + }) + expect(datadogConfig.authMethods).toEqual({ + apiKeyAuth: new ApiKeyAuthAuthentication('test-api-key'), + appKeyAuth: new AppKeyAuthAuthentication('test-app-key'), + }) + expect(datadogConfig.servers[0]?.getConfiguration()?.site).toBe( + 'us3.datadoghq.com', + ) + }) + + it('should create a datadog config with default site when DATADOG_SITE is not configured', () => { + const datadogConfig = createDatadogConfig({ + apiKeyAuth: 'test-api-key', + appKeyAuth: 'test-app-key', + }) + expect(datadogConfig.authMethods).toEqual({ + apiKeyAuth: new ApiKeyAuthAuthentication('test-api-key'), + appKeyAuth: new AppKeyAuthAuthentication('test-app-key'), + }) + expect(datadogConfig.servers[0]?.getConfiguration()?.site).toBe( + 'datadoghq.com', + ) + }) + + it('should throw an error when DATADOG_API_KEY are not configured', () => { + expect(() => + createDatadogConfig({ + apiKeyAuth: '', + appKeyAuth: 'test-app-key', + }), + ).toThrow('Datadog API key and APP key are required') + }) + + it('should throw an error when DATADOG_APP_KEY are not configured', () => { + expect(() => + createDatadogConfig({ + apiKeyAuth: 'test-api-key', + appKeyAuth: '', + }), + ).toThrow('Datadog API key and APP key are required') + }) +}) + +describe('getDatadogSite', () => { + it('should return custom site when DATADOG_SITE is configured', () => { + const datadogConfig = createDatadogConfig({ + apiKeyAuth: 'test-api-key', + appKeyAuth: 'test-app-key', + site: 'us3.datadoghq.com', + }) + const site = getDatadogSite(datadogConfig) + expect(site).toBe('us3.datadoghq.com') + }) + + it('should return default site when DATADOG_SITE is not configured', () => { + const datadogConfig = createDatadogConfig({ + apiKeyAuth: 'test-api-key', + appKeyAuth: 'test-app-key', + }) + const site = getDatadogSite(datadogConfig) + expect(site).toBe('datadoghq.com') + }) +}) diff --git a/src/utils/datadog.ts b/src/utils/datadog.ts index 06ce729..0839240 100644 --- a/src/utils/datadog.ts +++ b/src/utils/datadog.ts @@ -1,31 +1,42 @@ import { client } from '@datadog/datadog-api-client' -const config = { - apiKeyAuth: process.env.DATADOG_API_KEY, - appKeyAuth: process.env.DATADOG_APP_KEY, - site: process.env.DATADOG_SITE, +interface CreateDatadogConfigParams { + apiKeyAuth: string + appKeyAuth: string + site?: string } -if (!config.apiKeyAuth || !config.appKeyAuth) { - throw new Error('Datadog API key and APP key are required') -} +export function createDatadogConfig( + config: CreateDatadogConfigParams, +): client.Configuration { + if (!config.apiKeyAuth || !config.appKeyAuth) { + throw new Error('Datadog API key and APP key are required') + } + const datadogConfig = client.createConfiguration({ + authMethods: { + apiKeyAuth: config.apiKeyAuth, + appKeyAuth: config.appKeyAuth, + }, + }) -const datadogConfig = client.createConfiguration({ - authMethods: { - apiKeyAuth: config.apiKeyAuth, - appKeyAuth: config.appKeyAuth, - }, -}) + if (config.site != null) { + datadogConfig.setServerVariables({ + site: config.site, + }) + } -if (config.site != null) { - datadogConfig.setServerVariables({ - site: config.site, - }) -} + datadogConfig.unstableOperations = { + 'v2.listIncidents': true, + 'v2.getIncident': true, + } -datadogConfig.unstableOperations = { - 'v2.listIncidents': true, - 'v2.getIncident': true, + return datadogConfig } -export { datadogConfig } +export function getDatadogSite(ddConfig: client.Configuration): string { + const config = ddConfig.servers[0]?.getConfiguration() + if (config == null) { + throw new Error('Datadog site is not set') + } + return config.site +}