@rawdash/connector-datadog 0.15.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 +114 -0
- package/dist/index.d.ts +314 -0
- package/dist/index.js +841 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,841 @@
|
|
|
1
|
+
// ../../connector-shared/dist/index.js
|
|
2
|
+
var HTTP_CLIENT_VERSION = "0.0.0";
|
|
3
|
+
var DEFAULT_USER_AGENT = `rawdash-connector/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
4
|
+
function connectorUserAgent(connectorId) {
|
|
5
|
+
return `rawdash-connector-${connectorId}/${HTTP_CLIENT_VERSION} (+https://rawdash.dev)`;
|
|
6
|
+
}
|
|
7
|
+
function standardRateLimitPolicy(config) {
|
|
8
|
+
const { remainingHeader, resetHeader, resetUnit, resetFallbackMs } = config;
|
|
9
|
+
const multiplier = resetUnit === "s" ? 1e3 : 1;
|
|
10
|
+
return {
|
|
11
|
+
parse(h) {
|
|
12
|
+
const remainingRaw = h.get(remainingHeader);
|
|
13
|
+
if (remainingRaw === null || remainingRaw.trim() === "") {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const remaining = Number(remainingRaw);
|
|
17
|
+
if (!Number.isFinite(remaining)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const resetRaw = h.get(resetHeader);
|
|
21
|
+
if (resetRaw === null) {
|
|
22
|
+
if (resetFallbackMs === void 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
remaining,
|
|
27
|
+
resetAt: new Date(Date.now() + resetFallbackMs)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (resetRaw.trim() === "") {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const reset = Number(resetRaw);
|
|
34
|
+
if (!Number.isFinite(reset) || reset < 0) {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const resetMs = reset * multiplier;
|
|
38
|
+
if (!Number.isFinite(resetMs)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
return { remaining, resetAt: new Date(resetMs) };
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function sanitizeAllowedUrl(options) {
|
|
46
|
+
const { url, host, pathname, protocol = "https:" } = options;
|
|
47
|
+
if (url === null) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const u = new URL(url);
|
|
52
|
+
if (u.protocol !== protocol || u.host !== host || u.pathname !== pathname) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
return u.toString();
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function parseEpoch(value, unit) {
|
|
61
|
+
if (value === null || value === void 0) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (unit === "iso") {
|
|
65
|
+
if (typeof value !== "string") {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const ms = new Date(value).getTime();
|
|
69
|
+
return Number.isFinite(ms) ? ms : null;
|
|
70
|
+
}
|
|
71
|
+
if (typeof value === "string" && value.trim() === "") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const n = typeof value === "number" ? value : Number(value);
|
|
75
|
+
if (!Number.isFinite(n)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const result = unit === "s" ? n * 1e3 : n;
|
|
79
|
+
return Number.isFinite(result) ? result : null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// src/datadog.ts
|
|
83
|
+
import {
|
|
84
|
+
BaseConnector,
|
|
85
|
+
defineConfigFields,
|
|
86
|
+
defineConnectorDoc,
|
|
87
|
+
defineResources,
|
|
88
|
+
makeChunkedCursorGuard,
|
|
89
|
+
paginateChunked,
|
|
90
|
+
schemasFromResources,
|
|
91
|
+
selectActivePhases
|
|
92
|
+
} from "@rawdash/core";
|
|
93
|
+
import { z } from "zod";
|
|
94
|
+
var metricQuerySchema = z.object({
|
|
95
|
+
name: z.string().min(1).regex(/^[a-zA-Z0-9_]+$/, {
|
|
96
|
+
message: "Metric name must be alphanumeric / underscore"
|
|
97
|
+
}),
|
|
98
|
+
query: z.string().min(1),
|
|
99
|
+
interval: z.enum(["5m", "15m", "1h", "1d"]).optional().describe("Aggregation interval - defaults to 1h.")
|
|
100
|
+
});
|
|
101
|
+
var datadogSiteSchema = z.string().trim().min(1).toLowerCase().regex(/^(?:[a-z0-9-]+\.)*(?:datadoghq\.com|datadoghq\.eu|ddog-gov\.com)$/, {
|
|
102
|
+
message: "Site must be a Datadog hostname (e.g. datadoghq.com, datadoghq.eu, us3.datadoghq.com)"
|
|
103
|
+
});
|
|
104
|
+
var configFields = defineConfigFields(
|
|
105
|
+
z.object({
|
|
106
|
+
apiKey: z.object({ $secret: z.string().min(1) }).meta({
|
|
107
|
+
label: "API Key",
|
|
108
|
+
description: "Datadog API key. Create at Datadog \u2192 Organization Settings \u2192 API Keys.",
|
|
109
|
+
placeholder: "dd_api_key",
|
|
110
|
+
secret: true
|
|
111
|
+
}),
|
|
112
|
+
appKey: z.object({ $secret: z.string().min(1) }).meta({
|
|
113
|
+
label: "Application Key",
|
|
114
|
+
description: "Datadog Application key. Create at Datadog \u2192 Organization Settings \u2192 Application Keys. Used in tandem with the API key to authenticate REST calls.",
|
|
115
|
+
placeholder: "dd_app_key",
|
|
116
|
+
secret: true
|
|
117
|
+
}),
|
|
118
|
+
site: datadogSiteSchema.optional().meta({
|
|
119
|
+
label: "Site",
|
|
120
|
+
description: "Datadog site host (e.g. `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com`). Defaults to `datadoghq.com`.",
|
|
121
|
+
placeholder: "datadoghq.com"
|
|
122
|
+
}),
|
|
123
|
+
metricQueries: z.array(metricQuerySchema).nonempty().optional().meta({
|
|
124
|
+
label: "Metric queries (optional)",
|
|
125
|
+
description: "User-declared metric timeseries queries. Each entry produces `datadog_metric` samples named `<name>` from the Datadog Metrics Query API."
|
|
126
|
+
}),
|
|
127
|
+
resources: z.array(
|
|
128
|
+
z.enum([
|
|
129
|
+
"monitors",
|
|
130
|
+
"monitor_events",
|
|
131
|
+
"incidents",
|
|
132
|
+
"slos",
|
|
133
|
+
"metric_queries"
|
|
134
|
+
])
|
|
135
|
+
).nonempty().optional().meta({
|
|
136
|
+
label: "Resources",
|
|
137
|
+
description: "Which Datadog resources to sync. Omit to sync all of them. 'monitor_events' depends on 'monitors' being fetched - enabling it without 'monitors' still runs the monitors query but skips writing monitor entities."
|
|
138
|
+
}),
|
|
139
|
+
metricsLookbackHours: z.number().int().positive().max(168).optional().meta({
|
|
140
|
+
label: "Metrics lookback (hours)",
|
|
141
|
+
description: "Window of metric samples to pull on each sync, in hours. Defaults to 24.",
|
|
142
|
+
placeholder: "24"
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
var doc = defineConnectorDoc({
|
|
147
|
+
displayName: "Datadog",
|
|
148
|
+
category: "infrastructure",
|
|
149
|
+
brandColor: "#632CA6",
|
|
150
|
+
tagline: "Sync monitor health, monitor state-change events, incidents, SLOs, and user-declared metric queries from a Datadog org.",
|
|
151
|
+
vendor: {
|
|
152
|
+
name: "Datadog",
|
|
153
|
+
apiDocs: "https://docs.datadoghq.com/api/latest/",
|
|
154
|
+
website: "https://www.datadoghq.com"
|
|
155
|
+
},
|
|
156
|
+
auth: {
|
|
157
|
+
summary: "A Datadog API key and Application key are required, scoped to the org and site you want to read from. Both are stored as secrets.",
|
|
158
|
+
setup: [
|
|
159
|
+
"Open Datadog \u2192 Organization Settings \u2192 API Keys and create (or copy) an API key.",
|
|
160
|
+
"Open Datadog \u2192 Organization Settings \u2192 Application Keys and create an Application key with read access to monitors, incidents, SLOs, and metrics.",
|
|
161
|
+
'Store both as secrets and reference them from the connector config as `apiKey: secret("DD_API_KEY")` and `appKey: secret("DD_APP_KEY")`.',
|
|
162
|
+
"Set `site` to your Datadog site host (e.g. `datadoghq.com`, `datadoghq.eu`, `us3.datadoghq.com`); it defaults to `datadoghq.com`."
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
rateLimit: "Datadog returns X-RateLimit-Remaining / X-RateLimit-Reset headers (reset in seconds) on the v2 endpoints, wired through the standard rate-limit policy so the host scheduler backs off on near-empty windows.",
|
|
166
|
+
limitations: [
|
|
167
|
+
"Logs and RUM session data are out of scope (high volume, low dashboard signal).",
|
|
168
|
+
"Synthetic monitor results are out of scope.",
|
|
169
|
+
"Monitor entities are not cleared on a full sync - the monitor_events diff depends on the prior status being stored.",
|
|
170
|
+
"Pagination URLs are pinned to the configured `api.<site>` host."
|
|
171
|
+
]
|
|
172
|
+
});
|
|
173
|
+
var datadogCredentials = {
|
|
174
|
+
apiKey: {
|
|
175
|
+
description: "Datadog API key",
|
|
176
|
+
auth: "required"
|
|
177
|
+
},
|
|
178
|
+
appKey: {
|
|
179
|
+
description: "Datadog Application key",
|
|
180
|
+
auth: "required"
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
var datadogRateLimit = standardRateLimitPolicy({
|
|
184
|
+
remainingHeader: "x-ratelimit-remaining",
|
|
185
|
+
resetHeader: "x-ratelimit-reset",
|
|
186
|
+
resetUnit: "s"
|
|
187
|
+
});
|
|
188
|
+
var PHASE_ORDER = ["monitors", "incidents", "slos", "metrics"];
|
|
189
|
+
var isDatadogSyncCursor = makeChunkedCursorGuard(PHASE_ORDER);
|
|
190
|
+
var idString = z.string().min(1);
|
|
191
|
+
var monitorSchema = z.object({
|
|
192
|
+
id: z.number().int().nonnegative(),
|
|
193
|
+
name: z.string(),
|
|
194
|
+
type: z.string(),
|
|
195
|
+
status: z.enum(["OK", "Alert", "Warn", "No Data", "Ignored"]),
|
|
196
|
+
priority: z.number().int().nullable(),
|
|
197
|
+
tags: z.array(z.string()),
|
|
198
|
+
overall_state_modified: z.iso.datetime().nullable().optional(),
|
|
199
|
+
created: z.iso.datetime(),
|
|
200
|
+
modified: z.iso.datetime()
|
|
201
|
+
});
|
|
202
|
+
var monitorSearchResponseSchema = z.object({
|
|
203
|
+
monitors: z.array(monitorSchema),
|
|
204
|
+
metadata: z.object({
|
|
205
|
+
page: z.number().int().nonnegative(),
|
|
206
|
+
page_count: z.number().int().nonnegative(),
|
|
207
|
+
per_page: z.number().int().positive(),
|
|
208
|
+
total_count: z.number().int().nonnegative()
|
|
209
|
+
})
|
|
210
|
+
});
|
|
211
|
+
var incidentSchema = z.object({
|
|
212
|
+
id: idString,
|
|
213
|
+
type: z.literal("incidents"),
|
|
214
|
+
attributes: z.object({
|
|
215
|
+
title: z.string(),
|
|
216
|
+
severity: z.string().nullable().optional(),
|
|
217
|
+
state: z.string().nullable().optional(),
|
|
218
|
+
customer_impact_scope: z.string().nullable().optional(),
|
|
219
|
+
created: z.iso.datetime(),
|
|
220
|
+
modified: z.iso.datetime().nullable().optional(),
|
|
221
|
+
resolved: z.iso.datetime().nullable().optional()
|
|
222
|
+
})
|
|
223
|
+
});
|
|
224
|
+
var incidentsResponseSchema = z.object({
|
|
225
|
+
data: z.array(incidentSchema),
|
|
226
|
+
meta: z.object({
|
|
227
|
+
pagination: z.object({
|
|
228
|
+
next_offset: z.number().int().nullable().optional(),
|
|
229
|
+
offset: z.number().int().optional(),
|
|
230
|
+
size: z.number().int().optional()
|
|
231
|
+
}).optional()
|
|
232
|
+
}).optional()
|
|
233
|
+
});
|
|
234
|
+
var sloSchema = z.object({
|
|
235
|
+
id: idString,
|
|
236
|
+
name: z.string(),
|
|
237
|
+
type: z.string(),
|
|
238
|
+
thresholds: z.array(
|
|
239
|
+
z.object({
|
|
240
|
+
timeframe: z.string(),
|
|
241
|
+
target: z.number(),
|
|
242
|
+
warning: z.number().nullable().optional()
|
|
243
|
+
})
|
|
244
|
+
),
|
|
245
|
+
overall_status: z.array(
|
|
246
|
+
z.object({
|
|
247
|
+
sli_value: z.number().nullable().optional(),
|
|
248
|
+
indexed_at: z.number().nullable().optional()
|
|
249
|
+
})
|
|
250
|
+
).nullable().optional(),
|
|
251
|
+
created_at: z.number().nullable().optional(),
|
|
252
|
+
modified_at: z.number().nullable().optional()
|
|
253
|
+
});
|
|
254
|
+
var slosResponseSchema = z.object({
|
|
255
|
+
data: z.array(sloSchema)
|
|
256
|
+
});
|
|
257
|
+
var timeseriesResponseSchema = z.object({
|
|
258
|
+
data: z.object({
|
|
259
|
+
type: z.literal("timeseries_response"),
|
|
260
|
+
attributes: z.object({
|
|
261
|
+
series: z.array(
|
|
262
|
+
z.object({
|
|
263
|
+
group_tags: z.array(z.string()).optional(),
|
|
264
|
+
query_index: z.number().int().optional()
|
|
265
|
+
})
|
|
266
|
+
).optional(),
|
|
267
|
+
times: z.array(z.number()).optional(),
|
|
268
|
+
values: z.array(z.array(z.number())).optional()
|
|
269
|
+
})
|
|
270
|
+
})
|
|
271
|
+
});
|
|
272
|
+
var DEFAULT_SITE = "datadoghq.com";
|
|
273
|
+
var MONITORS_PAGE_SIZE = 100;
|
|
274
|
+
var INCIDENTS_PAGE_SIZE = 50;
|
|
275
|
+
var DEFAULT_METRICS_LOOKBACK_HOURS = 24;
|
|
276
|
+
var INTERVAL_MS = {
|
|
277
|
+
"5m": 5 * 60 * 1e3,
|
|
278
|
+
"15m": 15 * 60 * 1e3,
|
|
279
|
+
"1h": 60 * 60 * 1e3,
|
|
280
|
+
"1d": 24 * 60 * 60 * 1e3
|
|
281
|
+
};
|
|
282
|
+
var DEFAULT_INTERVAL_MS = INTERVAL_MS["1h"];
|
|
283
|
+
var datadogResources = defineResources({
|
|
284
|
+
datadog_monitor: {
|
|
285
|
+
shape: "entity",
|
|
286
|
+
description: "Datadog monitors with name, type, current status (OK / Alert / Warn / No Data), priority, and tags.",
|
|
287
|
+
endpoint: "GET /api/v1/monitor/search",
|
|
288
|
+
responses: { monitors: monitorSearchResponseSchema }
|
|
289
|
+
},
|
|
290
|
+
datadog_monitor_event: {
|
|
291
|
+
shape: "event",
|
|
292
|
+
description: "Monitor state-transition events, emitted whenever a monitor's status changes from its previously-stored value.",
|
|
293
|
+
notes: "Derived by diffing each monitor's current status against the last-synced status, so it depends on the monitors phase running and on prior monitor state being stored."
|
|
294
|
+
},
|
|
295
|
+
datadog_incident: {
|
|
296
|
+
shape: "entity",
|
|
297
|
+
description: "Datadog incidents with title, severity, state, and created / resolved timestamps.",
|
|
298
|
+
endpoint: "GET /api/v2/incidents",
|
|
299
|
+
responses: { incidents: incidentsResponseSchema }
|
|
300
|
+
},
|
|
301
|
+
datadog_slo: {
|
|
302
|
+
shape: "entity",
|
|
303
|
+
description: "Service Level Objectives with type, thresholds, primary target, and latest SLI value.",
|
|
304
|
+
endpoint: "GET /api/v1/slo",
|
|
305
|
+
responses: { slos: slosResponseSchema }
|
|
306
|
+
},
|
|
307
|
+
datadog_slo_sli: {
|
|
308
|
+
shape: "metric",
|
|
309
|
+
description: "SLI value samples per SLO, one per overall_status snapshot reported by Datadog.",
|
|
310
|
+
unit: "percent",
|
|
311
|
+
dimensions: [
|
|
312
|
+
{ name: "sloId", description: "Datadog SLO id." },
|
|
313
|
+
{ name: "sloType", description: "SLO type (metric, monitor, etc.)." }
|
|
314
|
+
]
|
|
315
|
+
},
|
|
316
|
+
datadog_metric: {
|
|
317
|
+
shape: "metric",
|
|
318
|
+
dynamic: true,
|
|
319
|
+
description: "User-declared metric timeseries samples, stored as `datadog_metric.<query name>`, from the Datadog Metrics Query API.",
|
|
320
|
+
endpoint: "POST /api/v2/query/timeseries",
|
|
321
|
+
dimensions: [
|
|
322
|
+
{ name: "queryName", description: "The user-declared query name." },
|
|
323
|
+
{ name: "query", description: "The Datadog metrics query string." },
|
|
324
|
+
{
|
|
325
|
+
name: "tags",
|
|
326
|
+
description: "Comma-joined group tags for the series, or `*` when the series is ungrouped."
|
|
327
|
+
}
|
|
328
|
+
],
|
|
329
|
+
responses: { metric_queries: timeseriesResponseSchema }
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
var DatadogConnector = class _DatadogConnector extends BaseConnector {
|
|
333
|
+
static id = "datadog";
|
|
334
|
+
static resources = datadogResources;
|
|
335
|
+
static schemas = schemasFromResources(datadogResources);
|
|
336
|
+
static create(input, ctx) {
|
|
337
|
+
const parsed = configFields.parse(input);
|
|
338
|
+
return new _DatadogConnector(
|
|
339
|
+
{
|
|
340
|
+
site: parsed.site,
|
|
341
|
+
metricQueries: parsed.metricQueries,
|
|
342
|
+
resources: parsed.resources,
|
|
343
|
+
metricsLookbackHours: parsed.metricsLookbackHours
|
|
344
|
+
},
|
|
345
|
+
{ apiKey: parsed.apiKey, appKey: parsed.appKey },
|
|
346
|
+
ctx
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
id = "datadog";
|
|
350
|
+
credentials = datadogCredentials;
|
|
351
|
+
get apiHost() {
|
|
352
|
+
return `api.${(this.settings.site ?? DEFAULT_SITE).toLowerCase()}`;
|
|
353
|
+
}
|
|
354
|
+
get apiBase() {
|
|
355
|
+
return `https://${this.apiHost}`;
|
|
356
|
+
}
|
|
357
|
+
buildHeaders() {
|
|
358
|
+
return {
|
|
359
|
+
"DD-API-KEY": this.creds.apiKey,
|
|
360
|
+
"DD-APPLICATION-KEY": this.creds.appKey,
|
|
361
|
+
"User-Agent": connectorUserAgent("datadog")
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
fetch(url, resource, signal) {
|
|
365
|
+
return this.get(url, {
|
|
366
|
+
resource,
|
|
367
|
+
headers: this.buildHeaders(),
|
|
368
|
+
signal,
|
|
369
|
+
rateLimit: datadogRateLimit
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
postJson(url, body, resource, signal) {
|
|
373
|
+
return this.post(url, {
|
|
374
|
+
resource,
|
|
375
|
+
headers: {
|
|
376
|
+
...this.buildHeaders(),
|
|
377
|
+
"Content-Type": "application/json"
|
|
378
|
+
},
|
|
379
|
+
body: JSON.stringify(body),
|
|
380
|
+
signal,
|
|
381
|
+
rateLimit: datadogRateLimit
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
// -------------------------------------------------------------------------
|
|
385
|
+
// Resource enablement
|
|
386
|
+
// -------------------------------------------------------------------------
|
|
387
|
+
activePhases() {
|
|
388
|
+
return selectActivePhases(
|
|
389
|
+
(r) => {
|
|
390
|
+
switch (r) {
|
|
391
|
+
case "monitors":
|
|
392
|
+
case "monitor_events":
|
|
393
|
+
return "monitors";
|
|
394
|
+
case "incidents":
|
|
395
|
+
return "incidents";
|
|
396
|
+
case "slos":
|
|
397
|
+
return "slos";
|
|
398
|
+
case "metric_queries":
|
|
399
|
+
return "metrics";
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
PHASE_ORDER,
|
|
403
|
+
this.settings.resources
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
// -------------------------------------------------------------------------
|
|
407
|
+
// URL building + sanitization
|
|
408
|
+
// -------------------------------------------------------------------------
|
|
409
|
+
allowedPagePath(phase) {
|
|
410
|
+
switch (phase) {
|
|
411
|
+
case "monitors":
|
|
412
|
+
return "/api/v1/monitor/search";
|
|
413
|
+
case "incidents":
|
|
414
|
+
return "/api/v2/incidents";
|
|
415
|
+
case "slos":
|
|
416
|
+
return "/api/v1/slo";
|
|
417
|
+
case "metrics":
|
|
418
|
+
return "/api/v2/query/timeseries";
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
sanitizePageUrl(phase, pageUrl) {
|
|
422
|
+
return sanitizeAllowedUrl({
|
|
423
|
+
url: pageUrl,
|
|
424
|
+
host: this.apiHost,
|
|
425
|
+
pathname: this.allowedPagePath(phase)
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
resolveCursor(cursor) {
|
|
429
|
+
if (!isDatadogSyncCursor(cursor)) {
|
|
430
|
+
return void 0;
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
phase: cursor.phase,
|
|
434
|
+
page: this.sanitizePageUrl(cursor.phase, cursor.page)
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
buildInitialMonitorsUrl() {
|
|
438
|
+
const u = new URL(`${this.apiBase}/api/v1/monitor/search`);
|
|
439
|
+
u.searchParams.set("per_page", String(MONITORS_PAGE_SIZE));
|
|
440
|
+
u.searchParams.set("page", "0");
|
|
441
|
+
u.searchParams.set("sort", "status,desc");
|
|
442
|
+
return u.toString();
|
|
443
|
+
}
|
|
444
|
+
buildNextMonitorsUrl(currentUrl, nextPage) {
|
|
445
|
+
const u = new URL(currentUrl);
|
|
446
|
+
u.searchParams.set("page", String(nextPage));
|
|
447
|
+
return u.toString();
|
|
448
|
+
}
|
|
449
|
+
buildInitialIncidentsUrl(options) {
|
|
450
|
+
const u = new URL(`${this.apiBase}/api/v2/incidents`);
|
|
451
|
+
u.searchParams.set("page[size]", String(INCIDENTS_PAGE_SIZE));
|
|
452
|
+
u.searchParams.set("page[offset]", "0");
|
|
453
|
+
u.searchParams.set("include", "");
|
|
454
|
+
if (options.since) {
|
|
455
|
+
u.searchParams.set("filter[created.from]", options.since);
|
|
456
|
+
}
|
|
457
|
+
return u.toString();
|
|
458
|
+
}
|
|
459
|
+
buildNextIncidentsUrl(currentUrl, nextOffset) {
|
|
460
|
+
const u = new URL(currentUrl);
|
|
461
|
+
u.searchParams.set("page[offset]", String(nextOffset));
|
|
462
|
+
return u.toString();
|
|
463
|
+
}
|
|
464
|
+
buildSlosUrl() {
|
|
465
|
+
const u = new URL(`${this.apiBase}/api/v1/slo`);
|
|
466
|
+
u.searchParams.set("limit", "1000");
|
|
467
|
+
u.searchParams.set("offset", "0");
|
|
468
|
+
return u.toString();
|
|
469
|
+
}
|
|
470
|
+
buildMetricsUrl() {
|
|
471
|
+
return `${this.apiBase}/api/v2/query/timeseries`;
|
|
472
|
+
}
|
|
473
|
+
// -------------------------------------------------------------------------
|
|
474
|
+
// Fetchers
|
|
475
|
+
// -------------------------------------------------------------------------
|
|
476
|
+
async fetchMonitorsPage(page, signal) {
|
|
477
|
+
const url = page ?? this.buildInitialMonitorsUrl();
|
|
478
|
+
const res = await this.fetch(
|
|
479
|
+
url,
|
|
480
|
+
"monitors",
|
|
481
|
+
signal
|
|
482
|
+
);
|
|
483
|
+
const meta = res.body.metadata;
|
|
484
|
+
const currentPage = meta.page;
|
|
485
|
+
const totalPages = meta.page_count;
|
|
486
|
+
const hasNext = currentPage + 1 < totalPages;
|
|
487
|
+
const next = hasNext ? this.sanitizePageUrl(
|
|
488
|
+
"monitors",
|
|
489
|
+
this.buildNextMonitorsUrl(url, currentPage + 1)
|
|
490
|
+
) : null;
|
|
491
|
+
return {
|
|
492
|
+
items: res.body.monitors.map((m) => ({ monitor: m })),
|
|
493
|
+
next
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
async fetchIncidentsPage(page, options, signal) {
|
|
497
|
+
const url = page ?? this.buildInitialIncidentsUrl(options);
|
|
498
|
+
const res = await this.fetch(
|
|
499
|
+
url,
|
|
500
|
+
"incidents",
|
|
501
|
+
signal
|
|
502
|
+
);
|
|
503
|
+
const nextOffset = res.body.meta?.pagination?.next_offset ?? null;
|
|
504
|
+
const incidents = res.body.data;
|
|
505
|
+
const cutoff = options.since ? parseEpoch(options.since, "iso") ?? null : null;
|
|
506
|
+
const filtered = cutoff !== null ? incidents.filter((inc) => {
|
|
507
|
+
const ts = parseEpoch(inc.attributes.created, "iso");
|
|
508
|
+
return ts === null || ts >= cutoff;
|
|
509
|
+
}) : incidents;
|
|
510
|
+
const lastIncident = incidents.at(-1);
|
|
511
|
+
const lastTs = lastIncident ? parseEpoch(lastIncident.attributes.created, "iso") : null;
|
|
512
|
+
const cutoffReached = cutoff !== null && lastTs !== null && lastTs < cutoff;
|
|
513
|
+
const next = !cutoffReached && nextOffset !== null ? this.sanitizePageUrl(
|
|
514
|
+
"incidents",
|
|
515
|
+
this.buildNextIncidentsUrl(url, nextOffset)
|
|
516
|
+
) : null;
|
|
517
|
+
return { items: filtered, next };
|
|
518
|
+
}
|
|
519
|
+
async fetchSlos(signal) {
|
|
520
|
+
const res = await this.fetch(
|
|
521
|
+
this.buildSlosUrl(),
|
|
522
|
+
"slos",
|
|
523
|
+
signal
|
|
524
|
+
);
|
|
525
|
+
return { items: res.body.data, next: null };
|
|
526
|
+
}
|
|
527
|
+
async fetchMetrics(options, signal) {
|
|
528
|
+
const queries = this.settings.metricQueries ?? [];
|
|
529
|
+
if (queries.length === 0) {
|
|
530
|
+
return { items: [], next: null };
|
|
531
|
+
}
|
|
532
|
+
const lookbackHours = this.settings.metricsLookbackHours ?? DEFAULT_METRICS_LOOKBACK_HOURS;
|
|
533
|
+
const now = Date.now();
|
|
534
|
+
const sinceMs = options.since ? parseEpoch(options.since, "iso") : null;
|
|
535
|
+
const fromMs = sinceMs !== null ? sinceMs : now - lookbackHours * 60 * 60 * 1e3;
|
|
536
|
+
const items = [];
|
|
537
|
+
for (const q of queries) {
|
|
538
|
+
signal?.throwIfAborted();
|
|
539
|
+
const intervalMs = q.interval ? INTERVAL_MS[q.interval] : DEFAULT_INTERVAL_MS;
|
|
540
|
+
const body = {
|
|
541
|
+
data: {
|
|
542
|
+
type: "timeseries_request",
|
|
543
|
+
attributes: {
|
|
544
|
+
from: fromMs,
|
|
545
|
+
to: now,
|
|
546
|
+
interval: intervalMs,
|
|
547
|
+
queries: [
|
|
548
|
+
{
|
|
549
|
+
name: "a",
|
|
550
|
+
data_source: "metrics",
|
|
551
|
+
query: q.query
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
formulas: [{ formula: "a" }]
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
const res = await this.postJson(
|
|
559
|
+
this.buildMetricsUrl(),
|
|
560
|
+
body,
|
|
561
|
+
"metric_queries",
|
|
562
|
+
signal
|
|
563
|
+
);
|
|
564
|
+
items.push({
|
|
565
|
+
queryName: q.name,
|
|
566
|
+
query: q.query,
|
|
567
|
+
response: res.body
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
return { items, next: null };
|
|
571
|
+
}
|
|
572
|
+
// -------------------------------------------------------------------------
|
|
573
|
+
// Writers
|
|
574
|
+
// -------------------------------------------------------------------------
|
|
575
|
+
async writeMonitorsBatch(storage, items) {
|
|
576
|
+
const writeEntities = this.isResourceEnabled("monitors");
|
|
577
|
+
const writeEvents = this.isResourceEnabled("monitor_events");
|
|
578
|
+
for (const item of items) {
|
|
579
|
+
const m = item.monitor;
|
|
580
|
+
const createdMs = parseEpoch(m.created, "iso");
|
|
581
|
+
const modifiedMs = parseEpoch(m.modified, "iso");
|
|
582
|
+
const stateModifiedMs = m.overall_state_modified !== void 0 && m.overall_state_modified !== null ? parseEpoch(m.overall_state_modified, "iso") : null;
|
|
583
|
+
if (createdMs === null || modifiedMs === null) {
|
|
584
|
+
console.warn(
|
|
585
|
+
`[connector-datadog] skipping monitor ${m.id} with unparseable created/modified timestamps`
|
|
586
|
+
);
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
const updatedMs = Math.max(modifiedMs, stateModifiedMs ?? 0);
|
|
590
|
+
const attributes = {
|
|
591
|
+
monitorId: m.id,
|
|
592
|
+
name: m.name,
|
|
593
|
+
monitorType: m.type,
|
|
594
|
+
status: m.status,
|
|
595
|
+
priority: m.priority,
|
|
596
|
+
tags: m.tags,
|
|
597
|
+
createdAt: createdMs,
|
|
598
|
+
modifiedAt: modifiedMs,
|
|
599
|
+
stateModifiedAt: stateModifiedMs
|
|
600
|
+
};
|
|
601
|
+
if (writeEvents) {
|
|
602
|
+
const prior = await storage.getEntity("datadog_monitor", String(m.id));
|
|
603
|
+
const priorStatus = prior !== null && typeof prior.attributes === "object" && prior.attributes !== null ? prior.attributes.status : void 0;
|
|
604
|
+
if (priorStatus !== m.status && stateModifiedMs !== null && Number.isFinite(stateModifiedMs)) {
|
|
605
|
+
await storage.event({
|
|
606
|
+
name: "datadog_monitor_event",
|
|
607
|
+
start_ts: stateModifiedMs,
|
|
608
|
+
end_ts: null,
|
|
609
|
+
attributes: {
|
|
610
|
+
monitorId: m.id,
|
|
611
|
+
name: m.name,
|
|
612
|
+
monitorType: m.type,
|
|
613
|
+
fromStatus: priorStatus ?? null,
|
|
614
|
+
toStatus: m.status,
|
|
615
|
+
priority: m.priority,
|
|
616
|
+
tags: m.tags
|
|
617
|
+
}
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (writeEntities) {
|
|
622
|
+
await storage.entity({
|
|
623
|
+
type: "datadog_monitor",
|
|
624
|
+
id: String(m.id),
|
|
625
|
+
attributes,
|
|
626
|
+
updated_at: updatedMs
|
|
627
|
+
});
|
|
628
|
+
} else if (writeEvents) {
|
|
629
|
+
await storage.entity({
|
|
630
|
+
type: "datadog_monitor",
|
|
631
|
+
id: String(m.id),
|
|
632
|
+
attributes,
|
|
633
|
+
updated_at: updatedMs
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async writeIncidents(storage, incidents) {
|
|
639
|
+
for (const inc of incidents) {
|
|
640
|
+
const createdMs = parseEpoch(inc.attributes.created, "iso");
|
|
641
|
+
if (createdMs === null) {
|
|
642
|
+
console.warn(
|
|
643
|
+
`[connector-datadog] skipping incident ${inc.id} with unparseable created timestamp`
|
|
644
|
+
);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
const modifiedMs = inc.attributes.modified ? parseEpoch(inc.attributes.modified, "iso") : null;
|
|
648
|
+
const resolvedMs = inc.attributes.resolved ? parseEpoch(inc.attributes.resolved, "iso") : null;
|
|
649
|
+
await storage.entity({
|
|
650
|
+
type: "datadog_incident",
|
|
651
|
+
id: inc.id,
|
|
652
|
+
attributes: {
|
|
653
|
+
incidentId: inc.id,
|
|
654
|
+
title: inc.attributes.title,
|
|
655
|
+
severity: inc.attributes.severity ?? null,
|
|
656
|
+
state: inc.attributes.state ?? null,
|
|
657
|
+
customerImpactScope: inc.attributes.customer_impact_scope ?? null,
|
|
658
|
+
createdAt: createdMs,
|
|
659
|
+
modifiedAt: modifiedMs,
|
|
660
|
+
resolvedAt: resolvedMs
|
|
661
|
+
},
|
|
662
|
+
updated_at: Math.max(createdMs, modifiedMs ?? 0, resolvedMs ?? 0)
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
async writeSlos(storage, slos) {
|
|
667
|
+
const sliSamples = [];
|
|
668
|
+
const entities = [];
|
|
669
|
+
for (const s of slos) {
|
|
670
|
+
const createdMs = s.created_at !== null && s.created_at !== void 0 ? parseEpoch(s.created_at, "s") : null;
|
|
671
|
+
const modifiedMs = s.modified_at !== null && s.modified_at !== void 0 ? parseEpoch(s.modified_at, "s") : null;
|
|
672
|
+
const targets = s.thresholds.map((t) => ({
|
|
673
|
+
timeframe: t.timeframe,
|
|
674
|
+
target: t.target
|
|
675
|
+
}));
|
|
676
|
+
const primaryTarget = s.thresholds[0]?.target ?? null;
|
|
677
|
+
const latestStatus = (s.overall_status ?? []).find(
|
|
678
|
+
(st) => st.sli_value !== null && st.sli_value !== void 0
|
|
679
|
+
);
|
|
680
|
+
const latestSli = latestStatus?.sli_value ?? null;
|
|
681
|
+
entities.push({
|
|
682
|
+
type: "datadog_slo",
|
|
683
|
+
id: s.id,
|
|
684
|
+
attributes: {
|
|
685
|
+
sloId: s.id,
|
|
686
|
+
name: s.name,
|
|
687
|
+
sloType: s.type,
|
|
688
|
+
thresholds: targets,
|
|
689
|
+
target: primaryTarget,
|
|
690
|
+
latestSliValue: latestSli,
|
|
691
|
+
createdAt: createdMs,
|
|
692
|
+
modifiedAt: modifiedMs
|
|
693
|
+
},
|
|
694
|
+
updated_at: modifiedMs ?? createdMs ?? Date.now()
|
|
695
|
+
});
|
|
696
|
+
for (const status of s.overall_status ?? []) {
|
|
697
|
+
const ts = status.indexed_at !== null && status.indexed_at !== void 0 ? parseEpoch(status.indexed_at, "s") : null;
|
|
698
|
+
const value = status.sli_value;
|
|
699
|
+
if (ts === null || value === null || value === void 0 || !Number.isFinite(value)) {
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
sliSamples.push({
|
|
703
|
+
name: "datadog_slo_sli",
|
|
704
|
+
ts,
|
|
705
|
+
value,
|
|
706
|
+
attributes: { sloId: s.id, sloType: s.type }
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
for (const entity of entities) {
|
|
711
|
+
await storage.entity(entity);
|
|
712
|
+
}
|
|
713
|
+
if (sliSamples.length > 0) {
|
|
714
|
+
await storage.metrics(sliSamples, { names: ["datadog_slo_sli"] });
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async writeMetrics(storage, items) {
|
|
718
|
+
if (items.length === 0) {
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const samplesByName = /* @__PURE__ */ new Map();
|
|
722
|
+
for (const item of items) {
|
|
723
|
+
const attrs = item.response.data.attributes;
|
|
724
|
+
const times = attrs.times ?? [];
|
|
725
|
+
const series = attrs.series ?? [];
|
|
726
|
+
const values = attrs.values ?? [];
|
|
727
|
+
for (let s = 0; s < series.length; s++) {
|
|
728
|
+
const seriesValues = values[s];
|
|
729
|
+
if (!seriesValues) {
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
const tagsArr = series[s]?.group_tags ?? [];
|
|
733
|
+
const tagsStr = tagsArr.length > 0 ? tagsArr.join(",") : "*";
|
|
734
|
+
for (let t = 0; t < times.length; t++) {
|
|
735
|
+
const rawTs = times[t];
|
|
736
|
+
const rawValue = seriesValues[t];
|
|
737
|
+
if (rawTs === void 0 || rawValue === void 0) {
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const ts = parseEpoch(rawTs, "ms");
|
|
741
|
+
if (ts === null || !Number.isFinite(rawValue)) {
|
|
742
|
+
continue;
|
|
743
|
+
}
|
|
744
|
+
const name = `datadog_metric.${item.queryName}`;
|
|
745
|
+
let bucket = samplesByName.get(name);
|
|
746
|
+
if (!bucket) {
|
|
747
|
+
bucket = [];
|
|
748
|
+
samplesByName.set(name, bucket);
|
|
749
|
+
}
|
|
750
|
+
bucket.push({
|
|
751
|
+
name,
|
|
752
|
+
ts,
|
|
753
|
+
value: rawValue,
|
|
754
|
+
attributes: {
|
|
755
|
+
queryName: item.queryName,
|
|
756
|
+
query: item.query,
|
|
757
|
+
tags: tagsStr
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
for (const [name, samples] of samplesByName) {
|
|
764
|
+
await storage.metrics(samples, { names: [name] });
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
// -------------------------------------------------------------------------
|
|
768
|
+
// sync
|
|
769
|
+
// -------------------------------------------------------------------------
|
|
770
|
+
async sync(options, storage, signal) {
|
|
771
|
+
const cursor = this.resolveCursor(options.cursor);
|
|
772
|
+
const isFull = options.mode === "full";
|
|
773
|
+
const phases = this.activePhases();
|
|
774
|
+
return paginateChunked({
|
|
775
|
+
phases,
|
|
776
|
+
cursor,
|
|
777
|
+
signal,
|
|
778
|
+
logger: this.logger,
|
|
779
|
+
fetchPage: async (phase, page, sig) => {
|
|
780
|
+
switch (phase) {
|
|
781
|
+
case "monitors":
|
|
782
|
+
return this.fetchMonitorsPage(page, sig);
|
|
783
|
+
case "incidents":
|
|
784
|
+
return this.fetchIncidentsPage(page, options, sig);
|
|
785
|
+
case "slos":
|
|
786
|
+
return this.fetchSlos(sig);
|
|
787
|
+
case "metrics":
|
|
788
|
+
return this.fetchMetrics(options, sig);
|
|
789
|
+
}
|
|
790
|
+
},
|
|
791
|
+
writeBatch: async (phase, items, page) => {
|
|
792
|
+
if (isFull && page === null) {
|
|
793
|
+
switch (phase) {
|
|
794
|
+
case "monitors":
|
|
795
|
+
if (this.isResourceEnabled("monitor_events")) {
|
|
796
|
+
await storage.events([], { names: ["datadog_monitor_event"] });
|
|
797
|
+
}
|
|
798
|
+
break;
|
|
799
|
+
case "incidents":
|
|
800
|
+
await storage.entities([], { types: ["datadog_incident"] });
|
|
801
|
+
break;
|
|
802
|
+
case "slos":
|
|
803
|
+
await storage.entities([], { types: ["datadog_slo"] });
|
|
804
|
+
await storage.metrics([], { names: ["datadog_slo_sli"] });
|
|
805
|
+
break;
|
|
806
|
+
case "metrics":
|
|
807
|
+
for (const q of this.settings.metricQueries ?? []) {
|
|
808
|
+
await storage.metrics([], {
|
|
809
|
+
names: [`datadog_metric.${q.name}`]
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
switch (phase) {
|
|
816
|
+
case "monitors":
|
|
817
|
+
return this.writeMonitorsBatch(
|
|
818
|
+
storage,
|
|
819
|
+
items
|
|
820
|
+
);
|
|
821
|
+
case "incidents":
|
|
822
|
+
return this.writeIncidents(storage, items);
|
|
823
|
+
case "slos":
|
|
824
|
+
return this.writeSlos(storage, items);
|
|
825
|
+
case "metrics":
|
|
826
|
+
return this.writeMetrics(storage, items);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
|
|
833
|
+
// src/index.ts
|
|
834
|
+
var index_default = DatadogConnector;
|
|
835
|
+
export {
|
|
836
|
+
DatadogConnector,
|
|
837
|
+
configFields,
|
|
838
|
+
index_default as default,
|
|
839
|
+
doc
|
|
840
|
+
};
|
|
841
|
+
//# sourceMappingURL=index.js.map
|