datadog-mcp 5.6.0 → 5.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -3
- package/dist/index.js +883 -70
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -292550,7 +292550,12 @@ var configSchema = external_exports.object({
|
|
|
292550
292550
|
// Fallback when AI doesn't specify log limit
|
|
292551
292551
|
defaultMetricDataPoints: external_exports.number().default(1e3),
|
|
292552
292552
|
// Fallback for timeseries data points
|
|
292553
|
-
defaultTimeRangeHours: external_exports.number().default(24)
|
|
292553
|
+
defaultTimeRangeHours: external_exports.number().default(24),
|
|
292554
|
+
// Maximum events scanned by the `events.histogram` action before returning a
|
|
292555
|
+
// partial result with `bucketCountIncomplete: true` and `nextCursor`.
|
|
292556
|
+
// Bounds latency on bucketed queries while still giving callers a path to
|
|
292557
|
+
// continue scanning if they want a fully complete histogram.
|
|
292558
|
+
maxEventsForHistogram: external_exports.number().default(5e3)
|
|
292554
292559
|
}).default({}),
|
|
292555
292560
|
features: external_exports.object({
|
|
292556
292561
|
readOnly: external_exports.boolean().default(false),
|
|
@@ -292625,7 +292630,8 @@ function loadConfig() {
|
|
|
292625
292630
|
defaultLimit: Number.parseInt(process.env.MCP_DEFAULT_LIMIT ?? "50", 10),
|
|
292626
292631
|
defaultLogLines: Number.parseInt(process.env.MCP_DEFAULT_LOG_LINES ?? "200", 10),
|
|
292627
292632
|
defaultMetricDataPoints: Number.parseInt(process.env.MCP_DEFAULT_METRIC_POINTS ?? "1000", 10),
|
|
292628
|
-
defaultTimeRangeHours: Number.parseInt(process.env.MCP_DEFAULT_TIME_RANGE ?? "24", 10)
|
|
292633
|
+
defaultTimeRangeHours: Number.parseInt(process.env.MCP_DEFAULT_TIME_RANGE ?? "24", 10),
|
|
292634
|
+
maxEventsForHistogram: Number.parseInt(process.env.MCP_MAX_EVENTS_HISTOGRAM ?? "5000", 10)
|
|
292629
292635
|
},
|
|
292630
292636
|
features: {
|
|
292631
292637
|
readOnly: args.booleans.has("read-only") || process.env.MCP_READ_ONLY === "true",
|
|
@@ -302904,6 +302910,182 @@ function formatDurationNs(ns) {
|
|
|
302904
302910
|
return `${(ns / 6e10).toFixed(2)}m`;
|
|
302905
302911
|
}
|
|
302906
302912
|
|
|
302913
|
+
// src/utils/timezone.ts
|
|
302914
|
+
var ERROR_PREFIX = "EINVALID_TIMEZONE";
|
|
302915
|
+
var WEEKDAY_TO_INDEX = {
|
|
302916
|
+
Sunday: 0,
|
|
302917
|
+
Monday: 1,
|
|
302918
|
+
Tuesday: 2,
|
|
302919
|
+
Wednesday: 3,
|
|
302920
|
+
Thursday: 4,
|
|
302921
|
+
Friday: 5,
|
|
302922
|
+
Saturday: 6
|
|
302923
|
+
};
|
|
302924
|
+
function validateIanaZone(tz) {
|
|
302925
|
+
if (typeof tz !== "string" || tz.length === 0) {
|
|
302926
|
+
throw new Error(
|
|
302927
|
+
`${ERROR_PREFIX}: timezone must be a non-empty IANA identifier (e.g. "UTC", "Europe/Paris", "America/New_York")`
|
|
302928
|
+
);
|
|
302929
|
+
}
|
|
302930
|
+
try {
|
|
302931
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
302932
|
+
return;
|
|
302933
|
+
} catch {
|
|
302934
|
+
const suggestions = suggestZones(tz);
|
|
302935
|
+
const suggestionText = suggestions.length > 0 ? ` Did you mean: ${suggestions.join(", ")}?` : "";
|
|
302936
|
+
throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.${suggestionText}`);
|
|
302937
|
+
}
|
|
302938
|
+
}
|
|
302939
|
+
function suggestZones(input) {
|
|
302940
|
+
let zones;
|
|
302941
|
+
try {
|
|
302942
|
+
zones = Intl.supportedValuesOf("timeZone");
|
|
302943
|
+
} catch {
|
|
302944
|
+
return [];
|
|
302945
|
+
}
|
|
302946
|
+
const lower = input.toLowerCase();
|
|
302947
|
+
const scored = zones.map((zone) => ({
|
|
302948
|
+
zone,
|
|
302949
|
+
score: scoreZone(zone, lower)
|
|
302950
|
+
}));
|
|
302951
|
+
scored.sort((a, b) => a.score - b.score);
|
|
302952
|
+
return scored.slice(0, 3).map((s) => s.zone);
|
|
302953
|
+
}
|
|
302954
|
+
function scoreZone(zone, lowerInput) {
|
|
302955
|
+
const lowerZone = zone.toLowerCase();
|
|
302956
|
+
const baseDistance = levenshtein(lowerZone, lowerInput);
|
|
302957
|
+
if (lowerZone.includes(lowerInput) || lowerInput.includes(lowerZone)) {
|
|
302958
|
+
return baseDistance - 100;
|
|
302959
|
+
}
|
|
302960
|
+
return baseDistance;
|
|
302961
|
+
}
|
|
302962
|
+
function levenshtein(a, b) {
|
|
302963
|
+
if (a.length === 0) return b.length;
|
|
302964
|
+
if (b.length === 0) return a.length;
|
|
302965
|
+
if (a.length > b.length) {
|
|
302966
|
+
const tmp = a;
|
|
302967
|
+
a = b;
|
|
302968
|
+
b = tmp;
|
|
302969
|
+
}
|
|
302970
|
+
let previous = new Array(a.length + 1);
|
|
302971
|
+
let current = new Array(a.length + 1);
|
|
302972
|
+
for (let i = 0; i <= a.length; i++) previous[i] = i;
|
|
302973
|
+
for (let j = 1; j <= b.length; j++) {
|
|
302974
|
+
current[0] = j;
|
|
302975
|
+
const bChar = b.charCodeAt(j - 1);
|
|
302976
|
+
for (let i = 1; i <= a.length; i++) {
|
|
302977
|
+
const cost = a.charCodeAt(i - 1) === bChar ? 0 : 1;
|
|
302978
|
+
const prevI = previous[i] ?? 0;
|
|
302979
|
+
const currIMinus1 = current[i - 1] ?? 0;
|
|
302980
|
+
const prevIMinus1 = previous[i - 1] ?? 0;
|
|
302981
|
+
current[i] = Math.min(prevI + 1, currIMinus1 + 1, prevIMinus1 + cost);
|
|
302982
|
+
}
|
|
302983
|
+
const tmp = previous;
|
|
302984
|
+
previous = current;
|
|
302985
|
+
current = tmp;
|
|
302986
|
+
}
|
|
302987
|
+
return previous[a.length] ?? 0;
|
|
302988
|
+
}
|
|
302989
|
+
var partsFormatterCache = /* @__PURE__ */ new Map();
|
|
302990
|
+
var offsetFormatterCache = /* @__PURE__ */ new Map();
|
|
302991
|
+
function getPartsFormatter(tz) {
|
|
302992
|
+
const cached2 = partsFormatterCache.get(tz);
|
|
302993
|
+
if (cached2) return cached2;
|
|
302994
|
+
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
302995
|
+
timeZone: tz,
|
|
302996
|
+
hourCycle: "h23",
|
|
302997
|
+
// 0-23, avoids "24" edge case at midnight
|
|
302998
|
+
year: "numeric",
|
|
302999
|
+
month: "2-digit",
|
|
303000
|
+
day: "2-digit",
|
|
303001
|
+
hour: "2-digit",
|
|
303002
|
+
minute: "2-digit",
|
|
303003
|
+
second: "2-digit",
|
|
303004
|
+
weekday: "long"
|
|
303005
|
+
});
|
|
303006
|
+
partsFormatterCache.set(tz, fmt);
|
|
303007
|
+
return fmt;
|
|
303008
|
+
}
|
|
303009
|
+
function getOffsetFormatter(tz) {
|
|
303010
|
+
const cached2 = offsetFormatterCache.get(tz);
|
|
303011
|
+
if (cached2) return cached2;
|
|
303012
|
+
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
303013
|
+
timeZone: tz,
|
|
303014
|
+
timeZoneName: "longOffset"
|
|
303015
|
+
});
|
|
303016
|
+
offsetFormatterCache.set(tz, fmt);
|
|
303017
|
+
return fmt;
|
|
303018
|
+
}
|
|
303019
|
+
function getDateParts(epochMs, tz) {
|
|
303020
|
+
let parts;
|
|
303021
|
+
try {
|
|
303022
|
+
parts = getPartsFormatter(tz).formatToParts(epochMs);
|
|
303023
|
+
} catch {
|
|
303024
|
+
throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.`);
|
|
303025
|
+
}
|
|
303026
|
+
const out = {
|
|
303027
|
+
year: "",
|
|
303028
|
+
month: "",
|
|
303029
|
+
day: "",
|
|
303030
|
+
hour: "",
|
|
303031
|
+
minute: "",
|
|
303032
|
+
second: "",
|
|
303033
|
+
weekday: ""
|
|
303034
|
+
};
|
|
303035
|
+
for (const part of parts) {
|
|
303036
|
+
if (part.type === "year") out.year = part.value;
|
|
303037
|
+
else if (part.type === "month") out.month = part.value;
|
|
303038
|
+
else if (part.type === "day") out.day = part.value;
|
|
303039
|
+
else if (part.type === "hour") out.hour = part.value;
|
|
303040
|
+
else if (part.type === "minute") out.minute = part.value;
|
|
303041
|
+
else if (part.type === "second") out.second = part.value;
|
|
303042
|
+
else if (part.type === "weekday") out.weekday = part.value;
|
|
303043
|
+
}
|
|
303044
|
+
return out;
|
|
303045
|
+
}
|
|
303046
|
+
function getOffsetIso(epochMs, tz) {
|
|
303047
|
+
let parts;
|
|
303048
|
+
try {
|
|
303049
|
+
parts = getOffsetFormatter(tz).formatToParts(epochMs);
|
|
303050
|
+
} catch {
|
|
303051
|
+
throw new Error(`${ERROR_PREFIX}: "${tz}" is not a valid IANA timezone.`);
|
|
303052
|
+
}
|
|
303053
|
+
const offsetPart = parts.find((p) => p.type === "timeZoneName");
|
|
303054
|
+
const raw = offsetPart?.value ?? "";
|
|
303055
|
+
if (raw === "GMT" || raw === "GMT+00:00" || raw === "GMT-00:00") {
|
|
303056
|
+
return "Z";
|
|
303057
|
+
}
|
|
303058
|
+
if (raw.startsWith("GMT")) {
|
|
303059
|
+
return raw.slice(3);
|
|
303060
|
+
}
|
|
303061
|
+
return raw;
|
|
303062
|
+
}
|
|
303063
|
+
function formatLocal(epochMs, tz) {
|
|
303064
|
+
validateIanaZone(tz);
|
|
303065
|
+
const parts = getDateParts(epochMs, tz);
|
|
303066
|
+
const offset = getOffsetIso(epochMs, tz);
|
|
303067
|
+
return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}${offset}`;
|
|
303068
|
+
}
|
|
303069
|
+
function bucketHourOfDay(epochMs, tz) {
|
|
303070
|
+
validateIanaZone(tz);
|
|
303071
|
+
const parts = getDateParts(epochMs, tz);
|
|
303072
|
+
return Number(parts.hour);
|
|
303073
|
+
}
|
|
303074
|
+
function bucketDayOfWeek(epochMs, tz) {
|
|
303075
|
+
validateIanaZone(tz);
|
|
303076
|
+
const parts = getDateParts(epochMs, tz);
|
|
303077
|
+
const index = WEEKDAY_TO_INDEX[parts.weekday];
|
|
303078
|
+
if (index === void 0) {
|
|
303079
|
+
throw new Error(`${ERROR_PREFIX}: could not resolve weekday for "${parts.weekday}" in "${tz}"`);
|
|
303080
|
+
}
|
|
303081
|
+
return index;
|
|
303082
|
+
}
|
|
303083
|
+
function bucketDayOfMonth(epochMs, tz) {
|
|
303084
|
+
validateIanaZone(tz);
|
|
303085
|
+
const parts = getDateParts(epochMs, tz);
|
|
303086
|
+
return Number(parts.day);
|
|
303087
|
+
}
|
|
303088
|
+
|
|
302907
303089
|
// src/tools/events.ts
|
|
302908
303090
|
var ActionSchema = external_exports.enum([
|
|
302909
303091
|
"list",
|
|
@@ -302914,8 +303096,10 @@ var ActionSchema = external_exports.enum([
|
|
|
302914
303096
|
"top",
|
|
302915
303097
|
"timeseries",
|
|
302916
303098
|
"incidents",
|
|
302917
|
-
"discover"
|
|
303099
|
+
"discover",
|
|
303100
|
+
"histogram"
|
|
302918
303101
|
]);
|
|
303102
|
+
var HistogramBucketBySchema = external_exports.enum(["hour_of_day", "day_of_week", "day_of_month"]);
|
|
302919
303103
|
var InputSchema = {
|
|
302920
303104
|
action: ActionSchema.describe("Action to perform"),
|
|
302921
303105
|
id: external_exports.string().optional().describe("Event ID (for get action)"),
|
|
@@ -302959,8 +303143,21 @@ var InputSchema = {
|
|
|
302959
303143
|
])
|
|
302960
303144
|
).optional().describe(
|
|
302961
303145
|
'Filter events by monitor state transition type. When set, restricts results to events with @monitor.transition.transition_type matching any value. Use ["alert","alert recovery"] to count real fires/recoveries and skip renotifies. Empty array is treated as undefined (no filter). For a fires-only count by monitor ID, prefer monitors action=history.'
|
|
303146
|
+
),
|
|
303147
|
+
// Histogram action (Requirement 3): bucket events by local hour/day-of-week/day-of-month.
|
|
303148
|
+
bucket_by: HistogramBucketBySchema.optional().describe(
|
|
303149
|
+
"Bucket dimension for histogram action: hour_of_day (0-23), day_of_week (0=Sun..6=Sat), day_of_month (1-31)."
|
|
303150
|
+
),
|
|
303151
|
+
timezone: external_exports.string().optional().describe(
|
|
303152
|
+
'Optional IANA timezone (e.g. "UTC", "Europe/Paris"). DST-safe. For histogram: controls hour/day bucketing (default: UTC). For search/aggregate/top/incidents read actions: adds sibling *Local ISO 8601 strings (e.g. timestampLocal) next to existing timestamps. Omit for byte-identical legacy shape.'
|
|
302962
303153
|
)
|
|
302963
303154
|
};
|
|
303155
|
+
function annotateEventTimezone(event, tz) {
|
|
303156
|
+
if (!event.timestamp) return event;
|
|
303157
|
+
const ms = new Date(event.timestamp).getTime();
|
|
303158
|
+
if (!Number.isFinite(ms)) return event;
|
|
303159
|
+
return { ...event, timestampLocal: formatLocal(ms, tz) };
|
|
303160
|
+
}
|
|
302964
303161
|
function extractMonitorInfo(title) {
|
|
302965
303162
|
const priorityMatch = title.match(/^\[P(\d+)\]\s*/);
|
|
302966
303163
|
const priority = priorityMatch ? `P${priorityMatch[1]}` : void 0;
|
|
@@ -303147,17 +303344,17 @@ async function listEventsV1(api, params, limits) {
|
|
|
303147
303344
|
tags: params.tags?.join(","),
|
|
303148
303345
|
unaggregated: true
|
|
303149
303346
|
});
|
|
303150
|
-
let
|
|
303347
|
+
let events2 = response.events ?? [];
|
|
303151
303348
|
if (params.query) {
|
|
303152
303349
|
const lowerQuery = params.query.toLowerCase();
|
|
303153
|
-
|
|
303350
|
+
events2 = events2.filter(
|
|
303154
303351
|
(e) => e.title?.toLowerCase().includes(lowerQuery) || e.text?.toLowerCase().includes(lowerQuery)
|
|
303155
303352
|
);
|
|
303156
303353
|
}
|
|
303157
|
-
const result =
|
|
303354
|
+
const result = events2.slice(0, effectiveLimit).map(formatEventV1);
|
|
303158
303355
|
return {
|
|
303159
303356
|
events: result,
|
|
303160
|
-
total:
|
|
303357
|
+
total: events2.length
|
|
303161
303358
|
};
|
|
303162
303359
|
}
|
|
303163
303360
|
async function getEventV1(api, id) {
|
|
@@ -303186,6 +303383,85 @@ async function createEventV1(api, params) {
|
|
|
303186
303383
|
}
|
|
303187
303384
|
};
|
|
303188
303385
|
}
|
|
303386
|
+
var UNINDEXED_ALERT_TAG_PREFIXES = [
|
|
303387
|
+
"monitor_priority",
|
|
303388
|
+
"notification_preset",
|
|
303389
|
+
"monitor_tags",
|
|
303390
|
+
"alert_cycle_key",
|
|
303391
|
+
"monitor_group_key",
|
|
303392
|
+
"notification_method"
|
|
303393
|
+
];
|
|
303394
|
+
var NARROW_TIME_RANGE_THRESHOLD_MS = 5 * 60 * 1e3;
|
|
303395
|
+
function extractTagPrefixes(query, tags) {
|
|
303396
|
+
const prefixes = [];
|
|
303397
|
+
if (query) {
|
|
303398
|
+
const re = /(?:^|\s)([a-zA-Z_][a-zA-Z0-9_]*):[^\s)]+/g;
|
|
303399
|
+
let match;
|
|
303400
|
+
while ((match = re.exec(query)) !== null) {
|
|
303401
|
+
if (match[1]) {
|
|
303402
|
+
prefixes.push(match[1]);
|
|
303403
|
+
}
|
|
303404
|
+
}
|
|
303405
|
+
}
|
|
303406
|
+
if (tags) {
|
|
303407
|
+
for (const tag of tags) {
|
|
303408
|
+
const colonIdx = tag.indexOf(":");
|
|
303409
|
+
if (colonIdx > 0) {
|
|
303410
|
+
prefixes.push(tag.slice(0, colonIdx));
|
|
303411
|
+
}
|
|
303412
|
+
}
|
|
303413
|
+
}
|
|
303414
|
+
return prefixes;
|
|
303415
|
+
}
|
|
303416
|
+
function countNonSourceTerms(query) {
|
|
303417
|
+
if (!query) return 0;
|
|
303418
|
+
const tokens = query.split(/\s+/).filter((t) => t.length > 0 && t !== "OR" && t !== "AND");
|
|
303419
|
+
let nonSource = 0;
|
|
303420
|
+
for (const token of tokens) {
|
|
303421
|
+
const stripped = token.replace(/[()]/g, "");
|
|
303422
|
+
if (stripped.length === 0) continue;
|
|
303423
|
+
if (stripped.startsWith("source:")) continue;
|
|
303424
|
+
nonSource++;
|
|
303425
|
+
}
|
|
303426
|
+
return nonSource;
|
|
303427
|
+
}
|
|
303428
|
+
function computeDiagnostics(input) {
|
|
303429
|
+
const diagnostics = [];
|
|
303430
|
+
const query = input.query ?? "";
|
|
303431
|
+
const queryHasSourceAlert = /(^|\s|\()source:alert(\s|\)|$)/.test(query) || // NOSONAR S5852: anchored alternation, bounded input, no nested quantifiers
|
|
303432
|
+
(input.sources?.includes("alert") ?? false) || (input.tags?.includes("source:alert") ?? false);
|
|
303433
|
+
if (queryHasSourceAlert) {
|
|
303434
|
+
const prefixes = extractTagPrefixes(input.query, input.tags);
|
|
303435
|
+
const unindexedHits = prefixes.filter((p) => UNINDEXED_ALERT_TAG_PREFIXES.includes(p));
|
|
303436
|
+
const uniqueHits = Array.from(new Set(unindexedHits));
|
|
303437
|
+
if (uniqueHits.length > 0) {
|
|
303438
|
+
diagnostics.push({
|
|
303439
|
+
code: "UNINDEXED_TAG_PREFIX",
|
|
303440
|
+
message: `Query filters on tag prefix(es) that Datadog does not index for source:alert events: ${uniqueHits.join(", ")}.`,
|
|
303441
|
+
hint: "Drop these filters and post-filter the results client-side, or aggregate via monitors/get + monitors.list with matching options."
|
|
303442
|
+
});
|
|
303443
|
+
}
|
|
303444
|
+
}
|
|
303445
|
+
if (typeof input.fromMs === "number" && typeof input.toMs === "number" && input.toMs > input.fromMs && input.toMs - input.fromMs < NARROW_TIME_RANGE_THRESHOLD_MS) {
|
|
303446
|
+
diagnostics.push({
|
|
303447
|
+
code: "NARROW_TIME_RANGE",
|
|
303448
|
+
message: "Time range is shorter than 5 minutes; alert events may not have been indexed yet.",
|
|
303449
|
+
hint: "Widen the range (e.g. last 1h) or retry after the indexing delay (~30s) has elapsed."
|
|
303450
|
+
});
|
|
303451
|
+
}
|
|
303452
|
+
if (queryHasSourceAlert) {
|
|
303453
|
+
const otherTerms = countNonSourceTerms(input.query);
|
|
303454
|
+
const otherTags = (input.tags ?? []).filter((t) => !t.startsWith("source:")).length;
|
|
303455
|
+
if (otherTerms === 0 && otherTags === 0) {
|
|
303456
|
+
diagnostics.push({
|
|
303457
|
+
code: "RESTRICTIVE_SOURCE_FILTER",
|
|
303458
|
+
message: "Only source:alert filter was applied; the matching event set may genuinely be empty.",
|
|
303459
|
+
hint: "Use events.aggregate or monitors.list to confirm no alerts fired in the window, or broaden sources (e.g. source:monitor, source:audit)."
|
|
303460
|
+
});
|
|
303461
|
+
}
|
|
303462
|
+
}
|
|
303463
|
+
return diagnostics;
|
|
303464
|
+
}
|
|
303189
303465
|
function quoteIfNeeded(value) {
|
|
303190
303466
|
return /^[A-Za-z0-9_.-]+$/.test(value) ? value : `"${value}"`;
|
|
303191
303467
|
}
|
|
@@ -303213,6 +303489,9 @@ function buildEventQuery(params) {
|
|
|
303213
303489
|
return parts.length > 0 ? parts.join(" ") : "*";
|
|
303214
303490
|
}
|
|
303215
303491
|
async function searchEventsV2(api, params, limits, site) {
|
|
303492
|
+
if (params.timezone !== void 0) {
|
|
303493
|
+
validateIanaZone(params.timezone);
|
|
303494
|
+
}
|
|
303216
303495
|
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
303217
303496
|
const defaultTo = now();
|
|
303218
303497
|
const [validFrom, validTo] = ensureValidTimeRange(
|
|
@@ -303242,12 +303521,13 @@ async function searchEventsV2(api, params, limits, site) {
|
|
|
303242
303521
|
}
|
|
303243
303522
|
};
|
|
303244
303523
|
const response = await api.searchEvents({ body });
|
|
303245
|
-
const
|
|
303524
|
+
const rawEvents = (response.data ?? []).map(formatEventV2);
|
|
303525
|
+
const events2 = params.timezone !== void 0 ? rawEvents.map((e) => annotateEventTimezone(e, params.timezone)) : rawEvents;
|
|
303246
303526
|
const nextCursor = response.meta?.page?.after;
|
|
303247
|
-
|
|
303248
|
-
events,
|
|
303527
|
+
const baseResult = {
|
|
303528
|
+
events: events2,
|
|
303249
303529
|
meta: {
|
|
303250
|
-
count:
|
|
303530
|
+
count: events2.length,
|
|
303251
303531
|
query: fullQuery,
|
|
303252
303532
|
from: fromTime,
|
|
303253
303533
|
to: toTime,
|
|
@@ -303255,8 +303535,131 @@ async function searchEventsV2(api, params, limits, site) {
|
|
|
303255
303535
|
datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
|
|
303256
303536
|
}
|
|
303257
303537
|
};
|
|
303538
|
+
if (events2.length === 0) {
|
|
303539
|
+
const diagnostics = computeDiagnostics({
|
|
303540
|
+
query: params.query,
|
|
303541
|
+
tags: params.tags,
|
|
303542
|
+
sources: params.sources,
|
|
303543
|
+
fromMs: validFrom * 1e3,
|
|
303544
|
+
toMs: validTo * 1e3
|
|
303545
|
+
});
|
|
303546
|
+
return { ...baseResult, diagnostics };
|
|
303547
|
+
}
|
|
303548
|
+
return baseResult;
|
|
303549
|
+
}
|
|
303550
|
+
function bucketEvent(epochMs, bucketBy, tz) {
|
|
303551
|
+
switch (bucketBy) {
|
|
303552
|
+
case "hour_of_day":
|
|
303553
|
+
return bucketHourOfDay(epochMs, tz);
|
|
303554
|
+
case "day_of_week":
|
|
303555
|
+
return bucketDayOfWeek(epochMs, tz);
|
|
303556
|
+
case "day_of_month":
|
|
303557
|
+
return bucketDayOfMonth(epochMs, tz);
|
|
303558
|
+
default: {
|
|
303559
|
+
const exhaustive = bucketBy;
|
|
303560
|
+
throw new Error(`Unhandled bucket_by: ${String(exhaustive)}`);
|
|
303561
|
+
}
|
|
303562
|
+
}
|
|
303563
|
+
}
|
|
303564
|
+
function eventEpochMs(event) {
|
|
303565
|
+
const ts = event.attributes?.timestamp;
|
|
303566
|
+
if (ts === void 0 || ts === null) return null;
|
|
303567
|
+
if (ts instanceof Date) {
|
|
303568
|
+
const ms2 = ts.getTime();
|
|
303569
|
+
return Number.isFinite(ms2) ? ms2 : null;
|
|
303570
|
+
}
|
|
303571
|
+
const ms = new Date(String(ts)).getTime();
|
|
303572
|
+
return Number.isFinite(ms) ? ms : null;
|
|
303573
|
+
}
|
|
303574
|
+
async function histogramEventsV2(api, params, limits, site) {
|
|
303575
|
+
const timezone = params.timezone ?? "UTC";
|
|
303576
|
+
validateIanaZone(timezone);
|
|
303577
|
+
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
303578
|
+
const defaultTo = now();
|
|
303579
|
+
const [validFrom, validTo] = ensureValidTimeRange(
|
|
303580
|
+
parseTime(params.from, defaultFrom),
|
|
303581
|
+
parseTime(params.to, defaultTo)
|
|
303582
|
+
);
|
|
303583
|
+
const fromTime = new Date(validFrom * 1e3).toISOString();
|
|
303584
|
+
const toTime = new Date(validTo * 1e3).toISOString();
|
|
303585
|
+
const fullQuery = buildEventQuery({
|
|
303586
|
+
query: params.query,
|
|
303587
|
+
sources: params.sources,
|
|
303588
|
+
tags: params.tags
|
|
303589
|
+
});
|
|
303590
|
+
const cap = limits.maxEventsForHistogram;
|
|
303591
|
+
const perPage = Math.max(1, Math.min(1e3, cap));
|
|
303592
|
+
const buckets = {};
|
|
303593
|
+
let totalEvents = 0;
|
|
303594
|
+
let cursor = params.cursor;
|
|
303595
|
+
let bucketCountIncomplete = false;
|
|
303596
|
+
let exhaustedPages = false;
|
|
303597
|
+
const maxPages = 100;
|
|
303598
|
+
let pageCount = 0;
|
|
303599
|
+
while (pageCount < maxPages) {
|
|
303600
|
+
const body = {
|
|
303601
|
+
filter: {
|
|
303602
|
+
query: fullQuery,
|
|
303603
|
+
from: fromTime,
|
|
303604
|
+
to: toTime
|
|
303605
|
+
},
|
|
303606
|
+
sort: "timestamp",
|
|
303607
|
+
page: {
|
|
303608
|
+
limit: perPage,
|
|
303609
|
+
cursor
|
|
303610
|
+
}
|
|
303611
|
+
};
|
|
303612
|
+
const response = await api.searchEvents({ body });
|
|
303613
|
+
const data = response.data ?? [];
|
|
303614
|
+
const responseCursor = response.meta?.page?.after ?? void 0;
|
|
303615
|
+
for (const event of data) {
|
|
303616
|
+
const epochMs = eventEpochMs(event);
|
|
303617
|
+
if (epochMs === null) continue;
|
|
303618
|
+
const bucket = bucketEvent(epochMs, params.bucket_by, timezone);
|
|
303619
|
+
const key = String(bucket);
|
|
303620
|
+
buckets[key] = (buckets[key] ?? 0) + 1;
|
|
303621
|
+
totalEvents++;
|
|
303622
|
+
if (totalEvents >= cap) {
|
|
303623
|
+
bucketCountIncomplete = true;
|
|
303624
|
+
cursor = responseCursor;
|
|
303625
|
+
break;
|
|
303626
|
+
}
|
|
303627
|
+
}
|
|
303628
|
+
if (bucketCountIncomplete) break;
|
|
303629
|
+
if (data.length === 0 || !responseCursor) {
|
|
303630
|
+
exhaustedPages = true;
|
|
303631
|
+
break;
|
|
303632
|
+
}
|
|
303633
|
+
cursor = responseCursor;
|
|
303634
|
+
pageCount++;
|
|
303635
|
+
}
|
|
303636
|
+
const result = {
|
|
303637
|
+
buckets,
|
|
303638
|
+
bucketBy: params.bucket_by,
|
|
303639
|
+
timezone,
|
|
303640
|
+
totalEvents,
|
|
303641
|
+
meta: {
|
|
303642
|
+
query: fullQuery,
|
|
303643
|
+
from: fromTime,
|
|
303644
|
+
to: toTime,
|
|
303645
|
+
datadog_url: buildEventsUrl(fullQuery, validFrom, validTo, site)
|
|
303646
|
+
}
|
|
303647
|
+
};
|
|
303648
|
+
if (bucketCountIncomplete) {
|
|
303649
|
+
result.bucketCountIncomplete = true;
|
|
303650
|
+
if (cursor) {
|
|
303651
|
+
result.nextCursor = cursor;
|
|
303652
|
+
}
|
|
303653
|
+
} else if (!exhaustedPages && pageCount >= maxPages && cursor) {
|
|
303654
|
+
result.bucketCountIncomplete = true;
|
|
303655
|
+
result.nextCursor = cursor;
|
|
303656
|
+
}
|
|
303657
|
+
return result;
|
|
303258
303658
|
}
|
|
303259
303659
|
async function aggregateEventsV2(api, params, limits, site) {
|
|
303660
|
+
if (params.timezone !== void 0) {
|
|
303661
|
+
validateIanaZone(params.timezone);
|
|
303662
|
+
}
|
|
303260
303663
|
const counts = /* @__PURE__ */ new Map();
|
|
303261
303664
|
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
303262
303665
|
const defaultTo = now();
|
|
@@ -303293,9 +303696,9 @@ async function aggregateEventsV2(api, params, limits, site) {
|
|
|
303293
303696
|
while (pageCount < maxPages && eventCount < maxEventsToAggregate) {
|
|
303294
303697
|
const pageBody = { ...body, page: { ...body.page, cursor } };
|
|
303295
303698
|
const response = await api.searchEvents({ body: pageBody });
|
|
303296
|
-
const
|
|
303297
|
-
if (
|
|
303298
|
-
for (const event of
|
|
303699
|
+
const events2 = response.data ?? [];
|
|
303700
|
+
if (events2.length === 0) break;
|
|
303701
|
+
for (const event of events2) {
|
|
303299
303702
|
const formatted = formatEventV2(event);
|
|
303300
303703
|
const groupKey = buildGroupKey(formatted, groupByFields);
|
|
303301
303704
|
const existing = counts.get(groupKey);
|
|
@@ -303316,7 +303719,8 @@ async function aggregateEventsV2(api, params, limits, site) {
|
|
|
303316
303719
|
const buckets = sorted.map(([key, data]) => ({
|
|
303317
303720
|
key,
|
|
303318
303721
|
count: data.count,
|
|
303319
|
-
|
|
303722
|
+
// Requirement 4: annotate sample timestamps only when timezone is supplied.
|
|
303723
|
+
sample: params.timezone !== void 0 ? annotateEventTimezone(data.sample, params.timezone) : data.sample
|
|
303320
303724
|
}));
|
|
303321
303725
|
return {
|
|
303322
303726
|
buckets,
|
|
@@ -303333,6 +303737,9 @@ async function aggregateEventsV2(api, params, limits, site) {
|
|
|
303333
303737
|
};
|
|
303334
303738
|
}
|
|
303335
303739
|
async function topEventsV2(api, params, limits, site) {
|
|
303740
|
+
if (params.timezone !== void 0) {
|
|
303741
|
+
validateIanaZone(params.timezone);
|
|
303742
|
+
}
|
|
303336
303743
|
if (params.contextTags !== void 0) {
|
|
303337
303744
|
if (!Array.isArray(params.contextTags)) {
|
|
303338
303745
|
throw new Error("contextTags must be an array");
|
|
@@ -303370,7 +303777,7 @@ async function topEventsV2(api, params, limits, site) {
|
|
|
303370
303777
|
value = event.monitorInfo?.name ?? event.title;
|
|
303371
303778
|
} else {
|
|
303372
303779
|
const tag = event.tags.find((t) => t.startsWith(`${field}:`));
|
|
303373
|
-
value = tag ? tag.split(":", 2)[1] : "unknown";
|
|
303780
|
+
value = tag ? tag.split(":", 2)[1] ?? "unknown" : "unknown";
|
|
303374
303781
|
}
|
|
303375
303782
|
groupValues[field] = value;
|
|
303376
303783
|
keyParts.push(`${field}:${value}`);
|
|
@@ -303431,6 +303838,9 @@ function parseIntervalToMs(interval) {
|
|
|
303431
303838
|
return ns ? Math.floor(ns / 1e6) : 36e5;
|
|
303432
303839
|
}
|
|
303433
303840
|
async function timeseriesEventsV2(api, params, limits, site) {
|
|
303841
|
+
if (params.timezone !== void 0) {
|
|
303842
|
+
validateIanaZone(params.timezone);
|
|
303843
|
+
}
|
|
303434
303844
|
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
303435
303845
|
const defaultTo = now();
|
|
303436
303846
|
const [validFrom, validTo] = ensureValidTimeRange(
|
|
@@ -303465,9 +303875,9 @@ async function timeseriesEventsV2(api, params, limits, site) {
|
|
|
303465
303875
|
while (pageCount < maxPages && eventCount < maxEventsToProcess) {
|
|
303466
303876
|
const pageBody = { ...body, page: { ...body.page, cursor } };
|
|
303467
303877
|
const response = await api.searchEvents({ body: pageBody });
|
|
303468
|
-
const
|
|
303469
|
-
if (
|
|
303470
|
-
for (const event of
|
|
303878
|
+
const events2 = response.data ?? [];
|
|
303879
|
+
if (events2.length === 0) break;
|
|
303880
|
+
for (const event of events2) {
|
|
303471
303881
|
const formatted = formatEventV2(event);
|
|
303472
303882
|
const groupKey = buildGroupKey(formatted, groupByFields);
|
|
303473
303883
|
const eventTs = new Date(formatted.timestamp).getTime();
|
|
@@ -303484,6 +303894,7 @@ async function timeseriesEventsV2(api, params, limits, site) {
|
|
|
303484
303894
|
if (!cursor) break;
|
|
303485
303895
|
pageCount++;
|
|
303486
303896
|
}
|
|
303897
|
+
const tz = params.timezone;
|
|
303487
303898
|
const sortedBuckets = [...timeBuckets.entries()].sort((a, b) => a[0] - b[0]).map(([bucketTs, groupCounts]) => {
|
|
303488
303899
|
const counts = {};
|
|
303489
303900
|
let total = 0;
|
|
@@ -303491,12 +303902,16 @@ async function timeseriesEventsV2(api, params, limits, site) {
|
|
|
303491
303902
|
counts[key] = count;
|
|
303492
303903
|
total += count;
|
|
303493
303904
|
}
|
|
303494
|
-
|
|
303905
|
+
const bucket = {
|
|
303495
303906
|
timestamp: new Date(bucketTs).toISOString(),
|
|
303496
303907
|
timestampMs: bucketTs,
|
|
303497
303908
|
counts,
|
|
303498
303909
|
total
|
|
303499
303910
|
};
|
|
303911
|
+
if (tz !== void 0) {
|
|
303912
|
+
bucket.timestampLocal = formatLocal(bucketTs, tz);
|
|
303913
|
+
}
|
|
303914
|
+
return bucket;
|
|
303500
303915
|
});
|
|
303501
303916
|
const effectiveLimit = params.limit ?? 100;
|
|
303502
303917
|
const limitedBuckets = sortedBuckets.slice(0, effectiveLimit);
|
|
@@ -303517,6 +303932,9 @@ async function timeseriesEventsV2(api, params, limits, site) {
|
|
|
303517
303932
|
};
|
|
303518
303933
|
}
|
|
303519
303934
|
async function incidentsEventsV2(api, params, limits, site) {
|
|
303935
|
+
if (params.timezone !== void 0) {
|
|
303936
|
+
validateIanaZone(params.timezone);
|
|
303937
|
+
}
|
|
303520
303938
|
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
303521
303939
|
const defaultTo = now();
|
|
303522
303940
|
const [validFrom, validTo] = ensureValidTimeRange(
|
|
@@ -303551,9 +303969,9 @@ async function incidentsEventsV2(api, params, limits, site) {
|
|
|
303551
303969
|
while (pageCount < maxPages && eventCount < maxEventsToProcess) {
|
|
303552
303970
|
const pageBody = { ...body, page: { ...body.page, cursor } };
|
|
303553
303971
|
const response = await api.searchEvents({ body: pageBody });
|
|
303554
|
-
const
|
|
303555
|
-
if (
|
|
303556
|
-
for (const event of
|
|
303972
|
+
const events2 = response.data ?? [];
|
|
303973
|
+
if (events2.length === 0) break;
|
|
303974
|
+
for (const event of events2) {
|
|
303557
303975
|
const formatted = formatEventV2(event);
|
|
303558
303976
|
const monitorName = formatted.monitorInfo?.name ?? formatted.title;
|
|
303559
303977
|
if (!monitorName) {
|
|
@@ -303621,6 +304039,7 @@ async function incidentsEventsV2(api, params, limits, site) {
|
|
|
303621
304039
|
if (!cursor) break;
|
|
303622
304040
|
pageCount++;
|
|
303623
304041
|
}
|
|
304042
|
+
const tz = params.timezone;
|
|
303624
304043
|
const incidentList = [...incidents.values()].map((inc) => {
|
|
303625
304044
|
let duration3;
|
|
303626
304045
|
if (inc.recoveredAt) {
|
|
@@ -303633,7 +304052,7 @@ async function incidentsEventsV2(api, params, limits, site) {
|
|
|
303633
304052
|
duration3 = `${(durationMs / 36e5).toFixed(1)}h`;
|
|
303634
304053
|
}
|
|
303635
304054
|
}
|
|
303636
|
-
|
|
304055
|
+
const base = {
|
|
303637
304056
|
monitorName: inc.monitorName,
|
|
303638
304057
|
firstTrigger: inc.firstTrigger.toISOString(),
|
|
303639
304058
|
lastTrigger: inc.lastTrigger.toISOString(),
|
|
@@ -303641,8 +304060,17 @@ async function incidentsEventsV2(api, params, limits, site) {
|
|
|
303641
304060
|
recovered: inc.recovered,
|
|
303642
304061
|
recoveredAt: inc.recoveredAt?.toISOString(),
|
|
303643
304062
|
duration: duration3,
|
|
303644
|
-
|
|
304063
|
+
// Requirement 4: annotate the nested sample event timestamp when tz is supplied.
|
|
304064
|
+
sample: tz !== void 0 ? annotateEventTimezone(inc.sample, tz) : inc.sample
|
|
303645
304065
|
};
|
|
304066
|
+
if (tz !== void 0) {
|
|
304067
|
+
base.firstTriggerLocal = formatLocal(inc.firstTrigger.getTime(), tz);
|
|
304068
|
+
base.lastTriggerLocal = formatLocal(inc.lastTrigger.getTime(), tz);
|
|
304069
|
+
if (inc.recoveredAt) {
|
|
304070
|
+
base.recoveredAtLocal = formatLocal(inc.recoveredAt.getTime(), tz);
|
|
304071
|
+
}
|
|
304072
|
+
}
|
|
304073
|
+
return base;
|
|
303646
304074
|
});
|
|
303647
304075
|
incidentList.sort(
|
|
303648
304076
|
(a, b) => new Date(b.firstTrigger).getTime() - new Date(a.firstTrigger).getTime()
|
|
@@ -303665,15 +304093,15 @@ async function incidentsEventsV2(api, params, limits, site) {
|
|
|
303665
304093
|
}
|
|
303666
304094
|
};
|
|
303667
304095
|
}
|
|
303668
|
-
async function enrichWithMonitorMetadata(
|
|
304096
|
+
async function enrichWithMonitorMetadata(events2, monitorsApi) {
|
|
303669
304097
|
const monitorIds = /* @__PURE__ */ new Set();
|
|
303670
|
-
for (const event of
|
|
304098
|
+
for (const event of events2) {
|
|
303671
304099
|
if (event.monitorId) {
|
|
303672
304100
|
monitorIds.add(event.monitorId);
|
|
303673
304101
|
}
|
|
303674
304102
|
}
|
|
303675
304103
|
if (monitorIds.size === 0) {
|
|
303676
|
-
return
|
|
304104
|
+
return events2;
|
|
303677
304105
|
}
|
|
303678
304106
|
const monitorCache = /* @__PURE__ */ new Map();
|
|
303679
304107
|
try {
|
|
@@ -303687,9 +304115,9 @@ async function enrichWithMonitorMetadata(events, monitorsApi) {
|
|
|
303687
304115
|
}
|
|
303688
304116
|
}
|
|
303689
304117
|
} catch {
|
|
303690
|
-
return
|
|
304118
|
+
return events2;
|
|
303691
304119
|
}
|
|
303692
|
-
return
|
|
304120
|
+
return events2.map((event) => {
|
|
303693
304121
|
const enriched = { ...event };
|
|
303694
304122
|
if (event.monitorId) {
|
|
303695
304123
|
const monitor = monitorCache.get(event.monitorId);
|
|
@@ -303714,7 +304142,7 @@ async function enrichWithMonitorMetadata(events, monitorsApi) {
|
|
|
303714
304142
|
function registerEventsTool(server, apiV1, apiV2, monitorsApi, limits, readOnly = false, site = "datadoghq.com") {
|
|
303715
304143
|
server.tool(
|
|
303716
304144
|
"events",
|
|
303717
|
-
`Track Datadog events. Actions: list, get, create, search, aggregate, top, timeseries, incidents, discover.
|
|
304145
|
+
`Track Datadog events. Actions: list, get, create, search, aggregate, top, timeseries, incidents, discover, histogram.
|
|
303718
304146
|
For monitor alerts, use tags: ["source:alert"].
|
|
303719
304147
|
|
|
303720
304148
|
IMPORTANT \u2014 re-evaluation vs transition:
|
|
@@ -303732,7 +304160,8 @@ discover: Returns available tag prefixes from events.
|
|
|
303732
304160
|
aggregate: Custom groupBy, returns pipe-delimited keys.
|
|
303733
304161
|
search: Full event details.
|
|
303734
304162
|
timeseries: Time-bucketed trends with interval.
|
|
303735
|
-
incidents: Deduplicate alerts with dedupeWindow
|
|
304163
|
+
incidents: Deduplicate alerts with dedupeWindow.
|
|
304164
|
+
histogram: Bucket events by local hour_of_day / day_of_week / day_of_month in the requested IANA timezone (DST-safe). Pass bucket_by (required) and optional timezone (default UTC) and cursor (for continuation). Caps at limits.maxEventsForHistogram (default 5000); when reached returns bucketCountIncomplete:true + nextCursor.`,
|
|
303736
304165
|
InputSchema,
|
|
303737
304166
|
async ({
|
|
303738
304167
|
action,
|
|
@@ -303754,7 +304183,9 @@ incidents: Deduplicate alerts with dedupeWindow.`,
|
|
|
303754
304183
|
enrich,
|
|
303755
304184
|
contextTags,
|
|
303756
304185
|
maxEvents,
|
|
303757
|
-
transitionType
|
|
304186
|
+
transitionType,
|
|
304187
|
+
bucket_by,
|
|
304188
|
+
timezone
|
|
303758
304189
|
}) => {
|
|
303759
304190
|
try {
|
|
303760
304191
|
checkReadOnly(action, readOnly);
|
|
@@ -303804,7 +304235,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
|
|
|
303804
304235
|
priority,
|
|
303805
304236
|
limit,
|
|
303806
304237
|
cursor,
|
|
303807
|
-
transitionType
|
|
304238
|
+
transitionType,
|
|
304239
|
+
timezone
|
|
303808
304240
|
},
|
|
303809
304241
|
limits,
|
|
303810
304242
|
site
|
|
@@ -303827,7 +304259,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
|
|
|
303827
304259
|
tags,
|
|
303828
304260
|
groupBy,
|
|
303829
304261
|
limit,
|
|
303830
|
-
transitionType
|
|
304262
|
+
transitionType,
|
|
304263
|
+
timezone
|
|
303831
304264
|
},
|
|
303832
304265
|
limits,
|
|
303833
304266
|
site
|
|
@@ -303847,7 +304280,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
|
|
|
303847
304280
|
groupBy,
|
|
303848
304281
|
contextTags,
|
|
303849
304282
|
maxEvents,
|
|
303850
|
-
transitionType
|
|
304283
|
+
transitionType,
|
|
304284
|
+
timezone
|
|
303851
304285
|
},
|
|
303852
304286
|
limits,
|
|
303853
304287
|
site
|
|
@@ -303881,7 +304315,8 @@ incidents: Deduplicate alerts with dedupeWindow.`,
|
|
|
303881
304315
|
groupBy,
|
|
303882
304316
|
interval,
|
|
303883
304317
|
limit,
|
|
303884
|
-
transitionType
|
|
304318
|
+
transitionType,
|
|
304319
|
+
timezone
|
|
303885
304320
|
},
|
|
303886
304321
|
limits,
|
|
303887
304322
|
site
|
|
@@ -303899,12 +304334,33 @@ incidents: Deduplicate alerts with dedupeWindow.`,
|
|
|
303899
304334
|
tags,
|
|
303900
304335
|
dedupeWindow,
|
|
303901
304336
|
limit,
|
|
303902
|
-
transitionType
|
|
304337
|
+
transitionType,
|
|
304338
|
+
timezone
|
|
304339
|
+
},
|
|
304340
|
+
limits,
|
|
304341
|
+
site
|
|
304342
|
+
)
|
|
304343
|
+
);
|
|
304344
|
+
case "histogram": {
|
|
304345
|
+
const histogramBucketBy = requireParam(bucket_by, "bucket_by", "histogram");
|
|
304346
|
+
return toolResult(
|
|
304347
|
+
await histogramEventsV2(
|
|
304348
|
+
apiV2,
|
|
304349
|
+
{
|
|
304350
|
+
query,
|
|
304351
|
+
from,
|
|
304352
|
+
to,
|
|
304353
|
+
sources,
|
|
304354
|
+
tags,
|
|
304355
|
+
bucket_by: histogramBucketBy,
|
|
304356
|
+
timezone,
|
|
304357
|
+
cursor
|
|
303903
304358
|
},
|
|
303904
304359
|
limits,
|
|
303905
304360
|
site
|
|
303906
304361
|
)
|
|
303907
304362
|
);
|
|
304363
|
+
}
|
|
303908
304364
|
default:
|
|
303909
304365
|
throw new Error(`Unknown action: ${action}`);
|
|
303910
304366
|
}
|
|
@@ -303915,6 +304371,177 @@ incidents: Deduplicate alerts with dedupeWindow.`,
|
|
|
303915
304371
|
);
|
|
303916
304372
|
}
|
|
303917
304373
|
|
|
304374
|
+
// src/utils/templatePreview.ts
|
|
304375
|
+
var SUPPORTED_CONDITIONALS = [
|
|
304376
|
+
"is_alert",
|
|
304377
|
+
"is_warning",
|
|
304378
|
+
"is_no_data",
|
|
304379
|
+
"is_recovery",
|
|
304380
|
+
"is_alert_to_warning",
|
|
304381
|
+
"is_warning_to_alert"
|
|
304382
|
+
];
|
|
304383
|
+
var SUPPORTED_SET = new Set(SUPPORTED_CONDITIONALS);
|
|
304384
|
+
function unsupportedSyntaxError(detail) {
|
|
304385
|
+
const supportedList = SUPPORTED_CONDITIONALS.join(", ");
|
|
304386
|
+
return new Error(
|
|
304387
|
+
`EUNSUPPORTED_TEMPLATE_SYNTAX: ${detail}. Supported syntax is {{variable.name}} and conditionals {{#name}}...{{/name}} / {{^name}}...{{/name}} where name is one of: ${supportedList}. Loops ({{#each ...}}) and partials ({{> ...}}) are not supported.`
|
|
304388
|
+
);
|
|
304389
|
+
}
|
|
304390
|
+
function lookupVariable(path, variables) {
|
|
304391
|
+
if (Object.prototype.hasOwnProperty.call(variables, path)) {
|
|
304392
|
+
const value = variables[path];
|
|
304393
|
+
return value === void 0 || value === null ? void 0 : String(value);
|
|
304394
|
+
}
|
|
304395
|
+
const segments = path.split(".");
|
|
304396
|
+
let cursor = variables;
|
|
304397
|
+
for (const segment of segments) {
|
|
304398
|
+
if (cursor === null || cursor === void 0) {
|
|
304399
|
+
return void 0;
|
|
304400
|
+
}
|
|
304401
|
+
if (typeof cursor !== "object") {
|
|
304402
|
+
return void 0;
|
|
304403
|
+
}
|
|
304404
|
+
const record2 = cursor;
|
|
304405
|
+
if (!Object.prototype.hasOwnProperty.call(record2, segment)) {
|
|
304406
|
+
return void 0;
|
|
304407
|
+
}
|
|
304408
|
+
cursor = record2[segment];
|
|
304409
|
+
}
|
|
304410
|
+
if (cursor === void 0 || cursor === null) {
|
|
304411
|
+
return void 0;
|
|
304412
|
+
}
|
|
304413
|
+
if (typeof cursor === "object") {
|
|
304414
|
+
return void 0;
|
|
304415
|
+
}
|
|
304416
|
+
return String(cursor);
|
|
304417
|
+
}
|
|
304418
|
+
var TAG_REGEX = /\{\{\s*([#^/>])?\s*([^}]*?)\s*\}\}/g;
|
|
304419
|
+
function parseBlocks(template) {
|
|
304420
|
+
const stack = [{ children: [] }];
|
|
304421
|
+
let cursor = 0;
|
|
304422
|
+
TAG_REGEX.lastIndex = 0;
|
|
304423
|
+
let match;
|
|
304424
|
+
while ((match = TAG_REGEX.exec(template)) !== null) {
|
|
304425
|
+
const [fullTag, prefix, rawName] = match;
|
|
304426
|
+
const tagStart = match.index;
|
|
304427
|
+
const name = rawName?.trim() ?? "";
|
|
304428
|
+
if (tagStart > cursor) {
|
|
304429
|
+
const top = stack[stack.length - 1];
|
|
304430
|
+
if (top) {
|
|
304431
|
+
top.children.push({ kind: "literal", text: template.slice(cursor, tagStart) });
|
|
304432
|
+
}
|
|
304433
|
+
}
|
|
304434
|
+
if (prefix === ">") {
|
|
304435
|
+
throw unsupportedSyntaxError(`partials are not supported (found {{> ${name}}})`);
|
|
304436
|
+
}
|
|
304437
|
+
if (prefix === "#" || prefix === "^") {
|
|
304438
|
+
if (name.startsWith("each") || /\s/.test(name)) {
|
|
304439
|
+
throw unsupportedSyntaxError(`loops are not supported (found {{${prefix}${name}}})`);
|
|
304440
|
+
}
|
|
304441
|
+
if (!SUPPORTED_SET.has(name)) {
|
|
304442
|
+
throw unsupportedSyntaxError(`unknown conditional '${name}' in {{${prefix}${name}}}`);
|
|
304443
|
+
}
|
|
304444
|
+
stack.push({
|
|
304445
|
+
children: [],
|
|
304446
|
+
closer: { conditional: name, negated: prefix === "^" }
|
|
304447
|
+
});
|
|
304448
|
+
} else if (prefix === "/") {
|
|
304449
|
+
const frame = stack.pop();
|
|
304450
|
+
if (!frame || !frame.closer) {
|
|
304451
|
+
throw unsupportedSyntaxError(`unmatched closing tag {{/${name}}}`);
|
|
304452
|
+
}
|
|
304453
|
+
if (frame.closer.conditional !== name) {
|
|
304454
|
+
throw unsupportedSyntaxError(
|
|
304455
|
+
`mismatched closing tag {{/${name}}} (expected {{/${frame.closer.conditional}}})`
|
|
304456
|
+
);
|
|
304457
|
+
}
|
|
304458
|
+
const parent = stack[stack.length - 1];
|
|
304459
|
+
if (!parent) {
|
|
304460
|
+
throw unsupportedSyntaxError("block stack underflow while closing tag");
|
|
304461
|
+
}
|
|
304462
|
+
parent.children.push({
|
|
304463
|
+
kind: "block",
|
|
304464
|
+
conditional: frame.closer.conditional,
|
|
304465
|
+
negated: frame.closer.negated,
|
|
304466
|
+
children: frame.children
|
|
304467
|
+
});
|
|
304468
|
+
} else {
|
|
304469
|
+
const top = stack[stack.length - 1];
|
|
304470
|
+
if (top) {
|
|
304471
|
+
top.children.push({ kind: "literal", text: fullTag });
|
|
304472
|
+
}
|
|
304473
|
+
}
|
|
304474
|
+
cursor = tagStart + fullTag.length;
|
|
304475
|
+
}
|
|
304476
|
+
if (cursor < template.length) {
|
|
304477
|
+
const top = stack[stack.length - 1];
|
|
304478
|
+
if (top) {
|
|
304479
|
+
top.children.push({ kind: "literal", text: template.slice(cursor) });
|
|
304480
|
+
}
|
|
304481
|
+
}
|
|
304482
|
+
if (stack.length !== 1) {
|
|
304483
|
+
const open = stack[stack.length - 1]?.closer?.conditional;
|
|
304484
|
+
throw unsupportedSyntaxError(`unclosed conditional block ${open ? `{{#${open}}}` : ""}`);
|
|
304485
|
+
}
|
|
304486
|
+
const root = stack[0];
|
|
304487
|
+
if (!root) {
|
|
304488
|
+
throw unsupportedSyntaxError("parser produced no root frame");
|
|
304489
|
+
}
|
|
304490
|
+
return root.children;
|
|
304491
|
+
}
|
|
304492
|
+
function renderBlocks(tokens, conditionals, resolved) {
|
|
304493
|
+
let out = "";
|
|
304494
|
+
for (const token of tokens) {
|
|
304495
|
+
if (token.kind === "literal") {
|
|
304496
|
+
out += token.text;
|
|
304497
|
+
continue;
|
|
304498
|
+
}
|
|
304499
|
+
const flag = conditionals[token.conditional] ?? false;
|
|
304500
|
+
resolved[token.conditional] = flag;
|
|
304501
|
+
const include = token.negated ? !flag : flag;
|
|
304502
|
+
if (include) {
|
|
304503
|
+
out += renderBlocks(token.children, conditionals, resolved);
|
|
304504
|
+
} else {
|
|
304505
|
+
renderBlocks(token.children, conditionals, resolved);
|
|
304506
|
+
}
|
|
304507
|
+
}
|
|
304508
|
+
return out;
|
|
304509
|
+
}
|
|
304510
|
+
var VARIABLE_TAG_REGEX = /\{\{\s*([^#^/>\s][^}]*?)\s*\}\}/g;
|
|
304511
|
+
function substituteVariables(text, variables) {
|
|
304512
|
+
const usedSet = /* @__PURE__ */ new Set();
|
|
304513
|
+
const missingSet = /* @__PURE__ */ new Set();
|
|
304514
|
+
const rendered = text.replace(VARIABLE_TAG_REGEX, (_match, captured) => {
|
|
304515
|
+
const name = captured.trim();
|
|
304516
|
+
const value = lookupVariable(name, variables);
|
|
304517
|
+
if (value === void 0) {
|
|
304518
|
+
missingSet.add(name);
|
|
304519
|
+
return `{{undefined:${name}}}`;
|
|
304520
|
+
}
|
|
304521
|
+
usedSet.add(name);
|
|
304522
|
+
return value;
|
|
304523
|
+
});
|
|
304524
|
+
return {
|
|
304525
|
+
rendered,
|
|
304526
|
+
used: [...usedSet],
|
|
304527
|
+
missing: [...missingSet]
|
|
304528
|
+
};
|
|
304529
|
+
}
|
|
304530
|
+
function renderMonitorTemplate(template, context) {
|
|
304531
|
+
const variables = context.variables ?? {};
|
|
304532
|
+
const conditionals = context.conditionals ?? {};
|
|
304533
|
+
const tree = parseBlocks(template);
|
|
304534
|
+
const conditionalsResolved = {};
|
|
304535
|
+
const afterConditionals = renderBlocks(tree, conditionals, conditionalsResolved);
|
|
304536
|
+
const { rendered, used, missing } = substituteVariables(afterConditionals, variables);
|
|
304537
|
+
return {
|
|
304538
|
+
rendered,
|
|
304539
|
+
variablesUsed: used,
|
|
304540
|
+
variablesMissing: missing,
|
|
304541
|
+
conditionalsResolved
|
|
304542
|
+
};
|
|
304543
|
+
}
|
|
304544
|
+
|
|
303918
304545
|
// src/tools/monitors.ts
|
|
303919
304546
|
var ActionSchema2 = external_exports.enum([
|
|
303920
304547
|
"list",
|
|
@@ -303926,8 +304553,14 @@ var ActionSchema2 = external_exports.enum([
|
|
|
303926
304553
|
"mute",
|
|
303927
304554
|
"unmute",
|
|
303928
304555
|
"top",
|
|
303929
|
-
"history"
|
|
304556
|
+
"history",
|
|
304557
|
+
"preview",
|
|
304558
|
+
"test_notification"
|
|
303930
304559
|
]);
|
|
304560
|
+
var PreviewContextSchema = external_exports.object({
|
|
304561
|
+
variables: external_exports.record(external_exports.unknown()).optional(),
|
|
304562
|
+
conditionals: external_exports.record(external_exports.enum(SUPPORTED_CONDITIONALS), external_exports.boolean()).optional()
|
|
304563
|
+
}).optional().describe("Substitution context for monitors.preview (variables + conditionals).");
|
|
303931
304564
|
var InputSchema2 = {
|
|
303932
304565
|
action: ActionSchema2.describe("Action to perform"),
|
|
303933
304566
|
id: external_exports.string().optional().describe("Monitor ID (required for get/update/delete/mute/unmute)"),
|
|
@@ -303939,8 +304572,14 @@ var InputSchema2 = {
|
|
|
303939
304572
|
),
|
|
303940
304573
|
limit: external_exports.number().min(1).optional().describe("Maximum number of monitors to return (default: 50)"),
|
|
303941
304574
|
config: external_exports.record(external_exports.unknown()).optional().describe("Monitor configuration (for create/update)"),
|
|
303942
|
-
message: external_exports.string().optional().describe(
|
|
304575
|
+
message: external_exports.string().optional().describe(
|
|
304576
|
+
"Mute message (for mute action) OR inline template source for the preview action. For preview, supply either this inline string or `monitor_id` (or the existing `id` field) so the action can load the monitor message via getMonitor."
|
|
304577
|
+
),
|
|
303943
304578
|
end: external_exports.number().optional().describe("Mute end timestamp (for mute action)"),
|
|
304579
|
+
monitor_id: external_exports.number().optional().describe(
|
|
304580
|
+
"Numeric monitor ID used by the preview action when no inline `message` is supplied. Equivalent to passing the existing `id` field as a numeric string."
|
|
304581
|
+
),
|
|
304582
|
+
context: PreviewContextSchema,
|
|
303944
304583
|
// Top action parameters
|
|
303945
304584
|
from: external_exports.string().optional().describe('Start time (ISO 8601, relative like "1h", or Unix timestamp)'),
|
|
303946
304585
|
to: external_exports.string().optional().describe('End time (ISO 8601, relative like "1h", or Unix timestamp)'),
|
|
@@ -303964,6 +304603,12 @@ var InputSchema2 = {
|
|
|
303964
304603
|
),
|
|
303965
304604
|
group: external_exports.string().optional().describe(
|
|
303966
304605
|
'For history action: filter transitions to a specific multi-alert monitor group (e.g., "pod_name:foo,kube_namespace:bar"). Optional; omit for all groups.'
|
|
304606
|
+
),
|
|
304607
|
+
dry_run: external_exports.boolean().optional().describe(
|
|
304608
|
+
"When create + dry_run=true, validate the monitor body via POST /api/v1/monitor/validate without creating it. Allowed under --read-only because no monitor is created. Returns { valid, dryRun, monitor }. 400 responses surface verbatim like a failed create."
|
|
304609
|
+
),
|
|
304610
|
+
timezone: external_exports.string().optional().describe(
|
|
304611
|
+
'Optional IANA timezone (e.g. "UTC", "Europe/Paris"). When supplied on get/list, the response adds sibling createdLocal/modifiedLocal ISO 8601 strings next to created/modified. Omit for byte-identical legacy shape. Invalid zones return EINVALID_TIMEZONE.'
|
|
303967
304612
|
)
|
|
303968
304613
|
};
|
|
303969
304614
|
var MonitorThresholdsSchema = external_exports.object({
|
|
@@ -304058,6 +304703,18 @@ function summarizeZodIssue(error2) {
|
|
|
304058
304703
|
const expected = issue2.code === "invalid_type" && "expected" in issue2 ? `expected ${String(issue2.expected)}` : issue2.message;
|
|
304059
304704
|
return `${path}: ${expected}`;
|
|
304060
304705
|
}
|
|
304706
|
+
function annotateMonitorTimezone(monitor, tz) {
|
|
304707
|
+
const annotated = { ...monitor };
|
|
304708
|
+
if (monitor.created) {
|
|
304709
|
+
const ms = new Date(monitor.created).getTime();
|
|
304710
|
+
if (Number.isFinite(ms)) annotated.createdLocal = formatLocal(ms, tz);
|
|
304711
|
+
}
|
|
304712
|
+
if (monitor.modified) {
|
|
304713
|
+
const ms = new Date(monitor.modified).getTime();
|
|
304714
|
+
if (Number.isFinite(ms)) annotated.modifiedLocal = formatLocal(ms, tz);
|
|
304715
|
+
}
|
|
304716
|
+
return annotated;
|
|
304717
|
+
}
|
|
304061
304718
|
function formatMonitor(m, site = "datadoghq.com") {
|
|
304062
304719
|
const monitorId = m.id ?? 0;
|
|
304063
304720
|
return {
|
|
@@ -304201,9 +304858,9 @@ async function historyMonitor(eventsApi, monitorId, params, limits, site) {
|
|
|
304201
304858
|
while (pageCount < maxPages && eventCount < maxEventsToProcess) {
|
|
304202
304859
|
const pageBody = { ...body, page: { ...body.page, cursor } };
|
|
304203
304860
|
const response = await eventsApi.searchEvents({ body: pageBody });
|
|
304204
|
-
const
|
|
304205
|
-
if (
|
|
304206
|
-
for (const event of
|
|
304861
|
+
const events2 = response.data ?? [];
|
|
304862
|
+
if (events2.length === 0) break;
|
|
304863
|
+
for (const event of events2) {
|
|
304207
304864
|
const transition = formatMonitorTransition(event);
|
|
304208
304865
|
if (transition !== null) {
|
|
304209
304866
|
transitions.push(transition);
|
|
@@ -304236,14 +304893,18 @@ async function historyMonitor(eventsApi, monitorId, params, limits, site) {
|
|
|
304236
304893
|
meta
|
|
304237
304894
|
};
|
|
304238
304895
|
}
|
|
304239
|
-
async function listMonitors(api, params, limits, site) {
|
|
304896
|
+
async function listMonitors(api, params, limits, site, timezone) {
|
|
304897
|
+
if (timezone !== void 0) {
|
|
304898
|
+
validateIanaZone(timezone);
|
|
304899
|
+
}
|
|
304240
304900
|
const effectiveLimit = params.limit ?? limits.defaultLimit;
|
|
304241
304901
|
const response = await api.listMonitors({
|
|
304242
304902
|
name: params.name,
|
|
304243
304903
|
tags: params.tags?.join(","),
|
|
304244
304904
|
groupStates: params.groupStates?.join(",")
|
|
304245
304905
|
});
|
|
304246
|
-
const
|
|
304906
|
+
const baseMonitors = response.slice(0, effectiveLimit).map((m) => formatMonitor(m, site));
|
|
304907
|
+
const monitors2 = timezone !== void 0 ? baseMonitors.map((m) => annotateMonitorTimezone(m, timezone)) : baseMonitors;
|
|
304247
304908
|
const statusCounts = {
|
|
304248
304909
|
total: response.length,
|
|
304249
304910
|
alert: response.filter((m) => m.overallState === "Alert").length,
|
|
@@ -304260,14 +304921,19 @@ async function listMonitors(api, params, limits, site) {
|
|
|
304260
304921
|
)
|
|
304261
304922
|
};
|
|
304262
304923
|
}
|
|
304263
|
-
async function getMonitor(api, id, site) {
|
|
304924
|
+
async function getMonitor(api, id, site, timezone) {
|
|
304925
|
+
if (timezone !== void 0) {
|
|
304926
|
+
validateIanaZone(timezone);
|
|
304927
|
+
}
|
|
304264
304928
|
const monitorId = Number.parseInt(id, 10);
|
|
304265
304929
|
if (Number.isNaN(monitorId)) {
|
|
304266
304930
|
throw new Error(`Invalid monitor ID: ${id}`);
|
|
304267
304931
|
}
|
|
304268
304932
|
const monitor = await api.getMonitor({ monitorId });
|
|
304933
|
+
const baseDetail = formatMonitorDetail(monitor, site);
|
|
304934
|
+
const detail = timezone !== void 0 ? annotateMonitorTimezone(baseDetail, timezone) : baseDetail;
|
|
304269
304935
|
return {
|
|
304270
|
-
monitor:
|
|
304936
|
+
monitor: detail,
|
|
304271
304937
|
datadog_url: buildMonitorUrl(monitorId, site)
|
|
304272
304938
|
};
|
|
304273
304939
|
}
|
|
@@ -304369,6 +305035,37 @@ async function createMonitor(api, config2, site = "datadoghq.com") {
|
|
|
304369
305035
|
}
|
|
304370
305036
|
return result;
|
|
304371
305037
|
}
|
|
305038
|
+
async function dryRunMonitor(api, config2) {
|
|
305039
|
+
const normalized = normalizeMonitorConfig(config2);
|
|
305040
|
+
const body = normalized;
|
|
305041
|
+
await api.validateMonitor({ body });
|
|
305042
|
+
return {
|
|
305043
|
+
valid: true,
|
|
305044
|
+
dryRun: true,
|
|
305045
|
+
monitor: normalized
|
|
305046
|
+
};
|
|
305047
|
+
}
|
|
305048
|
+
async function previewMonitor(api, args, site = "datadoghq.com") {
|
|
305049
|
+
let template;
|
|
305050
|
+
let monitorId;
|
|
305051
|
+
if (args.inlineMessage !== void 0 && args.inlineMessage !== "") {
|
|
305052
|
+
template = args.inlineMessage;
|
|
305053
|
+
} else if (args.monitorIdSource !== void 0 && args.monitorIdSource !== "") {
|
|
305054
|
+
const loaded = await getMonitor(api, args.monitorIdSource, site);
|
|
305055
|
+
template = loaded.monitor.message ?? "";
|
|
305056
|
+
monitorId = loaded.monitor.id;
|
|
305057
|
+
} else {
|
|
305058
|
+
throw new Error(
|
|
305059
|
+
"Action 'preview' requires either an inline 'message' or a 'monitor_id' (or 'id') to load the template from."
|
|
305060
|
+
);
|
|
305061
|
+
}
|
|
305062
|
+
const result = renderMonitorTemplate(template, args.context ?? {});
|
|
305063
|
+
return monitorId !== void 0 ? { ...result, monitorId } : result;
|
|
305064
|
+
}
|
|
305065
|
+
var TEST_NOTIFICATION_NOT_SUPPORTED_MESSAGE = "ENOT_SUPPORTED: action 'test_notification' is not implemented \u2014 the Datadog public REST API exposes no monitor test-notification endpoint at v1 or v2 as of the events-dx-improvements spec. Use the Datadog UI's 'Test Notifications' button on the monitor page, or open an issue if Datadog publishes such an endpoint. Reference: https://docs.datadoghq.com/api/latest/monitors/";
|
|
305066
|
+
function testNotificationMonitor(_args) {
|
|
305067
|
+
throw new McpError(ErrorCode.InvalidRequest, TEST_NOTIFICATION_NOT_SUPPORTED_MESSAGE);
|
|
305068
|
+
}
|
|
304372
305069
|
async function updateMonitor(api, id, config2, site = "datadoghq.com") {
|
|
304373
305070
|
const monitorId = Number.parseInt(id, 10);
|
|
304374
305071
|
const normalized = normalizeMonitorConfig(config2, true);
|
|
@@ -304453,7 +305150,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
|
|
|
304453
305150
|
}
|
|
304454
305151
|
});
|
|
304455
305152
|
const rawEvents = searchResponse.data ?? [];
|
|
304456
|
-
const
|
|
305153
|
+
const events2 = rawEvents.map(formatEventV2);
|
|
304457
305154
|
const contextPrefixes = new Set(
|
|
304458
305155
|
params.contextTags ?? [
|
|
304459
305156
|
"queue",
|
|
@@ -304465,7 +305162,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
|
|
|
304465
305162
|
]
|
|
304466
305163
|
);
|
|
304467
305164
|
const monitorGroups = /* @__PURE__ */ new Map();
|
|
304468
|
-
for (const event of
|
|
305165
|
+
for (const event of events2) {
|
|
304469
305166
|
const monitorId = event.monitorId;
|
|
304470
305167
|
if (typeof monitorId !== "number") continue;
|
|
304471
305168
|
let group = monitorGroups.get(monitorId);
|
|
@@ -304534,7 +305231,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
|
|
|
304534
305231
|
from: fromTime,
|
|
304535
305232
|
to: toTime,
|
|
304536
305233
|
totalMonitors: monitorGroups.size,
|
|
304537
|
-
totalEvents:
|
|
305234
|
+
totalEvents: events2.length,
|
|
304538
305235
|
contextPrefixes: Array.from(contextPrefixes),
|
|
304539
305236
|
datadog_url: buildEventsUrl(query, validFrom, validTo, site)
|
|
304540
305237
|
}
|
|
@@ -304543,7 +305240,7 @@ async function topMonitors(eventsApi, monitorsApi, params, limits, site) {
|
|
|
304543
305240
|
function registerMonitorsTool(server, api, eventsApi, limits, readOnly = false, site = "datadoghq.com") {
|
|
304544
305241
|
server.tool(
|
|
304545
305242
|
"monitors",
|
|
304546
|
-
`Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute, top, history.
|
|
305243
|
+
`Manage Datadog monitors. Actions: list, get, search, create, update, delete, mute, unmute, top, history, preview, test_notification.
|
|
304547
305244
|
Filters: name, tags, groupStates (alert/warn/ok/no data).
|
|
304548
305245
|
get/create/update return the full options object so callers can safely read-then-patch.
|
|
304549
305246
|
|
|
@@ -304581,6 +305278,21 @@ history: Count and list real state transitions for one monitor over a time windo
|
|
|
304581
305278
|
transition_type filter that excludes renotifies by default. To include renotifies, pass
|
|
304582
305279
|
transitionType including "renotify".
|
|
304583
305280
|
|
|
305281
|
+
preview: Render a Datadog monitor message template against a context (read-only safe).
|
|
305282
|
+
- Inputs: either inline 'message' OR 'monitor_id' (or existing 'id'); plus optional 'context' { variables, conditionals }.
|
|
305283
|
+
- Supported syntax: {{variable.name}} substitution and conditional blocks {{#name}}...{{/name}} / {{^name}}...{{/name}}
|
|
305284
|
+
where name is one of: ${SUPPORTED_CONDITIONALS.join(", ")}.
|
|
305285
|
+
- Missing variables render as {{undefined:name}} markers and are reported in 'variablesMissing'.
|
|
305286
|
+
- Loops ({{#each ...}}) and partials ({{> ...}}) return EUNSUPPORTED_TEMPLATE_SYNTAX.
|
|
305287
|
+
- Allowed under --read-only (no mutation; at most a getMonitor load).
|
|
305288
|
+
|
|
305289
|
+
test_notification: KNOWN LIMITATION \u2014 always returns ENOT_SUPPORTED.
|
|
305290
|
+
- Datadog's public REST API exposes no monitor test-notification endpoint at v1 or v2
|
|
305291
|
+
(audited against the official OpenAPI specs). The v1 SDK has no notifyMonitor / testMonitor method.
|
|
305292
|
+
- Allowed under --read-only because no Datadog HTTP call is attempted.
|
|
305293
|
+
- If Datadog publishes such an endpoint in future, this action will be reimplemented to invoke it.
|
|
305294
|
+
- Workaround: use the 'Test Notifications' button in the Datadog monitor UI.
|
|
305295
|
+
|
|
304584
305296
|
For generic event grouping (deployments, configs), use events tool instead. Note that the
|
|
304585
305297
|
events tool's action=search with source:alert ALSO includes renotifies; use its
|
|
304586
305298
|
transitionType filter (or this action=history) for fires-only counts.`,
|
|
@@ -304594,24 +305306,32 @@ transitionType filter (or this action=history) for fires-only counts.`,
|
|
|
304594
305306
|
groupStates,
|
|
304595
305307
|
limit,
|
|
304596
305308
|
config: config2,
|
|
305309
|
+
message,
|
|
304597
305310
|
end,
|
|
304598
305311
|
from,
|
|
304599
305312
|
to,
|
|
304600
305313
|
contextTags,
|
|
304601
305314
|
maxEvents,
|
|
304602
305315
|
transitionType,
|
|
304603
|
-
group
|
|
305316
|
+
group,
|
|
305317
|
+
dry_run: dryRun,
|
|
305318
|
+
monitor_id: monitorIdNum,
|
|
305319
|
+
context,
|
|
305320
|
+
timezone
|
|
304604
305321
|
}) => {
|
|
304605
305322
|
try {
|
|
304606
|
-
|
|
305323
|
+
const isDryRunCreate = action === "create" && dryRun === true;
|
|
305324
|
+
if (!isDryRunCreate) {
|
|
305325
|
+
checkReadOnly(action, readOnly);
|
|
305326
|
+
}
|
|
304607
305327
|
switch (action) {
|
|
304608
305328
|
case "list":
|
|
304609
305329
|
return toolResult(
|
|
304610
|
-
await listMonitors(api, { name, tags, groupStates, limit }, limits, site)
|
|
305330
|
+
await listMonitors(api, { name, tags, groupStates, limit }, limits, site, timezone)
|
|
304611
305331
|
);
|
|
304612
305332
|
case "get": {
|
|
304613
305333
|
const monitorId = requireParam(id, "id", "get");
|
|
304614
|
-
return toolResult(await getMonitor(api, monitorId, site));
|
|
305334
|
+
return toolResult(await getMonitor(api, monitorId, site, timezone));
|
|
304615
305335
|
}
|
|
304616
305336
|
case "search": {
|
|
304617
305337
|
const searchQuery = requireParam(query, "query", "search");
|
|
@@ -304619,6 +305339,9 @@ transitionType filter (or this action=history) for fires-only counts.`,
|
|
|
304619
305339
|
}
|
|
304620
305340
|
case "create": {
|
|
304621
305341
|
const monitorConfig = requireParam(config2, "config", "create");
|
|
305342
|
+
if (dryRun) {
|
|
305343
|
+
return toolResult(await dryRunMonitor(api, monitorConfig));
|
|
305344
|
+
}
|
|
304622
305345
|
return toolResult(await createMonitor(api, monitorConfig, site));
|
|
304623
305346
|
}
|
|
304624
305347
|
case "update": {
|
|
@@ -304671,6 +305394,24 @@ transitionType filter (or this action=history) for fires-only counts.`,
|
|
|
304671
305394
|
)
|
|
304672
305395
|
);
|
|
304673
305396
|
}
|
|
305397
|
+
case "preview": {
|
|
305398
|
+
const monitorIdSource = monitorIdNum !== void 0 ? String(monitorIdNum) : id ?? void 0;
|
|
305399
|
+
return toolResult(
|
|
305400
|
+
await previewMonitor(
|
|
305401
|
+
api,
|
|
305402
|
+
{
|
|
305403
|
+
inlineMessage: message,
|
|
305404
|
+
monitorIdSource,
|
|
305405
|
+
context
|
|
305406
|
+
},
|
|
305407
|
+
site
|
|
305408
|
+
)
|
|
305409
|
+
);
|
|
305410
|
+
}
|
|
305411
|
+
case "test_notification": {
|
|
305412
|
+
const monitorIdSource = monitorIdNum !== void 0 ? String(monitorIdNum) : id ?? void 0;
|
|
305413
|
+
return testNotificationMonitor({ monitorId: monitorIdSource });
|
|
305414
|
+
}
|
|
304674
305415
|
default:
|
|
304675
305416
|
throw new Error(`Unknown action: ${action}`);
|
|
304676
305417
|
}
|
|
@@ -305287,6 +306028,56 @@ var InputSchema5 = {
|
|
|
305287
306028
|
"Maximum data points per timeseries (for query action). AI controls resolution vs token usage (default: 1000)."
|
|
305288
306029
|
)
|
|
305289
306030
|
};
|
|
306031
|
+
var ROLLUP_METHODS = /* @__PURE__ */ new Set(["avg", "max", "min", "sum", "count"]);
|
|
306032
|
+
var DEFAULT_ROLLUP_METHOD = "avg";
|
|
306033
|
+
function parseRollupFromQuery(query) {
|
|
306034
|
+
if (typeof query !== "string") return null;
|
|
306035
|
+
const matches = [...query.matchAll(/\.rollup\(\s*([^)]*?)\s*\)/g)];
|
|
306036
|
+
const lastMatch = matches[matches.length - 1];
|
|
306037
|
+
if (lastMatch === void 0) return null;
|
|
306038
|
+
const inner = lastMatch[1];
|
|
306039
|
+
if (inner === void 0 || inner.length === 0) return null;
|
|
306040
|
+
const parts = inner.split(",").map((p) => p.trim());
|
|
306041
|
+
if (parts.length === 1) {
|
|
306042
|
+
const raw = parts[0] ?? "";
|
|
306043
|
+
const interval = Number.parseInt(raw, 10);
|
|
306044
|
+
if (!Number.isFinite(interval) || interval <= 0 || String(interval) !== raw) {
|
|
306045
|
+
return null;
|
|
306046
|
+
}
|
|
306047
|
+
return { interval, method: DEFAULT_ROLLUP_METHOD, methodInferred: true };
|
|
306048
|
+
}
|
|
306049
|
+
if (parts.length === 2) {
|
|
306050
|
+
const method = parts[0] ?? "";
|
|
306051
|
+
const secondsRaw = parts[1] ?? "";
|
|
306052
|
+
if (!ROLLUP_METHODS.has(method)) return null;
|
|
306053
|
+
const interval = Number.parseInt(secondsRaw, 10);
|
|
306054
|
+
if (!Number.isFinite(interval) || interval <= 0 || String(interval) !== secondsRaw) {
|
|
306055
|
+
return null;
|
|
306056
|
+
}
|
|
306057
|
+
return { interval, method, methodInferred: false };
|
|
306058
|
+
}
|
|
306059
|
+
return null;
|
|
306060
|
+
}
|
|
306061
|
+
function computeEffectiveRollup(series) {
|
|
306062
|
+
const intervals = [];
|
|
306063
|
+
for (const s of series) {
|
|
306064
|
+
const pts = s.pointlist ?? [];
|
|
306065
|
+
if (pts.length < 2) continue;
|
|
306066
|
+
const first = pts[0];
|
|
306067
|
+
const second = pts[1];
|
|
306068
|
+
if (first === void 0 || second === void 0) continue;
|
|
306069
|
+
const deltaMs = (second[0] ?? 0) - (first[0] ?? 0);
|
|
306070
|
+
if (deltaMs <= 0) continue;
|
|
306071
|
+
intervals.push(Math.round(deltaMs / 1e3));
|
|
306072
|
+
}
|
|
306073
|
+
const primary = intervals[0];
|
|
306074
|
+
if (primary === void 0) return null;
|
|
306075
|
+
const unique = Array.from(new Set(intervals)).sort((a, b) => a - b);
|
|
306076
|
+
if (unique.length === 1) {
|
|
306077
|
+
return { interval: primary };
|
|
306078
|
+
}
|
|
306079
|
+
return { interval: primary, intervalsObserved: unique };
|
|
306080
|
+
}
|
|
305290
306081
|
async function queryMetrics(api, params, limits, site) {
|
|
305291
306082
|
const defaultFrom = hoursAgo(limits.defaultTimeRangeHours);
|
|
305292
306083
|
const defaultTo = now();
|
|
@@ -305299,7 +306090,8 @@ async function queryMetrics(api, params, limits, site) {
|
|
|
305299
306090
|
to: toTs,
|
|
305300
306091
|
query: params.query
|
|
305301
306092
|
});
|
|
305302
|
-
const
|
|
306093
|
+
const rawSeries = response.series ?? [];
|
|
306094
|
+
const series = rawSeries.map((s) => ({
|
|
305303
306095
|
metric: s.metric ?? "",
|
|
305304
306096
|
points: (s.pointlist ?? []).slice(0, params.pointLimit ?? limits.defaultMetricDataPoints).map((p) => ({
|
|
305305
306097
|
timestamp: p[0] ?? 0,
|
|
@@ -305308,6 +306100,13 @@ async function queryMetrics(api, params, limits, site) {
|
|
|
305308
306100
|
scope: s.scope ?? "",
|
|
305309
306101
|
tags: s.tagSet ?? []
|
|
305310
306102
|
}));
|
|
306103
|
+
const rollupRequested = parseRollupFromQuery(params.query);
|
|
306104
|
+
const rollupEffective = computeEffectiveRollup(
|
|
306105
|
+
rawSeries.map((s) => ({
|
|
306106
|
+
pointlist: s.pointlist ?? []
|
|
306107
|
+
}))
|
|
306108
|
+
);
|
|
306109
|
+
const rollupOverridden = rollupRequested !== null && rollupEffective !== null && (rollupEffective.interval !== rollupRequested.interval || (rollupEffective.intervalsObserved?.some((i) => i !== rollupRequested.interval) ?? false));
|
|
305311
306110
|
return {
|
|
305312
306111
|
series,
|
|
305313
306112
|
meta: {
|
|
@@ -305315,7 +306114,10 @@ async function queryMetrics(api, params, limits, site) {
|
|
|
305315
306114
|
from: new Date(fromTs * 1e3).toISOString(),
|
|
305316
306115
|
to: new Date(toTs * 1e3).toISOString(),
|
|
305317
306116
|
seriesCount: series.length,
|
|
305318
|
-
datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site)
|
|
306117
|
+
datadog_url: buildMetricsUrl(params.query, fromTs, toTs, site),
|
|
306118
|
+
rollupRequested,
|
|
306119
|
+
rollupEffective,
|
|
306120
|
+
rollupOverridden
|
|
305319
306121
|
}
|
|
305320
306122
|
};
|
|
305321
306123
|
}
|
|
@@ -306856,11 +307658,11 @@ async function searchEvents(api, params, limits, site) {
|
|
|
306856
307658
|
sort: params.sort === "timestamp" ? "timestamp" : "-timestamp",
|
|
306857
307659
|
pageLimit: params.limit ?? limits.defaultLimit
|
|
306858
307660
|
});
|
|
306859
|
-
const
|
|
307661
|
+
const events2 = (response.data ?? []).map(formatEvent);
|
|
306860
307662
|
return {
|
|
306861
|
-
events,
|
|
307663
|
+
events: events2,
|
|
306862
307664
|
meta: {
|
|
306863
|
-
totalCount:
|
|
307665
|
+
totalCount: events2.length,
|
|
306864
307666
|
timeRange: {
|
|
306865
307667
|
from: new Date(fromTime * 1e3).toISOString(),
|
|
306866
307668
|
to: new Date(toTime * 1e3).toISOString()
|
|
@@ -307073,19 +307875,19 @@ async function getSessionWaterfall(api, params, limits, site) {
|
|
|
307073
307875
|
sort: "timestamp",
|
|
307074
307876
|
pageLimit: limits.defaultLimit
|
|
307075
307877
|
});
|
|
307076
|
-
const
|
|
307878
|
+
const events2 = (response.data ?? []).map(formatWaterfallEvent);
|
|
307077
307879
|
const summary = {
|
|
307078
|
-
views:
|
|
307079
|
-
resources:
|
|
307080
|
-
actions:
|
|
307081
|
-
errors:
|
|
307082
|
-
longTasks:
|
|
307880
|
+
views: events2.filter((e) => e.type === "view").length,
|
|
307881
|
+
resources: events2.filter((e) => e.type === "resource").length,
|
|
307882
|
+
actions: events2.filter((e) => e.type === "action").length,
|
|
307883
|
+
errors: events2.filter((e) => e.type === "error").length,
|
|
307884
|
+
longTasks: events2.filter((e) => e.type === "long_task").length
|
|
307083
307885
|
};
|
|
307084
307886
|
return {
|
|
307085
|
-
events,
|
|
307887
|
+
events: events2,
|
|
307086
307888
|
summary,
|
|
307087
307889
|
meta: {
|
|
307088
|
-
totalCount:
|
|
307890
|
+
totalCount: events2.length,
|
|
307089
307891
|
applicationId: params.applicationId,
|
|
307090
307892
|
sessionId: params.sessionId,
|
|
307091
307893
|
viewId: params.viewId ?? null,
|
|
@@ -308265,6 +309067,17 @@ var dashboards = {
|
|
|
308265
309067
|
docsUrl: "https://docs.datadoghq.com/api/latest/dashboards/"
|
|
308266
309068
|
};
|
|
308267
309069
|
|
|
309070
|
+
// src/schema/events.ts
|
|
309071
|
+
var events = {
|
|
309072
|
+
/**
|
|
309073
|
+
* Diagnostic codes emitted on zero-result `events.search` responses.
|
|
309074
|
+
* Each code is also documented inline in `src/tools/events.ts` next to the
|
|
309075
|
+
* heuristic that produces it.
|
|
309076
|
+
*/
|
|
309077
|
+
diagnosticCodes: ["UNINDEXED_TAG_PREFIX", "NARROW_TIME_RANGE", "RESTRICTIVE_SOURCE_FILTER"],
|
|
309078
|
+
docsUrl: "https://docs.datadoghq.com/api/latest/events/"
|
|
309079
|
+
};
|
|
309080
|
+
|
|
308268
309081
|
// src/schema/metrics.ts
|
|
308269
309082
|
var metrics = {
|
|
308270
309083
|
aggregators: ["avg", "max", "min", "sum", "count"],
|
|
@@ -308325,14 +309138,14 @@ var slos = {
|
|
|
308325
309138
|
};
|
|
308326
309139
|
|
|
308327
309140
|
// src/schema/index.ts
|
|
308328
|
-
var schemas = { dashboards, metrics, monitors, slos };
|
|
309141
|
+
var schemas = { dashboards, events, metrics, monitors, slos };
|
|
308329
309142
|
var schemaResources = Object.keys(schemas);
|
|
308330
309143
|
|
|
308331
309144
|
// src/tools/schema.ts
|
|
308332
309145
|
var ResourceSchema2 = external_exports.enum(schemaResources);
|
|
308333
309146
|
var InputSchema20 = {
|
|
308334
309147
|
resource: ResourceSchema2.describe(
|
|
308335
|
-
"Datadog resource type to get schema for: dashboards, metrics, monitors, slos"
|
|
309148
|
+
"Datadog resource type to get schema for: dashboards, events, metrics, monitors, slos"
|
|
308336
309149
|
)
|
|
308337
309150
|
};
|
|
308338
309151
|
function getSchema(resource) {
|