datadog-mcp 1.0.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/LICENSE +201 -0
- package/README.md +391 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +4579 -0
- package/dist/index.js.map +1 -0
- package/package.json +74 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4579 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/config/schema.ts
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
var ALL_TOOLS = [
|
|
6
|
+
"monitors",
|
|
7
|
+
"dashboards",
|
|
8
|
+
"logs",
|
|
9
|
+
"metrics",
|
|
10
|
+
"traces",
|
|
11
|
+
"events",
|
|
12
|
+
"incidents",
|
|
13
|
+
"slos",
|
|
14
|
+
"synthetics",
|
|
15
|
+
"hosts",
|
|
16
|
+
"downtimes",
|
|
17
|
+
"rum",
|
|
18
|
+
"security",
|
|
19
|
+
"notebooks",
|
|
20
|
+
"users",
|
|
21
|
+
"teams",
|
|
22
|
+
"tags",
|
|
23
|
+
"usage",
|
|
24
|
+
"auth"
|
|
25
|
+
];
|
|
26
|
+
var configSchema = z.object({
|
|
27
|
+
datadog: z.object({
|
|
28
|
+
apiKey: z.string().min(1, "DD_API_KEY is required"),
|
|
29
|
+
appKey: z.string().min(1, "DD_APP_KEY is required"),
|
|
30
|
+
site: z.string().default("datadoghq.com")
|
|
31
|
+
}),
|
|
32
|
+
server: z.object({
|
|
33
|
+
name: z.string().default("datadog-mcp"),
|
|
34
|
+
version: z.string().default("1.0.0"),
|
|
35
|
+
transport: z.enum(["stdio", "http"]).default("stdio"),
|
|
36
|
+
port: z.number().default(3e3),
|
|
37
|
+
host: z.string().default("localhost")
|
|
38
|
+
}),
|
|
39
|
+
limits: z.object({
|
|
40
|
+
maxResults: z.number().default(100),
|
|
41
|
+
maxLogLines: z.number().default(100),
|
|
42
|
+
// Reduced from 500 for token efficiency
|
|
43
|
+
defaultLimit: z.number().default(25),
|
|
44
|
+
// Default limit for initial queries
|
|
45
|
+
maxMetricDataPoints: z.number().default(1e3),
|
|
46
|
+
defaultTimeRangeHours: z.number().default(24)
|
|
47
|
+
}),
|
|
48
|
+
features: z.object({
|
|
49
|
+
readOnly: z.boolean().default(false),
|
|
50
|
+
disabledTools: z.array(z.string()).default([])
|
|
51
|
+
}).default({})
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// src/config/index.ts
|
|
55
|
+
function parseArgs() {
|
|
56
|
+
const strings = {};
|
|
57
|
+
const booleans = /* @__PURE__ */ new Set();
|
|
58
|
+
const argv = process.argv.slice(2);
|
|
59
|
+
for (let i = 0; i < argv.length; i++) {
|
|
60
|
+
const arg = argv[i];
|
|
61
|
+
if (!arg) continue;
|
|
62
|
+
if (arg.startsWith("--")) {
|
|
63
|
+
if (arg.includes("=")) {
|
|
64
|
+
const parts = arg.slice(2).split("=");
|
|
65
|
+
const key = parts[0];
|
|
66
|
+
const value = parts.slice(1).join("=");
|
|
67
|
+
if (key && value !== void 0) {
|
|
68
|
+
strings[key] = value;
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
const argName = arg.slice(2);
|
|
72
|
+
const nextArg = argv[i + 1];
|
|
73
|
+
if (nextArg && !nextArg.startsWith("--")) {
|
|
74
|
+
strings[argName] = nextArg;
|
|
75
|
+
i++;
|
|
76
|
+
} else {
|
|
77
|
+
booleans.add(argName);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { strings, booleans };
|
|
83
|
+
}
|
|
84
|
+
function parseDisabledTools(value) {
|
|
85
|
+
if (!value) return [];
|
|
86
|
+
const requested = value.split(",").map((s) => s.trim().toLowerCase());
|
|
87
|
+
return requested.filter((t) => ALL_TOOLS.includes(t));
|
|
88
|
+
}
|
|
89
|
+
function loadConfig() {
|
|
90
|
+
const args = parseArgs();
|
|
91
|
+
const raw = {
|
|
92
|
+
datadog: {
|
|
93
|
+
apiKey: process.env.DD_API_KEY ?? "",
|
|
94
|
+
appKey: process.env.DD_APP_KEY ?? "",
|
|
95
|
+
site: args.strings.site ?? process.env.DD_SITE ?? "datadoghq.com"
|
|
96
|
+
},
|
|
97
|
+
server: {
|
|
98
|
+
name: "datadog-mcp",
|
|
99
|
+
version: "1.0.0",
|
|
100
|
+
transport: args.strings.transport ?? process.env.MCP_TRANSPORT ?? "stdio",
|
|
101
|
+
port: parseInt(args.strings.port ?? process.env.MCP_PORT ?? "3000", 10),
|
|
102
|
+
host: args.strings.host ?? process.env.MCP_HOST ?? "localhost"
|
|
103
|
+
},
|
|
104
|
+
limits: {
|
|
105
|
+
maxResults: parseInt(process.env.MCP_MAX_RESULTS ?? "100", 10),
|
|
106
|
+
maxLogLines: parseInt(process.env.MCP_MAX_LOG_LINES ?? "500", 10),
|
|
107
|
+
maxMetricDataPoints: parseInt(process.env.MCP_MAX_METRIC_POINTS ?? "1000", 10),
|
|
108
|
+
defaultTimeRangeHours: parseInt(process.env.MCP_DEFAULT_TIME_RANGE ?? "24", 10)
|
|
109
|
+
},
|
|
110
|
+
features: {
|
|
111
|
+
readOnly: args.booleans.has("read-only") || process.env.MCP_READ_ONLY === "true",
|
|
112
|
+
disabledTools: parseDisabledTools(args.strings["disable-tools"] ?? process.env.MCP_DISABLE_TOOLS)
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
return configSchema.parse(raw);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// src/server.ts
|
|
119
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
120
|
+
|
|
121
|
+
// src/config/datadog.ts
|
|
122
|
+
import { client, v1, v2 } from "@datadog/datadog-api-client";
|
|
123
|
+
function createDatadogClients(config) {
|
|
124
|
+
const configuration = client.createConfiguration({
|
|
125
|
+
authMethods: {
|
|
126
|
+
apiKeyAuth: config.apiKey,
|
|
127
|
+
appKeyAuth: config.appKey
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
if (config.site && config.site !== "datadoghq.com") {
|
|
131
|
+
configuration.setServerVariables({ site: config.site });
|
|
132
|
+
}
|
|
133
|
+
configuration.unstableOperations = {
|
|
134
|
+
"v2.listIncidents": true,
|
|
135
|
+
"v2.getIncident": true,
|
|
136
|
+
"v2.searchIncidents": true,
|
|
137
|
+
"v2.createIncident": true,
|
|
138
|
+
"v2.updateIncident": true,
|
|
139
|
+
"v2.deleteIncident": true
|
|
140
|
+
};
|
|
141
|
+
return {
|
|
142
|
+
monitors: new v1.MonitorsApi(configuration),
|
|
143
|
+
dashboards: new v1.DashboardsApi(configuration),
|
|
144
|
+
dashboardLists: new v1.DashboardListsApi(configuration),
|
|
145
|
+
logs: new v2.LogsApi(configuration),
|
|
146
|
+
metricsV1: new v1.MetricsApi(configuration),
|
|
147
|
+
metricsV2: new v2.MetricsApi(configuration),
|
|
148
|
+
eventsV1: new v1.EventsApi(configuration),
|
|
149
|
+
eventsV2: new v2.EventsApi(configuration),
|
|
150
|
+
incidents: new v2.IncidentsApi(configuration),
|
|
151
|
+
downtimes: new v2.DowntimesApi(configuration),
|
|
152
|
+
hosts: new v1.HostsApi(configuration),
|
|
153
|
+
slo: new v1.ServiceLevelObjectivesApi(configuration),
|
|
154
|
+
synthetics: new v1.SyntheticsApi(configuration),
|
|
155
|
+
rum: new v2.RUMApi(configuration),
|
|
156
|
+
security: new v2.SecurityMonitoringApi(configuration),
|
|
157
|
+
notebooks: new v1.NotebooksApi(configuration),
|
|
158
|
+
users: new v2.UsersApi(configuration),
|
|
159
|
+
teams: new v2.TeamsApi(configuration),
|
|
160
|
+
tags: new v1.TagsApi(configuration),
|
|
161
|
+
usage: new v1.UsageMeteringApi(configuration),
|
|
162
|
+
spans: new v2.SpansApi(configuration),
|
|
163
|
+
services: new v2.ServiceDefinitionApi(configuration),
|
|
164
|
+
auth: new v1.AuthenticationApi(configuration)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/tools/monitors.ts
|
|
169
|
+
import { z as z2 } from "zod";
|
|
170
|
+
|
|
171
|
+
// src/errors/datadog.ts
|
|
172
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
173
|
+
var DatadogErrorCode = {
|
|
174
|
+
/** 401 - Invalid or missing API/APP key */
|
|
175
|
+
Unauthorized: -32050,
|
|
176
|
+
/** 403 - Valid credentials but insufficient permissions */
|
|
177
|
+
Forbidden: -32051,
|
|
178
|
+
/** 404 - Requested resource does not exist */
|
|
179
|
+
NotFound: -32052,
|
|
180
|
+
/** 429 - Rate limit exceeded, should retry after delay */
|
|
181
|
+
RateLimited: -32053,
|
|
182
|
+
/** 5xx - Datadog service temporarily unavailable */
|
|
183
|
+
ServiceUnavailable: -32054
|
|
184
|
+
};
|
|
185
|
+
function handleDatadogError(error) {
|
|
186
|
+
console.error("[Datadog Error]", error);
|
|
187
|
+
if (error instanceof McpError) {
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
const apiError = error;
|
|
191
|
+
if (typeof apiError.code === "number") {
|
|
192
|
+
const message = apiError.body?.errors?.[0] ?? apiError.message ?? "Unknown error";
|
|
193
|
+
switch (apiError.code) {
|
|
194
|
+
case 400:
|
|
195
|
+
throw new McpError(ErrorCode.InvalidRequest, `Invalid request: ${message}`);
|
|
196
|
+
case 401:
|
|
197
|
+
throw new McpError(DatadogErrorCode.Unauthorized, `Authentication failed: Invalid Datadog API key or APP key`);
|
|
198
|
+
case 403:
|
|
199
|
+
throw new McpError(DatadogErrorCode.Forbidden, `Authorization denied: ${message}`);
|
|
200
|
+
case 404:
|
|
201
|
+
throw new McpError(DatadogErrorCode.NotFound, `Resource not found: ${message}`);
|
|
202
|
+
case 429:
|
|
203
|
+
throw new McpError(DatadogErrorCode.RateLimited, "Rate limit exceeded. Retry after a short delay.");
|
|
204
|
+
case 500:
|
|
205
|
+
case 502:
|
|
206
|
+
case 503:
|
|
207
|
+
throw new McpError(DatadogErrorCode.ServiceUnavailable, "Datadog service temporarily unavailable. Retry later.");
|
|
208
|
+
default:
|
|
209
|
+
throw new McpError(ErrorCode.InternalError, `Datadog API error (${apiError.code}): ${message}`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
throw new McpError(
|
|
213
|
+
ErrorCode.InternalError,
|
|
214
|
+
error instanceof Error ? error.message : String(error)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
function requireParam(value, name, action) {
|
|
218
|
+
if (value === void 0 || value === null || value === "") {
|
|
219
|
+
throw new McpError(
|
|
220
|
+
ErrorCode.InvalidParams,
|
|
221
|
+
`Parameter '${name}' is required for action '${action}'`
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
|
226
|
+
var WRITE_ACTIONS = /* @__PURE__ */ new Set([
|
|
227
|
+
"create",
|
|
228
|
+
"update",
|
|
229
|
+
"delete",
|
|
230
|
+
"mute",
|
|
231
|
+
"unmute",
|
|
232
|
+
"cancel",
|
|
233
|
+
"add",
|
|
234
|
+
"trigger"
|
|
235
|
+
]);
|
|
236
|
+
function checkReadOnly(action, readOnly) {
|
|
237
|
+
if (readOnly && WRITE_ACTIONS.has(action)) {
|
|
238
|
+
throw new McpError(
|
|
239
|
+
ErrorCode.InvalidRequest,
|
|
240
|
+
`Action '${action}' is not allowed in read-only mode. Server started with --read-only flag.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/utils/format.ts
|
|
246
|
+
function formatResponse(data) {
|
|
247
|
+
return [
|
|
248
|
+
{
|
|
249
|
+
type: "text",
|
|
250
|
+
text: JSON.stringify(data, null, 2)
|
|
251
|
+
}
|
|
252
|
+
];
|
|
253
|
+
}
|
|
254
|
+
function toolResult(data) {
|
|
255
|
+
return {
|
|
256
|
+
content: formatResponse(data)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/utils/urls.ts
|
|
261
|
+
var SITE_TO_APP_URL = {
|
|
262
|
+
"datadoghq.com": "https://app.datadoghq.com",
|
|
263
|
+
"us3.datadoghq.com": "https://us3.datadoghq.com",
|
|
264
|
+
"us5.datadoghq.com": "https://us5.datadoghq.com",
|
|
265
|
+
"datadoghq.eu": "https://app.datadoghq.eu",
|
|
266
|
+
"ap1.datadoghq.com": "https://ap1.datadoghq.com",
|
|
267
|
+
"ddog-gov.com": "https://app.ddog-gov.com"
|
|
268
|
+
};
|
|
269
|
+
function getAppBaseUrl(site = "datadoghq.com") {
|
|
270
|
+
return SITE_TO_APP_URL[site] ?? SITE_TO_APP_URL["datadoghq.com"];
|
|
271
|
+
}
|
|
272
|
+
function toMs(seconds) {
|
|
273
|
+
return seconds * 1e3;
|
|
274
|
+
}
|
|
275
|
+
function buildLogsUrl(query, fromSec, toSec, site = "datadoghq.com") {
|
|
276
|
+
const base = getAppBaseUrl(site);
|
|
277
|
+
const params = new URLSearchParams({
|
|
278
|
+
query,
|
|
279
|
+
from_ts: toMs(fromSec).toString(),
|
|
280
|
+
to_ts: toMs(toSec).toString()
|
|
281
|
+
});
|
|
282
|
+
return `${base}/logs?${params.toString()}`;
|
|
283
|
+
}
|
|
284
|
+
function extractMetricName(query) {
|
|
285
|
+
const match = query.match(/^(?:\w+:)?([a-zA-Z0-9_.]+)/);
|
|
286
|
+
return match?.[1] ?? query;
|
|
287
|
+
}
|
|
288
|
+
function buildMetricsUrl(query, fromSec, toSec, site = "datadoghq.com") {
|
|
289
|
+
const base = getAppBaseUrl(site);
|
|
290
|
+
const metricName = extractMetricName(query);
|
|
291
|
+
const params = new URLSearchParams({
|
|
292
|
+
exp_metric: metricName,
|
|
293
|
+
exp_query: query,
|
|
294
|
+
from_ts: toMs(fromSec).toString(),
|
|
295
|
+
to_ts: toMs(toSec).toString()
|
|
296
|
+
});
|
|
297
|
+
return `${base}/metric/explorer?${params.toString()}`;
|
|
298
|
+
}
|
|
299
|
+
function buildTracesUrl(query, fromSec, toSec, site = "datadoghq.com") {
|
|
300
|
+
const base = getAppBaseUrl(site);
|
|
301
|
+
const params = new URLSearchParams({
|
|
302
|
+
query,
|
|
303
|
+
start: toMs(fromSec).toString(),
|
|
304
|
+
end: toMs(toSec).toString()
|
|
305
|
+
});
|
|
306
|
+
return `${base}/apm/traces?${params.toString()}`;
|
|
307
|
+
}
|
|
308
|
+
function buildEventsUrl(query, fromSec, toSec, site = "datadoghq.com") {
|
|
309
|
+
const base = getAppBaseUrl(site);
|
|
310
|
+
const params = new URLSearchParams({
|
|
311
|
+
query,
|
|
312
|
+
from_ts: toMs(fromSec).toString(),
|
|
313
|
+
to_ts: toMs(toSec).toString()
|
|
314
|
+
});
|
|
315
|
+
return `${base}/event/explorer?${params.toString()}`;
|
|
316
|
+
}
|
|
317
|
+
function buildMonitorUrl(monitorId, site = "datadoghq.com") {
|
|
318
|
+
const base = getAppBaseUrl(site);
|
|
319
|
+
return `${base}/monitors/${monitorId}`;
|
|
320
|
+
}
|
|
321
|
+
function buildMonitorsListUrl(query, site = "datadoghq.com") {
|
|
322
|
+
const base = getAppBaseUrl(site);
|
|
323
|
+
if (query) {
|
|
324
|
+
const params = new URLSearchParams({ query });
|
|
325
|
+
return `${base}/monitors/manage?${params.toString()}`;
|
|
326
|
+
}
|
|
327
|
+
return `${base}/monitors/manage`;
|
|
328
|
+
}
|
|
329
|
+
function buildRumUrl(query, fromSec, toSec, site = "datadoghq.com") {
|
|
330
|
+
const base = getAppBaseUrl(site);
|
|
331
|
+
const params = new URLSearchParams({
|
|
332
|
+
query,
|
|
333
|
+
from_ts: toMs(fromSec).toString(),
|
|
334
|
+
to_ts: toMs(toSec).toString()
|
|
335
|
+
});
|
|
336
|
+
return `${base}/rum/explorer?${params.toString()}`;
|
|
337
|
+
}
|
|
338
|
+
function buildRumSessionUrl(applicationId, sessionId, site = "datadoghq.com") {
|
|
339
|
+
const base = getAppBaseUrl(site);
|
|
340
|
+
return `${base}/rum/replay/sessions/${sessionId}?applicationId=${encodeURIComponent(applicationId)}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// src/tools/monitors.ts
|
|
344
|
+
var ActionSchema = z2.enum(["list", "get", "search", "create", "update", "delete", "mute", "unmute"]);
|
|
345
|
+
var InputSchema = {
|
|
346
|
+
action: ActionSchema.describe("Action to perform"),
|
|
347
|
+
id: z2.string().optional().describe("Monitor ID (required for get/update/delete/mute/unmute)"),
|
|
348
|
+
query: z2.string().optional().describe("Search query (for search action)"),
|
|
349
|
+
name: z2.string().optional().describe("Filter by name (for list action)"),
|
|
350
|
+
tags: z2.array(z2.string()).optional().describe("Filter by tags"),
|
|
351
|
+
groupStates: z2.array(z2.string()).optional().describe("Filter by group states: alert, warn, no data, ok"),
|
|
352
|
+
limit: z2.number().optional().describe("Maximum number of monitors to return"),
|
|
353
|
+
config: z2.record(z2.unknown()).optional().describe("Monitor configuration (for create/update)"),
|
|
354
|
+
message: z2.string().optional().describe("Mute message (for mute action)"),
|
|
355
|
+
end: z2.number().optional().describe("Mute end timestamp (for mute action)")
|
|
356
|
+
};
|
|
357
|
+
function formatMonitor(m) {
|
|
358
|
+
return {
|
|
359
|
+
id: m.id ?? 0,
|
|
360
|
+
name: m.name ?? "",
|
|
361
|
+
type: String(m.type ?? "unknown"),
|
|
362
|
+
status: String(m.overallState ?? "unknown"),
|
|
363
|
+
message: m.message ?? "",
|
|
364
|
+
tags: m.tags ?? [],
|
|
365
|
+
query: m.query ?? "",
|
|
366
|
+
created: m.created ? new Date(m.created).toISOString() : "",
|
|
367
|
+
modified: m.modified ? new Date(m.modified).toISOString() : ""
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
async function listMonitors(api, params, limits, site) {
|
|
371
|
+
const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
|
|
372
|
+
const response = await api.listMonitors({
|
|
373
|
+
name: params.name,
|
|
374
|
+
tags: params.tags?.join(","),
|
|
375
|
+
groupStates: params.groupStates?.join(",")
|
|
376
|
+
});
|
|
377
|
+
const monitors = response.slice(0, effectiveLimit).map(formatMonitor);
|
|
378
|
+
const statusCounts = {
|
|
379
|
+
total: response.length,
|
|
380
|
+
alert: response.filter((m) => m.overallState === "Alert").length,
|
|
381
|
+
warn: response.filter((m) => m.overallState === "Warn").length,
|
|
382
|
+
ok: response.filter((m) => m.overallState === "OK").length,
|
|
383
|
+
noData: response.filter((m) => m.overallState === "No Data").length
|
|
384
|
+
};
|
|
385
|
+
return {
|
|
386
|
+
monitors,
|
|
387
|
+
summary: statusCounts,
|
|
388
|
+
datadog_url: buildMonitorsListUrl(params.name, site)
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function getMonitor(api, id, site) {
|
|
392
|
+
const monitorId = parseInt(id, 10);
|
|
393
|
+
if (isNaN(monitorId)) {
|
|
394
|
+
throw new Error(`Invalid monitor ID: ${id}`);
|
|
395
|
+
}
|
|
396
|
+
const monitor = await api.getMonitor({ monitorId });
|
|
397
|
+
return {
|
|
398
|
+
monitor: formatMonitor(monitor),
|
|
399
|
+
datadog_url: buildMonitorUrl(monitorId, site)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
async function searchMonitors(api, query, limits, site) {
|
|
403
|
+
const response = await api.searchMonitors({ query });
|
|
404
|
+
const monitors = (response.monitors ?? []).slice(0, limits.maxResults).map((m) => ({
|
|
405
|
+
id: m.id ?? 0,
|
|
406
|
+
name: m.name ?? "",
|
|
407
|
+
status: String(m.status ?? "unknown"),
|
|
408
|
+
type: m.type ?? "",
|
|
409
|
+
tags: m.tags ?? []
|
|
410
|
+
}));
|
|
411
|
+
return {
|
|
412
|
+
monitors,
|
|
413
|
+
metadata: {
|
|
414
|
+
totalCount: response.metadata?.totalCount ?? monitors.length,
|
|
415
|
+
pageCount: response.metadata?.pageCount ?? 1,
|
|
416
|
+
page: response.metadata?.page ?? 0
|
|
417
|
+
},
|
|
418
|
+
datadog_url: buildMonitorsListUrl(query, site)
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function normalizeMonitorConfig(config) {
|
|
422
|
+
const normalized = { ...config };
|
|
423
|
+
if (!normalized.name && !normalized.type && !normalized.query) {
|
|
424
|
+
throw new Error("Monitor config requires at least 'name', 'type', and 'query' fields");
|
|
425
|
+
}
|
|
426
|
+
if (normalized.options && typeof normalized.options === "object") {
|
|
427
|
+
const opts = { ...normalized.options };
|
|
428
|
+
const optionMappings = [
|
|
429
|
+
["notify_no_data", "notifyNoData"],
|
|
430
|
+
["no_data_timeframe", "noDataTimeframe"],
|
|
431
|
+
["new_host_delay", "newHostDelay"],
|
|
432
|
+
["new_group_delay", "newGroupDelay"],
|
|
433
|
+
["evaluation_delay", "evaluationDelay"],
|
|
434
|
+
["renotify_interval", "renotifyInterval"],
|
|
435
|
+
["renotify_occurrences", "renotifyOccurrences"],
|
|
436
|
+
["renotify_statuses", "renotifyStatuses"],
|
|
437
|
+
["timeout_h", "timeoutH"],
|
|
438
|
+
["notify_audit", "notifyAudit"],
|
|
439
|
+
["include_tags", "includeTags"],
|
|
440
|
+
["require_full_window", "requireFullWindow"],
|
|
441
|
+
["escalation_message", "escalationMessage"],
|
|
442
|
+
["locked", "locked"],
|
|
443
|
+
["silenced", "silenced"]
|
|
444
|
+
];
|
|
445
|
+
for (const [snake, camel] of optionMappings) {
|
|
446
|
+
if (snake in opts && !(camel in opts)) {
|
|
447
|
+
opts[camel] = opts[snake];
|
|
448
|
+
delete opts[snake];
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (opts.thresholds && typeof opts.thresholds === "object") {
|
|
452
|
+
const thresholds = { ...opts.thresholds };
|
|
453
|
+
const thresholdMappings = [
|
|
454
|
+
["critical", "critical"],
|
|
455
|
+
["warning", "warning"],
|
|
456
|
+
["ok", "ok"],
|
|
457
|
+
["critical_recovery", "criticalRecovery"],
|
|
458
|
+
["warning_recovery", "warningRecovery"]
|
|
459
|
+
];
|
|
460
|
+
for (const [snake, camel] of thresholdMappings) {
|
|
461
|
+
if (snake in thresholds && !(camel in thresholds) && snake !== camel) {
|
|
462
|
+
thresholds[camel] = thresholds[snake];
|
|
463
|
+
delete thresholds[snake];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
opts.thresholds = thresholds;
|
|
467
|
+
}
|
|
468
|
+
normalized.options = opts;
|
|
469
|
+
}
|
|
470
|
+
return normalized;
|
|
471
|
+
}
|
|
472
|
+
async function createMonitor(api, config) {
|
|
473
|
+
const body = normalizeMonitorConfig(config);
|
|
474
|
+
const monitor = await api.createMonitor({ body });
|
|
475
|
+
return {
|
|
476
|
+
success: true,
|
|
477
|
+
monitor: formatMonitor(monitor)
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function updateMonitor(api, id, config) {
|
|
481
|
+
const monitorId = parseInt(id, 10);
|
|
482
|
+
const body = normalizeMonitorConfig(config);
|
|
483
|
+
const monitor = await api.updateMonitor({ monitorId, body });
|
|
484
|
+
return {
|
|
485
|
+
success: true,
|
|
486
|
+
monitor: formatMonitor(monitor)
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
async function deleteMonitor(api, id) {
|
|
490
|
+
const monitorId = parseInt(id, 10);
|
|
491
|
+
await api.deleteMonitor({ monitorId });
|
|
492
|
+
return { success: true, message: `Monitor ${id} deleted` };
|
|
493
|
+
}
|
|
494
|
+
async function muteMonitor(api, id, params) {
|
|
495
|
+
const monitorId = parseInt(id, 10);
|
|
496
|
+
const monitor = await api.getMonitor({ monitorId });
|
|
497
|
+
await api.updateMonitor({
|
|
498
|
+
monitorId,
|
|
499
|
+
body: {
|
|
500
|
+
options: {
|
|
501
|
+
...monitor.options,
|
|
502
|
+
silenced: { "*": params.end ?? null }
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
return { success: true, message: `Monitor ${id} muted` };
|
|
507
|
+
}
|
|
508
|
+
async function unmuteMonitor(api, id) {
|
|
509
|
+
const monitorId = parseInt(id, 10);
|
|
510
|
+
const monitor = await api.getMonitor({ monitorId });
|
|
511
|
+
await api.updateMonitor({
|
|
512
|
+
monitorId,
|
|
513
|
+
body: {
|
|
514
|
+
options: {
|
|
515
|
+
...monitor.options,
|
|
516
|
+
silenced: {}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
return { success: true, message: `Monitor ${id} unmuted` };
|
|
521
|
+
}
|
|
522
|
+
function registerMonitorsTool(server, api, limits, readOnly = false, site = "datadoghq.com") {
|
|
523
|
+
server.tool(
|
|
524
|
+
"monitors",
|
|
525
|
+
`Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute.
|
|
526
|
+
Filters: name, tags, groupStates (alert/warn/ok/no data).
|
|
527
|
+
TIP: For alert HISTORY (which monitors triggered), use the events tool with tags: ["source:alert"].`,
|
|
528
|
+
InputSchema,
|
|
529
|
+
async ({ action, id, query, name, tags, groupStates, limit, config, end }) => {
|
|
530
|
+
try {
|
|
531
|
+
checkReadOnly(action, readOnly);
|
|
532
|
+
switch (action) {
|
|
533
|
+
case "list":
|
|
534
|
+
return toolResult(await listMonitors(api, { name, tags, groupStates, limit }, limits, site));
|
|
535
|
+
case "get": {
|
|
536
|
+
const monitorId = requireParam(id, "id", "get");
|
|
537
|
+
return toolResult(await getMonitor(api, monitorId, site));
|
|
538
|
+
}
|
|
539
|
+
case "search": {
|
|
540
|
+
const searchQuery = requireParam(query, "query", "search");
|
|
541
|
+
return toolResult(await searchMonitors(api, searchQuery, limits, site));
|
|
542
|
+
}
|
|
543
|
+
case "create": {
|
|
544
|
+
const monitorConfig = requireParam(config, "config", "create");
|
|
545
|
+
return toolResult(await createMonitor(api, monitorConfig));
|
|
546
|
+
}
|
|
547
|
+
case "update": {
|
|
548
|
+
const monitorId = requireParam(id, "id", "update");
|
|
549
|
+
const updateConfig = requireParam(config, "config", "update");
|
|
550
|
+
return toolResult(await updateMonitor(api, monitorId, updateConfig));
|
|
551
|
+
}
|
|
552
|
+
case "delete": {
|
|
553
|
+
const monitorId = requireParam(id, "id", "delete");
|
|
554
|
+
return toolResult(await deleteMonitor(api, monitorId));
|
|
555
|
+
}
|
|
556
|
+
case "mute": {
|
|
557
|
+
const monitorId = requireParam(id, "id", "mute");
|
|
558
|
+
return toolResult(await muteMonitor(api, monitorId, { end }));
|
|
559
|
+
}
|
|
560
|
+
case "unmute": {
|
|
561
|
+
const monitorId = requireParam(id, "id", "unmute");
|
|
562
|
+
return toolResult(await unmuteMonitor(api, monitorId));
|
|
563
|
+
}
|
|
564
|
+
default:
|
|
565
|
+
throw new Error(`Unknown action: ${action}`);
|
|
566
|
+
}
|
|
567
|
+
} catch (error) {
|
|
568
|
+
handleDatadogError(error);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// src/tools/dashboards.ts
|
|
575
|
+
import { z as z3 } from "zod";
|
|
576
|
+
var ActionSchema2 = z3.enum(["list", "get", "create", "update", "delete"]);
|
|
577
|
+
var InputSchema2 = {
|
|
578
|
+
action: ActionSchema2.describe("Action to perform"),
|
|
579
|
+
id: z3.string().optional().describe("Dashboard ID (required for get/update/delete)"),
|
|
580
|
+
name: z3.string().optional().describe("Filter by name"),
|
|
581
|
+
tags: z3.array(z3.string()).optional().describe("Filter by tags"),
|
|
582
|
+
limit: z3.number().optional().describe("Maximum number of dashboards to return"),
|
|
583
|
+
config: z3.record(z3.unknown()).optional().describe("Dashboard configuration (for create/update)")
|
|
584
|
+
};
|
|
585
|
+
function formatDashboardSummary(d) {
|
|
586
|
+
return {
|
|
587
|
+
id: d.id ?? "",
|
|
588
|
+
title: d.title ?? "",
|
|
589
|
+
description: d.description ?? "",
|
|
590
|
+
url: d.url ?? "",
|
|
591
|
+
layoutType: String(d.layoutType ?? "unknown"),
|
|
592
|
+
created: d.createdAt ? new Date(d.createdAt).toISOString() : "",
|
|
593
|
+
modified: d.modifiedAt ? new Date(d.modifiedAt).toISOString() : "",
|
|
594
|
+
authorHandle: d.authorHandle ?? ""
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
async function listDashboards(api, params, limits) {
|
|
598
|
+
const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
|
|
599
|
+
const response = await api.listDashboards({
|
|
600
|
+
filterShared: false
|
|
601
|
+
});
|
|
602
|
+
let dashboards = response.dashboards ?? [];
|
|
603
|
+
if (params.name) {
|
|
604
|
+
const lowerName = params.name.toLowerCase();
|
|
605
|
+
dashboards = dashboards.filter(
|
|
606
|
+
(d) => d.title?.toLowerCase().includes(lowerName)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const result = dashboards.slice(0, effectiveLimit).map(formatDashboardSummary);
|
|
610
|
+
return {
|
|
611
|
+
dashboards: result,
|
|
612
|
+
total: response.dashboards?.length ?? 0
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
async function getDashboard(api, id) {
|
|
616
|
+
const dashboard = await api.getDashboard({ dashboardId: id });
|
|
617
|
+
return {
|
|
618
|
+
dashboard: {
|
|
619
|
+
id: dashboard.id ?? "",
|
|
620
|
+
title: dashboard.title ?? "",
|
|
621
|
+
description: dashboard.description ?? "",
|
|
622
|
+
layoutType: String(dashboard.layoutType ?? "unknown"),
|
|
623
|
+
widgets: dashboard.widgets?.length ?? 0,
|
|
624
|
+
url: dashboard.url ?? "",
|
|
625
|
+
created: dashboard.createdAt ? new Date(dashboard.createdAt).toISOString() : "",
|
|
626
|
+
modified: dashboard.modifiedAt ? new Date(dashboard.modifiedAt).toISOString() : "",
|
|
627
|
+
authorHandle: dashboard.authorHandle ?? ""
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
function normalizeDashboardConfig(config) {
|
|
632
|
+
const normalized = { ...config };
|
|
633
|
+
if ("layout_type" in normalized && !("layoutType" in normalized)) {
|
|
634
|
+
normalized.layoutType = normalized.layout_type;
|
|
635
|
+
delete normalized.layout_type;
|
|
636
|
+
}
|
|
637
|
+
if (!normalized.layoutType) {
|
|
638
|
+
throw new Error("Dashboard config requires 'layoutType' (e.g., 'ordered', 'free')");
|
|
639
|
+
}
|
|
640
|
+
return normalized;
|
|
641
|
+
}
|
|
642
|
+
async function createDashboard(api, config) {
|
|
643
|
+
const body = normalizeDashboardConfig(config);
|
|
644
|
+
const dashboard = await api.createDashboard({ body });
|
|
645
|
+
return {
|
|
646
|
+
success: true,
|
|
647
|
+
dashboard: {
|
|
648
|
+
id: dashboard.id ?? "",
|
|
649
|
+
title: dashboard.title ?? "",
|
|
650
|
+
url: dashboard.url ?? ""
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
async function updateDashboard(api, id, config) {
|
|
655
|
+
const body = normalizeDashboardConfig(config);
|
|
656
|
+
const dashboard = await api.updateDashboard({ dashboardId: id, body });
|
|
657
|
+
return {
|
|
658
|
+
success: true,
|
|
659
|
+
dashboard: {
|
|
660
|
+
id: dashboard.id ?? "",
|
|
661
|
+
title: dashboard.title ?? "",
|
|
662
|
+
url: dashboard.url ?? ""
|
|
663
|
+
}
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
async function deleteDashboard(api, id) {
|
|
667
|
+
await api.deleteDashboard({ dashboardId: id });
|
|
668
|
+
return { success: true, message: `Dashboard ${id} deleted` };
|
|
669
|
+
}
|
|
670
|
+
function registerDashboardsTool(server, api, limits, readOnly = false) {
|
|
671
|
+
server.tool(
|
|
672
|
+
"dashboards",
|
|
673
|
+
"Access Datadog dashboards and visualizations. Actions: list (filter by name/tags), get, create, update, delete. Use for: finding existing views, team dashboards, understanding what is monitored.",
|
|
674
|
+
InputSchema2,
|
|
675
|
+
async ({ action, id, name, tags, limit, config }) => {
|
|
676
|
+
try {
|
|
677
|
+
checkReadOnly(action, readOnly);
|
|
678
|
+
switch (action) {
|
|
679
|
+
case "list":
|
|
680
|
+
return toolResult(await listDashboards(api, { name, tags, limit }, limits));
|
|
681
|
+
case "get": {
|
|
682
|
+
const dashboardId = requireParam(id, "id", "get");
|
|
683
|
+
return toolResult(await getDashboard(api, dashboardId));
|
|
684
|
+
}
|
|
685
|
+
case "create": {
|
|
686
|
+
const dashboardConfig = requireParam(config, "config", "create");
|
|
687
|
+
return toolResult(await createDashboard(api, dashboardConfig));
|
|
688
|
+
}
|
|
689
|
+
case "update": {
|
|
690
|
+
const dashboardId = requireParam(id, "id", "update");
|
|
691
|
+
const updateConfig = requireParam(config, "config", "update");
|
|
692
|
+
return toolResult(await updateDashboard(api, dashboardId, updateConfig));
|
|
693
|
+
}
|
|
694
|
+
case "delete": {
|
|
695
|
+
const dashboardId = requireParam(id, "id", "delete");
|
|
696
|
+
return toolResult(await deleteDashboard(api, dashboardId));
|
|
697
|
+
}
|
|
698
|
+
default:
|
|
699
|
+
throw new Error(`Unknown action: ${action}`);
|
|
700
|
+
}
|
|
701
|
+
} catch (error) {
|
|
702
|
+
handleDatadogError(error);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// src/tools/logs.ts
|
|
709
|
+
import { z as z4 } from "zod";
|
|
710
|
+
|
|
711
|
+
// src/utils/time.ts
|
|
712
|
+
function hoursAgo(hours) {
|
|
713
|
+
return Math.floor(Date.now() / 1e3) - hours * 3600;
|
|
714
|
+
}
|
|
715
|
+
function now() {
|
|
716
|
+
return Math.floor(Date.now() / 1e3);
|
|
717
|
+
}
|
|
718
|
+
function startOfDayAgo(days) {
|
|
719
|
+
const date = /* @__PURE__ */ new Date();
|
|
720
|
+
date.setDate(date.getDate() - days);
|
|
721
|
+
date.setHours(0, 0, 0, 0);
|
|
722
|
+
return date;
|
|
723
|
+
}
|
|
724
|
+
function parseTime(input, defaultValue) {
|
|
725
|
+
if (input === void 0) {
|
|
726
|
+
return defaultValue;
|
|
727
|
+
}
|
|
728
|
+
if (typeof input === "number") {
|
|
729
|
+
return input;
|
|
730
|
+
}
|
|
731
|
+
const trimmed = input.trim();
|
|
732
|
+
const simpleRelativeMatch = trimmed.match(/^(\d+)(s|m|h|d)$/);
|
|
733
|
+
if (simpleRelativeMatch) {
|
|
734
|
+
const value = parseInt(simpleRelativeMatch[1] ?? "0", 10);
|
|
735
|
+
const unit = simpleRelativeMatch[2];
|
|
736
|
+
const nowTs = now();
|
|
737
|
+
switch (unit) {
|
|
738
|
+
case "s":
|
|
739
|
+
return nowTs - value;
|
|
740
|
+
case "m":
|
|
741
|
+
return nowTs - value * 60;
|
|
742
|
+
case "h":
|
|
743
|
+
return nowTs - value * 3600;
|
|
744
|
+
case "d":
|
|
745
|
+
return nowTs - value * 86400;
|
|
746
|
+
default:
|
|
747
|
+
return defaultValue;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
const relativeWithTimeMatch = trimmed.match(/^(\d+)(d|h)[@\s](\d{1,2}):(\d{2})(?::(\d{2}))?$/);
|
|
751
|
+
if (relativeWithTimeMatch) {
|
|
752
|
+
const value = parseInt(relativeWithTimeMatch[1] ?? "0", 10);
|
|
753
|
+
const unit = relativeWithTimeMatch[2];
|
|
754
|
+
const hours = parseInt(relativeWithTimeMatch[3] ?? "0", 10);
|
|
755
|
+
const minutes = parseInt(relativeWithTimeMatch[4] ?? "0", 10);
|
|
756
|
+
const seconds = parseInt(relativeWithTimeMatch[5] ?? "0", 10);
|
|
757
|
+
if (unit === "d") {
|
|
758
|
+
const date3 = startOfDayAgo(value);
|
|
759
|
+
date3.setHours(hours, minutes, seconds, 0);
|
|
760
|
+
return Math.floor(date3.getTime() / 1e3);
|
|
761
|
+
}
|
|
762
|
+
const date2 = /* @__PURE__ */ new Date();
|
|
763
|
+
date2.setHours(date2.getHours() - value);
|
|
764
|
+
date2.setMinutes(minutes, seconds, 0);
|
|
765
|
+
return Math.floor(date2.getTime() / 1e3);
|
|
766
|
+
}
|
|
767
|
+
const keywordMatch = trimmed.match(/^(today|yesterday)[@\s](\d{1,2}):(\d{2})(?::(\d{2}))?$/i);
|
|
768
|
+
if (keywordMatch) {
|
|
769
|
+
const keyword = keywordMatch[1]?.toLowerCase();
|
|
770
|
+
const hours = parseInt(keywordMatch[2] ?? "0", 10);
|
|
771
|
+
const minutes = parseInt(keywordMatch[3] ?? "0", 10);
|
|
772
|
+
const seconds = parseInt(keywordMatch[4] ?? "0", 10);
|
|
773
|
+
const daysAgo = keyword === "yesterday" ? 1 : 0;
|
|
774
|
+
const date2 = startOfDayAgo(daysAgo);
|
|
775
|
+
date2.setHours(hours, minutes, seconds, 0);
|
|
776
|
+
return Math.floor(date2.getTime() / 1e3);
|
|
777
|
+
}
|
|
778
|
+
const date = new Date(trimmed);
|
|
779
|
+
if (!isNaN(date.getTime())) {
|
|
780
|
+
return Math.floor(date.getTime() / 1e3);
|
|
781
|
+
}
|
|
782
|
+
const ts = parseInt(trimmed, 10);
|
|
783
|
+
if (!isNaN(ts)) {
|
|
784
|
+
return ts;
|
|
785
|
+
}
|
|
786
|
+
return defaultValue;
|
|
787
|
+
}
|
|
788
|
+
function ensureValidTimeRange(from, to, minRangeSeconds = 60) {
|
|
789
|
+
if (from > to) {
|
|
790
|
+
[from, to] = [to, from];
|
|
791
|
+
}
|
|
792
|
+
if (to - from < minRangeSeconds) {
|
|
793
|
+
to = from + minRangeSeconds;
|
|
794
|
+
}
|
|
795
|
+
return [from, to];
|
|
796
|
+
}
|
|
797
|
+
function parseDurationToNs(input) {
|
|
798
|
+
if (input === void 0) {
|
|
799
|
+
return void 0;
|
|
800
|
+
}
|
|
801
|
+
if (typeof input === "number") {
|
|
802
|
+
return input;
|
|
803
|
+
}
|
|
804
|
+
const trimmed = input.trim().toLowerCase();
|
|
805
|
+
const match = trimmed.match(/^(\d+(?:\.\d+)?)(ns|µs|us|ms|s|m|h|d|w)?$/);
|
|
806
|
+
if (!match) {
|
|
807
|
+
const raw = parseInt(trimmed, 10);
|
|
808
|
+
return isNaN(raw) ? void 0 : raw;
|
|
809
|
+
}
|
|
810
|
+
const value = parseFloat(match[1] ?? "0");
|
|
811
|
+
const unit = match[2] ?? "ns";
|
|
812
|
+
const multipliers = {
|
|
813
|
+
"ns": 1,
|
|
814
|
+
"\xB5s": 1e3,
|
|
815
|
+
"us": 1e3,
|
|
816
|
+
"ms": 1e6,
|
|
817
|
+
"s": 1e9,
|
|
818
|
+
"m": 6e10,
|
|
819
|
+
"h": 36e11,
|
|
820
|
+
"d": 864e11,
|
|
821
|
+
"w": 6048e11
|
|
822
|
+
};
|
|
823
|
+
return Math.floor(value * (multipliers[unit] ?? 1));
|
|
824
|
+
}
|
|
825
|
+
function formatDurationNs(ns) {
|
|
826
|
+
if (ns < 1e3) return `${ns}ns`;
|
|
827
|
+
if (ns < 1e6) return `${(ns / 1e3).toFixed(1)}\xB5s`;
|
|
828
|
+
if (ns < 1e9) return `${(ns / 1e6).toFixed(1)}ms`;
|
|
829
|
+
if (ns < 6e10) return `${(ns / 1e9).toFixed(2)}s`;
|
|
830
|
+
return `${(ns / 6e10).toFixed(2)}m`;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// src/tools/logs.ts
|
|
834
|
+
var ActionSchema3 = z4.enum(["search", "aggregate"]);
|
|
835
|
+
var InputSchema3 = {
|
|
836
|
+
action: ActionSchema3.describe("Action to perform"),
|
|
837
|
+
query: z4.string().optional().describe('Log search query (Datadog syntax). Examples: "error", "service:my-service status:error", "error AND timeout"'),
|
|
838
|
+
keyword: z4.string().optional().describe("Simple text search - finds logs containing this text (grep-like). Merged with query using AND"),
|
|
839
|
+
pattern: z4.string().optional().describe('Regex pattern to match in log message (grep -E style). Example: "ERROR.*timeout|connection refused"'),
|
|
840
|
+
from: z4.string().optional().describe("Start time. Formats: ISO 8601, relative (30s, 15m, 2h, 7d), precise (3d@11:45:23, yesterday@14:00)"),
|
|
841
|
+
to: z4.string().optional().describe('End time. Same formats as "from". Example: from="3d@11:45:23" to="3d@12:55:34"'),
|
|
842
|
+
service: z4.string().optional().describe("Filter by service name"),
|
|
843
|
+
host: z4.string().optional().describe("Filter by host"),
|
|
844
|
+
status: z4.enum(["error", "warn", "info", "debug"]).optional().describe("Filter by log status/level"),
|
|
845
|
+
indexes: z4.array(z4.string()).optional().describe("Log indexes to search"),
|
|
846
|
+
limit: z4.number().optional().describe("Maximum number of logs to return"),
|
|
847
|
+
sort: z4.enum(["timestamp", "-timestamp"]).optional().describe("Sort order"),
|
|
848
|
+
sample: z4.enum(["first", "spread", "diverse"]).optional().describe("Sampling mode: first (chronological, default), spread (evenly across time range), diverse (distinct message patterns)"),
|
|
849
|
+
compact: z4.boolean().optional().describe("Strip custom attributes for token efficiency. Keeps: id, timestamp, service, status, message (truncated), dd.trace_id, error info"),
|
|
850
|
+
groupBy: z4.array(z4.string()).optional().describe("Fields to group by (for aggregate)"),
|
|
851
|
+
compute: z4.record(z4.unknown()).optional().describe("Compute operations (for aggregate)")
|
|
852
|
+
};
|
|
853
|
+
function formatLog(log) {
|
|
854
|
+
const attrs = log.attributes ?? {};
|
|
855
|
+
let timestamp = "";
|
|
856
|
+
if (attrs.timestamp) {
|
|
857
|
+
const ts = attrs.timestamp;
|
|
858
|
+
timestamp = ts instanceof Date ? ts.toISOString() : new Date(String(ts)).toISOString();
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
id: log.id ?? "",
|
|
862
|
+
timestamp,
|
|
863
|
+
service: attrs.service ?? "",
|
|
864
|
+
host: attrs.host ?? "",
|
|
865
|
+
status: attrs.status ?? "",
|
|
866
|
+
message: attrs.message ?? "",
|
|
867
|
+
tags: attrs.tags ?? [],
|
|
868
|
+
attributes: attrs.attributes ?? {}
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
function formatLogCompact(log) {
|
|
872
|
+
const attrs = log.attributes ?? {};
|
|
873
|
+
const nestedAttrs = attrs.attributes ?? {};
|
|
874
|
+
let timestamp = "";
|
|
875
|
+
if (attrs.timestamp) {
|
|
876
|
+
const ts = attrs.timestamp;
|
|
877
|
+
timestamp = ts instanceof Date ? ts.toISOString() : new Date(String(ts)).toISOString();
|
|
878
|
+
}
|
|
879
|
+
const traceId = nestedAttrs["dd.trace_id"] ?? nestedAttrs["trace_id"] ?? attrs["dd.trace_id"] ?? "";
|
|
880
|
+
const spanId = nestedAttrs["dd.span_id"] ?? nestedAttrs["span_id"] ?? attrs["dd.span_id"] ?? "";
|
|
881
|
+
const errorType = nestedAttrs["error.type"] ?? nestedAttrs["error.kind"] ?? "";
|
|
882
|
+
const errorMessage = nestedAttrs["error.message"] ?? nestedAttrs["error.msg"] ?? "";
|
|
883
|
+
const fullMessage = attrs.message ?? "";
|
|
884
|
+
const message = fullMessage.length > 500 ? fullMessage.slice(0, 500) + "..." : fullMessage;
|
|
885
|
+
const entry = {
|
|
886
|
+
id: log.id ?? "",
|
|
887
|
+
timestamp,
|
|
888
|
+
service: attrs.service ?? "",
|
|
889
|
+
host: attrs.host ?? "",
|
|
890
|
+
status: attrs.status ?? "",
|
|
891
|
+
message,
|
|
892
|
+
traceId,
|
|
893
|
+
spanId
|
|
894
|
+
};
|
|
895
|
+
if (errorType || errorMessage) {
|
|
896
|
+
entry.error = {
|
|
897
|
+
type: errorType,
|
|
898
|
+
message: errorMessage.length > 200 ? errorMessage.slice(0, 200) + "..." : errorMessage
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
return entry;
|
|
902
|
+
}
|
|
903
|
+
function normalizeToPattern(message) {
|
|
904
|
+
return message.replace(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/gi, "{UUID}").replace(/\b[0-9a-f]{16,}\b/gi, "{HEX}").replace(/\b[0-9a-f]{8,15}\b/gi, "{ID}").replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}[.\dZ]*/g, "{TS}").replace(/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/g, "{IP}").replace(/\b\d{4,}\b/g, "{N}").slice(0, 200);
|
|
905
|
+
}
|
|
906
|
+
function spreadSample(items, limit) {
|
|
907
|
+
if (items.length <= limit) return items;
|
|
908
|
+
const step = items.length / limit;
|
|
909
|
+
return Array.from({ length: limit }, (_, i) => items[Math.floor(i * step)]);
|
|
910
|
+
}
|
|
911
|
+
function diverseSample(items, limit) {
|
|
912
|
+
const seen = /* @__PURE__ */ new Map();
|
|
913
|
+
for (const item of items) {
|
|
914
|
+
const pattern = normalizeToPattern(item.message);
|
|
915
|
+
if (!seen.has(pattern)) {
|
|
916
|
+
seen.set(pattern, item);
|
|
917
|
+
if (seen.size >= limit) break;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
return {
|
|
921
|
+
samples: Array.from(seen.values()),
|
|
922
|
+
patterns: seen.size
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
function buildLogQuery(params) {
|
|
926
|
+
const parts = [];
|
|
927
|
+
if (params.query) {
|
|
928
|
+
parts.push(params.query);
|
|
929
|
+
}
|
|
930
|
+
if (params.keyword) {
|
|
931
|
+
const escaped = params.keyword.replace(/"/g, '\\"');
|
|
932
|
+
parts.push(`"${escaped}"`);
|
|
933
|
+
}
|
|
934
|
+
if (params.pattern) {
|
|
935
|
+
const escaped = params.pattern.replace(/"/g, '\\"');
|
|
936
|
+
parts.push(`@message:~"${escaped}"`);
|
|
937
|
+
}
|
|
938
|
+
if (params.service) {
|
|
939
|
+
parts.push(`service:${params.service}`);
|
|
940
|
+
}
|
|
941
|
+
if (params.host) {
|
|
942
|
+
parts.push(`host:${params.host}`);
|
|
943
|
+
}
|
|
944
|
+
if (params.status) {
|
|
945
|
+
parts.push(`status:${params.status}`);
|
|
946
|
+
}
|
|
947
|
+
return parts.length > 0 ? parts.join(" ") : "*";
|
|
948
|
+
}
|
|
949
|
+
async function searchLogs(api, params, limits, site) {
|
|
950
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
951
|
+
const defaultTo = now();
|
|
952
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
953
|
+
parseTime(params.from, defaultFrom),
|
|
954
|
+
parseTime(params.to, defaultTo)
|
|
955
|
+
);
|
|
956
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
957
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
958
|
+
const fullQuery = buildLogQuery({
|
|
959
|
+
query: params.query,
|
|
960
|
+
keyword: params.keyword,
|
|
961
|
+
pattern: params.pattern,
|
|
962
|
+
service: params.service,
|
|
963
|
+
host: params.host,
|
|
964
|
+
status: params.status
|
|
965
|
+
});
|
|
966
|
+
const requestedLimit = params.limit ?? limits.defaultLimit;
|
|
967
|
+
const sampleMode = params.sample ?? "first";
|
|
968
|
+
const fetchMultiplier = sampleMode === "first" ? 1 : 4;
|
|
969
|
+
const fetchLimit = Math.min(requestedLimit * fetchMultiplier, limits.maxLogLines);
|
|
970
|
+
const body = {
|
|
971
|
+
filter: {
|
|
972
|
+
query: fullQuery,
|
|
973
|
+
from: fromTime,
|
|
974
|
+
to: toTime,
|
|
975
|
+
indexes: params.indexes
|
|
976
|
+
},
|
|
977
|
+
sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
|
|
978
|
+
page: {
|
|
979
|
+
limit: fetchLimit
|
|
980
|
+
}
|
|
981
|
+
};
|
|
982
|
+
const response = await api.listLogs({ body });
|
|
983
|
+
const formattedLogs = params.compact ? (response.data ?? []).map(formatLogCompact) : (response.data ?? []).map(formatLog);
|
|
984
|
+
let logs;
|
|
985
|
+
let distinctPatterns;
|
|
986
|
+
switch (sampleMode) {
|
|
987
|
+
case "spread":
|
|
988
|
+
logs = spreadSample(formattedLogs, requestedLimit);
|
|
989
|
+
break;
|
|
990
|
+
case "diverse": {
|
|
991
|
+
const result = diverseSample(formattedLogs, requestedLimit);
|
|
992
|
+
logs = result.samples;
|
|
993
|
+
distinctPatterns = result.patterns;
|
|
994
|
+
break;
|
|
995
|
+
}
|
|
996
|
+
case "first":
|
|
997
|
+
default:
|
|
998
|
+
logs = formattedLogs.slice(0, requestedLimit);
|
|
999
|
+
}
|
|
1000
|
+
return {
|
|
1001
|
+
logs,
|
|
1002
|
+
meta: {
|
|
1003
|
+
count: logs.length,
|
|
1004
|
+
query: fullQuery,
|
|
1005
|
+
from: fromTime,
|
|
1006
|
+
to: toTime,
|
|
1007
|
+
compact: params.compact ?? false,
|
|
1008
|
+
sample: sampleMode,
|
|
1009
|
+
...sampleMode !== "first" && { fetched: formattedLogs.length },
|
|
1010
|
+
...distinctPatterns !== void 0 && { distinctPatterns },
|
|
1011
|
+
datadog_url: buildLogsUrl(fullQuery, validFrom, validTo, site)
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
async function aggregateLogs(api, params, limits, site) {
|
|
1016
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
1017
|
+
const defaultTo = now();
|
|
1018
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
1019
|
+
parseTime(params.from, defaultFrom),
|
|
1020
|
+
parseTime(params.to, defaultTo)
|
|
1021
|
+
);
|
|
1022
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
1023
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
1024
|
+
const computeOps = params.compute ? [params.compute] : [{ aggregation: "count", type: "total" }];
|
|
1025
|
+
const body = {
|
|
1026
|
+
filter: {
|
|
1027
|
+
query: params.query,
|
|
1028
|
+
from: fromTime,
|
|
1029
|
+
to: toTime
|
|
1030
|
+
},
|
|
1031
|
+
compute: computeOps,
|
|
1032
|
+
groupBy: params.groupBy?.map((field) => ({
|
|
1033
|
+
facet: field,
|
|
1034
|
+
limit: 10
|
|
1035
|
+
}))
|
|
1036
|
+
};
|
|
1037
|
+
const response = await api.aggregateLogs({ body });
|
|
1038
|
+
return {
|
|
1039
|
+
buckets: response.data?.buckets ?? [],
|
|
1040
|
+
meta: {
|
|
1041
|
+
query: params.query,
|
|
1042
|
+
from: fromTime,
|
|
1043
|
+
to: toTime,
|
|
1044
|
+
groupBy: params.groupBy,
|
|
1045
|
+
datadog_url: buildLogsUrl(params.query, validFrom, validTo, site)
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
function registerLogsTool(server, api, limits, site = "datadoghq.com") {
|
|
1050
|
+
server.tool(
|
|
1051
|
+
"logs",
|
|
1052
|
+
`Search Datadog logs with grep-like text filtering. Actions: search (find logs), aggregate (count/group). Key filters: keyword (text grep), pattern (regex), service, host, status (error/warn/info). Time ranges: "1h", "3d@11:45:23".
|
|
1053
|
+
CORRELATION: Logs contain dd.trace_id in attributes for linking to traces and APM metrics.
|
|
1054
|
+
SAMPLING: Use sample:"diverse" for error investigation (dedupes by message pattern), sample:"spread" for time distribution.
|
|
1055
|
+
TOKEN TIP: Use compact:true to reduce payload size (strips heavy fields) when querying large volumes.`,
|
|
1056
|
+
InputSchema3,
|
|
1057
|
+
async ({ action, query, keyword, pattern, service, host, status, from, to, indexes, limit, sort, sample, compact, groupBy, compute }) => {
|
|
1058
|
+
try {
|
|
1059
|
+
switch (action) {
|
|
1060
|
+
case "search": {
|
|
1061
|
+
return toolResult(await searchLogs(api, {
|
|
1062
|
+
query,
|
|
1063
|
+
keyword,
|
|
1064
|
+
pattern,
|
|
1065
|
+
service,
|
|
1066
|
+
host,
|
|
1067
|
+
status,
|
|
1068
|
+
from,
|
|
1069
|
+
to,
|
|
1070
|
+
indexes,
|
|
1071
|
+
limit,
|
|
1072
|
+
sort,
|
|
1073
|
+
sample,
|
|
1074
|
+
compact
|
|
1075
|
+
}, limits, site));
|
|
1076
|
+
}
|
|
1077
|
+
case "aggregate": {
|
|
1078
|
+
const aggregateQuery = requireParam(query, "query", "aggregate");
|
|
1079
|
+
return toolResult(await aggregateLogs(api, {
|
|
1080
|
+
query: aggregateQuery,
|
|
1081
|
+
from,
|
|
1082
|
+
to,
|
|
1083
|
+
groupBy,
|
|
1084
|
+
compute
|
|
1085
|
+
}, limits, site));
|
|
1086
|
+
}
|
|
1087
|
+
default:
|
|
1088
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1089
|
+
}
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
handleDatadogError(error);
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
// src/tools/metrics.ts
|
|
1098
|
+
import { z as z5 } from "zod";
|
|
1099
|
+
var ActionSchema4 = z5.enum(["query", "search", "list", "metadata"]);
|
|
1100
|
+
var InputSchema4 = {
|
|
1101
|
+
action: ActionSchema4.describe("Action to perform"),
|
|
1102
|
+
query: z5.string().optional().describe('For query: PromQL expression (e.g., "avg:system.cpu.user{*}"). For search: grep-like filter on metric names. For list: tag filter.'),
|
|
1103
|
+
from: z5.string().optional().describe("Start time (ONLY for query action). Formats: ISO 8601, relative (30s, 15m, 2h, 7d), precise (3d@11:45:23)"),
|
|
1104
|
+
to: z5.string().optional().describe('End time (ONLY for query action). Same formats as "from".'),
|
|
1105
|
+
metric: z5.string().optional().describe("Metric name (for metadata action)"),
|
|
1106
|
+
tag: z5.string().optional().describe("Filter by tag"),
|
|
1107
|
+
limit: z5.number().optional().describe("Maximum number of results (for search/list)")
|
|
1108
|
+
};
|
|
1109
|
+
async function queryMetrics(api, params, limits, site) {
|
|
1110
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
1111
|
+
const defaultTo = now();
|
|
1112
|
+
const [fromTs, toTs] = ensureValidTimeRange(
|
|
1113
|
+
parseTime(params.from, defaultFrom),
|
|
1114
|
+
parseTime(params.to, defaultTo)
|
|
1115
|
+
);
|
|
1116
|
+
const response = await api.queryMetrics({
|
|
1117
|
+
from: fromTs,
|
|
1118
|
+
to: toTs,
|
|
1119
|
+
query: params.query
|
|
1120
|
+
});
|
|
1121
|
+
const series = (response.series ?? []).map((s) => ({
|
|
1122
|
+
metric: s.metric ?? "",
|
|
1123
|
+
points: (s.pointlist ?? []).slice(0, limits.maxMetricDataPoints).map((p) => ({
|
|
1124
|
+
timestamp: p[0] ?? 0,
|
|
1125
|
+
value: p[1] ?? 0
|
|
1126
|
+
})),
|
|
1127
|
+
scope: s.scope ?? "",
|
|
1128
|
+
tags: s.tagSet ?? []
|
|
1129
|
+
}));
|
|
1130
|
+
return {
|
|
1131
|
+
series,
|
|
1132
|
+
meta: {
|
|
1133
|
+
query: params.query,
|
|
1134
|
+
from: new Date(fromTs * 1e3).toISOString(),
|
|
1135
|
+
to: new Date(toTs * 1e3).toISOString(),
|
|
1136
|
+
seriesCount: series.length,
|
|
1137
|
+
datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site)
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
async function searchMetrics(api, params, limits) {
|
|
1142
|
+
const response = await api.listActiveMetrics({
|
|
1143
|
+
from: hoursAgo(24),
|
|
1144
|
+
host: void 0,
|
|
1145
|
+
tagFilter: void 0
|
|
1146
|
+
// Must match listMetrics exactly
|
|
1147
|
+
});
|
|
1148
|
+
const allMetrics = response.metrics ?? [];
|
|
1149
|
+
const lowerQuery = params.query.toLowerCase();
|
|
1150
|
+
const filtered = allMetrics.filter((name) => name.toLowerCase().includes(lowerQuery)).slice(0, params.limit ?? limits.maxResults);
|
|
1151
|
+
return {
|
|
1152
|
+
metrics: filtered,
|
|
1153
|
+
total: filtered.length,
|
|
1154
|
+
searchedFrom: allMetrics.length
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
async function listMetrics(api, params, limits) {
|
|
1158
|
+
const response = await api.listActiveMetrics({
|
|
1159
|
+
from: hoursAgo(24),
|
|
1160
|
+
host: void 0,
|
|
1161
|
+
tagFilter: params.query
|
|
1162
|
+
});
|
|
1163
|
+
const metrics = (response.metrics ?? []).slice(0, limits.maxResults);
|
|
1164
|
+
return {
|
|
1165
|
+
metrics,
|
|
1166
|
+
total: response.metrics?.length ?? 0
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
async function getMetricMetadata(api, metricName) {
|
|
1170
|
+
const metadata = await api.getMetricMetadata({ metricName });
|
|
1171
|
+
return {
|
|
1172
|
+
metric: metricName,
|
|
1173
|
+
description: metadata.description ?? "",
|
|
1174
|
+
unit: metadata.unit ?? "",
|
|
1175
|
+
perUnit: metadata.perUnit ?? "",
|
|
1176
|
+
type: metadata.type ?? "",
|
|
1177
|
+
shortName: metadata.shortName ?? "",
|
|
1178
|
+
integration: metadata.integration ?? ""
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
function registerMetricsTool(server, metricsV1Api, metricsV2Api, limits, site = "datadoghq.com") {
|
|
1182
|
+
server.tool(
|
|
1183
|
+
"metrics",
|
|
1184
|
+
`Query Datadog metrics. Actions:
|
|
1185
|
+
- query: Get timeseries data (requires from/to time range, PromQL query)
|
|
1186
|
+
- search: Find metrics by name (grep-like, NO time param needed)
|
|
1187
|
+
- list: Get recently active metrics (last 24h, optionally filter by tag)
|
|
1188
|
+
- metadata: Get metric details (unit, type, description)
|
|
1189
|
+
|
|
1190
|
+
APM METRICS (auto-generated from traces):
|
|
1191
|
+
- trace.{service}.hits - Request count
|
|
1192
|
+
- trace.{service}.errors - Error count
|
|
1193
|
+
- trace.{service}.duration - Latency (use avg:, p95:, max:)
|
|
1194
|
+
Example: max:trace.{service}.request.duration{*}`,
|
|
1195
|
+
InputSchema4,
|
|
1196
|
+
async ({ action, query, from, to, metric, limit }) => {
|
|
1197
|
+
try {
|
|
1198
|
+
switch (action) {
|
|
1199
|
+
case "query": {
|
|
1200
|
+
const metricsQuery = requireParam(query, "query", "query");
|
|
1201
|
+
return toolResult(await queryMetrics(metricsV1Api, {
|
|
1202
|
+
query: metricsQuery,
|
|
1203
|
+
from,
|
|
1204
|
+
to
|
|
1205
|
+
}, limits, site));
|
|
1206
|
+
}
|
|
1207
|
+
case "search": {
|
|
1208
|
+
const searchQuery = requireParam(query, "query", "search");
|
|
1209
|
+
return toolResult(await searchMetrics(metricsV1Api, {
|
|
1210
|
+
query: searchQuery,
|
|
1211
|
+
limit
|
|
1212
|
+
}, limits));
|
|
1213
|
+
}
|
|
1214
|
+
case "list":
|
|
1215
|
+
return toolResult(await listMetrics(metricsV1Api, { query }, limits));
|
|
1216
|
+
case "metadata": {
|
|
1217
|
+
const metricName = requireParam(metric, "metric", "metadata");
|
|
1218
|
+
return toolResult(await getMetricMetadata(metricsV1Api, metricName));
|
|
1219
|
+
}
|
|
1220
|
+
default:
|
|
1221
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1222
|
+
}
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
handleDatadogError(error);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// src/tools/traces.ts
|
|
1231
|
+
import { z as z6 } from "zod";
|
|
1232
|
+
var ActionSchema5 = z6.enum(["search", "aggregate", "services"]);
|
|
1233
|
+
var RESERVED_SPAN_FACETS = /* @__PURE__ */ new Set([
|
|
1234
|
+
"service",
|
|
1235
|
+
"resource_name",
|
|
1236
|
+
"operation_name",
|
|
1237
|
+
"span_name",
|
|
1238
|
+
"status",
|
|
1239
|
+
"env",
|
|
1240
|
+
"host",
|
|
1241
|
+
"type",
|
|
1242
|
+
"duration",
|
|
1243
|
+
"trace_id",
|
|
1244
|
+
"span_id"
|
|
1245
|
+
]);
|
|
1246
|
+
var InputSchema5 = {
|
|
1247
|
+
action: ActionSchema5.describe("Action to perform"),
|
|
1248
|
+
query: z6.string().optional().describe('APM trace search query (Datadog syntax). Example: "@http.status_code:500", "service:my-service status:error"'),
|
|
1249
|
+
from: z6.string().optional().describe("Start time. Formats: ISO 8601, relative (30s, 15m, 2h, 7d), precise (3d@11:45:23, yesterday@14:00)"),
|
|
1250
|
+
to: z6.string().optional().describe('End time. Same formats as "from". Example: from="3d@11:45" to="3d@12:55"'),
|
|
1251
|
+
service: z6.string().optional().describe('Filter by service name. Example: "my-service", "postgres"'),
|
|
1252
|
+
operation: z6.string().optional().describe('Filter by operation name. Example: "express.request", "mongodb.query"'),
|
|
1253
|
+
resource: z6.string().optional().describe('Filter by resource name (endpoint/query). Supports wildcards. Example: "GET /api/*", "*orders*"'),
|
|
1254
|
+
status: z6.enum(["ok", "error"]).optional().describe('Filter by span status - "ok" for successful, "error" for failed spans'),
|
|
1255
|
+
env: z6.string().optional().describe('Filter by environment. Example: "production", "staging"'),
|
|
1256
|
+
minDuration: z6.string().optional().describe('Minimum span duration (find slow spans). Examples: "1s", "500ms", "100ms"'),
|
|
1257
|
+
maxDuration: z6.string().optional().describe('Maximum span duration. Examples: "5s", "1000ms"'),
|
|
1258
|
+
httpStatus: z6.string().optional().describe('HTTP status code filter. Examples: "500", "5xx" (500-599), "4xx" (400-499), ">=400"'),
|
|
1259
|
+
errorType: z6.string().optional().describe('Filter by error type (grep-like). Example: "TimeoutError", "ConnectionRefused"'),
|
|
1260
|
+
errorMessage: z6.string().optional().describe('Filter by error message (grep-like). Example: "timeout", "connection refused"'),
|
|
1261
|
+
limit: z6.number().optional().describe("Maximum number of results"),
|
|
1262
|
+
sort: z6.enum(["timestamp", "-timestamp"]).optional().describe("Sort order"),
|
|
1263
|
+
groupBy: z6.array(z6.string()).optional().describe('Fields to group by (for aggregate). Example: ["resource_name", "status"]')
|
|
1264
|
+
};
|
|
1265
|
+
function formatSpan(span) {
|
|
1266
|
+
const attrs = span.attributes ?? {};
|
|
1267
|
+
const tags = attrs.tags ?? [];
|
|
1268
|
+
const nestedAttrs = attrs.attributes ?? {};
|
|
1269
|
+
const custom = attrs.custom ?? {};
|
|
1270
|
+
const tagMap = {};
|
|
1271
|
+
for (const tag of tags) {
|
|
1272
|
+
const [key, value] = tag.split(":");
|
|
1273
|
+
if (key && value) tagMap[key] = value;
|
|
1274
|
+
}
|
|
1275
|
+
let durationNs = 0;
|
|
1276
|
+
if (attrs.startTimestamp && attrs.endTimestamp) {
|
|
1277
|
+
const startMs = attrs.startTimestamp.getTime();
|
|
1278
|
+
const endMs = attrs.endTimestamp.getTime();
|
|
1279
|
+
durationNs = (endMs - startMs) * 1e6;
|
|
1280
|
+
} else if (typeof nestedAttrs["duration"] === "number") {
|
|
1281
|
+
durationNs = nestedAttrs["duration"];
|
|
1282
|
+
} else if (typeof custom["duration"] === "number") {
|
|
1283
|
+
durationNs = custom["duration"];
|
|
1284
|
+
}
|
|
1285
|
+
const status = nestedAttrs["status"] ?? custom["status"] ?? tagMap["status"] ?? "";
|
|
1286
|
+
return {
|
|
1287
|
+
traceId: attrs.traceId ?? "",
|
|
1288
|
+
spanId: attrs.spanId ?? "",
|
|
1289
|
+
service: attrs.service ?? "",
|
|
1290
|
+
resource: attrs.resourceName ?? "",
|
|
1291
|
+
operation: nestedAttrs["operation_name"] ?? custom["operation_name"] ?? "",
|
|
1292
|
+
type: attrs.type ?? "",
|
|
1293
|
+
status,
|
|
1294
|
+
duration: formatDurationNs(durationNs),
|
|
1295
|
+
durationNs,
|
|
1296
|
+
http: {
|
|
1297
|
+
statusCode: tagMap["http.status_code"] ?? "",
|
|
1298
|
+
method: tagMap["http.method"] ?? "",
|
|
1299
|
+
url: tagMap["http.url"] ?? ""
|
|
1300
|
+
},
|
|
1301
|
+
error: {
|
|
1302
|
+
type: tagMap["error.type"] ?? "",
|
|
1303
|
+
message: tagMap["error.message"] ?? tagMap["error.msg"] ?? ""
|
|
1304
|
+
},
|
|
1305
|
+
env: attrs.env ?? tagMap["env"] ?? "",
|
|
1306
|
+
tags
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function buildTraceQuery(params) {
|
|
1310
|
+
const parts = [];
|
|
1311
|
+
if (params.query) {
|
|
1312
|
+
parts.push(params.query);
|
|
1313
|
+
}
|
|
1314
|
+
if (params.service) {
|
|
1315
|
+
parts.push(`service:${params.service}`);
|
|
1316
|
+
}
|
|
1317
|
+
if (params.operation) {
|
|
1318
|
+
parts.push(`operation_name:${params.operation}`);
|
|
1319
|
+
}
|
|
1320
|
+
if (params.resource) {
|
|
1321
|
+
if (params.resource.includes("*") || params.resource.includes(" ")) {
|
|
1322
|
+
parts.push(`resource_name:${params.resource}`);
|
|
1323
|
+
} else {
|
|
1324
|
+
parts.push(`resource_name:${params.resource}`);
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
if (params.status) {
|
|
1328
|
+
parts.push(`status:${params.status}`);
|
|
1329
|
+
}
|
|
1330
|
+
if (params.env) {
|
|
1331
|
+
parts.push(`env:${params.env}`);
|
|
1332
|
+
}
|
|
1333
|
+
if (params.minDuration) {
|
|
1334
|
+
const ns = parseDurationToNs(params.minDuration);
|
|
1335
|
+
if (ns !== void 0) {
|
|
1336
|
+
parts.push(`@duration:>=${ns}`);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
if (params.maxDuration) {
|
|
1340
|
+
const ns = parseDurationToNs(params.maxDuration);
|
|
1341
|
+
if (ns !== void 0) {
|
|
1342
|
+
parts.push(`@duration:<=${ns}`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
if (params.httpStatus) {
|
|
1346
|
+
const status = params.httpStatus.toLowerCase();
|
|
1347
|
+
if (status.endsWith("xx")) {
|
|
1348
|
+
const base = parseInt(status[0] ?? "0", 10) * 100;
|
|
1349
|
+
parts.push(`@http.status_code:[${base} TO ${base + 99}]`);
|
|
1350
|
+
} else if (status.startsWith(">=")) {
|
|
1351
|
+
parts.push(`@http.status_code:>=${status.slice(2)}`);
|
|
1352
|
+
} else if (status.startsWith(">")) {
|
|
1353
|
+
parts.push(`@http.status_code:>${status.slice(1)}`);
|
|
1354
|
+
} else if (status.startsWith("<=")) {
|
|
1355
|
+
parts.push(`@http.status_code:<=${status.slice(2)}`);
|
|
1356
|
+
} else if (status.startsWith("<")) {
|
|
1357
|
+
parts.push(`@http.status_code:<${status.slice(1)}`);
|
|
1358
|
+
} else {
|
|
1359
|
+
parts.push(`@http.status_code:${params.httpStatus}`);
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
if (params.errorType) {
|
|
1363
|
+
const escaped = params.errorType.replace(/"/g, '\\"');
|
|
1364
|
+
parts.push(`error.type:*${escaped}*`);
|
|
1365
|
+
}
|
|
1366
|
+
if (params.errorMessage) {
|
|
1367
|
+
const escaped = params.errorMessage.replace(/"/g, '\\"');
|
|
1368
|
+
parts.push(`error.message:*${escaped}*`);
|
|
1369
|
+
}
|
|
1370
|
+
return parts.length > 0 ? parts.join(" ") : "*";
|
|
1371
|
+
}
|
|
1372
|
+
async function searchTraces(api, params, limits, site) {
|
|
1373
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
1374
|
+
const defaultTo = now();
|
|
1375
|
+
const fullQuery = buildTraceQuery({
|
|
1376
|
+
query: params.query,
|
|
1377
|
+
service: params.service,
|
|
1378
|
+
operation: params.operation,
|
|
1379
|
+
resource: params.resource,
|
|
1380
|
+
status: params.status,
|
|
1381
|
+
env: params.env,
|
|
1382
|
+
minDuration: params.minDuration,
|
|
1383
|
+
maxDuration: params.maxDuration,
|
|
1384
|
+
httpStatus: params.httpStatus,
|
|
1385
|
+
errorType: params.errorType,
|
|
1386
|
+
errorMessage: params.errorMessage
|
|
1387
|
+
});
|
|
1388
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
1389
|
+
parseTime(params.from, defaultFrom),
|
|
1390
|
+
parseTime(params.to, defaultTo)
|
|
1391
|
+
);
|
|
1392
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
1393
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
1394
|
+
const body = {
|
|
1395
|
+
data: {
|
|
1396
|
+
type: "search_request",
|
|
1397
|
+
attributes: {
|
|
1398
|
+
filter: {
|
|
1399
|
+
query: fullQuery,
|
|
1400
|
+
from: fromTime,
|
|
1401
|
+
to: toTime
|
|
1402
|
+
},
|
|
1403
|
+
sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
|
|
1404
|
+
page: {
|
|
1405
|
+
limit: Math.min(params.limit ?? limits.maxResults, limits.maxResults)
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
};
|
|
1410
|
+
const response = await api.listSpans({ body });
|
|
1411
|
+
const spans = (response.data ?? []).map(formatSpan);
|
|
1412
|
+
return {
|
|
1413
|
+
spans,
|
|
1414
|
+
meta: {
|
|
1415
|
+
count: spans.length,
|
|
1416
|
+
query: fullQuery,
|
|
1417
|
+
from: fromTime,
|
|
1418
|
+
to: toTime,
|
|
1419
|
+
datadog_url: buildTracesUrl(fullQuery, validFrom, validTo, site)
|
|
1420
|
+
}
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
async function aggregateTraces(api, params, limits, site) {
|
|
1424
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
1425
|
+
const defaultTo = now();
|
|
1426
|
+
const fullQuery = buildTraceQuery({
|
|
1427
|
+
query: params.query,
|
|
1428
|
+
service: params.service,
|
|
1429
|
+
operation: params.operation,
|
|
1430
|
+
resource: params.resource,
|
|
1431
|
+
status: params.status,
|
|
1432
|
+
env: params.env,
|
|
1433
|
+
minDuration: params.minDuration,
|
|
1434
|
+
maxDuration: params.maxDuration,
|
|
1435
|
+
httpStatus: params.httpStatus,
|
|
1436
|
+
errorType: params.errorType,
|
|
1437
|
+
errorMessage: params.errorMessage
|
|
1438
|
+
});
|
|
1439
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
1440
|
+
parseTime(params.from, defaultFrom),
|
|
1441
|
+
parseTime(params.to, defaultTo)
|
|
1442
|
+
);
|
|
1443
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
1444
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
1445
|
+
const body = {
|
|
1446
|
+
data: {
|
|
1447
|
+
type: "aggregate_request",
|
|
1448
|
+
attributes: {
|
|
1449
|
+
filter: {
|
|
1450
|
+
query: fullQuery,
|
|
1451
|
+
from: fromTime,
|
|
1452
|
+
to: toTime
|
|
1453
|
+
},
|
|
1454
|
+
compute: [{ aggregation: "count", type: "total" }],
|
|
1455
|
+
groupBy: params.groupBy?.map((field) => ({
|
|
1456
|
+
facet: RESERVED_SPAN_FACETS.has(field) || field.startsWith("@") ? field : `@${field}`,
|
|
1457
|
+
limit: 10
|
|
1458
|
+
}))
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
};
|
|
1462
|
+
const response = await api.aggregateSpans({ body });
|
|
1463
|
+
return {
|
|
1464
|
+
data: response.data ?? [],
|
|
1465
|
+
meta: {
|
|
1466
|
+
query: fullQuery,
|
|
1467
|
+
from: fromTime,
|
|
1468
|
+
to: toTime,
|
|
1469
|
+
groupBy: params.groupBy,
|
|
1470
|
+
datadog_url: buildTracesUrl(fullQuery, validFrom, validTo, site)
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
}
|
|
1474
|
+
async function listApmServices(api, params, limits) {
|
|
1475
|
+
const defaultFrom = hoursAgo(24);
|
|
1476
|
+
const defaultTo = now();
|
|
1477
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
1478
|
+
parseTime(params.from, defaultFrom),
|
|
1479
|
+
parseTime(params.to, defaultTo)
|
|
1480
|
+
);
|
|
1481
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
1482
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
1483
|
+
const query = params.env ? `env:${params.env}` : "*";
|
|
1484
|
+
const body = {
|
|
1485
|
+
data: {
|
|
1486
|
+
type: "aggregate_request",
|
|
1487
|
+
attributes: {
|
|
1488
|
+
filter: {
|
|
1489
|
+
query,
|
|
1490
|
+
from: fromTime,
|
|
1491
|
+
to: toTime
|
|
1492
|
+
},
|
|
1493
|
+
compute: [{ aggregation: "count", type: "total" }],
|
|
1494
|
+
groupBy: [{
|
|
1495
|
+
facet: "service",
|
|
1496
|
+
limit: limits.maxResults
|
|
1497
|
+
}]
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
const response = await api.aggregateSpans({ body });
|
|
1502
|
+
const buckets = response.data ?? [];
|
|
1503
|
+
const services = buckets.map((bucket) => ({
|
|
1504
|
+
name: bucket.attributes?.by?.["service"] ?? "",
|
|
1505
|
+
spanCount: bucket.attributes?.computes?.["c0"] ?? 0
|
|
1506
|
+
})).filter((s) => s.name !== "");
|
|
1507
|
+
return {
|
|
1508
|
+
services,
|
|
1509
|
+
total: services.length,
|
|
1510
|
+
meta: {
|
|
1511
|
+
query,
|
|
1512
|
+
env: params.env ?? "all",
|
|
1513
|
+
from: fromTime,
|
|
1514
|
+
to: toTime
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
function registerTracesTool(server, spansApi, _servicesApi, limits, site = "datadoghq.com") {
|
|
1519
|
+
server.tool(
|
|
1520
|
+
"traces",
|
|
1521
|
+
`Analyze APM traces for request flow and latency debugging. Actions: search (find spans), aggregate (group stats), services (list APM services). Key filters: minDuration/maxDuration ("500ms", "2s"), httpStatus ("5xx", ">=400"), status (ok/error), errorMessage (grep).
|
|
1522
|
+
APM METRICS: Traces auto-generate metrics in trace.{service}.* namespace. Use metrics tool to query: avg:trace.{service}.request.duration{*}`,
|
|
1523
|
+
InputSchema5,
|
|
1524
|
+
async ({ action, query, from, to, service, operation, resource, status, env, minDuration, maxDuration, httpStatus, errorType, errorMessage, limit, sort, groupBy }) => {
|
|
1525
|
+
try {
|
|
1526
|
+
switch (action) {
|
|
1527
|
+
case "search": {
|
|
1528
|
+
return toolResult(await searchTraces(spansApi, {
|
|
1529
|
+
query,
|
|
1530
|
+
from,
|
|
1531
|
+
to,
|
|
1532
|
+
service,
|
|
1533
|
+
operation,
|
|
1534
|
+
resource,
|
|
1535
|
+
status,
|
|
1536
|
+
env,
|
|
1537
|
+
minDuration,
|
|
1538
|
+
maxDuration,
|
|
1539
|
+
httpStatus,
|
|
1540
|
+
errorType,
|
|
1541
|
+
errorMessage,
|
|
1542
|
+
limit,
|
|
1543
|
+
sort
|
|
1544
|
+
}, limits, site));
|
|
1545
|
+
}
|
|
1546
|
+
case "aggregate": {
|
|
1547
|
+
return toolResult(await aggregateTraces(spansApi, {
|
|
1548
|
+
query,
|
|
1549
|
+
from,
|
|
1550
|
+
to,
|
|
1551
|
+
service,
|
|
1552
|
+
operation,
|
|
1553
|
+
resource,
|
|
1554
|
+
status,
|
|
1555
|
+
env,
|
|
1556
|
+
minDuration,
|
|
1557
|
+
maxDuration,
|
|
1558
|
+
httpStatus,
|
|
1559
|
+
errorType,
|
|
1560
|
+
errorMessage,
|
|
1561
|
+
groupBy
|
|
1562
|
+
}, limits, site));
|
|
1563
|
+
}
|
|
1564
|
+
case "services":
|
|
1565
|
+
return toolResult(await listApmServices(spansApi, { env, from, to }, limits));
|
|
1566
|
+
default:
|
|
1567
|
+
throw new Error(`Unknown action: ${action}`);
|
|
1568
|
+
}
|
|
1569
|
+
} catch (error) {
|
|
1570
|
+
handleDatadogError(error);
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
// src/tools/events.ts
|
|
1577
|
+
import { z as z7 } from "zod";
|
|
1578
|
+
var ActionSchema6 = z7.enum(["list", "get", "create", "search", "aggregate", "top", "timeseries", "incidents"]);
|
|
1579
|
+
var InputSchema6 = {
|
|
1580
|
+
action: ActionSchema6.describe("Action to perform"),
|
|
1581
|
+
id: z7.string().optional().describe("Event ID (for get action)"),
|
|
1582
|
+
query: z7.string().optional().describe("Search query"),
|
|
1583
|
+
from: z7.string().optional().describe('Start time (ISO 8601, relative like "1h", or Unix timestamp)'),
|
|
1584
|
+
to: z7.string().optional().describe('End time (ISO 8601, relative like "1h", or Unix timestamp)'),
|
|
1585
|
+
priority: z7.enum(["normal", "low"]).optional().describe("Event priority"),
|
|
1586
|
+
sources: z7.array(z7.string()).optional().describe("Filter by sources"),
|
|
1587
|
+
tags: z7.array(z7.string()).optional().describe("Filter by tags"),
|
|
1588
|
+
limit: z7.number().optional().describe("Maximum number of events to return"),
|
|
1589
|
+
title: z7.string().optional().describe("Event title (for create)"),
|
|
1590
|
+
text: z7.string().optional().describe("Event text (for create)"),
|
|
1591
|
+
alertType: z7.enum(["error", "warning", "info", "success"]).optional().describe("Alert type (for create)"),
|
|
1592
|
+
groupBy: z7.array(z7.string()).optional().describe("Fields to group by: monitor_name, priority, alert_type, source"),
|
|
1593
|
+
cursor: z7.string().optional().describe("Pagination cursor from previous response"),
|
|
1594
|
+
// Phase 2: Timeseries
|
|
1595
|
+
interval: z7.string().optional().describe("Time bucket interval for timeseries: 1h, 4h, 1d (default: 1h)"),
|
|
1596
|
+
// Phase 2: Incidents deduplication
|
|
1597
|
+
dedupeWindow: z7.string().optional().describe("Deduplication window for incidents: 5m, 15m, 1h (default: 5m)"),
|
|
1598
|
+
// Phase 3: Monitor enrichment
|
|
1599
|
+
enrich: z7.boolean().optional().describe("Enrich events with monitor metadata (slower, adds monitor details)")
|
|
1600
|
+
};
|
|
1601
|
+
function extractMonitorInfo(title) {
|
|
1602
|
+
const priorityMatch = title.match(/^\[P(\d+)\]\s*/);
|
|
1603
|
+
const priority = priorityMatch ? `P${priorityMatch[1]}` : void 0;
|
|
1604
|
+
const withoutPriority = title.replace(/^\[P\d+\]\s*/, "");
|
|
1605
|
+
const match = withoutPriority.match(
|
|
1606
|
+
/^\[(Triggered|Recovered|Warn|Alert|OK|No Data|Re-Triggered|Renotify)(?:\s+on\s+\{([^}]+)\})?\]\s*(.+)$/i
|
|
1607
|
+
);
|
|
1608
|
+
if (match) {
|
|
1609
|
+
return {
|
|
1610
|
+
status: match[1] ?? "",
|
|
1611
|
+
scope: match[2] ?? "",
|
|
1612
|
+
name: match[3]?.trim() ?? title,
|
|
1613
|
+
priority
|
|
1614
|
+
};
|
|
1615
|
+
}
|
|
1616
|
+
return { status: "", scope: "", name: title, priority };
|
|
1617
|
+
}
|
|
1618
|
+
function extractTitleFromMessage(message) {
|
|
1619
|
+
if (!message) return "";
|
|
1620
|
+
const content = message.replace(/^%%%\s*\n?/, "").trim();
|
|
1621
|
+
const firstLine = content.split("\n")[0]?.trim() ?? "";
|
|
1622
|
+
return firstLine.replace(/\s+!?\s*$/, "").trim();
|
|
1623
|
+
}
|
|
1624
|
+
function extractMonitorIdFromMessage(message) {
|
|
1625
|
+
if (!message) return void 0;
|
|
1626
|
+
const match = message.match(/\/monitors\/(\d+)/);
|
|
1627
|
+
if (match && match[1]) {
|
|
1628
|
+
const id = parseInt(match[1], 10);
|
|
1629
|
+
return isNaN(id) ? void 0 : id;
|
|
1630
|
+
}
|
|
1631
|
+
return void 0;
|
|
1632
|
+
}
|
|
1633
|
+
function buildGroupKey(event, groupBy) {
|
|
1634
|
+
const parts = [];
|
|
1635
|
+
for (const field of groupBy) {
|
|
1636
|
+
switch (field) {
|
|
1637
|
+
case "monitor_name":
|
|
1638
|
+
parts.push(event.monitorInfo?.name ?? event.title);
|
|
1639
|
+
break;
|
|
1640
|
+
case "monitor_id":
|
|
1641
|
+
parts.push(event.monitorId?.toString() ?? "");
|
|
1642
|
+
break;
|
|
1643
|
+
case "priority":
|
|
1644
|
+
parts.push(event.monitorInfo?.priority ?? event.priority);
|
|
1645
|
+
break;
|
|
1646
|
+
case "source":
|
|
1647
|
+
parts.push(event.source);
|
|
1648
|
+
break;
|
|
1649
|
+
case "alert_type":
|
|
1650
|
+
parts.push(event.alertType);
|
|
1651
|
+
break;
|
|
1652
|
+
case "status":
|
|
1653
|
+
parts.push(event.monitorInfo?.status ?? "");
|
|
1654
|
+
break;
|
|
1655
|
+
case "host":
|
|
1656
|
+
parts.push(event.host);
|
|
1657
|
+
break;
|
|
1658
|
+
default: {
|
|
1659
|
+
const tagValue = event.tags.find((t) => t.startsWith(`${field}:`))?.split(":")[1] ?? "";
|
|
1660
|
+
parts.push(tagValue);
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return parts.join("|");
|
|
1665
|
+
}
|
|
1666
|
+
function formatEventV1(e) {
|
|
1667
|
+
const event = e;
|
|
1668
|
+
return {
|
|
1669
|
+
id: e.id ?? 0,
|
|
1670
|
+
title: e.title ?? "",
|
|
1671
|
+
text: e.text ?? "",
|
|
1672
|
+
dateHappened: e.dateHappened ? new Date(e.dateHappened * 1e3).toISOString() : "",
|
|
1673
|
+
priority: String(e.priority ?? "normal"),
|
|
1674
|
+
source: event.sourceTypeName ?? "",
|
|
1675
|
+
tags: e.tags ?? [],
|
|
1676
|
+
alertType: String(e.alertType ?? "info"),
|
|
1677
|
+
host: e.host ?? ""
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
function formatEventV2(e) {
|
|
1681
|
+
const attrs = e.attributes ?? {};
|
|
1682
|
+
let timestamp = "";
|
|
1683
|
+
if (attrs.timestamp) {
|
|
1684
|
+
const ts = attrs.timestamp;
|
|
1685
|
+
timestamp = ts instanceof Date ? ts.toISOString() : new Date(String(ts)).toISOString();
|
|
1686
|
+
}
|
|
1687
|
+
const message = attrs.message ?? "";
|
|
1688
|
+
let title = attrs.title ?? "";
|
|
1689
|
+
if (!title && message) {
|
|
1690
|
+
title = extractTitleFromMessage(message);
|
|
1691
|
+
}
|
|
1692
|
+
const monitorInfo = extractMonitorInfo(title);
|
|
1693
|
+
const monitorId = extractMonitorIdFromMessage(message);
|
|
1694
|
+
const tags = attrs.tags ?? [];
|
|
1695
|
+
const sourceTag = tags.find((t) => t.startsWith("source:"));
|
|
1696
|
+
const source = sourceTag?.split(":")[1] ?? "";
|
|
1697
|
+
const alertTypeTag = tags.find((t) => t.startsWith("alert_type:"));
|
|
1698
|
+
const alertType = alertTypeTag?.split(":")[1] ?? "";
|
|
1699
|
+
const hostTag = tags.find((t) => t.startsWith("host:"));
|
|
1700
|
+
const host = hostTag?.split(":")[1] ?? "";
|
|
1701
|
+
const priorityTag = tags.find((t) => t.startsWith("priority:"));
|
|
1702
|
+
const priority = priorityTag?.split(":")[1] ?? "normal";
|
|
1703
|
+
return {
|
|
1704
|
+
id: String(e.id ?? ""),
|
|
1705
|
+
title,
|
|
1706
|
+
message,
|
|
1707
|
+
timestamp,
|
|
1708
|
+
priority,
|
|
1709
|
+
source,
|
|
1710
|
+
tags,
|
|
1711
|
+
alertType,
|
|
1712
|
+
host,
|
|
1713
|
+
monitorId,
|
|
1714
|
+
monitorInfo: monitorInfo.name !== title ? {
|
|
1715
|
+
name: monitorInfo.name,
|
|
1716
|
+
status: monitorInfo.status,
|
|
1717
|
+
scope: monitorInfo.scope,
|
|
1718
|
+
priority: monitorInfo.priority
|
|
1719
|
+
} : void 0
|
|
1720
|
+
};
|
|
1721
|
+
}
|
|
1722
|
+
async function listEventsV1(api, params, limits) {
|
|
1723
|
+
const effectiveLimit = Math.min(params.limit ?? limits.defaultLimit, limits.maxResults);
|
|
1724
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
1725
|
+
const defaultTo = now();
|
|
1726
|
+
const response = await api.listEvents({
|
|
1727
|
+
start: parseTime(params.from, defaultFrom),
|
|
1728
|
+
end: parseTime(params.to, defaultTo),
|
|
1729
|
+
priority: params.priority === "low" ? "low" : "normal",
|
|
1730
|
+
sources: params.sources?.join(","),
|
|
1731
|
+
tags: params.tags?.join(","),
|
|
1732
|
+
unaggregated: true
|
|
1733
|
+
});
|
|
1734
|
+
let events = response.events ?? [];
|
|
1735
|
+
if (params.query) {
|
|
1736
|
+
const lowerQuery = params.query.toLowerCase();
|
|
1737
|
+
events = events.filter(
|
|
1738
|
+
(e) => e.title?.toLowerCase().includes(lowerQuery) || e.text?.toLowerCase().includes(lowerQuery)
|
|
1739
|
+
);
|
|
1740
|
+
}
|
|
1741
|
+
const result = events.slice(0, effectiveLimit).map(formatEventV1);
|
|
1742
|
+
return {
|
|
1743
|
+
events: result,
|
|
1744
|
+
total: events.length
|
|
1745
|
+
};
|
|
1746
|
+
}
|
|
1747
|
+
async function getEventV1(api, id) {
|
|
1748
|
+
const eventId = parseInt(id, 10);
|
|
1749
|
+
if (isNaN(eventId)) {
|
|
1750
|
+
throw new Error(`Invalid event ID: ${id}`);
|
|
1751
|
+
}
|
|
1752
|
+
const response = await api.getEvent({ eventId });
|
|
1753
|
+
return { event: formatEventV1(response.event ?? {}) };
|
|
1754
|
+
}
|
|
1755
|
+
async function createEventV1(api, params) {
|
|
1756
|
+
const body = {
|
|
1757
|
+
title: params.title,
|
|
1758
|
+
text: params.text,
|
|
1759
|
+
priority: params.priority === "low" ? "low" : "normal",
|
|
1760
|
+
tags: params.tags,
|
|
1761
|
+
alertType: params.alertType ?? "info"
|
|
1762
|
+
};
|
|
1763
|
+
const response = await api.createEvent({ body });
|
|
1764
|
+
return {
|
|
1765
|
+
success: true,
|
|
1766
|
+
event: {
|
|
1767
|
+
id: response.event?.id ?? 0,
|
|
1768
|
+
title: response.event?.title ?? "",
|
|
1769
|
+
status: response.status ?? ""
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
}
|
|
1773
|
+
function buildEventQuery(params) {
|
|
1774
|
+
const parts = [];
|
|
1775
|
+
if (params.query) {
|
|
1776
|
+
parts.push(params.query);
|
|
1777
|
+
}
|
|
1778
|
+
if (params.sources && params.sources.length > 0) {
|
|
1779
|
+
const sourceFilter = params.sources.map((s) => `source:${s}`).join(" OR ");
|
|
1780
|
+
parts.push(`(${sourceFilter})`);
|
|
1781
|
+
}
|
|
1782
|
+
if (params.tags && params.tags.length > 0) {
|
|
1783
|
+
for (const tag of params.tags) {
|
|
1784
|
+
parts.push(tag);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
if (params.priority) {
|
|
1788
|
+
parts.push(`priority:${params.priority}`);
|
|
1789
|
+
}
|
|
1790
|
+
return parts.length > 0 ? parts.join(" ") : "*";
|
|
1791
|
+
}
|
|
1792
|
+
async function searchEventsV2(api, params, limits, site) {
|
|
1793
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
1794
|
+
const defaultTo = now();
|
|
1795
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
1796
|
+
parseTime(params.from, defaultFrom),
|
|
1797
|
+
parseTime(params.to, defaultTo)
|
|
1798
|
+
);
|
|
1799
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
1800
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
1801
|
+
const fullQuery = buildEventQuery({
|
|
1802
|
+
query: params.query,
|
|
1803
|
+
sources: params.sources,
|
|
1804
|
+
tags: params.tags,
|
|
1805
|
+
priority: params.priority
|
|
1806
|
+
});
|
|
1807
|
+
const effectiveLimit = Math.min(params.limit ?? limits.defaultLimit, limits.maxResults);
|
|
1808
|
+
const body = {
|
|
1809
|
+
filter: {
|
|
1810
|
+
query: fullQuery,
|
|
1811
|
+
from: fromTime,
|
|
1812
|
+
to: toTime
|
|
1813
|
+
},
|
|
1814
|
+
sort: "timestamp",
|
|
1815
|
+
page: {
|
|
1816
|
+
limit: effectiveLimit,
|
|
1817
|
+
cursor: params.cursor
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
const response = await api.searchEvents({ body });
|
|
1821
|
+
const events = (response.data ?? []).map(formatEventV2);
|
|
1822
|
+
const nextCursor = response.meta?.page?.after;
|
|
1823
|
+
return {
|
|
1824
|
+
events,
|
|
1825
|
+
meta: {
|
|
1826
|
+
count: events.length,
|
|
1827
|
+
query: fullQuery,
|
|
1828
|
+
from: fromTime,
|
|
1829
|
+
to: toTime,
|
|
1830
|
+
nextCursor,
|
|
1831
|
+
datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
|
|
1832
|
+
}
|
|
1833
|
+
};
|
|
1834
|
+
}
|
|
1835
|
+
async function aggregateEventsV2(api, params, limits, site) {
|
|
1836
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1837
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
1838
|
+
const defaultTo = now();
|
|
1839
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
1840
|
+
parseTime(params.from, defaultFrom),
|
|
1841
|
+
parseTime(params.to, defaultTo)
|
|
1842
|
+
);
|
|
1843
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
1844
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
1845
|
+
const fullQuery = buildEventQuery({
|
|
1846
|
+
query: params.query,
|
|
1847
|
+
sources: params.sources,
|
|
1848
|
+
tags: params.tags
|
|
1849
|
+
});
|
|
1850
|
+
const groupByFields = params.groupBy ?? ["monitor_name"];
|
|
1851
|
+
const maxEventsToAggregate = 1e4;
|
|
1852
|
+
let eventCount = 0;
|
|
1853
|
+
let pageCount = 0;
|
|
1854
|
+
const maxPages = 100;
|
|
1855
|
+
const body = {
|
|
1856
|
+
filter: {
|
|
1857
|
+
query: fullQuery,
|
|
1858
|
+
from: fromTime,
|
|
1859
|
+
to: toTime
|
|
1860
|
+
},
|
|
1861
|
+
sort: "timestamp",
|
|
1862
|
+
page: {
|
|
1863
|
+
limit: 1e3
|
|
1864
|
+
// Max per page
|
|
1865
|
+
}
|
|
1866
|
+
};
|
|
1867
|
+
let cursor;
|
|
1868
|
+
while (pageCount < maxPages && eventCount < maxEventsToAggregate) {
|
|
1869
|
+
const pageBody = { ...body, page: { ...body.page, cursor } };
|
|
1870
|
+
const response = await api.searchEvents({ body: pageBody });
|
|
1871
|
+
const events = response.data ?? [];
|
|
1872
|
+
if (events.length === 0) break;
|
|
1873
|
+
for (const event of events) {
|
|
1874
|
+
const formatted = formatEventV2(event);
|
|
1875
|
+
const groupKey = buildGroupKey(formatted, groupByFields);
|
|
1876
|
+
const existing = counts.get(groupKey);
|
|
1877
|
+
if (existing) {
|
|
1878
|
+
existing.count++;
|
|
1879
|
+
} else {
|
|
1880
|
+
counts.set(groupKey, { count: 1, sample: formatted });
|
|
1881
|
+
}
|
|
1882
|
+
eventCount++;
|
|
1883
|
+
if (eventCount >= maxEventsToAggregate) break;
|
|
1884
|
+
}
|
|
1885
|
+
cursor = response.meta?.page?.after;
|
|
1886
|
+
if (!cursor) break;
|
|
1887
|
+
pageCount++;
|
|
1888
|
+
}
|
|
1889
|
+
const effectiveLimit = Math.min(params.limit ?? 100, 1e3);
|
|
1890
|
+
const sorted = [...counts.entries()].sort((a, b) => b[1].count - a[1].count).slice(0, effectiveLimit);
|
|
1891
|
+
const buckets = sorted.map(([key, data]) => ({
|
|
1892
|
+
key,
|
|
1893
|
+
count: data.count,
|
|
1894
|
+
sample: data.sample
|
|
1895
|
+
}));
|
|
1896
|
+
return {
|
|
1897
|
+
buckets,
|
|
1898
|
+
meta: {
|
|
1899
|
+
query: fullQuery,
|
|
1900
|
+
from: fromTime,
|
|
1901
|
+
to: toTime,
|
|
1902
|
+
groupBy: groupByFields,
|
|
1903
|
+
totalGroups: counts.size,
|
|
1904
|
+
totalEvents: eventCount,
|
|
1905
|
+
truncated: eventCount >= maxEventsToAggregate,
|
|
1906
|
+
datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
}
|
|
1910
|
+
async function topEventsV2(api, params, limits, site) {
|
|
1911
|
+
const effectiveQuery = params.query ?? "source:alert";
|
|
1912
|
+
const effectiveTags = params.tags ?? ["source:alert"];
|
|
1913
|
+
const result = await aggregateEventsV2(api, {
|
|
1914
|
+
...params,
|
|
1915
|
+
query: effectiveQuery,
|
|
1916
|
+
tags: effectiveTags,
|
|
1917
|
+
groupBy: params.groupBy ?? ["monitor_name"],
|
|
1918
|
+
limit: params.limit ?? 10
|
|
1919
|
+
}, limits, site);
|
|
1920
|
+
return {
|
|
1921
|
+
top: result.buckets.map((bucket, index) => ({
|
|
1922
|
+
rank: index + 1,
|
|
1923
|
+
name: bucket.key,
|
|
1924
|
+
monitorId: bucket.sample.monitorId,
|
|
1925
|
+
alertCount: bucket.count,
|
|
1926
|
+
lastAlert: bucket.sample.timestamp,
|
|
1927
|
+
sample: {
|
|
1928
|
+
title: bucket.sample.title,
|
|
1929
|
+
source: bucket.sample.source,
|
|
1930
|
+
alertType: bucket.sample.alertType
|
|
1931
|
+
}
|
|
1932
|
+
})),
|
|
1933
|
+
meta: result.meta
|
|
1934
|
+
};
|
|
1935
|
+
}
|
|
1936
|
+
function parseIntervalToMs(interval) {
|
|
1937
|
+
const ns = parseDurationToNs(interval ?? "1h");
|
|
1938
|
+
return ns ? Math.floor(ns / 1e6) : 36e5;
|
|
1939
|
+
}
|
|
1940
|
+
async function timeseriesEventsV2(api, params, limits, site) {
|
|
1941
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
1942
|
+
const defaultTo = now();
|
|
1943
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
1944
|
+
parseTime(params.from, defaultFrom),
|
|
1945
|
+
parseTime(params.to, defaultTo)
|
|
1946
|
+
);
|
|
1947
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
1948
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
1949
|
+
const fullQuery = buildEventQuery({
|
|
1950
|
+
query: params.query ?? "source:alert",
|
|
1951
|
+
sources: params.sources,
|
|
1952
|
+
tags: params.tags ?? ["source:alert"]
|
|
1953
|
+
});
|
|
1954
|
+
const intervalMs = parseIntervalToMs(params.interval);
|
|
1955
|
+
const groupByFields = params.groupBy ?? ["monitor_name"];
|
|
1956
|
+
const timeBuckets = /* @__PURE__ */ new Map();
|
|
1957
|
+
const maxEventsToProcess = 1e4;
|
|
1958
|
+
let eventCount = 0;
|
|
1959
|
+
let pageCount = 0;
|
|
1960
|
+
const maxPages = 100;
|
|
1961
|
+
const body = {
|
|
1962
|
+
filter: {
|
|
1963
|
+
query: fullQuery,
|
|
1964
|
+
from: fromTime,
|
|
1965
|
+
to: toTime
|
|
1966
|
+
},
|
|
1967
|
+
sort: "timestamp",
|
|
1968
|
+
page: { limit: 1e3 }
|
|
1969
|
+
};
|
|
1970
|
+
let cursor;
|
|
1971
|
+
while (pageCount < maxPages && eventCount < maxEventsToProcess) {
|
|
1972
|
+
const pageBody = { ...body, page: { ...body.page, cursor } };
|
|
1973
|
+
const response = await api.searchEvents({ body: pageBody });
|
|
1974
|
+
const events = response.data ?? [];
|
|
1975
|
+
if (events.length === 0) break;
|
|
1976
|
+
for (const event of events) {
|
|
1977
|
+
const formatted = formatEventV2(event);
|
|
1978
|
+
const groupKey = buildGroupKey(formatted, groupByFields);
|
|
1979
|
+
const eventTs = new Date(formatted.timestamp).getTime();
|
|
1980
|
+
const bucketTs = Math.floor(eventTs / intervalMs) * intervalMs;
|
|
1981
|
+
if (!timeBuckets.has(bucketTs)) {
|
|
1982
|
+
timeBuckets.set(bucketTs, /* @__PURE__ */ new Map());
|
|
1983
|
+
}
|
|
1984
|
+
const groupCounts = timeBuckets.get(bucketTs);
|
|
1985
|
+
groupCounts.set(groupKey, (groupCounts.get(groupKey) ?? 0) + 1);
|
|
1986
|
+
eventCount++;
|
|
1987
|
+
if (eventCount >= maxEventsToProcess) break;
|
|
1988
|
+
}
|
|
1989
|
+
cursor = response.meta?.page?.after;
|
|
1990
|
+
if (!cursor) break;
|
|
1991
|
+
pageCount++;
|
|
1992
|
+
}
|
|
1993
|
+
const sortedBuckets = [...timeBuckets.entries()].sort((a, b) => a[0] - b[0]).map(([bucketTs, groupCounts]) => {
|
|
1994
|
+
const counts = {};
|
|
1995
|
+
let total = 0;
|
|
1996
|
+
for (const [key, count] of groupCounts) {
|
|
1997
|
+
counts[key] = count;
|
|
1998
|
+
total += count;
|
|
1999
|
+
}
|
|
2000
|
+
return {
|
|
2001
|
+
timestamp: new Date(bucketTs).toISOString(),
|
|
2002
|
+
timestampMs: bucketTs,
|
|
2003
|
+
counts,
|
|
2004
|
+
total
|
|
2005
|
+
};
|
|
2006
|
+
});
|
|
2007
|
+
const effectiveLimit = params.limit ?? 100;
|
|
2008
|
+
const limitedBuckets = sortedBuckets.slice(0, effectiveLimit);
|
|
2009
|
+
return {
|
|
2010
|
+
timeseries: limitedBuckets,
|
|
2011
|
+
meta: {
|
|
2012
|
+
query: fullQuery,
|
|
2013
|
+
from: fromTime,
|
|
2014
|
+
to: toTime,
|
|
2015
|
+
interval: params.interval ?? "1h",
|
|
2016
|
+
intervalMs,
|
|
2017
|
+
groupBy: groupByFields,
|
|
2018
|
+
totalBuckets: sortedBuckets.length,
|
|
2019
|
+
totalEvents: eventCount,
|
|
2020
|
+
truncated: eventCount >= maxEventsToProcess,
|
|
2021
|
+
datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
async function incidentsEventsV2(api, params, limits, site) {
|
|
2026
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
2027
|
+
const defaultTo = now();
|
|
2028
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
2029
|
+
parseTime(params.from, defaultFrom),
|
|
2030
|
+
parseTime(params.to, defaultTo)
|
|
2031
|
+
);
|
|
2032
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
2033
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
2034
|
+
const fullQuery = buildEventQuery({
|
|
2035
|
+
query: params.query ?? "source:alert",
|
|
2036
|
+
sources: params.sources,
|
|
2037
|
+
tags: params.tags ?? ["source:alert"]
|
|
2038
|
+
});
|
|
2039
|
+
const dedupeWindowNs = parseDurationToNs(params.dedupeWindow ?? "5m");
|
|
2040
|
+
const dedupeWindowMs = dedupeWindowNs ? Math.floor(dedupeWindowNs / 1e6) : 3e5;
|
|
2041
|
+
const incidents = /* @__PURE__ */ new Map();
|
|
2042
|
+
const maxEventsToProcess = 1e4;
|
|
2043
|
+
let eventCount = 0;
|
|
2044
|
+
let pageCount = 0;
|
|
2045
|
+
const maxPages = 100;
|
|
2046
|
+
const body = {
|
|
2047
|
+
filter: {
|
|
2048
|
+
query: fullQuery,
|
|
2049
|
+
from: fromTime,
|
|
2050
|
+
to: toTime
|
|
2051
|
+
},
|
|
2052
|
+
sort: "timestamp",
|
|
2053
|
+
page: { limit: 1e3 }
|
|
2054
|
+
};
|
|
2055
|
+
let cursor;
|
|
2056
|
+
while (pageCount < maxPages && eventCount < maxEventsToProcess) {
|
|
2057
|
+
const pageBody = { ...body, page: { ...body.page, cursor } };
|
|
2058
|
+
const response = await api.searchEvents({ body: pageBody });
|
|
2059
|
+
const events = response.data ?? [];
|
|
2060
|
+
if (events.length === 0) break;
|
|
2061
|
+
for (const event of events) {
|
|
2062
|
+
const formatted = formatEventV2(event);
|
|
2063
|
+
const monitorName = formatted.monitorInfo?.name ?? formatted.title;
|
|
2064
|
+
if (!monitorName) {
|
|
2065
|
+
eventCount++;
|
|
2066
|
+
continue;
|
|
2067
|
+
}
|
|
2068
|
+
const eventTs = new Date(formatted.timestamp);
|
|
2069
|
+
let status = formatted.monitorInfo?.status?.toLowerCase() ?? "";
|
|
2070
|
+
if (!status && formatted.alertType) {
|
|
2071
|
+
const alertType = formatted.alertType.toLowerCase();
|
|
2072
|
+
if (alertType === "error" || alertType === "warning") {
|
|
2073
|
+
status = "triggered";
|
|
2074
|
+
} else if (alertType === "success") {
|
|
2075
|
+
status = "recovered";
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
if (!status && formatted.source === "alert") {
|
|
2079
|
+
const msgLower = formatted.message.toLowerCase();
|
|
2080
|
+
if (msgLower.includes("recovered") || msgLower.includes("[ok]") || msgLower.includes("resolved")) {
|
|
2081
|
+
status = "recovered";
|
|
2082
|
+
} else {
|
|
2083
|
+
status = "triggered";
|
|
2084
|
+
}
|
|
2085
|
+
}
|
|
2086
|
+
const existing = incidents.get(monitorName);
|
|
2087
|
+
if (status === "triggered" || status === "alert" || status === "re-triggered" || status === "renotify") {
|
|
2088
|
+
if (existing) {
|
|
2089
|
+
const timeSinceLastTrigger = eventTs.getTime() - existing.lastTrigger.getTime();
|
|
2090
|
+
if (timeSinceLastTrigger <= dedupeWindowMs) {
|
|
2091
|
+
existing.lastTrigger = eventTs;
|
|
2092
|
+
existing.triggerCount++;
|
|
2093
|
+
existing.sample = formatted;
|
|
2094
|
+
} else {
|
|
2095
|
+
const oldKey = `${monitorName}::${existing.firstTrigger.toISOString()}`;
|
|
2096
|
+
incidents.set(oldKey, existing);
|
|
2097
|
+
incidents.set(monitorName, {
|
|
2098
|
+
monitorName,
|
|
2099
|
+
firstTrigger: eventTs,
|
|
2100
|
+
lastTrigger: eventTs,
|
|
2101
|
+
triggerCount: 1,
|
|
2102
|
+
recovered: false,
|
|
2103
|
+
sample: formatted
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
} else {
|
|
2107
|
+
incidents.set(monitorName, {
|
|
2108
|
+
monitorName,
|
|
2109
|
+
firstTrigger: eventTs,
|
|
2110
|
+
lastTrigger: eventTs,
|
|
2111
|
+
triggerCount: 1,
|
|
2112
|
+
recovered: false,
|
|
2113
|
+
sample: formatted
|
|
2114
|
+
});
|
|
2115
|
+
}
|
|
2116
|
+
} else if (status === "recovered" || status === "ok") {
|
|
2117
|
+
if (existing && !existing.recovered) {
|
|
2118
|
+
existing.recovered = true;
|
|
2119
|
+
existing.recoveredAt = eventTs;
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
eventCount++;
|
|
2123
|
+
if (eventCount >= maxEventsToProcess) break;
|
|
2124
|
+
}
|
|
2125
|
+
cursor = response.meta?.page?.after;
|
|
2126
|
+
if (!cursor) break;
|
|
2127
|
+
pageCount++;
|
|
2128
|
+
}
|
|
2129
|
+
const incidentList = [...incidents.values()].map((inc) => {
|
|
2130
|
+
let duration;
|
|
2131
|
+
if (inc.recoveredAt) {
|
|
2132
|
+
const durationMs = inc.recoveredAt.getTime() - inc.firstTrigger.getTime();
|
|
2133
|
+
if (durationMs < 6e4) {
|
|
2134
|
+
duration = `${Math.round(durationMs / 1e3)}s`;
|
|
2135
|
+
} else if (durationMs < 36e5) {
|
|
2136
|
+
duration = `${Math.round(durationMs / 6e4)}m`;
|
|
2137
|
+
} else {
|
|
2138
|
+
duration = `${(durationMs / 36e5).toFixed(1)}h`;
|
|
2139
|
+
}
|
|
2140
|
+
}
|
|
2141
|
+
return {
|
|
2142
|
+
monitorName: inc.monitorName,
|
|
2143
|
+
firstTrigger: inc.firstTrigger.toISOString(),
|
|
2144
|
+
lastTrigger: inc.lastTrigger.toISOString(),
|
|
2145
|
+
triggerCount: inc.triggerCount,
|
|
2146
|
+
recovered: inc.recovered,
|
|
2147
|
+
recoveredAt: inc.recoveredAt?.toISOString(),
|
|
2148
|
+
duration,
|
|
2149
|
+
sample: inc.sample
|
|
2150
|
+
};
|
|
2151
|
+
});
|
|
2152
|
+
incidentList.sort((a, b) => new Date(b.firstTrigger).getTime() - new Date(a.firstTrigger).getTime());
|
|
2153
|
+
const effectiveLimit = Math.min(params.limit ?? 100, 500);
|
|
2154
|
+
return {
|
|
2155
|
+
incidents: incidentList.slice(0, effectiveLimit),
|
|
2156
|
+
meta: {
|
|
2157
|
+
query: fullQuery,
|
|
2158
|
+
from: fromTime,
|
|
2159
|
+
to: toTime,
|
|
2160
|
+
dedupeWindow: params.dedupeWindow ?? "5m",
|
|
2161
|
+
dedupeWindowMs,
|
|
2162
|
+
totalIncidents: incidentList.length,
|
|
2163
|
+
totalEvents: eventCount,
|
|
2164
|
+
recoveredCount: incidentList.filter((i) => i.recovered).length,
|
|
2165
|
+
activeCount: incidentList.filter((i) => !i.recovered).length,
|
|
2166
|
+
truncated: eventCount >= maxEventsToProcess,
|
|
2167
|
+
datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
}
|
|
2171
|
+
async function enrichWithMonitorMetadata(events, monitorsApi) {
|
|
2172
|
+
const monitorNames = /* @__PURE__ */ new Set();
|
|
2173
|
+
for (const event of events) {
|
|
2174
|
+
if (event.monitorInfo?.name) {
|
|
2175
|
+
monitorNames.add(event.monitorInfo.name);
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
if (monitorNames.size === 0) {
|
|
2179
|
+
return events;
|
|
2180
|
+
}
|
|
2181
|
+
const monitorCache = /* @__PURE__ */ new Map();
|
|
2182
|
+
try {
|
|
2183
|
+
const response = await monitorsApi.listMonitors({
|
|
2184
|
+
pageSize: 1e3
|
|
2185
|
+
});
|
|
2186
|
+
const monitors = response ?? [];
|
|
2187
|
+
for (const monitor of monitors) {
|
|
2188
|
+
if (monitor.name) {
|
|
2189
|
+
monitorCache.set(monitor.name, monitor);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
} catch {
|
|
2193
|
+
return events;
|
|
2194
|
+
}
|
|
2195
|
+
return events.map((event) => {
|
|
2196
|
+
const enriched = { ...event };
|
|
2197
|
+
if (event.monitorInfo?.name) {
|
|
2198
|
+
const monitor = monitorCache.get(event.monitorInfo.name);
|
|
2199
|
+
if (monitor) {
|
|
2200
|
+
enriched.monitorMetadata = {
|
|
2201
|
+
id: monitor.id ?? 0,
|
|
2202
|
+
type: String(monitor.type ?? ""),
|
|
2203
|
+
message: monitor.message ?? "",
|
|
2204
|
+
tags: monitor.tags ?? [],
|
|
2205
|
+
options: {
|
|
2206
|
+
thresholds: monitor.options?.thresholds,
|
|
2207
|
+
notifyNoData: monitor.options?.notifyNoData,
|
|
2208
|
+
escalationMessage: monitor.options?.escalationMessage
|
|
2209
|
+
}
|
|
2210
|
+
};
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return enriched;
|
|
2214
|
+
});
|
|
2215
|
+
}
|
|
2216
|
+
function registerEventsTool(server, apiV1, apiV2, monitorsApi, limits, readOnly = false, site = "datadoghq.com") {
|
|
2217
|
+
server.tool(
|
|
2218
|
+
"events",
|
|
2219
|
+
`Track Datadog events. Actions: list, get, create, search, aggregate, top, timeseries, incidents.
|
|
2220
|
+
IMPORTANT: For monitor alert history, use tags: ["source:alert"] to find all triggered monitors.
|
|
2221
|
+
Filters: query (text search), sources, tags, priority, time range.
|
|
2222
|
+
Use for: monitor alerts, deployments, incidents, change tracking.
|
|
2223
|
+
|
|
2224
|
+
Use action:"top" with from:"7d" to find the noisiest monitors.
|
|
2225
|
+
Use action:"aggregate" with groupBy:["monitor_name"] for alert counts per monitor.
|
|
2226
|
+
Use action:"timeseries" with interval:"1h" to see alert trends over time.
|
|
2227
|
+
Use action:"incidents" with dedupeWindow:"5m" to deduplicate alerts into incidents.
|
|
2228
|
+
Use enrich:true with search to get monitor metadata (slower).`,
|
|
2229
|
+
InputSchema6,
|
|
2230
|
+
async ({ action, id, query, from, to, priority, sources, tags, limit, title, text, alertType, groupBy, cursor, interval, dedupeWindow, enrich }) => {
|
|
2231
|
+
try {
|
|
2232
|
+
checkReadOnly(action, readOnly);
|
|
2233
|
+
switch (action) {
|
|
2234
|
+
case "list":
|
|
2235
|
+
return toolResult(await listEventsV1(apiV1, {
|
|
2236
|
+
query,
|
|
2237
|
+
from,
|
|
2238
|
+
to,
|
|
2239
|
+
priority,
|
|
2240
|
+
sources,
|
|
2241
|
+
tags,
|
|
2242
|
+
limit
|
|
2243
|
+
}, limits));
|
|
2244
|
+
case "get": {
|
|
2245
|
+
const eventId = requireParam(id, "id", "get");
|
|
2246
|
+
return toolResult(await getEventV1(apiV1, eventId));
|
|
2247
|
+
}
|
|
2248
|
+
case "create": {
|
|
2249
|
+
const eventTitle = requireParam(title, "title", "create");
|
|
2250
|
+
const eventText = requireParam(text, "text", "create");
|
|
2251
|
+
return toolResult(await createEventV1(apiV1, {
|
|
2252
|
+
title: eventTitle,
|
|
2253
|
+
text: eventText,
|
|
2254
|
+
priority,
|
|
2255
|
+
tags,
|
|
2256
|
+
alertType
|
|
2257
|
+
}));
|
|
2258
|
+
}
|
|
2259
|
+
case "search": {
|
|
2260
|
+
const result = await searchEventsV2(apiV2, {
|
|
2261
|
+
query,
|
|
2262
|
+
from,
|
|
2263
|
+
to,
|
|
2264
|
+
sources,
|
|
2265
|
+
tags,
|
|
2266
|
+
priority,
|
|
2267
|
+
limit,
|
|
2268
|
+
cursor
|
|
2269
|
+
}, limits, site);
|
|
2270
|
+
if (enrich && result.events.length > 0) {
|
|
2271
|
+
const enrichedEvents = await enrichWithMonitorMetadata(result.events, monitorsApi);
|
|
2272
|
+
return toolResult({ ...result, events: enrichedEvents });
|
|
2273
|
+
}
|
|
2274
|
+
return toolResult(result);
|
|
2275
|
+
}
|
|
2276
|
+
case "aggregate":
|
|
2277
|
+
return toolResult(await aggregateEventsV2(apiV2, {
|
|
2278
|
+
query,
|
|
2279
|
+
from,
|
|
2280
|
+
to,
|
|
2281
|
+
sources,
|
|
2282
|
+
tags,
|
|
2283
|
+
groupBy,
|
|
2284
|
+
limit
|
|
2285
|
+
}, limits, site));
|
|
2286
|
+
case "top":
|
|
2287
|
+
return toolResult(await topEventsV2(apiV2, {
|
|
2288
|
+
query,
|
|
2289
|
+
from,
|
|
2290
|
+
to,
|
|
2291
|
+
sources,
|
|
2292
|
+
tags,
|
|
2293
|
+
groupBy,
|
|
2294
|
+
limit
|
|
2295
|
+
}, limits, site));
|
|
2296
|
+
case "timeseries":
|
|
2297
|
+
return toolResult(await timeseriesEventsV2(apiV2, {
|
|
2298
|
+
query,
|
|
2299
|
+
from,
|
|
2300
|
+
to,
|
|
2301
|
+
sources,
|
|
2302
|
+
tags,
|
|
2303
|
+
groupBy,
|
|
2304
|
+
interval,
|
|
2305
|
+
limit
|
|
2306
|
+
}, limits, site));
|
|
2307
|
+
case "incidents":
|
|
2308
|
+
return toolResult(await incidentsEventsV2(apiV2, {
|
|
2309
|
+
query,
|
|
2310
|
+
from,
|
|
2311
|
+
to,
|
|
2312
|
+
sources,
|
|
2313
|
+
tags,
|
|
2314
|
+
dedupeWindow,
|
|
2315
|
+
limit
|
|
2316
|
+
}, limits, site));
|
|
2317
|
+
default:
|
|
2318
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2319
|
+
}
|
|
2320
|
+
} catch (error) {
|
|
2321
|
+
handleDatadogError(error);
|
|
2322
|
+
}
|
|
2323
|
+
}
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// src/tools/incidents.ts
|
|
2328
|
+
import { z as z8 } from "zod";
|
|
2329
|
+
var ActionSchema7 = z8.enum(["list", "get", "search", "create", "update", "delete"]);
|
|
2330
|
+
var InputSchema7 = {
|
|
2331
|
+
action: ActionSchema7.describe("Action to perform"),
|
|
2332
|
+
id: z8.string().optional().describe("Incident ID (required for get/update/delete)"),
|
|
2333
|
+
query: z8.string().optional().describe("Search query (for search action)"),
|
|
2334
|
+
status: z8.enum(["active", "stable", "resolved"]).optional().describe("Filter by status (for list)"),
|
|
2335
|
+
limit: z8.number().optional().describe("Maximum number of incidents to return"),
|
|
2336
|
+
config: z8.record(z8.unknown()).optional().describe("Incident configuration (for create/update). Create requires: title. Update can modify: title, status, severity, fields.")
|
|
2337
|
+
};
|
|
2338
|
+
function formatIncident(i) {
|
|
2339
|
+
const attrs = i.attributes;
|
|
2340
|
+
const commander = i.relationships?.commanderUser?.data;
|
|
2341
|
+
return {
|
|
2342
|
+
id: i.id ?? "",
|
|
2343
|
+
title: attrs?.title ?? "",
|
|
2344
|
+
status: String(attrs?.state ?? "unknown"),
|
|
2345
|
+
severity: attrs?.severity ? String(attrs.severity) : null,
|
|
2346
|
+
state: attrs?.state ? String(attrs.state) : null,
|
|
2347
|
+
customerImpactScope: attrs?.customerImpactScope ?? null,
|
|
2348
|
+
customerImpacted: attrs?.customerImpacted ?? false,
|
|
2349
|
+
commander: {
|
|
2350
|
+
name: null,
|
|
2351
|
+
// Would need to resolve from relationships
|
|
2352
|
+
email: null,
|
|
2353
|
+
handle: commander?.id ?? null
|
|
2354
|
+
},
|
|
2355
|
+
createdAt: attrs?.created ? new Date(attrs.created).toISOString() : "",
|
|
2356
|
+
modifiedAt: attrs?.modified ? new Date(attrs.modified).toISOString() : "",
|
|
2357
|
+
resolvedAt: attrs?.resolved ? new Date(attrs.resolved).toISOString() : null,
|
|
2358
|
+
timeToDetect: attrs?.timeToDetect ?? null,
|
|
2359
|
+
timeToRepair: attrs?.timeToRepair ?? null
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
async function listIncidents(api, params, limits) {
|
|
2363
|
+
const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
|
|
2364
|
+
const response = await api.listIncidents({
|
|
2365
|
+
pageSize: effectiveLimit
|
|
2366
|
+
});
|
|
2367
|
+
let incidents = (response.data ?? []).map(formatIncident);
|
|
2368
|
+
if (params.status) {
|
|
2369
|
+
incidents = incidents.filter((i) => i.state?.toLowerCase() === params.status);
|
|
2370
|
+
}
|
|
2371
|
+
incidents = incidents.slice(0, effectiveLimit);
|
|
2372
|
+
return {
|
|
2373
|
+
incidents,
|
|
2374
|
+
total: incidents.length
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
async function getIncident(api, id) {
|
|
2378
|
+
const response = await api.getIncident({ incidentId: id });
|
|
2379
|
+
return {
|
|
2380
|
+
incident: response.data ? formatIncident(response.data) : null
|
|
2381
|
+
};
|
|
2382
|
+
}
|
|
2383
|
+
async function searchIncidents(api, query, limits) {
|
|
2384
|
+
const response = await api.searchIncidents({
|
|
2385
|
+
query,
|
|
2386
|
+
pageSize: limits.maxResults
|
|
2387
|
+
});
|
|
2388
|
+
const incidents = (response.data?.attributes?.incidents ?? []).map((i) => ({
|
|
2389
|
+
id: i.data?.id ?? "",
|
|
2390
|
+
title: i.data?.attributes?.title ?? "",
|
|
2391
|
+
state: i.data?.attributes?.state ?? "unknown"
|
|
2392
|
+
}));
|
|
2393
|
+
return {
|
|
2394
|
+
incidents,
|
|
2395
|
+
total: response.data?.attributes?.total ?? incidents.length
|
|
2396
|
+
};
|
|
2397
|
+
}
|
|
2398
|
+
async function createIncident(api, config) {
|
|
2399
|
+
const body = {
|
|
2400
|
+
data: {
|
|
2401
|
+
type: "incidents",
|
|
2402
|
+
attributes: config
|
|
2403
|
+
}
|
|
2404
|
+
};
|
|
2405
|
+
const response = await api.createIncident({ body });
|
|
2406
|
+
return {
|
|
2407
|
+
success: true,
|
|
2408
|
+
incident: response.data ? formatIncident(response.data) : null
|
|
2409
|
+
};
|
|
2410
|
+
}
|
|
2411
|
+
async function updateIncident(api, id, config) {
|
|
2412
|
+
const body = {
|
|
2413
|
+
data: {
|
|
2414
|
+
type: "incidents",
|
|
2415
|
+
id,
|
|
2416
|
+
attributes: config
|
|
2417
|
+
}
|
|
2418
|
+
};
|
|
2419
|
+
const response = await api.updateIncident({ incidentId: id, body });
|
|
2420
|
+
return {
|
|
2421
|
+
success: true,
|
|
2422
|
+
incident: response.data ? formatIncident(response.data) : null
|
|
2423
|
+
};
|
|
2424
|
+
}
|
|
2425
|
+
async function deleteIncident(api, id) {
|
|
2426
|
+
await api.deleteIncident({ incidentId: id });
|
|
2427
|
+
return {
|
|
2428
|
+
success: true,
|
|
2429
|
+
message: `Incident ${id} deleted`
|
|
2430
|
+
};
|
|
2431
|
+
}
|
|
2432
|
+
function registerIncidentsTool(server, api, limits, readOnly = false) {
|
|
2433
|
+
server.tool(
|
|
2434
|
+
"incidents",
|
|
2435
|
+
"Manage Datadog incidents for incident response. Actions: list, get, search, create, update, delete. Use for: incident management, on-call response, postmortems, tracking MTTR/MTTD.",
|
|
2436
|
+
InputSchema7,
|
|
2437
|
+
async ({ action, id, query, status, limit, config }) => {
|
|
2438
|
+
try {
|
|
2439
|
+
checkReadOnly(action, readOnly);
|
|
2440
|
+
switch (action) {
|
|
2441
|
+
case "list":
|
|
2442
|
+
return toolResult(await listIncidents(api, { status, limit }, limits));
|
|
2443
|
+
case "get": {
|
|
2444
|
+
const incidentId = requireParam(id, "id", "get");
|
|
2445
|
+
return toolResult(await getIncident(api, incidentId));
|
|
2446
|
+
}
|
|
2447
|
+
case "search": {
|
|
2448
|
+
const searchQuery = requireParam(query, "query", "search");
|
|
2449
|
+
return toolResult(await searchIncidents(api, searchQuery, limits));
|
|
2450
|
+
}
|
|
2451
|
+
case "create": {
|
|
2452
|
+
const incidentConfig = requireParam(config, "config", "create");
|
|
2453
|
+
return toolResult(await createIncident(api, incidentConfig));
|
|
2454
|
+
}
|
|
2455
|
+
case "update": {
|
|
2456
|
+
const incidentId = requireParam(id, "id", "update");
|
|
2457
|
+
const incidentConfig = requireParam(config, "config", "update");
|
|
2458
|
+
return toolResult(await updateIncident(api, incidentId, incidentConfig));
|
|
2459
|
+
}
|
|
2460
|
+
case "delete": {
|
|
2461
|
+
const incidentId = requireParam(id, "id", "delete");
|
|
2462
|
+
return toolResult(await deleteIncident(api, incidentId));
|
|
2463
|
+
}
|
|
2464
|
+
default:
|
|
2465
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2466
|
+
}
|
|
2467
|
+
} catch (error) {
|
|
2468
|
+
handleDatadogError(error);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
);
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// src/tools/slos.ts
|
|
2475
|
+
import { z as z9 } from "zod";
|
|
2476
|
+
var ActionSchema8 = z9.enum(["list", "get", "create", "update", "delete", "history"]);
|
|
2477
|
+
var InputSchema8 = {
|
|
2478
|
+
action: ActionSchema8.describe("Action to perform"),
|
|
2479
|
+
id: z9.string().optional().describe("SLO ID (required for get/update/delete/history)"),
|
|
2480
|
+
ids: z9.array(z9.string()).optional().describe("Multiple SLO IDs (for list with specific IDs)"),
|
|
2481
|
+
query: z9.string().optional().describe("Search query (for list)"),
|
|
2482
|
+
tags: z9.array(z9.string()).optional().describe("Filter by tags (for list)"),
|
|
2483
|
+
limit: z9.number().optional().describe("Maximum number of SLOs to return"),
|
|
2484
|
+
config: z9.record(z9.unknown()).optional().describe("SLO configuration (for create/update). Must include type, name, thresholds."),
|
|
2485
|
+
from: z9.string().optional().describe('Start time for history (ISO 8601 or relative like "7d", "1w")'),
|
|
2486
|
+
to: z9.string().optional().describe("End time for history (ISO 8601 or relative, default: now)")
|
|
2487
|
+
};
|
|
2488
|
+
function formatSlo(s) {
|
|
2489
|
+
const primaryThreshold = s.thresholds?.[0];
|
|
2490
|
+
return {
|
|
2491
|
+
id: s.id ?? "",
|
|
2492
|
+
name: s.name ?? "",
|
|
2493
|
+
description: s.description ?? null,
|
|
2494
|
+
type: String(s.type ?? "unknown"),
|
|
2495
|
+
targetThreshold: primaryThreshold?.target ?? 0,
|
|
2496
|
+
warningThreshold: primaryThreshold?.warning ?? null,
|
|
2497
|
+
timeframe: String(primaryThreshold?.timeframe ?? ""),
|
|
2498
|
+
tags: s.tags ?? [],
|
|
2499
|
+
status: {
|
|
2500
|
+
// Note: SLI status requires a separate API call to getSLOHistory
|
|
2501
|
+
sli: null,
|
|
2502
|
+
errorBudgetRemaining: null,
|
|
2503
|
+
state: "unknown"
|
|
2504
|
+
},
|
|
2505
|
+
createdAt: s.createdAt ? new Date(s.createdAt * 1e3).toISOString() : "",
|
|
2506
|
+
modifiedAt: s.modifiedAt ? new Date(s.modifiedAt * 1e3).toISOString() : ""
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
async function listSlos(api, params, limits) {
|
|
2510
|
+
const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
|
|
2511
|
+
const response = await api.listSLOs({
|
|
2512
|
+
ids: params.ids?.join(","),
|
|
2513
|
+
query: params.query,
|
|
2514
|
+
tagsQuery: params.tags?.join(","),
|
|
2515
|
+
limit: effectiveLimit
|
|
2516
|
+
});
|
|
2517
|
+
const slos = (response.data ?? []).map(formatSlo);
|
|
2518
|
+
return {
|
|
2519
|
+
slos,
|
|
2520
|
+
total: response.data?.length ?? 0
|
|
2521
|
+
};
|
|
2522
|
+
}
|
|
2523
|
+
async function getSlo(api, id) {
|
|
2524
|
+
const response = await api.getSLO({ sloId: id });
|
|
2525
|
+
return {
|
|
2526
|
+
slo: response.data ? formatSlo(response.data) : null
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2529
|
+
function snakeToCamel(str) {
|
|
2530
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
2531
|
+
}
|
|
2532
|
+
function normalizeConfigKeys(obj) {
|
|
2533
|
+
if (obj === null || obj === void 0) return obj;
|
|
2534
|
+
if (Array.isArray(obj)) return obj.map(normalizeConfigKeys);
|
|
2535
|
+
if (typeof obj !== "object") return obj;
|
|
2536
|
+
const normalized = {};
|
|
2537
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2538
|
+
const camelKey = snakeToCamel(key);
|
|
2539
|
+
normalized[camelKey] = normalizeConfigKeys(value);
|
|
2540
|
+
}
|
|
2541
|
+
return normalized;
|
|
2542
|
+
}
|
|
2543
|
+
function normalizeSloConfig(config) {
|
|
2544
|
+
const normalized = normalizeConfigKeys(config);
|
|
2545
|
+
if (!normalized.name) {
|
|
2546
|
+
throw new Error("SLO config requires 'name' field");
|
|
2547
|
+
}
|
|
2548
|
+
if (!normalized.type) {
|
|
2549
|
+
throw new Error("SLO config requires 'type' field (e.g., 'metric', 'monitor')");
|
|
2550
|
+
}
|
|
2551
|
+
if (!normalized.thresholds || !Array.isArray(normalized.thresholds)) {
|
|
2552
|
+
throw new Error("SLO config requires 'thresholds' array with at least one threshold");
|
|
2553
|
+
}
|
|
2554
|
+
return normalized;
|
|
2555
|
+
}
|
|
2556
|
+
async function createSlo(api, config) {
|
|
2557
|
+
const body = normalizeSloConfig(config);
|
|
2558
|
+
const response = await api.createSLO({ body });
|
|
2559
|
+
return {
|
|
2560
|
+
success: true,
|
|
2561
|
+
slo: response.data?.[0] ? formatSlo(response.data[0]) : null
|
|
2562
|
+
};
|
|
2563
|
+
}
|
|
2564
|
+
async function updateSlo(api, id, config) {
|
|
2565
|
+
const body = normalizeConfigKeys(config);
|
|
2566
|
+
const response = await api.updateSLO({ sloId: id, body });
|
|
2567
|
+
return {
|
|
2568
|
+
success: true,
|
|
2569
|
+
slo: response.data?.[0] ? formatSlo(response.data[0]) : null
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
async function deleteSlo(api, id) {
|
|
2573
|
+
await api.deleteSLO({ sloId: id });
|
|
2574
|
+
return {
|
|
2575
|
+
success: true,
|
|
2576
|
+
message: `SLO ${id} deleted`
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
async function getSloHistory(api, id, params) {
|
|
2580
|
+
const nowMs = Date.now();
|
|
2581
|
+
const defaultFromMs = nowMs - 7 * 24 * 60 * 60 * 1e3;
|
|
2582
|
+
const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3)) * 1e3;
|
|
2583
|
+
const toTime = parseTime(params.to, Math.floor(nowMs / 1e3)) * 1e3;
|
|
2584
|
+
const [validFrom, validTo] = ensureValidTimeRange(fromTime, toTime);
|
|
2585
|
+
const response = await api.getSLOHistory({
|
|
2586
|
+
sloId: id,
|
|
2587
|
+
fromTs: Math.floor(validFrom / 1e3),
|
|
2588
|
+
toTs: Math.floor(validTo / 1e3)
|
|
2589
|
+
});
|
|
2590
|
+
const data = response.data;
|
|
2591
|
+
return {
|
|
2592
|
+
history: {
|
|
2593
|
+
overall: {
|
|
2594
|
+
sliValue: data?.overall?.sliValue ?? null,
|
|
2595
|
+
spanPrecision: data?.overall?.spanPrecision ?? null,
|
|
2596
|
+
uptime: data?.overall?.uptime ?? null
|
|
2597
|
+
},
|
|
2598
|
+
series: {
|
|
2599
|
+
numerator: data?.series?.numerator?.values ?? [],
|
|
2600
|
+
denominator: data?.series?.denominator?.values ?? [],
|
|
2601
|
+
times: data?.series?.times ?? []
|
|
2602
|
+
},
|
|
2603
|
+
thresholds: data?.thresholds ?? {},
|
|
2604
|
+
fromTs: new Date(validFrom).toISOString(),
|
|
2605
|
+
toTs: new Date(validTo).toISOString()
|
|
2606
|
+
}
|
|
2607
|
+
};
|
|
2608
|
+
}
|
|
2609
|
+
function registerSlosTool(server, api, limits, readOnly = false) {
|
|
2610
|
+
server.tool(
|
|
2611
|
+
"slos",
|
|
2612
|
+
"Manage Datadog Service Level Objectives. Actions: list, get, create, update, delete, history. SLO types: metric-based, monitor-based. Use for: reliability tracking, error budgets, SLA compliance, performance targets.",
|
|
2613
|
+
InputSchema8,
|
|
2614
|
+
async ({ action, id, ids, query, tags, limit, config, from, to }) => {
|
|
2615
|
+
try {
|
|
2616
|
+
checkReadOnly(action, readOnly);
|
|
2617
|
+
switch (action) {
|
|
2618
|
+
case "list":
|
|
2619
|
+
return toolResult(await listSlos(api, { ids, query, tags, limit }, limits));
|
|
2620
|
+
case "get": {
|
|
2621
|
+
const sloId = requireParam(id, "id", "get");
|
|
2622
|
+
return toolResult(await getSlo(api, sloId));
|
|
2623
|
+
}
|
|
2624
|
+
case "create": {
|
|
2625
|
+
const sloConfig = requireParam(config, "config", "create");
|
|
2626
|
+
return toolResult(await createSlo(api, sloConfig));
|
|
2627
|
+
}
|
|
2628
|
+
case "update": {
|
|
2629
|
+
const sloId = requireParam(id, "id", "update");
|
|
2630
|
+
const sloConfig = requireParam(config, "config", "update");
|
|
2631
|
+
return toolResult(await updateSlo(api, sloId, sloConfig));
|
|
2632
|
+
}
|
|
2633
|
+
case "delete": {
|
|
2634
|
+
const sloId = requireParam(id, "id", "delete");
|
|
2635
|
+
return toolResult(await deleteSlo(api, sloId));
|
|
2636
|
+
}
|
|
2637
|
+
case "history": {
|
|
2638
|
+
const sloId = requireParam(id, "id", "history");
|
|
2639
|
+
return toolResult(await getSloHistory(api, sloId, { from, to }));
|
|
2640
|
+
}
|
|
2641
|
+
default:
|
|
2642
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2643
|
+
}
|
|
2644
|
+
} catch (error) {
|
|
2645
|
+
handleDatadogError(error);
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
);
|
|
2649
|
+
}
|
|
2650
|
+
|
|
2651
|
+
// src/tools/synthetics.ts
|
|
2652
|
+
import { z as z10 } from "zod";
|
|
2653
|
+
var ActionSchema9 = z10.enum(["list", "get", "create", "update", "delete", "trigger", "results"]);
|
|
2654
|
+
var InputSchema9 = {
|
|
2655
|
+
action: ActionSchema9.describe("Action to perform"),
|
|
2656
|
+
id: z10.string().optional().describe("Test public ID (required for get/update/delete/trigger/results)"),
|
|
2657
|
+
ids: z10.array(z10.string()).optional().describe("Multiple test IDs (for bulk trigger)"),
|
|
2658
|
+
testType: z10.enum(["api", "browser"]).optional().describe("Test type filter (for list) or type for create"),
|
|
2659
|
+
locations: z10.array(z10.string()).optional().describe("Filter by locations (for list)"),
|
|
2660
|
+
tags: z10.array(z10.string()).optional().describe("Filter by tags (for list)"),
|
|
2661
|
+
limit: z10.number().optional().describe("Maximum number of tests to return"),
|
|
2662
|
+
config: z10.record(z10.unknown()).optional().describe("Test configuration (for create/update). Includes: name, type, config, options, locations, message.")
|
|
2663
|
+
};
|
|
2664
|
+
function formatTest(t) {
|
|
2665
|
+
return {
|
|
2666
|
+
publicId: t.publicId ?? "",
|
|
2667
|
+
name: t.name ?? "",
|
|
2668
|
+
type: String(t.type ?? "unknown"),
|
|
2669
|
+
subtype: t.subtype ? String(t.subtype) : null,
|
|
2670
|
+
status: String(t.status ?? "unknown"),
|
|
2671
|
+
message: t.message ?? "",
|
|
2672
|
+
tags: t.tags ?? [],
|
|
2673
|
+
locations: t.locations ?? [],
|
|
2674
|
+
monitorId: t.monitorId ?? null
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
async function listTests(api, params, limits) {
|
|
2678
|
+
const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
|
|
2679
|
+
const response = await api.listTests({
|
|
2680
|
+
pageSize: effectiveLimit
|
|
2681
|
+
});
|
|
2682
|
+
let tests = (response.tests ?? []).map(formatTest);
|
|
2683
|
+
if (params.tags && params.tags.length > 0) {
|
|
2684
|
+
tests = tests.filter((t) => params.tags.some((tag) => t.tags.includes(tag)));
|
|
2685
|
+
}
|
|
2686
|
+
if (params.locations && params.locations.length > 0) {
|
|
2687
|
+
tests = tests.filter((t) => params.locations.some((loc) => t.locations.includes(loc)));
|
|
2688
|
+
}
|
|
2689
|
+
tests = tests.slice(0, effectiveLimit);
|
|
2690
|
+
const summary = {
|
|
2691
|
+
total: response.tests?.length ?? 0,
|
|
2692
|
+
api: tests.filter((t) => t.type === "api").length,
|
|
2693
|
+
browser: tests.filter((t) => t.type === "browser").length,
|
|
2694
|
+
passing: tests.filter((t) => t.status === "OK" || t.status === "live").length,
|
|
2695
|
+
failing: tests.filter((t) => t.status === "Alert").length
|
|
2696
|
+
};
|
|
2697
|
+
return { tests, summary };
|
|
2698
|
+
}
|
|
2699
|
+
async function getTest(api, id) {
|
|
2700
|
+
try {
|
|
2701
|
+
const response = await api.getAPITest({ publicId: id });
|
|
2702
|
+
return { test: formatTest(response) };
|
|
2703
|
+
} catch {
|
|
2704
|
+
const response = await api.getBrowserTest({ publicId: id });
|
|
2705
|
+
return { test: formatTest(response) };
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
function snakeToCamel2(str) {
|
|
2709
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
2710
|
+
}
|
|
2711
|
+
function normalizeConfigKeys2(obj) {
|
|
2712
|
+
if (obj === null || obj === void 0) return obj;
|
|
2713
|
+
if (Array.isArray(obj)) return obj.map(normalizeConfigKeys2);
|
|
2714
|
+
if (typeof obj !== "object") return obj;
|
|
2715
|
+
const normalized = {};
|
|
2716
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
2717
|
+
const camelKey = snakeToCamel2(key);
|
|
2718
|
+
normalized[camelKey] = normalizeConfigKeys2(value);
|
|
2719
|
+
}
|
|
2720
|
+
return normalized;
|
|
2721
|
+
}
|
|
2722
|
+
function normalizeSyntheticsConfig(config) {
|
|
2723
|
+
const normalized = normalizeConfigKeys2(config);
|
|
2724
|
+
if (!normalized.name) {
|
|
2725
|
+
throw new Error("Synthetics test config requires 'name' field");
|
|
2726
|
+
}
|
|
2727
|
+
if (!normalized.locations || !Array.isArray(normalized.locations) || normalized.locations.length === 0) {
|
|
2728
|
+
throw new Error("Synthetics test config requires 'locations' array (e.g., ['aws:us-east-1'])");
|
|
2729
|
+
}
|
|
2730
|
+
return normalized;
|
|
2731
|
+
}
|
|
2732
|
+
async function createTest(api, config, testType) {
|
|
2733
|
+
const normalizedConfig = normalizeSyntheticsConfig(config);
|
|
2734
|
+
const type = testType ?? (normalizedConfig.type === "browser" ? "browser" : "api");
|
|
2735
|
+
if (type === "browser") {
|
|
2736
|
+
const body = normalizedConfig;
|
|
2737
|
+
const response = await api.createSyntheticsBrowserTest({ body });
|
|
2738
|
+
return {
|
|
2739
|
+
success: true,
|
|
2740
|
+
test: formatTest(response)
|
|
2741
|
+
};
|
|
2742
|
+
} else {
|
|
2743
|
+
const body = normalizedConfig;
|
|
2744
|
+
const response = await api.createSyntheticsAPITest({ body });
|
|
2745
|
+
return {
|
|
2746
|
+
success: true,
|
|
2747
|
+
test: formatTest(response)
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
async function updateTest(api, id, config) {
|
|
2752
|
+
const normalizedConfig = normalizeConfigKeys2(config);
|
|
2753
|
+
let testType = "api";
|
|
2754
|
+
try {
|
|
2755
|
+
await api.getAPITest({ publicId: id });
|
|
2756
|
+
testType = "api";
|
|
2757
|
+
} catch {
|
|
2758
|
+
testType = "browser";
|
|
2759
|
+
}
|
|
2760
|
+
if (testType === "browser") {
|
|
2761
|
+
const body = normalizedConfig;
|
|
2762
|
+
const response = await api.updateBrowserTest({ publicId: id, body });
|
|
2763
|
+
return {
|
|
2764
|
+
success: true,
|
|
2765
|
+
test: formatTest(response)
|
|
2766
|
+
};
|
|
2767
|
+
} else {
|
|
2768
|
+
const body = normalizedConfig;
|
|
2769
|
+
const response = await api.updateAPITest({ publicId: id, body });
|
|
2770
|
+
return {
|
|
2771
|
+
success: true,
|
|
2772
|
+
test: formatTest(response)
|
|
2773
|
+
};
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
async function deleteTests(api, ids) {
|
|
2777
|
+
await api.deleteTests({
|
|
2778
|
+
body: { publicIds: ids }
|
|
2779
|
+
});
|
|
2780
|
+
return {
|
|
2781
|
+
success: true,
|
|
2782
|
+
message: `Deleted ${ids.length} test(s): ${ids.join(", ")}`
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
async function triggerTests(api, ids) {
|
|
2786
|
+
const response = await api.triggerTests({
|
|
2787
|
+
body: {
|
|
2788
|
+
tests: ids.map((id) => ({ publicId: id }))
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
const results = response.results?.map((r) => ({
|
|
2792
|
+
publicId: r.publicId ?? "",
|
|
2793
|
+
resultId: r.resultId ?? "",
|
|
2794
|
+
triggered: true
|
|
2795
|
+
})) ?? [];
|
|
2796
|
+
return {
|
|
2797
|
+
triggered: results,
|
|
2798
|
+
total: results.length
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
async function getTestResults(api, id) {
|
|
2802
|
+
try {
|
|
2803
|
+
const response = await api.getAPITestLatestResults({ publicId: id });
|
|
2804
|
+
const results = (response.results ?? []).map((r) => ({
|
|
2805
|
+
resultId: r.resultId ?? "",
|
|
2806
|
+
status: r.result?.passed ? "passed" : "failed",
|
|
2807
|
+
checkTime: r.checkTime ? new Date(r.checkTime * 1e3).toISOString() : "",
|
|
2808
|
+
responseTime: r.result?.timings?.total ?? null
|
|
2809
|
+
}));
|
|
2810
|
+
return { results, testType: "api" };
|
|
2811
|
+
} catch {
|
|
2812
|
+
const response = await api.getBrowserTestLatestResults({ publicId: id });
|
|
2813
|
+
const results = (response.results ?? []).map((r) => ({
|
|
2814
|
+
resultId: r.resultId ?? "",
|
|
2815
|
+
// Browser tests don't have 'passed' - determine from errorCount
|
|
2816
|
+
status: (r.result?.errorCount ?? 0) === 0 ? "passed" : "failed",
|
|
2817
|
+
checkTime: r.checkTime ? new Date(r.checkTime * 1e3).toISOString() : "",
|
|
2818
|
+
duration: r.result?.duration ?? null
|
|
2819
|
+
}));
|
|
2820
|
+
return { results, testType: "browser" };
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
function registerSyntheticsTool(server, api, limits, readOnly = false) {
|
|
2824
|
+
server.tool(
|
|
2825
|
+
"synthetics",
|
|
2826
|
+
"Manage Datadog Synthetic tests (API and Browser). Actions: list, get, create, update, delete, trigger, results. Use for: uptime monitoring, API testing, user journey testing, performance testing, canary deployments.",
|
|
2827
|
+
InputSchema9,
|
|
2828
|
+
async ({ action, id, ids, testType, locations, tags, limit, config }) => {
|
|
2829
|
+
try {
|
|
2830
|
+
checkReadOnly(action, readOnly);
|
|
2831
|
+
switch (action) {
|
|
2832
|
+
case "list":
|
|
2833
|
+
return toolResult(await listTests(api, { locations, tags, limit }, limits));
|
|
2834
|
+
case "get": {
|
|
2835
|
+
const testId = requireParam(id, "id", "get");
|
|
2836
|
+
return toolResult(await getTest(api, testId));
|
|
2837
|
+
}
|
|
2838
|
+
case "create": {
|
|
2839
|
+
const testConfig = requireParam(config, "config", "create");
|
|
2840
|
+
return toolResult(await createTest(api, testConfig, testType));
|
|
2841
|
+
}
|
|
2842
|
+
case "update": {
|
|
2843
|
+
const testId = requireParam(id, "id", "update");
|
|
2844
|
+
const testConfig = requireParam(config, "config", "update");
|
|
2845
|
+
return toolResult(await updateTest(api, testId, testConfig));
|
|
2846
|
+
}
|
|
2847
|
+
case "delete": {
|
|
2848
|
+
const testIds = ids ?? (id ? [id] : void 0);
|
|
2849
|
+
const deleteIds = requireParam(testIds, "id or ids", "delete");
|
|
2850
|
+
return toolResult(await deleteTests(api, deleteIds));
|
|
2851
|
+
}
|
|
2852
|
+
case "trigger": {
|
|
2853
|
+
const testIds = ids ?? (id ? [id] : void 0);
|
|
2854
|
+
const triggerIds = requireParam(testIds, "id or ids", "trigger");
|
|
2855
|
+
return toolResult(await triggerTests(api, triggerIds));
|
|
2856
|
+
}
|
|
2857
|
+
case "results": {
|
|
2858
|
+
const testId = requireParam(id, "id", "results");
|
|
2859
|
+
return toolResult(await getTestResults(api, testId));
|
|
2860
|
+
}
|
|
2861
|
+
default:
|
|
2862
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2863
|
+
}
|
|
2864
|
+
} catch (error) {
|
|
2865
|
+
handleDatadogError(error);
|
|
2866
|
+
}
|
|
2867
|
+
}
|
|
2868
|
+
);
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
// src/tools/hosts.ts
|
|
2872
|
+
import { z as z11 } from "zod";
|
|
2873
|
+
var ActionSchema10 = z11.enum(["list", "totals", "mute", "unmute"]);
|
|
2874
|
+
var InputSchema10 = {
|
|
2875
|
+
action: ActionSchema10.describe("Action to perform"),
|
|
2876
|
+
filter: z11.string().optional().describe('Filter hosts by name, alias, or tag (e.g., "env:prod")'),
|
|
2877
|
+
from: z11.number().optional().describe("Starting offset for pagination"),
|
|
2878
|
+
count: z11.number().optional().describe("Number of hosts to return"),
|
|
2879
|
+
sortField: z11.string().optional().describe('Field to sort by (e.g., "apps", "cpu", "name")'),
|
|
2880
|
+
sortDir: z11.enum(["asc", "desc"]).optional().describe("Sort direction"),
|
|
2881
|
+
hostName: z11.string().optional().describe("Host name (required for mute/unmute)"),
|
|
2882
|
+
message: z11.string().optional().describe("Mute reason message"),
|
|
2883
|
+
end: z11.number().optional().describe("Mute end timestamp (POSIX). Omit for indefinite mute"),
|
|
2884
|
+
override: z11.boolean().optional().describe("If true, replaces existing mute instead of failing")
|
|
2885
|
+
};
|
|
2886
|
+
function formatHost(h) {
|
|
2887
|
+
return {
|
|
2888
|
+
hostName: h.hostName ?? "",
|
|
2889
|
+
aliases: h.aliases ?? [],
|
|
2890
|
+
apps: h.apps ?? [],
|
|
2891
|
+
sources: h.sources ?? [],
|
|
2892
|
+
up: h.up ?? false,
|
|
2893
|
+
isMuted: h.isMuted ?? false,
|
|
2894
|
+
muteTimeout: h.muteTimeout ?? null,
|
|
2895
|
+
lastReportedTime: h.lastReportedTime ? new Date(h.lastReportedTime * 1e3).toISOString() : "",
|
|
2896
|
+
meta: {
|
|
2897
|
+
cpuCores: h.meta?.cpuCores ?? null,
|
|
2898
|
+
platform: h.meta?.platform ?? null,
|
|
2899
|
+
gohai: h.meta?.gohai ?? null
|
|
2900
|
+
}
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
async function listHosts(api, params, limits) {
|
|
2904
|
+
const response = await api.listHosts({
|
|
2905
|
+
filter: params.filter,
|
|
2906
|
+
from: params.from,
|
|
2907
|
+
count: Math.min(params.count ?? limits.maxResults, limits.maxResults),
|
|
2908
|
+
sortField: params.sortField,
|
|
2909
|
+
sortDir: params.sortDir
|
|
2910
|
+
});
|
|
2911
|
+
const hosts = (response.hostList ?? []).map(formatHost);
|
|
2912
|
+
return {
|
|
2913
|
+
hosts,
|
|
2914
|
+
totalReturned: response.totalReturned ?? hosts.length,
|
|
2915
|
+
totalMatching: response.totalMatching ?? hosts.length
|
|
2916
|
+
};
|
|
2917
|
+
}
|
|
2918
|
+
async function getHostTotals(api) {
|
|
2919
|
+
const response = await api.getHostTotals({});
|
|
2920
|
+
return {
|
|
2921
|
+
totals: {
|
|
2922
|
+
totalUp: response.totalUp ?? 0,
|
|
2923
|
+
totalActive: response.totalActive ?? 0
|
|
2924
|
+
}
|
|
2925
|
+
};
|
|
2926
|
+
}
|
|
2927
|
+
async function muteHost(api, hostName, params) {
|
|
2928
|
+
await api.muteHost({
|
|
2929
|
+
hostName,
|
|
2930
|
+
body: {
|
|
2931
|
+
message: params.message,
|
|
2932
|
+
end: params.end,
|
|
2933
|
+
override: params.override
|
|
2934
|
+
}
|
|
2935
|
+
});
|
|
2936
|
+
return {
|
|
2937
|
+
success: true,
|
|
2938
|
+
message: `Host ${hostName} muted${params.end ? ` until ${new Date(params.end * 1e3).toISOString()}` : " indefinitely"}`
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
async function unmuteHost(api, hostName) {
|
|
2942
|
+
await api.unmuteHost({ hostName });
|
|
2943
|
+
return {
|
|
2944
|
+
success: true,
|
|
2945
|
+
message: `Host ${hostName} unmuted`
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
function registerHostsTool(server, api, limits, readOnly = false) {
|
|
2949
|
+
server.tool(
|
|
2950
|
+
"hosts",
|
|
2951
|
+
"Manage Datadog infrastructure hosts. Actions: list (with filters), totals (counts), mute (silence alerts), unmute. Use for: infrastructure inventory, host health, silencing noisy hosts during maintenance.",
|
|
2952
|
+
InputSchema10,
|
|
2953
|
+
async ({ action, filter, from, count, sortField, sortDir, hostName, message, end, override }) => {
|
|
2954
|
+
try {
|
|
2955
|
+
checkReadOnly(action, readOnly);
|
|
2956
|
+
switch (action) {
|
|
2957
|
+
case "list":
|
|
2958
|
+
return toolResult(await listHosts(api, { filter, from, count, sortField, sortDir }, limits));
|
|
2959
|
+
case "totals":
|
|
2960
|
+
return toolResult(await getHostTotals(api));
|
|
2961
|
+
case "mute": {
|
|
2962
|
+
const host = requireParam(hostName, "hostName", "mute");
|
|
2963
|
+
return toolResult(await muteHost(api, host, { message, end, override }));
|
|
2964
|
+
}
|
|
2965
|
+
case "unmute": {
|
|
2966
|
+
const host = requireParam(hostName, "hostName", "unmute");
|
|
2967
|
+
return toolResult(await unmuteHost(api, host));
|
|
2968
|
+
}
|
|
2969
|
+
default:
|
|
2970
|
+
throw new Error(`Unknown action: ${action}`);
|
|
2971
|
+
}
|
|
2972
|
+
} catch (error) {
|
|
2973
|
+
handleDatadogError(error);
|
|
2974
|
+
}
|
|
2975
|
+
}
|
|
2976
|
+
);
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// src/tools/downtimes.ts
|
|
2980
|
+
import { z as z12 } from "zod";
|
|
2981
|
+
var ActionSchema11 = z12.enum(["list", "get", "create", "update", "cancel", "listByMonitor"]);
|
|
2982
|
+
var InputSchema11 = {
|
|
2983
|
+
action: ActionSchema11.describe("Action to perform"),
|
|
2984
|
+
id: z12.string().optional().describe("Downtime ID (required for get/update/cancel)"),
|
|
2985
|
+
monitorId: z12.number().optional().describe("Monitor ID (required for listByMonitor)"),
|
|
2986
|
+
currentOnly: z12.boolean().optional().describe("Only return active downtimes (for list)"),
|
|
2987
|
+
limit: z12.number().optional().describe("Maximum number of downtimes to return"),
|
|
2988
|
+
config: z12.record(z12.unknown()).optional().describe("Downtime configuration (for create/update). Must include scope and schedule.")
|
|
2989
|
+
};
|
|
2990
|
+
function extractMonitorIdentifier(mi) {
|
|
2991
|
+
if (!mi) return { monitorId: null, monitorTags: [] };
|
|
2992
|
+
if ("monitorId" in mi && typeof mi.monitorId === "number") {
|
|
2993
|
+
return { monitorId: mi.monitorId, monitorTags: [] };
|
|
2994
|
+
}
|
|
2995
|
+
if ("monitorTags" in mi && Array.isArray(mi.monitorTags)) {
|
|
2996
|
+
return { monitorId: null, monitorTags: mi.monitorTags };
|
|
2997
|
+
}
|
|
2998
|
+
return { monitorId: null, monitorTags: [] };
|
|
2999
|
+
}
|
|
3000
|
+
function formatDowntime(d) {
|
|
3001
|
+
const attrs = d.attributes;
|
|
3002
|
+
const status = attrs?.status;
|
|
3003
|
+
return {
|
|
3004
|
+
id: d.id ?? "",
|
|
3005
|
+
displayTimezone: attrs?.displayTimezone ?? "UTC",
|
|
3006
|
+
message: attrs?.message ?? null,
|
|
3007
|
+
monitorIdentifier: extractMonitorIdentifier(attrs?.monitorIdentifier),
|
|
3008
|
+
scope: attrs?.scope ?? "",
|
|
3009
|
+
status: typeof status === "string" ? status : "unknown",
|
|
3010
|
+
schedule: attrs?.schedule ?? null,
|
|
3011
|
+
createdAt: attrs?.created ? new Date(attrs.created).toISOString() : "",
|
|
3012
|
+
modifiedAt: attrs?.modified ? new Date(attrs.modified).toISOString() : ""
|
|
3013
|
+
};
|
|
3014
|
+
}
|
|
3015
|
+
async function listDowntimes(api, params, limits) {
|
|
3016
|
+
const effectiveLimit = Math.min(params.limit ?? limits.maxResults, limits.maxResults);
|
|
3017
|
+
const response = await api.listDowntimes({
|
|
3018
|
+
currentOnly: params.currentOnly
|
|
3019
|
+
});
|
|
3020
|
+
const downtimes = (response.data ?? []).slice(0, effectiveLimit).map(formatDowntime);
|
|
3021
|
+
return {
|
|
3022
|
+
downtimes,
|
|
3023
|
+
total: response.data?.length ?? 0
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
async function getDowntime(api, id) {
|
|
3027
|
+
const response = await api.getDowntime({ downtimeId: id });
|
|
3028
|
+
return {
|
|
3029
|
+
downtime: response.data ? formatDowntime(response.data) : null
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
async function createDowntime(api, config) {
|
|
3033
|
+
const body = {
|
|
3034
|
+
data: {
|
|
3035
|
+
type: "downtime",
|
|
3036
|
+
attributes: config
|
|
3037
|
+
}
|
|
3038
|
+
};
|
|
3039
|
+
const response = await api.createDowntime({ body });
|
|
3040
|
+
return {
|
|
3041
|
+
success: true,
|
|
3042
|
+
downtime: response.data ? formatDowntime(response.data) : null
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
async function updateDowntime(api, id, config) {
|
|
3046
|
+
const body = {
|
|
3047
|
+
data: {
|
|
3048
|
+
type: "downtime",
|
|
3049
|
+
id,
|
|
3050
|
+
attributes: config
|
|
3051
|
+
}
|
|
3052
|
+
};
|
|
3053
|
+
const response = await api.updateDowntime({ downtimeId: id, body });
|
|
3054
|
+
return {
|
|
3055
|
+
success: true,
|
|
3056
|
+
downtime: response.data ? formatDowntime(response.data) : null
|
|
3057
|
+
};
|
|
3058
|
+
}
|
|
3059
|
+
async function cancelDowntime(api, id) {
|
|
3060
|
+
await api.cancelDowntime({ downtimeId: id });
|
|
3061
|
+
return {
|
|
3062
|
+
success: true,
|
|
3063
|
+
message: `Downtime ${id} cancelled`
|
|
3064
|
+
};
|
|
3065
|
+
}
|
|
3066
|
+
function formatMonitorDowntime(d) {
|
|
3067
|
+
const attrs = d.attributes;
|
|
3068
|
+
return {
|
|
3069
|
+
id: d.id ?? "",
|
|
3070
|
+
scope: attrs?.scope ?? null,
|
|
3071
|
+
start: attrs?.start ? new Date(attrs.start).toISOString() : null,
|
|
3072
|
+
end: attrs?.end ? new Date(attrs.end).toISOString() : null
|
|
3073
|
+
};
|
|
3074
|
+
}
|
|
3075
|
+
async function listMonitorDowntimes(api, monitorId, limits) {
|
|
3076
|
+
const response = await api.listMonitorDowntimes({ monitorId });
|
|
3077
|
+
const downtimes = (response.data ?? []).slice(0, limits.maxResults).map(formatMonitorDowntime);
|
|
3078
|
+
return {
|
|
3079
|
+
downtimes,
|
|
3080
|
+
monitorId,
|
|
3081
|
+
total: response.data?.length ?? 0
|
|
3082
|
+
};
|
|
3083
|
+
}
|
|
3084
|
+
function registerDowntimesTool(server, api, limits, readOnly = false) {
|
|
3085
|
+
server.tool(
|
|
3086
|
+
"downtimes",
|
|
3087
|
+
"Manage Datadog scheduled downtimes for maintenance windows. Actions: list, get, create, update, cancel, listByMonitor. Use for: scheduling maintenance, preventing false alerts during deployments, managing recurring maintenance windows.",
|
|
3088
|
+
InputSchema11,
|
|
3089
|
+
async ({ action, id, monitorId, currentOnly, limit, config }) => {
|
|
3090
|
+
try {
|
|
3091
|
+
checkReadOnly(action, readOnly);
|
|
3092
|
+
switch (action) {
|
|
3093
|
+
case "list":
|
|
3094
|
+
return toolResult(await listDowntimes(api, { currentOnly, limit }, limits));
|
|
3095
|
+
case "get": {
|
|
3096
|
+
const downtimeId = requireParam(id, "id", "get");
|
|
3097
|
+
return toolResult(await getDowntime(api, downtimeId));
|
|
3098
|
+
}
|
|
3099
|
+
case "create": {
|
|
3100
|
+
const downtimeConfig = requireParam(config, "config", "create");
|
|
3101
|
+
return toolResult(await createDowntime(api, downtimeConfig));
|
|
3102
|
+
}
|
|
3103
|
+
case "update": {
|
|
3104
|
+
const downtimeId = requireParam(id, "id", "update");
|
|
3105
|
+
const downtimeConfig = requireParam(config, "config", "update");
|
|
3106
|
+
return toolResult(await updateDowntime(api, downtimeId, downtimeConfig));
|
|
3107
|
+
}
|
|
3108
|
+
case "cancel": {
|
|
3109
|
+
const downtimeId = requireParam(id, "id", "cancel");
|
|
3110
|
+
return toolResult(await cancelDowntime(api, downtimeId));
|
|
3111
|
+
}
|
|
3112
|
+
case "listByMonitor": {
|
|
3113
|
+
const monitor = requireParam(monitorId, "monitorId", "listByMonitor");
|
|
3114
|
+
return toolResult(await listMonitorDowntimes(api, monitor, limits));
|
|
3115
|
+
}
|
|
3116
|
+
default:
|
|
3117
|
+
throw new Error(`Unknown action: ${action}`);
|
|
3118
|
+
}
|
|
3119
|
+
} catch (error) {
|
|
3120
|
+
handleDatadogError(error);
|
|
3121
|
+
}
|
|
3122
|
+
}
|
|
3123
|
+
);
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
// src/tools/rum.ts
|
|
3127
|
+
import { z as z13 } from "zod";
|
|
3128
|
+
var ActionSchema12 = z13.enum(["applications", "events", "aggregate", "performance", "waterfall"]);
|
|
3129
|
+
var InputSchema12 = {
|
|
3130
|
+
action: ActionSchema12.describe("Action to perform"),
|
|
3131
|
+
query: z13.string().optional().describe('RUM query string (e.g., "@type:view @application.id:abc")'),
|
|
3132
|
+
from: z13.string().optional().describe('Start time (ISO 8601, relative like "1h", "7d", or precise like "1d@10:00")'),
|
|
3133
|
+
to: z13.string().optional().describe('End time (ISO 8601, relative like "now", or precise timestamp)'),
|
|
3134
|
+
type: z13.enum(["all", "view", "action", "error", "long_task", "resource"]).optional().describe("RUM event type filter"),
|
|
3135
|
+
sort: z13.enum(["timestamp", "-timestamp"]).optional().describe("Sort order for events"),
|
|
3136
|
+
limit: z13.number().optional().describe("Maximum number of events to return"),
|
|
3137
|
+
groupBy: z13.array(z13.string()).optional().describe('Fields to group by for aggregation (e.g., ["@view.url_path", "@session.type"])'),
|
|
3138
|
+
compute: z13.object({
|
|
3139
|
+
aggregation: z13.enum(["count", "cardinality", "avg", "sum", "min", "max", "percentile"]).optional(),
|
|
3140
|
+
metric: z13.string().optional(),
|
|
3141
|
+
interval: z13.string().optional()
|
|
3142
|
+
}).optional().describe("Compute configuration for aggregation"),
|
|
3143
|
+
// Performance action parameters
|
|
3144
|
+
metrics: z13.array(z13.enum(["lcp", "fcp", "cls", "fid", "inp", "loading_time"])).optional().describe("Core Web Vitals metrics to retrieve (default: all). lcp=Largest Contentful Paint, fcp=First Contentful Paint, cls=Cumulative Layout Shift, fid=First Input Delay, inp=Interaction to Next Paint, loading_time=View loading time"),
|
|
3145
|
+
// Waterfall action parameters
|
|
3146
|
+
applicationId: z13.string().optional().describe("Application ID for waterfall action"),
|
|
3147
|
+
sessionId: z13.string().optional().describe("Session ID for waterfall action"),
|
|
3148
|
+
viewId: z13.string().optional().describe("View ID for waterfall action (optional, filters to specific view)")
|
|
3149
|
+
};
|
|
3150
|
+
function formatApplication(app) {
|
|
3151
|
+
const attrs = app.attributes ?? {};
|
|
3152
|
+
return {
|
|
3153
|
+
id: app.id ?? "",
|
|
3154
|
+
name: attrs.name ?? "",
|
|
3155
|
+
type: String(attrs.type ?? ""),
|
|
3156
|
+
orgId: attrs.orgId ?? 0,
|
|
3157
|
+
hash: attrs.hash ?? null,
|
|
3158
|
+
createdAt: attrs.createdAt ? new Date(attrs.createdAt).toISOString() : "",
|
|
3159
|
+
updatedAt: attrs.updatedAt ? new Date(attrs.updatedAt).toISOString() : ""
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
function formatEvent(event) {
|
|
3163
|
+
const attrs = event.attributes ?? {};
|
|
3164
|
+
const appAttrs = attrs.attributes ?? {};
|
|
3165
|
+
const application = appAttrs["application"] ?? {};
|
|
3166
|
+
const session = appAttrs["session"] ?? {};
|
|
3167
|
+
const view = appAttrs["view"] ?? {};
|
|
3168
|
+
const usr = appAttrs["usr"] ?? {};
|
|
3169
|
+
const action = appAttrs["action"] ?? {};
|
|
3170
|
+
const error = appAttrs["error"] ?? {};
|
|
3171
|
+
const resource = appAttrs["resource"] ?? {};
|
|
3172
|
+
return {
|
|
3173
|
+
id: event.id ?? "",
|
|
3174
|
+
type: String(event.type ?? ""),
|
|
3175
|
+
timestamp: attrs.timestamp?.toISOString() ?? "",
|
|
3176
|
+
attributes: {
|
|
3177
|
+
application: {
|
|
3178
|
+
id: application["id"] ?? null,
|
|
3179
|
+
name: application["name"] ?? null
|
|
3180
|
+
},
|
|
3181
|
+
session: {
|
|
3182
|
+
id: session["id"] ?? null,
|
|
3183
|
+
type: session["type"] ?? null
|
|
3184
|
+
},
|
|
3185
|
+
view: {
|
|
3186
|
+
id: view["id"] ?? null,
|
|
3187
|
+
url: view["url"] ?? null,
|
|
3188
|
+
urlPath: view["url_path"] ?? null,
|
|
3189
|
+
name: view["name"] ?? null
|
|
3190
|
+
},
|
|
3191
|
+
user: {
|
|
3192
|
+
id: usr["id"] ?? null,
|
|
3193
|
+
email: usr["email"] ?? null,
|
|
3194
|
+
name: usr["name"] ?? null
|
|
3195
|
+
},
|
|
3196
|
+
action: action["id"] ? {
|
|
3197
|
+
id: action["id"] ?? null,
|
|
3198
|
+
type: action["type"] ?? null,
|
|
3199
|
+
name: action["name"] ?? null
|
|
3200
|
+
} : void 0,
|
|
3201
|
+
error: error["message"] ? {
|
|
3202
|
+
message: error["message"] ?? null,
|
|
3203
|
+
source: error["source"] ?? null,
|
|
3204
|
+
stack: error["stack"] ?? null
|
|
3205
|
+
} : void 0,
|
|
3206
|
+
resource: resource["url"] ? {
|
|
3207
|
+
url: resource["url"] ?? null,
|
|
3208
|
+
type: resource["type"] ?? null,
|
|
3209
|
+
duration: resource["duration"] ?? null
|
|
3210
|
+
} : void 0
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
async function listApplications(api) {
|
|
3215
|
+
const response = await api.getRUMApplications();
|
|
3216
|
+
const applications = (response.data ?? []).map(formatApplication);
|
|
3217
|
+
return {
|
|
3218
|
+
applications,
|
|
3219
|
+
totalCount: applications.length
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
async function searchEvents(api, params, limits, site) {
|
|
3223
|
+
let queryString = params.query ?? "*";
|
|
3224
|
+
if (params.type && params.type !== "all") {
|
|
3225
|
+
queryString = `@type:${params.type} ${queryString}`.trim();
|
|
3226
|
+
}
|
|
3227
|
+
const nowMs = Date.now();
|
|
3228
|
+
const defaultFromMs = nowMs - 15 * 60 * 1e3;
|
|
3229
|
+
const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3));
|
|
3230
|
+
const toTime = parseTime(params.to, Math.floor(nowMs / 1e3));
|
|
3231
|
+
const response = await api.listRUMEvents({
|
|
3232
|
+
filterQuery: queryString,
|
|
3233
|
+
filterFrom: new Date(fromTime * 1e3),
|
|
3234
|
+
filterTo: new Date(toTime * 1e3),
|
|
3235
|
+
sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
|
|
3236
|
+
pageLimit: Math.min(params.limit ?? limits.maxResults, limits.maxResults)
|
|
3237
|
+
});
|
|
3238
|
+
const events = (response.data ?? []).map(formatEvent);
|
|
3239
|
+
return {
|
|
3240
|
+
events,
|
|
3241
|
+
meta: {
|
|
3242
|
+
totalCount: events.length,
|
|
3243
|
+
timeRange: {
|
|
3244
|
+
from: new Date(fromTime * 1e3).toISOString(),
|
|
3245
|
+
to: new Date(toTime * 1e3).toISOString()
|
|
3246
|
+
},
|
|
3247
|
+
datadog_url: buildRumUrl(queryString, fromTime, toTime, site)
|
|
3248
|
+
}
|
|
3249
|
+
};
|
|
3250
|
+
}
|
|
3251
|
+
async function aggregateEvents(api, params, _limits, site) {
|
|
3252
|
+
const nowMs = Date.now();
|
|
3253
|
+
const defaultFromMs = nowMs - 60 * 60 * 1e3;
|
|
3254
|
+
const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3));
|
|
3255
|
+
const toTime = parseTime(params.to, Math.floor(nowMs / 1e3));
|
|
3256
|
+
const groupByConfigs = (params.groupBy ?? []).map((field) => ({
|
|
3257
|
+
facet: field,
|
|
3258
|
+
limit: 10,
|
|
3259
|
+
sort: {
|
|
3260
|
+
type: "measure",
|
|
3261
|
+
aggregation: "count",
|
|
3262
|
+
order: "desc"
|
|
3263
|
+
}
|
|
3264
|
+
}));
|
|
3265
|
+
const computeConfig = {
|
|
3266
|
+
aggregation: params.compute?.aggregation ?? "count"
|
|
3267
|
+
};
|
|
3268
|
+
if (params.compute?.metric) {
|
|
3269
|
+
computeConfig.metric = params.compute.metric;
|
|
3270
|
+
computeConfig.type = "total";
|
|
3271
|
+
}
|
|
3272
|
+
if (params.compute?.interval) {
|
|
3273
|
+
computeConfig.interval = params.compute.interval;
|
|
3274
|
+
computeConfig.type = "timeseries";
|
|
3275
|
+
}
|
|
3276
|
+
const computeConfigs = [computeConfig];
|
|
3277
|
+
const queryString = params.query ?? "*";
|
|
3278
|
+
const response = await api.aggregateRUMEvents({
|
|
3279
|
+
body: {
|
|
3280
|
+
filter: {
|
|
3281
|
+
query: queryString,
|
|
3282
|
+
from: new Date(fromTime * 1e3).toISOString(),
|
|
3283
|
+
to: new Date(toTime * 1e3).toISOString()
|
|
3284
|
+
},
|
|
3285
|
+
groupBy: groupByConfigs.length > 0 ? groupByConfigs : void 0,
|
|
3286
|
+
compute: computeConfigs
|
|
3287
|
+
}
|
|
3288
|
+
});
|
|
3289
|
+
const buckets = (response.data?.buckets ?? []).map((bucket) => ({
|
|
3290
|
+
by: bucket.by ?? {},
|
|
3291
|
+
computes: bucket.computes ?? {}
|
|
3292
|
+
}));
|
|
3293
|
+
return {
|
|
3294
|
+
buckets,
|
|
3295
|
+
meta: {
|
|
3296
|
+
elapsed: response.meta?.elapsed ?? 0,
|
|
3297
|
+
timeRange: {
|
|
3298
|
+
from: new Date(fromTime * 1e3).toISOString(),
|
|
3299
|
+
to: new Date(toTime * 1e3).toISOString()
|
|
3300
|
+
},
|
|
3301
|
+
datadog_url: buildRumUrl(queryString, fromTime, toTime, site)
|
|
3302
|
+
}
|
|
3303
|
+
};
|
|
3304
|
+
}
|
|
3305
|
+
var METRIC_CONFIGS = {
|
|
3306
|
+
lcp: {
|
|
3307
|
+
field: "@view.largest_contentful_paint",
|
|
3308
|
+
aggregations: ["avg", "pc75", "pc90"]
|
|
3309
|
+
},
|
|
3310
|
+
fcp: {
|
|
3311
|
+
field: "@view.first_contentful_paint",
|
|
3312
|
+
aggregations: ["avg", "pc75", "pc90"]
|
|
3313
|
+
},
|
|
3314
|
+
cls: {
|
|
3315
|
+
field: "@view.cumulative_layout_shift",
|
|
3316
|
+
aggregations: ["avg", "pc75"]
|
|
3317
|
+
},
|
|
3318
|
+
fid: {
|
|
3319
|
+
field: "@view.first_input_delay",
|
|
3320
|
+
aggregations: ["avg", "pc75", "pc90"]
|
|
3321
|
+
},
|
|
3322
|
+
inp: {
|
|
3323
|
+
field: "@view.interaction_to_next_paint",
|
|
3324
|
+
aggregations: ["avg", "pc75", "pc90"]
|
|
3325
|
+
},
|
|
3326
|
+
loading_time: {
|
|
3327
|
+
field: "@view.loading_time",
|
|
3328
|
+
aggregations: ["avg", "pc75", "pc90"]
|
|
3329
|
+
}
|
|
3330
|
+
};
|
|
3331
|
+
async function getPerformanceMetrics(api, params, _limits, site) {
|
|
3332
|
+
const nowMs = Date.now();
|
|
3333
|
+
const defaultFromMs = nowMs - 60 * 60 * 1e3;
|
|
3334
|
+
const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3));
|
|
3335
|
+
const toTime = parseTime(params.to, Math.floor(nowMs / 1e3));
|
|
3336
|
+
const requestedMetrics = params.metrics ?? ["lcp", "fcp", "cls"];
|
|
3337
|
+
const computeConfigs = [];
|
|
3338
|
+
for (const metricName of requestedMetrics) {
|
|
3339
|
+
const config = METRIC_CONFIGS[metricName];
|
|
3340
|
+
if (!config) continue;
|
|
3341
|
+
for (const aggregation of config.aggregations) {
|
|
3342
|
+
computeConfigs.push({
|
|
3343
|
+
aggregation,
|
|
3344
|
+
metric: config.field,
|
|
3345
|
+
type: "total"
|
|
3346
|
+
});
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
const groupByConfigs = (params.groupBy ?? []).map((field) => ({
|
|
3350
|
+
facet: field,
|
|
3351
|
+
limit: 10,
|
|
3352
|
+
sort: {
|
|
3353
|
+
type: "measure",
|
|
3354
|
+
aggregation: "count",
|
|
3355
|
+
order: "desc"
|
|
3356
|
+
}
|
|
3357
|
+
}));
|
|
3358
|
+
const viewQuery = params.query ? `@type:view ${params.query}` : "@type:view";
|
|
3359
|
+
const response = await api.aggregateRUMEvents({
|
|
3360
|
+
body: {
|
|
3361
|
+
filter: {
|
|
3362
|
+
query: viewQuery,
|
|
3363
|
+
from: new Date(fromTime * 1e3).toISOString(),
|
|
3364
|
+
to: new Date(toTime * 1e3).toISOString()
|
|
3365
|
+
},
|
|
3366
|
+
groupBy: groupByConfigs.length > 0 ? groupByConfigs : void 0,
|
|
3367
|
+
compute: computeConfigs
|
|
3368
|
+
}
|
|
3369
|
+
});
|
|
3370
|
+
const buckets = (response.data?.buckets ?? []).map((bucket) => {
|
|
3371
|
+
const computes = bucket.computes ?? {};
|
|
3372
|
+
const metrics = {};
|
|
3373
|
+
for (const metricName of requestedMetrics) {
|
|
3374
|
+
const config = METRIC_CONFIGS[metricName];
|
|
3375
|
+
if (!config) continue;
|
|
3376
|
+
metrics[metricName] = {};
|
|
3377
|
+
for (const aggregation of config.aggregations) {
|
|
3378
|
+
const computeIndex = computeConfigs.findIndex(
|
|
3379
|
+
(c) => c.metric === config.field && c.aggregation === aggregation
|
|
3380
|
+
);
|
|
3381
|
+
const key = `c${computeIndex}`;
|
|
3382
|
+
const value = computes[key]?.value;
|
|
3383
|
+
metrics[metricName][String(aggregation)] = value ?? null;
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return {
|
|
3387
|
+
by: bucket.by ?? {},
|
|
3388
|
+
metrics
|
|
3389
|
+
};
|
|
3390
|
+
});
|
|
3391
|
+
return {
|
|
3392
|
+
buckets,
|
|
3393
|
+
meta: {
|
|
3394
|
+
metrics: requestedMetrics,
|
|
3395
|
+
timeRange: {
|
|
3396
|
+
from: new Date(fromTime * 1e3).toISOString(),
|
|
3397
|
+
to: new Date(toTime * 1e3).toISOString()
|
|
3398
|
+
},
|
|
3399
|
+
datadog_url: buildRumUrl(viewQuery, fromTime, toTime, site)
|
|
3400
|
+
}
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
function formatWaterfallEvent(event) {
|
|
3404
|
+
const attrs = event.attributes ?? {};
|
|
3405
|
+
const appAttrs = attrs.attributes ?? {};
|
|
3406
|
+
const view = appAttrs["view"] ?? {};
|
|
3407
|
+
const resource = appAttrs["resource"] ?? {};
|
|
3408
|
+
const action = appAttrs["action"] ?? {};
|
|
3409
|
+
const error = appAttrs["error"] ?? {};
|
|
3410
|
+
const longTask = appAttrs["long_task"] ?? {};
|
|
3411
|
+
const eventType = appAttrs["type"] ?? "unknown";
|
|
3412
|
+
return {
|
|
3413
|
+
id: event.id ?? "",
|
|
3414
|
+
type: eventType,
|
|
3415
|
+
timestamp: attrs.timestamp?.toISOString() ?? "",
|
|
3416
|
+
duration: view["loading_time"] ?? resource["duration"] ?? null,
|
|
3417
|
+
view: {
|
|
3418
|
+
id: view["id"] ?? null,
|
|
3419
|
+
url: view["url"] ?? null,
|
|
3420
|
+
name: view["name"] ?? null
|
|
3421
|
+
},
|
|
3422
|
+
resource: resource["url"] ? {
|
|
3423
|
+
url: resource["url"] ?? null,
|
|
3424
|
+
type: resource["type"] ?? null,
|
|
3425
|
+
duration: resource["duration"] ?? null,
|
|
3426
|
+
size: resource["size"] ?? null,
|
|
3427
|
+
statusCode: resource["status_code"] ?? null
|
|
3428
|
+
} : void 0,
|
|
3429
|
+
action: action["id"] ? {
|
|
3430
|
+
id: action["id"] ?? null,
|
|
3431
|
+
type: action["type"] ?? null,
|
|
3432
|
+
name: action["name"] ?? null,
|
|
3433
|
+
target: action["target"] ?? null
|
|
3434
|
+
} : void 0,
|
|
3435
|
+
error: error["message"] ? {
|
|
3436
|
+
message: error["message"] ?? null,
|
|
3437
|
+
source: error["source"] ?? null,
|
|
3438
|
+
type: error["type"] ?? null
|
|
3439
|
+
} : void 0,
|
|
3440
|
+
longTask: longTask["duration"] ? {
|
|
3441
|
+
duration: longTask["duration"] ?? null
|
|
3442
|
+
} : void 0
|
|
3443
|
+
};
|
|
3444
|
+
}
|
|
3445
|
+
async function getSessionWaterfall(api, params, limits, site) {
|
|
3446
|
+
const queryParts = [
|
|
3447
|
+
`@application.id:${params.applicationId}`,
|
|
3448
|
+
`@session.id:${params.sessionId}`
|
|
3449
|
+
];
|
|
3450
|
+
if (params.viewId) {
|
|
3451
|
+
queryParts.push(`@view.id:${params.viewId}`);
|
|
3452
|
+
}
|
|
3453
|
+
const response = await api.listRUMEvents({
|
|
3454
|
+
filterQuery: queryParts.join(" "),
|
|
3455
|
+
sort: "timestamp",
|
|
3456
|
+
pageLimit: Math.min(limits.maxResults, 1e3)
|
|
3457
|
+
});
|
|
3458
|
+
const events = (response.data ?? []).map(formatWaterfallEvent);
|
|
3459
|
+
const summary = {
|
|
3460
|
+
views: events.filter((e) => e.type === "view").length,
|
|
3461
|
+
resources: events.filter((e) => e.type === "resource").length,
|
|
3462
|
+
actions: events.filter((e) => e.type === "action").length,
|
|
3463
|
+
errors: events.filter((e) => e.type === "error").length,
|
|
3464
|
+
longTasks: events.filter((e) => e.type === "long_task").length
|
|
3465
|
+
};
|
|
3466
|
+
return {
|
|
3467
|
+
events,
|
|
3468
|
+
summary,
|
|
3469
|
+
meta: {
|
|
3470
|
+
totalCount: events.length,
|
|
3471
|
+
applicationId: params.applicationId,
|
|
3472
|
+
sessionId: params.sessionId,
|
|
3473
|
+
viewId: params.viewId ?? null,
|
|
3474
|
+
datadog_url: buildRumSessionUrl(params.applicationId, params.sessionId, site)
|
|
3475
|
+
}
|
|
3476
|
+
};
|
|
3477
|
+
}
|
|
3478
|
+
function registerRumTool(server, api, limits, site = "datadoghq.com") {
|
|
3479
|
+
server.tool(
|
|
3480
|
+
"rum",
|
|
3481
|
+
"Query Datadog Real User Monitoring (RUM) data. Actions: applications (list RUM apps), events (search RUM events), aggregate (group and count events), performance (Core Web Vitals: LCP, FCP, CLS, FID, INP), waterfall (session timeline with resources/actions/errors). Use for: frontend performance, user sessions, page views, errors, resource loading.",
|
|
3482
|
+
InputSchema12,
|
|
3483
|
+
async ({ action, query, from, to, type, sort, limit, groupBy, compute, metrics, applicationId, sessionId, viewId }) => {
|
|
3484
|
+
try {
|
|
3485
|
+
switch (action) {
|
|
3486
|
+
case "applications":
|
|
3487
|
+
return toolResult(await listApplications(api));
|
|
3488
|
+
case "events":
|
|
3489
|
+
return toolResult(await searchEvents(api, { query, from, to, type, sort, limit }, limits, site));
|
|
3490
|
+
case "aggregate":
|
|
3491
|
+
return toolResult(await aggregateEvents(api, { query, from, to, groupBy, compute }, limits, site));
|
|
3492
|
+
case "performance":
|
|
3493
|
+
return toolResult(await getPerformanceMetrics(api, { query, from, to, groupBy, metrics }, limits, site));
|
|
3494
|
+
case "waterfall":
|
|
3495
|
+
if (!applicationId || !sessionId) {
|
|
3496
|
+
throw new Error("waterfall action requires applicationId and sessionId parameters");
|
|
3497
|
+
}
|
|
3498
|
+
return toolResult(await getSessionWaterfall(api, { applicationId, sessionId, viewId }, limits, site));
|
|
3499
|
+
default:
|
|
3500
|
+
throw new Error(`Unknown action: ${action}`);
|
|
3501
|
+
}
|
|
3502
|
+
} catch (error) {
|
|
3503
|
+
handleDatadogError(error);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
3506
|
+
);
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
// src/tools/security.ts
|
|
3510
|
+
import { z as z14 } from "zod";
|
|
3511
|
+
var ActionSchema13 = z14.enum(["rules", "signals", "findings"]);
|
|
3512
|
+
var InputSchema13 = {
|
|
3513
|
+
action: ActionSchema13.describe("Action to perform"),
|
|
3514
|
+
id: z14.string().optional().describe("Rule or signal ID (for specific lookups)"),
|
|
3515
|
+
query: z14.string().optional().describe("Search query for signals or findings"),
|
|
3516
|
+
from: z14.string().optional().describe('Start time (ISO 8601, relative like "1h", "7d")'),
|
|
3517
|
+
to: z14.string().optional().describe('End time (ISO 8601, relative like "now")'),
|
|
3518
|
+
severity: z14.enum(["info", "low", "medium", "high", "critical"]).optional().describe("Filter by severity"),
|
|
3519
|
+
status: z14.enum(["open", "under_review", "archived"]).optional().describe("Filter signals by status"),
|
|
3520
|
+
pageSize: z14.number().optional().describe("Number of results to return"),
|
|
3521
|
+
pageCursor: z14.string().optional().describe("Cursor for pagination")
|
|
3522
|
+
};
|
|
3523
|
+
function formatRule(rule) {
|
|
3524
|
+
const ruleData = rule;
|
|
3525
|
+
return {
|
|
3526
|
+
id: ruleData["id"] ?? "",
|
|
3527
|
+
name: ruleData["name"] ?? "",
|
|
3528
|
+
type: ruleData["type"] ?? "",
|
|
3529
|
+
isEnabled: ruleData["isEnabled"] ?? false,
|
|
3530
|
+
hasExtendedTitle: ruleData["hasExtendedTitle"] ?? false,
|
|
3531
|
+
message: ruleData["message"] ?? null,
|
|
3532
|
+
tags: ruleData["tags"] ?? [],
|
|
3533
|
+
createdAt: ruleData["createdAt"] ? new Date(ruleData["createdAt"]).toISOString() : "",
|
|
3534
|
+
updatedAt: ruleData["updatedAt"] ? new Date(ruleData["updatedAt"]).toISOString() : "",
|
|
3535
|
+
creationAuthorId: ruleData["creationAuthorId"] ?? null,
|
|
3536
|
+
isDefault: ruleData["isDefault"] ?? false,
|
|
3537
|
+
isDeleted: ruleData["isDeleted"] ?? false,
|
|
3538
|
+
filters: (ruleData["filters"] ?? []).map((f) => ({
|
|
3539
|
+
action: f.action ?? "",
|
|
3540
|
+
query: f.query ?? ""
|
|
3541
|
+
}))
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
function formatSignal(signal) {
|
|
3545
|
+
const attrs = signal.attributes ?? {};
|
|
3546
|
+
const customAttrs = attrs;
|
|
3547
|
+
return {
|
|
3548
|
+
id: signal.id ?? "",
|
|
3549
|
+
type: String(signal.type ?? ""),
|
|
3550
|
+
timestamp: attrs.timestamp?.toISOString() ?? "",
|
|
3551
|
+
attributes: {
|
|
3552
|
+
message: attrs.message ?? null,
|
|
3553
|
+
status: customAttrs["status"] ?? null,
|
|
3554
|
+
severity: customAttrs["severity"] ?? null,
|
|
3555
|
+
tags: attrs.tags ?? [],
|
|
3556
|
+
custom: attrs.custom ?? {}
|
|
3557
|
+
}
|
|
3558
|
+
};
|
|
3559
|
+
}
|
|
3560
|
+
async function listRules(api, params, limits) {
|
|
3561
|
+
const response = await api.listSecurityMonitoringRules({
|
|
3562
|
+
pageSize: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
|
|
3563
|
+
pageNumber: 0
|
|
3564
|
+
});
|
|
3565
|
+
const rules = (response.data ?? []).map(formatRule);
|
|
3566
|
+
return {
|
|
3567
|
+
rules,
|
|
3568
|
+
meta: {
|
|
3569
|
+
totalCount: rules.length
|
|
3570
|
+
}
|
|
3571
|
+
};
|
|
3572
|
+
}
|
|
3573
|
+
async function getRule(api, ruleId) {
|
|
3574
|
+
const response = await api.getSecurityMonitoringRule({ ruleId });
|
|
3575
|
+
return {
|
|
3576
|
+
rule: formatRule(response)
|
|
3577
|
+
};
|
|
3578
|
+
}
|
|
3579
|
+
async function searchSignals(api, params, limits) {
|
|
3580
|
+
const nowMs = Date.now();
|
|
3581
|
+
const defaultFromMs = nowMs - 24 * 60 * 60 * 1e3;
|
|
3582
|
+
const fromTime = parseTime(params.from, Math.floor(defaultFromMs / 1e3));
|
|
3583
|
+
const toTime = parseTime(params.to, Math.floor(nowMs / 1e3));
|
|
3584
|
+
let queryString = params.query ?? "*";
|
|
3585
|
+
if (params.severity) {
|
|
3586
|
+
queryString = `severity:${params.severity} ${queryString}`.trim();
|
|
3587
|
+
}
|
|
3588
|
+
if (params.status) {
|
|
3589
|
+
queryString = `status:${params.status} ${queryString}`.trim();
|
|
3590
|
+
}
|
|
3591
|
+
const response = await api.searchSecurityMonitoringSignals({
|
|
3592
|
+
body: {
|
|
3593
|
+
filter: {
|
|
3594
|
+
query: queryString,
|
|
3595
|
+
from: new Date(fromTime * 1e3),
|
|
3596
|
+
to: new Date(toTime * 1e3)
|
|
3597
|
+
},
|
|
3598
|
+
page: {
|
|
3599
|
+
limit: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
|
|
3600
|
+
cursor: params.pageCursor
|
|
3601
|
+
},
|
|
3602
|
+
sort: "timestamp"
|
|
3603
|
+
}
|
|
3604
|
+
});
|
|
3605
|
+
const signals = (response.data ?? []).map(formatSignal);
|
|
3606
|
+
return {
|
|
3607
|
+
signals,
|
|
3608
|
+
meta: {
|
|
3609
|
+
nextCursor: response.meta?.page?.after ?? null,
|
|
3610
|
+
totalCount: signals.length,
|
|
3611
|
+
timeRange: {
|
|
3612
|
+
from: new Date(fromTime * 1e3).toISOString(),
|
|
3613
|
+
to: new Date(toTime * 1e3).toISOString()
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
async function listFindings(api, params, limits) {
|
|
3619
|
+
const response = await api.searchSecurityMonitoringSignals({
|
|
3620
|
+
body: {
|
|
3621
|
+
filter: {
|
|
3622
|
+
query: params.query ?? "@workflow.rule.type:workload_security OR @workflow.rule.type:cloud_configuration",
|
|
3623
|
+
from: new Date(Date.now() - 7 * 24 * 60 * 60 * 1e3),
|
|
3624
|
+
// Last 7 days
|
|
3625
|
+
to: /* @__PURE__ */ new Date()
|
|
3626
|
+
},
|
|
3627
|
+
page: {
|
|
3628
|
+
limit: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
|
|
3629
|
+
cursor: params.pageCursor
|
|
3630
|
+
}
|
|
3631
|
+
}
|
|
3632
|
+
});
|
|
3633
|
+
const findings = (response.data ?? []).map(formatSignal);
|
|
3634
|
+
return {
|
|
3635
|
+
findings,
|
|
3636
|
+
meta: {
|
|
3637
|
+
nextCursor: response.meta?.page?.after ?? null,
|
|
3638
|
+
totalCount: findings.length
|
|
3639
|
+
}
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
function registerSecurityTool(server, api, limits) {
|
|
3643
|
+
server.tool(
|
|
3644
|
+
"security",
|
|
3645
|
+
"Query Datadog Security Monitoring. Actions: rules (list detection rules), signals (search security signals), findings (list security findings). Use for: threat detection, compliance, security posture, incident investigation.",
|
|
3646
|
+
InputSchema13,
|
|
3647
|
+
async ({ action, id, query, from, to, severity, status, pageSize, pageCursor }) => {
|
|
3648
|
+
try {
|
|
3649
|
+
switch (action) {
|
|
3650
|
+
case "rules":
|
|
3651
|
+
if (id) {
|
|
3652
|
+
return toolResult(await getRule(api, id));
|
|
3653
|
+
}
|
|
3654
|
+
return toolResult(await listRules(api, { pageSize, pageCursor }, limits));
|
|
3655
|
+
case "signals":
|
|
3656
|
+
return toolResult(await searchSignals(api, { query, from, to, severity, status, pageSize, pageCursor }, limits));
|
|
3657
|
+
case "findings":
|
|
3658
|
+
return toolResult(await listFindings(api, { query, pageSize, pageCursor }, limits));
|
|
3659
|
+
default:
|
|
3660
|
+
throw new Error(`Unknown action: ${action}`);
|
|
3661
|
+
}
|
|
3662
|
+
} catch (error) {
|
|
3663
|
+
handleDatadogError(error);
|
|
3664
|
+
}
|
|
3665
|
+
}
|
|
3666
|
+
);
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
// src/tools/notebooks.ts
|
|
3670
|
+
import { z as z15 } from "zod";
|
|
3671
|
+
var ActionSchema14 = z15.enum(["list", "get", "create", "update", "delete"]);
|
|
3672
|
+
var InputSchema14 = {
|
|
3673
|
+
action: ActionSchema14.describe("Action to perform"),
|
|
3674
|
+
id: z15.number().optional().describe("Notebook ID (required for get/update/delete actions)"),
|
|
3675
|
+
query: z15.string().optional().describe("Search query for notebooks"),
|
|
3676
|
+
authorHandle: z15.string().optional().describe("Filter by author handle (email)"),
|
|
3677
|
+
excludeAuthorHandle: z15.string().optional().describe("Exclude notebooks by author handle"),
|
|
3678
|
+
includeCells: z15.boolean().optional().describe("Include cell content in response (default: true for get)"),
|
|
3679
|
+
name: z15.string().optional().describe("Notebook name (for create/update)"),
|
|
3680
|
+
cells: z15.array(z15.object({
|
|
3681
|
+
type: z15.enum(["markdown", "timeseries", "toplist", "heatmap", "distribution", "log_stream"]),
|
|
3682
|
+
content: z15.unknown()
|
|
3683
|
+
})).optional().describe("Notebook cells (for create/update)"),
|
|
3684
|
+
time: z15.object({
|
|
3685
|
+
liveSpan: z15.string().optional(),
|
|
3686
|
+
start: z15.number().optional(),
|
|
3687
|
+
end: z15.number().optional()
|
|
3688
|
+
}).optional().describe("Time configuration for notebook"),
|
|
3689
|
+
status: z15.enum(["published"]).optional().describe("Notebook status"),
|
|
3690
|
+
pageSize: z15.number().optional().describe("Number of notebooks to return"),
|
|
3691
|
+
pageNumber: z15.number().optional().describe("Page number for pagination")
|
|
3692
|
+
};
|
|
3693
|
+
function formatNotebookSummary(nb) {
|
|
3694
|
+
const attrs = nb.attributes ?? {};
|
|
3695
|
+
return {
|
|
3696
|
+
id: nb.id ?? 0,
|
|
3697
|
+
name: attrs.name ?? "",
|
|
3698
|
+
author: {
|
|
3699
|
+
handle: attrs.author?.handle ?? null,
|
|
3700
|
+
name: attrs.author?.name ?? null
|
|
3701
|
+
},
|
|
3702
|
+
status: String(attrs.status ?? ""),
|
|
3703
|
+
cellCount: attrs.cells?.length ?? 0,
|
|
3704
|
+
created: attrs.created?.toISOString() ?? "",
|
|
3705
|
+
modified: attrs.modified?.toISOString() ?? "",
|
|
3706
|
+
metadata: {
|
|
3707
|
+
isTemplate: attrs.metadata?.isTemplate ?? null,
|
|
3708
|
+
takeSnapshots: attrs.metadata?.takeSnapshots ?? null
|
|
3709
|
+
}
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
function formatNotebookDetail(nb) {
|
|
3713
|
+
const attrs = nb.attributes ?? {};
|
|
3714
|
+
return {
|
|
3715
|
+
id: nb.id ?? 0,
|
|
3716
|
+
name: attrs.name ?? "",
|
|
3717
|
+
author: {
|
|
3718
|
+
handle: attrs.author?.handle ?? null,
|
|
3719
|
+
name: attrs.author?.name ?? null
|
|
3720
|
+
},
|
|
3721
|
+
status: String(attrs.status ?? ""),
|
|
3722
|
+
cellCount: attrs.cells?.length ?? 0,
|
|
3723
|
+
created: attrs.created?.toISOString() ?? "",
|
|
3724
|
+
modified: attrs.modified?.toISOString() ?? "",
|
|
3725
|
+
metadata: {
|
|
3726
|
+
isTemplate: attrs.metadata?.isTemplate ?? null,
|
|
3727
|
+
takeSnapshots: attrs.metadata?.takeSnapshots ?? null
|
|
3728
|
+
},
|
|
3729
|
+
cells: (attrs.cells ?? []).map((cell) => ({
|
|
3730
|
+
id: String(cell.id ?? ""),
|
|
3731
|
+
type: String(cell.type ?? ""),
|
|
3732
|
+
attributes: cell.attributes ?? {}
|
|
3733
|
+
})),
|
|
3734
|
+
time: {
|
|
3735
|
+
liveSpan: attrs.time ? String(attrs.time["liveSpan"] ?? "") : null
|
|
3736
|
+
}
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
async function listNotebooks(api, params, limits) {
|
|
3740
|
+
const response = await api.listNotebooks({
|
|
3741
|
+
query: params.query,
|
|
3742
|
+
authorHandle: params.authorHandle,
|
|
3743
|
+
excludeAuthorHandle: params.excludeAuthorHandle,
|
|
3744
|
+
includeCells: params.includeCells ?? false,
|
|
3745
|
+
count: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
|
|
3746
|
+
start: (params.pageNumber ?? 0) * (params.pageSize ?? limits.maxResults)
|
|
3747
|
+
});
|
|
3748
|
+
const notebooks = (response.data ?? []).map(formatNotebookSummary);
|
|
3749
|
+
return {
|
|
3750
|
+
notebooks,
|
|
3751
|
+
meta: {
|
|
3752
|
+
totalCount: response.meta?.page?.totalCount ?? notebooks.length,
|
|
3753
|
+
totalFilteredCount: response.meta?.page?.totalFilteredCount ?? notebooks.length
|
|
3754
|
+
}
|
|
3755
|
+
};
|
|
3756
|
+
}
|
|
3757
|
+
async function getNotebook(api, notebookId) {
|
|
3758
|
+
const response = await api.getNotebook({ notebookId });
|
|
3759
|
+
if (!response.data) {
|
|
3760
|
+
throw new Error(`Notebook ${notebookId} not found`);
|
|
3761
|
+
}
|
|
3762
|
+
return {
|
|
3763
|
+
notebook: formatNotebookDetail(response.data)
|
|
3764
|
+
};
|
|
3765
|
+
}
|
|
3766
|
+
async function createNotebook(api, params) {
|
|
3767
|
+
const cells = (params.cells ?? []).map((cell) => {
|
|
3768
|
+
if (cell.type === "markdown") {
|
|
3769
|
+
return {
|
|
3770
|
+
type: "notebook_cells",
|
|
3771
|
+
attributes: {
|
|
3772
|
+
definition: {
|
|
3773
|
+
type: "markdown",
|
|
3774
|
+
text: String(cell.content ?? "")
|
|
3775
|
+
}
|
|
3776
|
+
}
|
|
3777
|
+
};
|
|
3778
|
+
}
|
|
3779
|
+
return {
|
|
3780
|
+
type: "notebook_cells",
|
|
3781
|
+
attributes: {
|
|
3782
|
+
definition: cell.content
|
|
3783
|
+
}
|
|
3784
|
+
};
|
|
3785
|
+
});
|
|
3786
|
+
if (cells.length === 0) {
|
|
3787
|
+
cells.push({
|
|
3788
|
+
type: "notebook_cells",
|
|
3789
|
+
attributes: {
|
|
3790
|
+
definition: {
|
|
3791
|
+
type: "markdown",
|
|
3792
|
+
text: "# New Notebook\n\nStart adding content here."
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
});
|
|
3796
|
+
}
|
|
3797
|
+
const timeConfig = params.time?.liveSpan ? { liveSpan: params.time.liveSpan } : { liveSpan: "1h" };
|
|
3798
|
+
const response = await api.createNotebook({
|
|
3799
|
+
body: {
|
|
3800
|
+
data: {
|
|
3801
|
+
type: "notebooks",
|
|
3802
|
+
attributes: {
|
|
3803
|
+
name: params.name,
|
|
3804
|
+
cells,
|
|
3805
|
+
time: timeConfig,
|
|
3806
|
+
status: params.status ?? "published"
|
|
3807
|
+
}
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
});
|
|
3811
|
+
if (!response.data) {
|
|
3812
|
+
throw new Error("Failed to create notebook");
|
|
3813
|
+
}
|
|
3814
|
+
return {
|
|
3815
|
+
success: true,
|
|
3816
|
+
notebook: formatNotebookDetail(response.data),
|
|
3817
|
+
message: `Notebook "${params.name}" created successfully`
|
|
3818
|
+
};
|
|
3819
|
+
}
|
|
3820
|
+
async function updateNotebook(api, notebookId, params) {
|
|
3821
|
+
const existing = await api.getNotebook({ notebookId });
|
|
3822
|
+
if (!existing.data) {
|
|
3823
|
+
throw new Error(`Notebook ${notebookId} not found`);
|
|
3824
|
+
}
|
|
3825
|
+
const existingAttrs = existing.data.attributes ?? {};
|
|
3826
|
+
let cells;
|
|
3827
|
+
if (params.cells) {
|
|
3828
|
+
cells = params.cells.map((cell) => {
|
|
3829
|
+
if (cell.type === "markdown") {
|
|
3830
|
+
return {
|
|
3831
|
+
type: "notebook_cells",
|
|
3832
|
+
attributes: {
|
|
3833
|
+
definition: {
|
|
3834
|
+
type: "markdown",
|
|
3835
|
+
text: String(cell.content ?? "")
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3838
|
+
};
|
|
3839
|
+
}
|
|
3840
|
+
return {
|
|
3841
|
+
type: "notebook_cells",
|
|
3842
|
+
attributes: {
|
|
3843
|
+
definition: cell.content
|
|
3844
|
+
}
|
|
3845
|
+
};
|
|
3846
|
+
});
|
|
3847
|
+
}
|
|
3848
|
+
const timeConfig = params.time?.liveSpan ? { liveSpan: params.time.liveSpan } : void 0;
|
|
3849
|
+
const response = await api.updateNotebook({
|
|
3850
|
+
notebookId,
|
|
3851
|
+
body: {
|
|
3852
|
+
data: {
|
|
3853
|
+
type: "notebooks",
|
|
3854
|
+
attributes: {
|
|
3855
|
+
name: params.name ?? existingAttrs.name ?? "",
|
|
3856
|
+
cells: cells ?? existingAttrs.cells?.map((c) => ({
|
|
3857
|
+
id: c.id,
|
|
3858
|
+
type: "notebook_cells",
|
|
3859
|
+
attributes: c.attributes
|
|
3860
|
+
})) ?? [],
|
|
3861
|
+
time: timeConfig ?? existingAttrs.time ?? { liveSpan: "1h" },
|
|
3862
|
+
status: params.status ?? existingAttrs.status
|
|
3863
|
+
}
|
|
3864
|
+
}
|
|
3865
|
+
}
|
|
3866
|
+
});
|
|
3867
|
+
if (!response.data) {
|
|
3868
|
+
throw new Error("Failed to update notebook");
|
|
3869
|
+
}
|
|
3870
|
+
return {
|
|
3871
|
+
success: true,
|
|
3872
|
+
notebook: formatNotebookDetail(response.data),
|
|
3873
|
+
message: `Notebook ${notebookId} updated successfully`
|
|
3874
|
+
};
|
|
3875
|
+
}
|
|
3876
|
+
async function deleteNotebook(api, notebookId) {
|
|
3877
|
+
await api.deleteNotebook({ notebookId });
|
|
3878
|
+
return {
|
|
3879
|
+
success: true,
|
|
3880
|
+
message: `Notebook ${notebookId} deleted successfully`
|
|
3881
|
+
};
|
|
3882
|
+
}
|
|
3883
|
+
function registerNotebooksTool(server, api, limits, readOnly = false) {
|
|
3884
|
+
server.tool(
|
|
3885
|
+
"notebooks",
|
|
3886
|
+
"Manage Datadog Notebooks. Actions: list (search notebooks), get (by ID with cells), create (new notebook), update (modify notebook), delete (remove notebook). Use for: runbooks, incident documentation, investigation notes, dashboards as code.",
|
|
3887
|
+
InputSchema14,
|
|
3888
|
+
async ({ action, id, query, authorHandle, excludeAuthorHandle, includeCells, name, cells, time, status, pageSize, pageNumber }) => {
|
|
3889
|
+
try {
|
|
3890
|
+
checkReadOnly(action, readOnly);
|
|
3891
|
+
switch (action) {
|
|
3892
|
+
case "list":
|
|
3893
|
+
return toolResult(await listNotebooks(api, { query, authorHandle, excludeAuthorHandle, includeCells, pageSize, pageNumber }, limits));
|
|
3894
|
+
case "get": {
|
|
3895
|
+
const notebookId = requireParam(id, "id", "get");
|
|
3896
|
+
return toolResult(await getNotebook(api, notebookId));
|
|
3897
|
+
}
|
|
3898
|
+
case "create": {
|
|
3899
|
+
const notebookName = requireParam(name, "name", "create");
|
|
3900
|
+
return toolResult(await createNotebook(api, {
|
|
3901
|
+
name: notebookName,
|
|
3902
|
+
cells,
|
|
3903
|
+
time,
|
|
3904
|
+
status
|
|
3905
|
+
}));
|
|
3906
|
+
}
|
|
3907
|
+
case "update": {
|
|
3908
|
+
const notebookId = requireParam(id, "id", "update");
|
|
3909
|
+
return toolResult(await updateNotebook(api, notebookId, {
|
|
3910
|
+
name,
|
|
3911
|
+
cells,
|
|
3912
|
+
time,
|
|
3913
|
+
status
|
|
3914
|
+
}));
|
|
3915
|
+
}
|
|
3916
|
+
case "delete": {
|
|
3917
|
+
const notebookId = requireParam(id, "id", "delete");
|
|
3918
|
+
return toolResult(await deleteNotebook(api, notebookId));
|
|
3919
|
+
}
|
|
3920
|
+
default:
|
|
3921
|
+
throw new Error(`Unknown action: ${action}`);
|
|
3922
|
+
}
|
|
3923
|
+
} catch (error) {
|
|
3924
|
+
handleDatadogError(error);
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
);
|
|
3928
|
+
}
|
|
3929
|
+
|
|
3930
|
+
// src/tools/users.ts
|
|
3931
|
+
import { z as z16 } from "zod";
|
|
3932
|
+
var ActionSchema15 = z16.enum(["list", "get"]);
|
|
3933
|
+
var InputSchema15 = {
|
|
3934
|
+
action: ActionSchema15.describe("Action to perform"),
|
|
3935
|
+
id: z16.string().optional().describe("User ID (required for get action)"),
|
|
3936
|
+
filter: z16.string().optional().describe("Filter users by name or email"),
|
|
3937
|
+
status: z16.enum(["Active", "Pending", "Disabled"]).optional().describe("Filter by user status"),
|
|
3938
|
+
pageSize: z16.number().optional().describe("Number of users to return per page"),
|
|
3939
|
+
pageNumber: z16.number().optional().describe("Page number for pagination")
|
|
3940
|
+
};
|
|
3941
|
+
function formatUser(user) {
|
|
3942
|
+
const attrs = user.attributes ?? {};
|
|
3943
|
+
const relationships = user.relationships ?? {};
|
|
3944
|
+
const roles = (relationships.roles?.data ?? []).map((r) => r.id ?? "");
|
|
3945
|
+
const orgId = relationships.org?.data?.id ?? null;
|
|
3946
|
+
return {
|
|
3947
|
+
id: user.id ?? "",
|
|
3948
|
+
email: attrs.email ?? "",
|
|
3949
|
+
name: attrs.name ?? "",
|
|
3950
|
+
status: attrs.status ?? "",
|
|
3951
|
+
title: attrs.title ?? null,
|
|
3952
|
+
verified: attrs.verified ?? false,
|
|
3953
|
+
disabled: attrs.disabled ?? false,
|
|
3954
|
+
createdAt: attrs.createdAt?.toISOString() ?? "",
|
|
3955
|
+
modifiedAt: attrs.modifiedAt?.toISOString() ?? "",
|
|
3956
|
+
relationships: {
|
|
3957
|
+
roles,
|
|
3958
|
+
org: orgId
|
|
3959
|
+
}
|
|
3960
|
+
};
|
|
3961
|
+
}
|
|
3962
|
+
async function listUsers(api, params, limits) {
|
|
3963
|
+
const response = await api.listUsers({
|
|
3964
|
+
filter: params.filter,
|
|
3965
|
+
filterStatus: params.status,
|
|
3966
|
+
pageSize: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
|
|
3967
|
+
pageNumber: params.pageNumber ?? 0
|
|
3968
|
+
});
|
|
3969
|
+
const users = (response.data ?? []).map(formatUser);
|
|
3970
|
+
return {
|
|
3971
|
+
users,
|
|
3972
|
+
meta: {
|
|
3973
|
+
page: response.meta?.page ?? {},
|
|
3974
|
+
totalCount: users.length
|
|
3975
|
+
}
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
async function getUser(api, userId) {
|
|
3979
|
+
const response = await api.getUser({ userId });
|
|
3980
|
+
if (!response.data) {
|
|
3981
|
+
throw new Error(`User ${userId} not found`);
|
|
3982
|
+
}
|
|
3983
|
+
return {
|
|
3984
|
+
user: formatUser(response.data)
|
|
3985
|
+
};
|
|
3986
|
+
}
|
|
3987
|
+
function registerUsersTool(server, api, limits) {
|
|
3988
|
+
server.tool(
|
|
3989
|
+
"users",
|
|
3990
|
+
"Manage Datadog users. Actions: list (with filters), get (by ID). Use for: access management, user auditing, team organization.",
|
|
3991
|
+
InputSchema15,
|
|
3992
|
+
async ({ action, id, filter, status, pageSize, pageNumber }) => {
|
|
3993
|
+
try {
|
|
3994
|
+
switch (action) {
|
|
3995
|
+
case "list":
|
|
3996
|
+
return toolResult(await listUsers(api, { filter, status, pageSize, pageNumber }, limits));
|
|
3997
|
+
case "get": {
|
|
3998
|
+
const userId = requireParam(id, "id", "get");
|
|
3999
|
+
return toolResult(await getUser(api, userId));
|
|
4000
|
+
}
|
|
4001
|
+
default:
|
|
4002
|
+
throw new Error(`Unknown action: ${action}`);
|
|
4003
|
+
}
|
|
4004
|
+
} catch (error) {
|
|
4005
|
+
handleDatadogError(error);
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
);
|
|
4009
|
+
}
|
|
4010
|
+
|
|
4011
|
+
// src/tools/teams.ts
|
|
4012
|
+
import { z as z17 } from "zod";
|
|
4013
|
+
var ActionSchema16 = z17.enum(["list", "get", "members"]);
|
|
4014
|
+
var InputSchema16 = {
|
|
4015
|
+
action: ActionSchema16.describe("Action to perform"),
|
|
4016
|
+
id: z17.string().optional().describe("Team ID (required for get/members actions)"),
|
|
4017
|
+
filter: z17.string().optional().describe("Filter teams by name"),
|
|
4018
|
+
pageSize: z17.number().optional().describe("Number of teams to return per page"),
|
|
4019
|
+
pageNumber: z17.number().optional().describe("Page number for pagination")
|
|
4020
|
+
};
|
|
4021
|
+
function formatTeam(team) {
|
|
4022
|
+
const attrs = team.attributes ?? {};
|
|
4023
|
+
return {
|
|
4024
|
+
id: team.id ?? "",
|
|
4025
|
+
name: attrs.name ?? "",
|
|
4026
|
+
handle: attrs.handle ?? "",
|
|
4027
|
+
description: attrs.description ?? null,
|
|
4028
|
+
summary: attrs.summary ?? null,
|
|
4029
|
+
linkCount: attrs.linkCount ?? 0,
|
|
4030
|
+
userCount: attrs.userCount ?? 0,
|
|
4031
|
+
createdAt: attrs.createdAt?.toISOString() ?? "",
|
|
4032
|
+
modifiedAt: attrs.modifiedAt?.toISOString() ?? ""
|
|
4033
|
+
};
|
|
4034
|
+
}
|
|
4035
|
+
function formatTeamMember(member) {
|
|
4036
|
+
const attrs = member.attributes ?? {};
|
|
4037
|
+
const relationships = member.relationships ?? {};
|
|
4038
|
+
return {
|
|
4039
|
+
id: member.id ?? "",
|
|
4040
|
+
type: String(member.type ?? ""),
|
|
4041
|
+
attributes: {
|
|
4042
|
+
role: String(attrs.role ?? "")
|
|
4043
|
+
},
|
|
4044
|
+
relationships: {
|
|
4045
|
+
userId: relationships.user?.data?.id ?? null
|
|
4046
|
+
}
|
|
4047
|
+
};
|
|
4048
|
+
}
|
|
4049
|
+
async function listTeams(api, params, limits) {
|
|
4050
|
+
const response = await api.listTeams({
|
|
4051
|
+
filterKeyword: params.filter,
|
|
4052
|
+
pageSize: Math.min(params.pageSize ?? limits.maxResults, limits.maxResults),
|
|
4053
|
+
pageNumber: params.pageNumber ?? 0
|
|
4054
|
+
});
|
|
4055
|
+
const teams = (response.data ?? []).map(formatTeam);
|
|
4056
|
+
return {
|
|
4057
|
+
teams,
|
|
4058
|
+
meta: {
|
|
4059
|
+
totalCount: teams.length
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
}
|
|
4063
|
+
async function getTeam(api, teamId) {
|
|
4064
|
+
const response = await api.getTeam({ teamId });
|
|
4065
|
+
if (!response.data) {
|
|
4066
|
+
throw new Error(`Team ${teamId} not found`);
|
|
4067
|
+
}
|
|
4068
|
+
return {
|
|
4069
|
+
team: formatTeam(response.data)
|
|
4070
|
+
};
|
|
4071
|
+
}
|
|
4072
|
+
async function getTeamMembers(api, teamId, limits) {
|
|
4073
|
+
const response = await api.getTeamMemberships({
|
|
4074
|
+
teamId,
|
|
4075
|
+
pageSize: limits.maxResults
|
|
4076
|
+
});
|
|
4077
|
+
const members = (response.data ?? []).map(formatTeamMember);
|
|
4078
|
+
return {
|
|
4079
|
+
members,
|
|
4080
|
+
meta: {
|
|
4081
|
+
totalCount: members.length
|
|
4082
|
+
}
|
|
4083
|
+
};
|
|
4084
|
+
}
|
|
4085
|
+
function registerTeamsTool(server, api, limits) {
|
|
4086
|
+
server.tool(
|
|
4087
|
+
"teams",
|
|
4088
|
+
"Manage Datadog teams. Actions: list (with filters), get (by ID), members (list team members). Use for: team organization, access management, collaboration.",
|
|
4089
|
+
InputSchema16,
|
|
4090
|
+
async ({ action, id, filter, pageSize, pageNumber }) => {
|
|
4091
|
+
try {
|
|
4092
|
+
switch (action) {
|
|
4093
|
+
case "list":
|
|
4094
|
+
return toolResult(await listTeams(api, { filter, pageSize, pageNumber }, limits));
|
|
4095
|
+
case "get": {
|
|
4096
|
+
const teamId = requireParam(id, "id", "get");
|
|
4097
|
+
return toolResult(await getTeam(api, teamId));
|
|
4098
|
+
}
|
|
4099
|
+
case "members": {
|
|
4100
|
+
const teamId = requireParam(id, "id", "members");
|
|
4101
|
+
return toolResult(await getTeamMembers(api, teamId, limits));
|
|
4102
|
+
}
|
|
4103
|
+
default:
|
|
4104
|
+
throw new Error(`Unknown action: ${action}`);
|
|
4105
|
+
}
|
|
4106
|
+
} catch (error) {
|
|
4107
|
+
handleDatadogError(error);
|
|
4108
|
+
}
|
|
4109
|
+
}
|
|
4110
|
+
);
|
|
4111
|
+
}
|
|
4112
|
+
|
|
4113
|
+
// src/tools/tags.ts
|
|
4114
|
+
import { z as z18 } from "zod";
|
|
4115
|
+
var ActionSchema17 = z18.enum(["list", "get", "add", "update", "delete"]);
|
|
4116
|
+
var InputSchema17 = {
|
|
4117
|
+
action: ActionSchema17.describe("Action to perform"),
|
|
4118
|
+
hostName: z18.string().optional().describe("Host name (required for get/add/update/delete actions)"),
|
|
4119
|
+
tags: z18.array(z18.string()).optional().describe('Tags to add or set (for add/update actions). Format: "key:value"'),
|
|
4120
|
+
source: z18.string().optional().describe('Source of the tags (e.g., "users", "datadog"). Defaults to "users"')
|
|
4121
|
+
};
|
|
4122
|
+
async function listAllTags(api, source) {
|
|
4123
|
+
const response = await api.listHostTags({
|
|
4124
|
+
source
|
|
4125
|
+
});
|
|
4126
|
+
const tags = response.tags ?? {};
|
|
4127
|
+
return {
|
|
4128
|
+
hosts: tags,
|
|
4129
|
+
totalHosts: Object.keys(tags).length
|
|
4130
|
+
};
|
|
4131
|
+
}
|
|
4132
|
+
async function getHostTags(api, hostName, source) {
|
|
4133
|
+
const response = await api.getHostTags({
|
|
4134
|
+
hostName,
|
|
4135
|
+
source
|
|
4136
|
+
});
|
|
4137
|
+
return {
|
|
4138
|
+
hostName,
|
|
4139
|
+
tags: response.tags ?? [],
|
|
4140
|
+
source: source ?? null
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
async function addHostTags(api, hostName, tags, source) {
|
|
4144
|
+
const response = await api.createHostTags({
|
|
4145
|
+
hostName,
|
|
4146
|
+
body: {
|
|
4147
|
+
host: hostName,
|
|
4148
|
+
tags
|
|
4149
|
+
},
|
|
4150
|
+
source
|
|
4151
|
+
});
|
|
4152
|
+
return {
|
|
4153
|
+
success: true,
|
|
4154
|
+
hostName,
|
|
4155
|
+
tags: response.tags ?? tags,
|
|
4156
|
+
message: `Tags added to host ${hostName}`
|
|
4157
|
+
};
|
|
4158
|
+
}
|
|
4159
|
+
async function updateHostTags(api, hostName, tags, source) {
|
|
4160
|
+
const response = await api.updateHostTags({
|
|
4161
|
+
hostName,
|
|
4162
|
+
body: {
|
|
4163
|
+
host: hostName,
|
|
4164
|
+
tags
|
|
4165
|
+
},
|
|
4166
|
+
source
|
|
4167
|
+
});
|
|
4168
|
+
return {
|
|
4169
|
+
success: true,
|
|
4170
|
+
hostName,
|
|
4171
|
+
tags: response.tags ?? tags,
|
|
4172
|
+
message: `Tags updated for host ${hostName}`
|
|
4173
|
+
};
|
|
4174
|
+
}
|
|
4175
|
+
async function deleteHostTags(api, hostName, source) {
|
|
4176
|
+
await api.deleteHostTags({
|
|
4177
|
+
hostName,
|
|
4178
|
+
source
|
|
4179
|
+
});
|
|
4180
|
+
return {
|
|
4181
|
+
success: true,
|
|
4182
|
+
hostName,
|
|
4183
|
+
message: `Tags deleted from host ${hostName}`
|
|
4184
|
+
};
|
|
4185
|
+
}
|
|
4186
|
+
function registerTagsTool(server, api, _limits, readOnly = false) {
|
|
4187
|
+
server.tool(
|
|
4188
|
+
"tags",
|
|
4189
|
+
"Manage Datadog host tags. Actions: list (all host tags), get (tags for specific host), add (create tags), update (replace tags), delete (remove all tags). Use for: infrastructure organization, filtering, grouping.",
|
|
4190
|
+
InputSchema17,
|
|
4191
|
+
async ({ action, hostName, tags, source }) => {
|
|
4192
|
+
try {
|
|
4193
|
+
checkReadOnly(action, readOnly);
|
|
4194
|
+
switch (action) {
|
|
4195
|
+
case "list":
|
|
4196
|
+
return toolResult(await listAllTags(api, source));
|
|
4197
|
+
case "get": {
|
|
4198
|
+
const host = requireParam(hostName, "hostName", "get");
|
|
4199
|
+
return toolResult(await getHostTags(api, host, source));
|
|
4200
|
+
}
|
|
4201
|
+
case "add": {
|
|
4202
|
+
const host = requireParam(hostName, "hostName", "add");
|
|
4203
|
+
const tagList = requireParam(tags, "tags", "add");
|
|
4204
|
+
return toolResult(await addHostTags(api, host, tagList, source));
|
|
4205
|
+
}
|
|
4206
|
+
case "update": {
|
|
4207
|
+
const host = requireParam(hostName, "hostName", "update");
|
|
4208
|
+
const tagList = requireParam(tags, "tags", "update");
|
|
4209
|
+
return toolResult(await updateHostTags(api, host, tagList, source));
|
|
4210
|
+
}
|
|
4211
|
+
case "delete": {
|
|
4212
|
+
const host = requireParam(hostName, "hostName", "delete");
|
|
4213
|
+
return toolResult(await deleteHostTags(api, host, source));
|
|
4214
|
+
}
|
|
4215
|
+
default:
|
|
4216
|
+
throw new Error(`Unknown action: ${action}`);
|
|
4217
|
+
}
|
|
4218
|
+
} catch (error) {
|
|
4219
|
+
handleDatadogError(error);
|
|
4220
|
+
}
|
|
4221
|
+
}
|
|
4222
|
+
);
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
// src/tools/usage.ts
|
|
4226
|
+
import { z as z19 } from "zod";
|
|
4227
|
+
var ActionSchema18 = z19.enum(["summary", "hosts", "logs", "custom_metrics", "indexed_spans", "ingested_spans"]);
|
|
4228
|
+
var InputSchema18 = {
|
|
4229
|
+
action: ActionSchema18.describe("Action to perform: summary (overall usage), hosts, logs, custom_metrics, indexed_spans, ingested_spans"),
|
|
4230
|
+
from: z19.string().optional().describe('Start time (ISO 8601 date like "2024-01-01", or relative like "30d")'),
|
|
4231
|
+
to: z19.string().optional().describe('End time (ISO 8601 date like "2024-01-31", or relative like "now")'),
|
|
4232
|
+
includeOrgDetails: z19.boolean().optional().describe("Include usage breakdown by organization (for multi-org accounts)")
|
|
4233
|
+
};
|
|
4234
|
+
function parseDate(dateStr, defaultDate) {
|
|
4235
|
+
if (!dateStr) return defaultDate;
|
|
4236
|
+
if (dateStr.match(/^\d+[hdwmy]$/)) {
|
|
4237
|
+
const seconds = parseTime(dateStr, Math.floor(Date.now() / 1e3));
|
|
4238
|
+
return new Date(seconds * 1e3);
|
|
4239
|
+
}
|
|
4240
|
+
const parsed = new Date(dateStr);
|
|
4241
|
+
if (!isNaN(parsed.getTime())) {
|
|
4242
|
+
return parsed;
|
|
4243
|
+
}
|
|
4244
|
+
return defaultDate;
|
|
4245
|
+
}
|
|
4246
|
+
async function getUsageSummary(api, params) {
|
|
4247
|
+
const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
|
|
4248
|
+
const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
|
|
4249
|
+
const response = await api.getUsageSummary({
|
|
4250
|
+
startMonth: startDate,
|
|
4251
|
+
endMonth: endDate,
|
|
4252
|
+
includeOrgDetails: params.includeOrgDetails
|
|
4253
|
+
});
|
|
4254
|
+
return {
|
|
4255
|
+
startDate: response.startDate?.toISOString() ?? startDate.toISOString(),
|
|
4256
|
+
endDate: response.endDate?.toISOString() ?? endDate.toISOString(),
|
|
4257
|
+
aggsTotal: {
|
|
4258
|
+
apmHostTop99p: response.apmHostTop99PSum ?? null,
|
|
4259
|
+
infraHostTop99p: response.infraHostTop99PSum ?? null
|
|
4260
|
+
},
|
|
4261
|
+
usage: (response.usage ?? []).map((u) => ({
|
|
4262
|
+
date: u.date?.toISOString() ?? "",
|
|
4263
|
+
orgName: u["orgName"] ?? null,
|
|
4264
|
+
apmHostTop99pSum: u.apmHostTop99P ?? null,
|
|
4265
|
+
infraHostTop99pSum: u.infraHostTop99P ?? null,
|
|
4266
|
+
logsIndexedLogsUsageSum: u.indexedEventsCountSum ?? null,
|
|
4267
|
+
ingestedEventsBytesSum: u.ingestedEventsBytesSum ?? null,
|
|
4268
|
+
customMetricsAvgPerHour: null
|
|
4269
|
+
// Not in summary
|
|
4270
|
+
}))
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
async function getHostsUsage(api, params) {
|
|
4274
|
+
const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
|
|
4275
|
+
const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
|
|
4276
|
+
const response = await api.getUsageHosts({
|
|
4277
|
+
startHr: startDate,
|
|
4278
|
+
endHr: endDate
|
|
4279
|
+
});
|
|
4280
|
+
return {
|
|
4281
|
+
startDate: startDate.toISOString(),
|
|
4282
|
+
endDate: endDate.toISOString(),
|
|
4283
|
+
usage: (response.usage ?? []).map((u) => ({
|
|
4284
|
+
date: u.hour?.toISOString() ?? "",
|
|
4285
|
+
agentHostTop99p: u.agentHostCount ?? null,
|
|
4286
|
+
awsHostTop99p: u.awsHostCount ?? null,
|
|
4287
|
+
azureHostTop99p: u.azureHostCount ?? null,
|
|
4288
|
+
gcpHostTop99p: u.gcpHostCount ?? null,
|
|
4289
|
+
infraHostTop99p: u.hostCount ?? null,
|
|
4290
|
+
containerTop99p: u.containerCount ?? null
|
|
4291
|
+
}))
|
|
4292
|
+
};
|
|
4293
|
+
}
|
|
4294
|
+
async function getLogsUsage(api, params) {
|
|
4295
|
+
const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
|
|
4296
|
+
const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
|
|
4297
|
+
const response = await api.getUsageLogs({
|
|
4298
|
+
startHr: startDate,
|
|
4299
|
+
endHr: endDate
|
|
4300
|
+
});
|
|
4301
|
+
return {
|
|
4302
|
+
startDate: startDate.toISOString(),
|
|
4303
|
+
endDate: endDate.toISOString(),
|
|
4304
|
+
usage: (response.usage ?? []).map((u) => ({
|
|
4305
|
+
date: u.hour?.toISOString() ?? "",
|
|
4306
|
+
logsIndexedLogsUsageSum: u.indexedEventsCount ?? null,
|
|
4307
|
+
logsLiveIndexedLogsUsageSum: u.indexedEventsCount ?? null,
|
|
4308
|
+
logsRehydratedIndexedLogsUsageSum: u["logsRehydratedIndexedCount"] ?? null
|
|
4309
|
+
}))
|
|
4310
|
+
};
|
|
4311
|
+
}
|
|
4312
|
+
async function getCustomMetricsUsage(api, params) {
|
|
4313
|
+
const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
|
|
4314
|
+
const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
|
|
4315
|
+
const response = await api.getUsageTimeseries({
|
|
4316
|
+
startHr: startDate,
|
|
4317
|
+
endHr: endDate
|
|
4318
|
+
});
|
|
4319
|
+
return {
|
|
4320
|
+
startDate: startDate.toISOString(),
|
|
4321
|
+
endDate: endDate.toISOString(),
|
|
4322
|
+
usage: (response.usage ?? []).map((u) => ({
|
|
4323
|
+
date: u.hour?.toISOString() ?? "",
|
|
4324
|
+
avgMetricsCount: u.numCustomTimeseries ?? null,
|
|
4325
|
+
maxMetricsCount: null
|
|
4326
|
+
// Not directly available
|
|
4327
|
+
}))
|
|
4328
|
+
};
|
|
4329
|
+
}
|
|
4330
|
+
async function getIndexedSpansUsage(api, params) {
|
|
4331
|
+
const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
|
|
4332
|
+
const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
|
|
4333
|
+
const response = await api.getUsageIndexedSpans({
|
|
4334
|
+
startHr: startDate,
|
|
4335
|
+
endHr: endDate
|
|
4336
|
+
});
|
|
4337
|
+
return {
|
|
4338
|
+
startDate: startDate.toISOString(),
|
|
4339
|
+
endDate: endDate.toISOString(),
|
|
4340
|
+
usage: (response.usage ?? []).map((u) => ({
|
|
4341
|
+
date: u.hour?.toISOString() ?? "",
|
|
4342
|
+
indexedSpansCount: u.indexedEventsCount ?? null,
|
|
4343
|
+
ingestedSpansBytes: null
|
|
4344
|
+
}))
|
|
4345
|
+
};
|
|
4346
|
+
}
|
|
4347
|
+
async function getIngestedSpansUsage(api, params) {
|
|
4348
|
+
const endDate = parseDate(params.to, /* @__PURE__ */ new Date());
|
|
4349
|
+
const startDate = parseDate(params.from, new Date(endDate.getTime() - 30 * 24 * 60 * 60 * 1e3));
|
|
4350
|
+
const response = await api.getIngestedSpans({
|
|
4351
|
+
startHr: startDate,
|
|
4352
|
+
endHr: endDate
|
|
4353
|
+
});
|
|
4354
|
+
return {
|
|
4355
|
+
startDate: startDate.toISOString(),
|
|
4356
|
+
endDate: endDate.toISOString(),
|
|
4357
|
+
usage: (response.usage ?? []).map((u) => ({
|
|
4358
|
+
date: u.hour?.toISOString() ?? "",
|
|
4359
|
+
indexedSpansCount: null,
|
|
4360
|
+
ingestedSpansBytes: u["ingestedTracesBytes"] ?? null
|
|
4361
|
+
}))
|
|
4362
|
+
};
|
|
4363
|
+
}
|
|
4364
|
+
function registerUsageTool(server, api, _limits) {
|
|
4365
|
+
server.tool(
|
|
4366
|
+
"usage",
|
|
4367
|
+
"Query Datadog usage metering data. Actions: summary (overall usage), hosts (infrastructure), logs, custom_metrics, indexed_spans, ingested_spans. Use for: cost management, capacity planning, usage tracking, billing analysis.",
|
|
4368
|
+
InputSchema18,
|
|
4369
|
+
async ({ action, from, to, includeOrgDetails }) => {
|
|
4370
|
+
try {
|
|
4371
|
+
switch (action) {
|
|
4372
|
+
case "summary":
|
|
4373
|
+
return toolResult(await getUsageSummary(api, { from, to, includeOrgDetails }));
|
|
4374
|
+
case "hosts":
|
|
4375
|
+
return toolResult(await getHostsUsage(api, { from, to }));
|
|
4376
|
+
case "logs":
|
|
4377
|
+
return toolResult(await getLogsUsage(api, { from, to }));
|
|
4378
|
+
case "custom_metrics":
|
|
4379
|
+
return toolResult(await getCustomMetricsUsage(api, { from, to }));
|
|
4380
|
+
case "indexed_spans":
|
|
4381
|
+
return toolResult(await getIndexedSpansUsage(api, { from, to }));
|
|
4382
|
+
case "ingested_spans":
|
|
4383
|
+
return toolResult(await getIngestedSpansUsage(api, { from, to }));
|
|
4384
|
+
default:
|
|
4385
|
+
throw new Error(`Unknown action: ${action}`);
|
|
4386
|
+
}
|
|
4387
|
+
} catch (error) {
|
|
4388
|
+
handleDatadogError(error);
|
|
4389
|
+
}
|
|
4390
|
+
}
|
|
4391
|
+
);
|
|
4392
|
+
}
|
|
4393
|
+
|
|
4394
|
+
// src/tools/auth.ts
|
|
4395
|
+
import { z as z20 } from "zod";
|
|
4396
|
+
var ActionSchema19 = z20.enum(["validate"]);
|
|
4397
|
+
var InputSchema19 = {
|
|
4398
|
+
action: ActionSchema19.describe("Action to perform: validate - test if API key and App key are valid")
|
|
4399
|
+
};
|
|
4400
|
+
function registerAuthTool(server, clients) {
|
|
4401
|
+
server.tool(
|
|
4402
|
+
"auth",
|
|
4403
|
+
"Validate Datadog API credentials. Use this to verify that the API key and App key are correctly configured before performing other operations.",
|
|
4404
|
+
InputSchema19,
|
|
4405
|
+
async ({ action }) => {
|
|
4406
|
+
try {
|
|
4407
|
+
switch (action) {
|
|
4408
|
+
case "validate":
|
|
4409
|
+
return toolResult(await validateCredentials(clients));
|
|
4410
|
+
default:
|
|
4411
|
+
throw new Error(`Unknown action: ${action}`);
|
|
4412
|
+
}
|
|
4413
|
+
} catch (error) {
|
|
4414
|
+
handleDatadogError(error);
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
);
|
|
4418
|
+
}
|
|
4419
|
+
async function validateCredentials(clients) {
|
|
4420
|
+
const apiKeyResult = await clients.auth.validate();
|
|
4421
|
+
if (!apiKeyResult.valid) {
|
|
4422
|
+
return {
|
|
4423
|
+
valid: false,
|
|
4424
|
+
apiKeyValid: false,
|
|
4425
|
+
appKeyValid: false,
|
|
4426
|
+
error: "API key is invalid",
|
|
4427
|
+
suggestion: "Check that your DD_API_KEY environment variable is correct"
|
|
4428
|
+
};
|
|
4429
|
+
}
|
|
4430
|
+
try {
|
|
4431
|
+
await clients.users.listUsers({ pageSize: 1 });
|
|
4432
|
+
return {
|
|
4433
|
+
valid: true,
|
|
4434
|
+
apiKeyValid: true,
|
|
4435
|
+
appKeyValid: true,
|
|
4436
|
+
message: "Both API key and App key are valid and working",
|
|
4437
|
+
permissions: "Credentials have sufficient permissions to access the Datadog API"
|
|
4438
|
+
};
|
|
4439
|
+
} catch (appKeyError) {
|
|
4440
|
+
const errorMessage = appKeyError instanceof Error ? appKeyError.message : String(appKeyError);
|
|
4441
|
+
const isAuthError = errorMessage.includes("401") || errorMessage.includes("403") || errorMessage.includes("Forbidden");
|
|
4442
|
+
return {
|
|
4443
|
+
valid: !isAuthError,
|
|
4444
|
+
apiKeyValid: true,
|
|
4445
|
+
appKeyValid: !isAuthError,
|
|
4446
|
+
warning: isAuthError ? "App key may be invalid or have insufficient permissions" : "API key is valid. App key validation inconclusive.",
|
|
4447
|
+
error: errorMessage,
|
|
4448
|
+
suggestion: isAuthError ? "Check that your DD_APP_KEY environment variable is correct and has appropriate scopes" : "Credentials appear valid but encountered an issue during validation"
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
}
|
|
4452
|
+
|
|
4453
|
+
// src/tools/index.ts
|
|
4454
|
+
function registerAllTools(server, clients, limits, features, site = "datadoghq.com") {
|
|
4455
|
+
const { readOnly, disabledTools } = features;
|
|
4456
|
+
const enabled = (tool) => !disabledTools.includes(tool);
|
|
4457
|
+
if (enabled("monitors")) registerMonitorsTool(server, clients.monitors, limits, readOnly, site);
|
|
4458
|
+
if (enabled("dashboards")) registerDashboardsTool(server, clients.dashboards, limits, readOnly, site);
|
|
4459
|
+
if (enabled("logs")) registerLogsTool(server, clients.logs, limits, site);
|
|
4460
|
+
if (enabled("metrics")) registerMetricsTool(server, clients.metricsV1, clients.metricsV2, limits, site);
|
|
4461
|
+
if (enabled("traces")) registerTracesTool(server, clients.spans, clients.services, limits, site);
|
|
4462
|
+
if (enabled("events")) registerEventsTool(server, clients.eventsV1, clients.eventsV2, clients.monitors, limits, readOnly, site);
|
|
4463
|
+
if (enabled("incidents")) registerIncidentsTool(server, clients.incidents, limits, readOnly, site);
|
|
4464
|
+
if (enabled("slos")) registerSlosTool(server, clients.slo, limits, readOnly, site);
|
|
4465
|
+
if (enabled("synthetics")) registerSyntheticsTool(server, clients.synthetics, limits, readOnly, site);
|
|
4466
|
+
if (enabled("hosts")) registerHostsTool(server, clients.hosts, limits, readOnly);
|
|
4467
|
+
if (enabled("downtimes")) registerDowntimesTool(server, clients.downtimes, limits, readOnly);
|
|
4468
|
+
if (enabled("rum")) registerRumTool(server, clients.rum, limits, site);
|
|
4469
|
+
if (enabled("security")) registerSecurityTool(server, clients.security, limits);
|
|
4470
|
+
if (enabled("notebooks")) registerNotebooksTool(server, clients.notebooks, limits, readOnly, site);
|
|
4471
|
+
if (enabled("users")) registerUsersTool(server, clients.users, limits);
|
|
4472
|
+
if (enabled("teams")) registerTeamsTool(server, clients.teams, limits);
|
|
4473
|
+
if (enabled("tags")) registerTagsTool(server, clients.tags, limits, readOnly);
|
|
4474
|
+
if (enabled("usage")) registerUsageTool(server, clients.usage, limits);
|
|
4475
|
+
if (enabled("auth")) registerAuthTool(server, clients);
|
|
4476
|
+
}
|
|
4477
|
+
|
|
4478
|
+
// src/server.ts
|
|
4479
|
+
function createServer(config) {
|
|
4480
|
+
const server = new McpServer({
|
|
4481
|
+
name: config.server.name,
|
|
4482
|
+
version: config.server.version
|
|
4483
|
+
});
|
|
4484
|
+
const clients = createDatadogClients(config.datadog);
|
|
4485
|
+
registerAllTools(server, clients, config.limits, config.features, config.datadog.site);
|
|
4486
|
+
return server;
|
|
4487
|
+
}
|
|
4488
|
+
|
|
4489
|
+
// src/transport/stdio.ts
|
|
4490
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4491
|
+
async function connectStdio(server) {
|
|
4492
|
+
const transport = new StdioServerTransport();
|
|
4493
|
+
await server.connect(transport);
|
|
4494
|
+
console.error("[MCP] Datadog MCP server running on stdio");
|
|
4495
|
+
}
|
|
4496
|
+
|
|
4497
|
+
// src/transport/http.ts
|
|
4498
|
+
import express from "express";
|
|
4499
|
+
import { randomUUID } from "crypto";
|
|
4500
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
4501
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
4502
|
+
var transports = {};
|
|
4503
|
+
async function connectHttp(server, config) {
|
|
4504
|
+
const app = express();
|
|
4505
|
+
app.use(express.json());
|
|
4506
|
+
app.get("/health", (_req, res) => {
|
|
4507
|
+
res.json({ status: "ok", name: config.name, version: config.version });
|
|
4508
|
+
});
|
|
4509
|
+
app.post("/mcp", async (req, res) => {
|
|
4510
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
4511
|
+
let transport;
|
|
4512
|
+
if (sessionId && transports[sessionId]) {
|
|
4513
|
+
transport = transports[sessionId];
|
|
4514
|
+
} else if (!sessionId && isInitializeRequest(req.body)) {
|
|
4515
|
+
transport = new StreamableHTTPServerTransport({
|
|
4516
|
+
sessionIdGenerator: () => randomUUID(),
|
|
4517
|
+
onsessioninitialized: (id) => {
|
|
4518
|
+
transports[id] = transport;
|
|
4519
|
+
console.error(`[MCP] Session initialized: ${id}`);
|
|
4520
|
+
}
|
|
4521
|
+
});
|
|
4522
|
+
transport.onclose = () => {
|
|
4523
|
+
if (transport.sessionId) {
|
|
4524
|
+
delete transports[transport.sessionId];
|
|
4525
|
+
console.error(`[MCP] Session closed: ${transport.sessionId}`);
|
|
4526
|
+
}
|
|
4527
|
+
};
|
|
4528
|
+
await server.connect(transport);
|
|
4529
|
+
} else {
|
|
4530
|
+
res.status(400).json({
|
|
4531
|
+
jsonrpc: "2.0",
|
|
4532
|
+
error: { code: -32e3, message: "Invalid session" },
|
|
4533
|
+
id: null
|
|
4534
|
+
});
|
|
4535
|
+
return;
|
|
4536
|
+
}
|
|
4537
|
+
await transport.handleRequest(req, res, req.body);
|
|
4538
|
+
});
|
|
4539
|
+
app.get("/mcp", async (req, res) => {
|
|
4540
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
4541
|
+
const transport = transports[sessionId];
|
|
4542
|
+
if (transport) {
|
|
4543
|
+
await transport.handleRequest(req, res);
|
|
4544
|
+
} else {
|
|
4545
|
+
res.status(400).json({ error: "Invalid session" });
|
|
4546
|
+
}
|
|
4547
|
+
});
|
|
4548
|
+
app.delete("/mcp", async (req, res) => {
|
|
4549
|
+
const sessionId = req.headers["mcp-session-id"];
|
|
4550
|
+
const transport = transports[sessionId];
|
|
4551
|
+
if (transport) {
|
|
4552
|
+
await transport.handleRequest(req, res);
|
|
4553
|
+
} else {
|
|
4554
|
+
res.status(400).json({ error: "Invalid session" });
|
|
4555
|
+
}
|
|
4556
|
+
});
|
|
4557
|
+
app.listen(config.port, config.host, () => {
|
|
4558
|
+
console.error(`[MCP] Datadog MCP server running on http://${config.host}:${config.port}/mcp`);
|
|
4559
|
+
console.error(`[MCP] Health check available at http://${config.host}:${config.port}/health`);
|
|
4560
|
+
});
|
|
4561
|
+
}
|
|
4562
|
+
|
|
4563
|
+
// src/index.ts
|
|
4564
|
+
async function main() {
|
|
4565
|
+
try {
|
|
4566
|
+
const config = loadConfig();
|
|
4567
|
+
const server = createServer(config);
|
|
4568
|
+
if (config.server.transport === "http") {
|
|
4569
|
+
await connectHttp(server, config.server);
|
|
4570
|
+
} else {
|
|
4571
|
+
await connectStdio(server);
|
|
4572
|
+
}
|
|
4573
|
+
} catch (error) {
|
|
4574
|
+
console.error("[MCP] Failed to start server:", error);
|
|
4575
|
+
process.exit(1);
|
|
4576
|
+
}
|
|
4577
|
+
}
|
|
4578
|
+
main();
|
|
4579
|
+
//# sourceMappingURL=index.js.map
|