@voltro/plugin-datadog 0.1.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.
@@ -0,0 +1,106 @@
1
+ import { LogRecord } from '@voltro/logger';
2
+ import { MetricSample } from '@voltro/runtime';
3
+ import { VoltroPlugin } from '@voltro/protocol';
4
+
5
+ export declare interface DatadogLog {
6
+ readonly message: string;
7
+ readonly status: string;
8
+ readonly ddsource: string;
9
+ readonly [attribute: string]: unknown;
10
+ }
11
+
12
+ export declare interface DatadogPayload {
13
+ readonly series: ReadonlyArray<DatadogSeries>;
14
+ }
15
+
16
+ export declare const datadogPlugin: (options?: DatadogPluginOptions) => VoltroPlugin;
17
+
18
+ export declare interface DatadogPluginOptions {
19
+ /** Datadog API key. Default `DD_API_KEY` env. Needed for metrics + logs
20
+ * (agentless). Traces use the Agent and don't need it. */
21
+ readonly apiKey?: string;
22
+ /** Datadog site host. Default `DD_SITE` env, else `datadoghq.com` (US1). */
23
+ readonly site?: string;
24
+ /** Metrics push interval in ms. Default 30000. */
25
+ readonly intervalMs?: number;
26
+ /** Prepended to every metric name (e.g. `voltro.`). */
27
+ readonly prefix?: string;
28
+ /** Tags added to every series + log (`["env:prod", "service:api"]`). */
29
+ readonly tags?: ReadonlyArray<string>;
30
+ /** Per-replica identity, emitted as the Datadog v2
31
+ * `resources: [{ type: 'host', name }]` field on every pushed series.
32
+ * Default `os.hostname()`. Without a per-replica value, N replicas
33
+ * submitting the same metric+tags merge into ONE Datadog series and the
34
+ * cumulative per-process counters interleave — `.as_count()`/`diff()`
35
+ * rates go silently wrong. */
36
+ readonly host?: string;
37
+ /** Forward the framework log sink to Datadog logs intake. Default false. */
38
+ readonly logs?: boolean;
39
+ /** Logs flush interval in ms. Default 5000. */
40
+ readonly logsIntervalMs?: number;
41
+ /** Route the framework's OTel spans to the Datadog Agent's OTLP receiver.
42
+ * Default false. */
43
+ readonly traces?: boolean;
44
+ /** Datadog Agent OTLP base url (`/v1/traces` is appended). Default
45
+ * `DD_TRACE_AGENT_URL` env, else `http://localhost:4318`. */
46
+ readonly agentUrl?: string;
47
+ /** Continuous profiler via dd-trace (profiler-only mode). Default false. */
48
+ readonly profiling?: boolean;
49
+ /** `service` resource/tag (DD unified service tagging). Default `DD_SERVICE` env. */
50
+ readonly service?: string;
51
+ /** `env` resource/tag. Default `DD_ENV` env. */
52
+ readonly env?: string;
53
+ /** `version` resource/tag. Default `DD_VERSION` env. */
54
+ readonly version?: string;
55
+ /** Disambiguates multiple instances. */
56
+ readonly name?: string;
57
+ }
58
+
59
+ export declare interface DatadogPoint {
60
+ readonly timestamp: number;
61
+ readonly value: number;
62
+ }
63
+
64
+ /** Datadog v2 series resource — `{ type: 'host', name }` carries the
65
+ * per-replica identity Datadog uses to keep same-name+tags series apart. */
66
+ export declare interface DatadogResource {
67
+ readonly type: string;
68
+ readonly name: string;
69
+ }
70
+
71
+ export declare interface DatadogSeries {
72
+ readonly metric: string;
73
+ readonly type: 0 | 1 | 2 | 3;
74
+ readonly points: ReadonlyArray<DatadogPoint>;
75
+ readonly tags?: ReadonlyArray<string>;
76
+ readonly resources?: ReadonlyArray<DatadogResource>;
77
+ }
78
+
79
+ export declare const toDatadogLog: (r: LogRecord, opts?: ToLogsOptions) => DatadogLog;
80
+
81
+ export declare const toDatadogLogs: (records: ReadonlyArray<LogRecord>, opts?: ToLogsOptions) => ReadonlyArray<DatadogLog>;
82
+
83
+ export declare const toDatadogSeries: (samples: ReadonlyArray<MetricSample>, opts: ToSeriesOptions) => DatadogPayload;
84
+
85
+ declare interface ToLogsOptions {
86
+ readonly service?: string;
87
+ /** Comma-joined into `ddtags`. */
88
+ readonly tags?: ReadonlyArray<string>;
89
+ readonly hostname?: string;
90
+ }
91
+
92
+ declare interface ToSeriesOptions {
93
+ /** Unix SECONDS for every point in this batch. */
94
+ readonly timestamp: number;
95
+ /** Prepended to every metric name (e.g. `voltro.`). Default none. */
96
+ readonly prefix?: string;
97
+ /** Tags added to every series. */
98
+ readonly globalTags?: ReadonlyArray<string>;
99
+ /** Per-replica identity, emitted as the v2 `resources: [{ type: 'host', name }]`
100
+ * field on EVERY series. Without it, N replicas submitting the same
101
+ * metric+tags merge into ONE Datadog series and the cumulative per-process
102
+ * counters interleave into a meaningless sawtooth. */
103
+ readonly host?: string;
104
+ }
105
+
106
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,200 @@
1
+ import { hostname as e } from "node:os";
2
+ import { Effect as t } from "effect";
3
+ import { pluginEnv as n } from "@voltro/env";
4
+ import { addSink as r, createLogger as i, makeBufferedSink as a } from "@voltro/logger";
5
+ import { definePlugin as o } from "@voltro/protocol";
6
+ import { snapshotMetrics as s } from "@voltro/runtime";
7
+ //#region src/format.ts
8
+ var c = 3, l = (e) => Object.entries(e).map(([e, t]) => `${e}:${t}`), u = (e, t) => {
9
+ let n = t.prefix ?? "", r = t.globalTags ?? [], i = t.host === void 0 ? void 0 : [{
10
+ type: "host",
11
+ name: t.host
12
+ }], a = [], o = (e, o, s) => {
13
+ a.push({
14
+ metric: `${n}${e}`,
15
+ type: c,
16
+ points: [{
17
+ timestamp: t.timestamp,
18
+ value: o
19
+ }],
20
+ tags: [...r, ...s],
21
+ ...i ? { resources: i } : {}
22
+ });
23
+ };
24
+ for (let t of e) {
25
+ let e = l(t.labels);
26
+ if (t.type === "counter" || t.type === "gauge") o(t.name, t.value ?? 0, e);
27
+ else if (t.type === "histogram") {
28
+ let n = t.count ?? 0, r = t.sum ?? 0;
29
+ o(`${t.name}.count`, n, e), o(`${t.name}.sum`, r, e), o(`${t.name}.avg`, n > 0 ? r / n : 0, e);
30
+ }
31
+ }
32
+ return { series: a };
33
+ }, d = (e, t = 16) => {
34
+ let n = e.length > t ? e.slice(-t) : e;
35
+ try {
36
+ return BigInt(`0x${n}`).toString(10);
37
+ } catch {
38
+ return "";
39
+ }
40
+ }, f = (e, t = {}) => {
41
+ let n = typeof e.fields.traceId == "string" ? e.fields.traceId : void 0, r = typeof e.fields.spanId == "string" ? e.fields.spanId : void 0, { traceId: i, spanId: a, ...o } = e.fields;
42
+ return {
43
+ message: e.message,
44
+ status: e.level,
45
+ ddsource: "voltro",
46
+ ...t.service ? { service: t.service } : {},
47
+ ...t.tags && t.tags.length > 0 ? { ddtags: t.tags.join(",") } : {},
48
+ ...t.hostname ? { hostname: t.hostname } : {},
49
+ ...e.scope ? { scope: e.scope } : {},
50
+ ...o,
51
+ ...n ? {
52
+ trace_id: n,
53
+ "dd.trace_id": d(n)
54
+ } : {},
55
+ ...r ? { "dd.span_id": d(r) } : {}
56
+ };
57
+ }, p = (e, t = {}) => e.map((e) => f(e, t)), m = n([
58
+ {
59
+ name: "DD_API_KEY",
60
+ required: !1,
61
+ secret: !0,
62
+ description: "Datadog API key for agentless metrics + logs intake; traces/profiling work without it."
63
+ },
64
+ {
65
+ name: "DD_SITE",
66
+ required: !1,
67
+ secret: !1,
68
+ description: "Datadog site host (e.g. datadoghq.com, datadoghq.eu); defaults to datadoghq.com."
69
+ },
70
+ {
71
+ name: "DD_SERVICE",
72
+ required: !1,
73
+ secret: !1,
74
+ description: "Unified service-tagging `service` resource/tag."
75
+ },
76
+ {
77
+ name: "DD_ENV",
78
+ required: !1,
79
+ secret: !1,
80
+ description: "Unified service-tagging `env` resource/tag."
81
+ },
82
+ {
83
+ name: "DD_VERSION",
84
+ required: !1,
85
+ secret: !1,
86
+ description: "Unified service-tagging `version` resource/tag."
87
+ },
88
+ {
89
+ name: "DD_TRACE_AGENT_URL",
90
+ required: !1,
91
+ secret: !1,
92
+ description: "Datadog Agent OTLP base URL for traces; defaults to http://localhost:4318."
93
+ }
94
+ ]), h = i({ scope: "plugin:datadog" }), g = (n = {}) => {
95
+ let i = m.read("DD_API_KEY", n.apiKey), c = m.read("DD_SITE", n.site) ?? "datadoghq.com", l = n.intervalMs ?? 3e4, d = n.logsIntervalMs ?? 5e3, f = m.read("DD_SERVICE", n.service), g = m.read("DD_ENV", n.env), _ = m.read("DD_VERSION", n.version), v = m.read("DD_TRACE_AGENT_URL", n.agentUrl) ?? "http://localhost:4318", y = n.host ?? e(), b = `https://api.${c}/api/v2/series`, x = `https://http-intake.logs.${c}/api/v2/logs`, S, C, w, T = async () => {
96
+ try {
97
+ let e = await t.runPromise(s);
98
+ if (e.length === 0) return;
99
+ let r = u(e, {
100
+ timestamp: Math.floor(Date.now() / 1e3),
101
+ host: y,
102
+ ...n.prefix === void 0 ? {} : { prefix: n.prefix },
103
+ ...n.tags === void 0 ? {} : { globalTags: n.tags }
104
+ }), a = await fetch(b, {
105
+ method: "POST",
106
+ headers: {
107
+ "content-type": "application/json",
108
+ "DD-API-KEY": i
109
+ },
110
+ body: JSON.stringify(r)
111
+ });
112
+ a.ok || h.warn("datadog series POST failed", {
113
+ status: a.status,
114
+ series: r.series.length
115
+ });
116
+ } catch (e) {
117
+ h.warn("datadog metrics flush errored", { error: String(e) });
118
+ }
119
+ }, E = async (e) => {
120
+ let t = p(e, {
121
+ ...f ? { service: f } : {},
122
+ ...n.tags ? { tags: n.tags } : {}
123
+ }), r = await fetch(x, {
124
+ method: "POST",
125
+ headers: {
126
+ "content-type": "application/json",
127
+ "DD-API-KEY": i
128
+ },
129
+ body: JSON.stringify(t)
130
+ });
131
+ r.ok || h.warn("datadog logs POST failed", {
132
+ status: r.status,
133
+ count: e.length
134
+ });
135
+ }, D = () => ({
136
+ ...f ? { "service.name": f } : {},
137
+ ...g ? { "deployment.environment": g } : {},
138
+ ..._ ? { "service.version": _ } : {}
139
+ }), O = !!i, k = !!n.logs && !!i, A = (() => {
140
+ try {
141
+ return new URL(v).host;
142
+ } catch {
143
+ return "localhost";
144
+ }
145
+ })(), j = [`network:outbound:${c}`, ...n.traces || n.profiling ? [`network:outbound:${A}`] : []];
146
+ return o({
147
+ name: n.name ? `@voltro/plugin-datadog#${n.name}` : "@voltro/plugin-datadog",
148
+ description: "Deep Datadog integration — metrics + logs + traces (OTLP→Agent) + profiling, trace-correlated.",
149
+ permissions: j,
150
+ declaredEnv: m.declared,
151
+ contributeObservability: async (e) => {
152
+ if (n.profiling) try {
153
+ (await import("dd-trace")).default.init({
154
+ profiling: !0,
155
+ tracing: !1,
156
+ ...f ? { service: f } : {},
157
+ ...g ? { env: g } : {},
158
+ ..._ ? { version: _ } : {}
159
+ });
160
+ } catch {
161
+ e.logger.warn("datadog profiling requested but dd-trace is not installed");
162
+ }
163
+ let t = D();
164
+ if (!n.traces) return Object.keys(t).length > 0 ? { resourceAttributes: t } : void 0;
165
+ try {
166
+ let { BatchSpanProcessor: n } = await import("@opentelemetry/sdk-trace-base"), { OTLPTraceExporter: r } = await import("@opentelemetry/exporter-trace-otlp-http"), i = new r({ url: `${v.replace(/\/$/, "")}/v1/traces` });
167
+ return e.logger.info("datadog traces → agent OTLP", { url: `${v}/v1/traces` }), {
168
+ spanProcessors: [new n(i)],
169
+ resourceAttributes: t
170
+ };
171
+ } catch {
172
+ return e.logger.warn("datadog traces requested but @opentelemetry/sdk-trace-base + @opentelemetry/exporter-trace-otlp-http are not installed — skipping span export"), Object.keys(t).length > 0 ? { resourceAttributes: t } : void 0;
173
+ }
174
+ },
175
+ onActivate: (e) => t.sync(() => {
176
+ if (O && (S = setInterval(() => void T(), l), typeof S.unref == "function" && S.unref()), k && (C = a({
177
+ intervalMs: d,
178
+ send: E,
179
+ maxBuffer: 5e3,
180
+ onError: (e) => h.warn("datadog logs flush errored", { error: String(e) })
181
+ }), w = r((e) => C?.push(e))), !O && !n.traces && !n.profiling) {
182
+ e.logger.warn("datadog inactive — no DD_API_KEY and no traces/profiling enabled");
183
+ return;
184
+ }
185
+ e.logger.info("datadog active", {
186
+ metrics: O,
187
+ logs: k,
188
+ traces: !!n.traces,
189
+ profiling: !!n.profiling,
190
+ service: f,
191
+ env: g
192
+ });
193
+ }),
194
+ onDeactivate: () => t.promise(async () => {
195
+ S && clearInterval(S), S = void 0, w?.(), w = void 0, await C?.dispose(), C = void 0;
196
+ })
197
+ });
198
+ };
199
+ //#endregion
200
+ export { g as datadogPlugin, f as toDatadogLog, p as toDatadogLogs, u as toDatadogSeries };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@voltro/plugin-datadog",
3
+ "version": "0.1.0",
4
+ "description": "Deep Datadog integration — agentless metrics push (unified Metrics-API → /api/v2/series) + opt-in log forwarding (dd.trace_id-correlated → /api/v2/logs) + traces (framework OTel spans → Datadog Agent OTLP) + the dd-trace continuous profiler, all correlated by the same trace id. Inert without DD_API_KEY.",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "sideEffects": false,
31
+ "engines": {
32
+ "node": ">=24.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@voltro/env": "0.1.0",
36
+ "@voltro/logger": "0.1.0",
37
+ "@voltro/protocol": "0.1.0",
38
+ "@voltro/runtime": "0.1.0"
39
+ },
40
+ "optionalDependencies": {
41
+ "@opentelemetry/exporter-trace-otlp-http": "^0.220.0",
42
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
43
+ "dd-trace": "^6.2.0"
44
+ },
45
+ "peerDependencies": {
46
+ "effect": "^3.21.4"
47
+ },
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }