@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/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