@arcote.tech/arc-otel 0.7.6
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 +246 -0
- package/dist/context-propagation.d.ts +29 -0
- package/dist/context-propagation.d.ts.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +362 -0
- package/dist/init-browser.d.ts +8 -0
- package/dist/init-browser.d.ts.map +1 -0
- package/dist/init-browser.js +326 -0
- package/dist/init-server.d.ts +8 -0
- package/dist/init-server.d.ts.map +1 -0
- package/dist/init-server.js +339 -0
- package/dist/sanitize.d.ts +39 -0
- package/dist/sanitize.d.ts.map +1 -0
- package/dist/telemetry.d.ts +99 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/wrap-db-adapter.d.ts +28 -0
- package/dist/wrap-db-adapter.d.ts.map +1 -0
- package/package.json +51 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
// src/init-browser.ts
|
|
2
|
+
import { propagation as propagation2 } from "@opentelemetry/api";
|
|
3
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
4
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
5
|
+
import { Resource } from "@opentelemetry/resources";
|
|
6
|
+
import {
|
|
7
|
+
BatchSpanProcessor,
|
|
8
|
+
ParentBasedSampler,
|
|
9
|
+
TraceIdRatioBasedSampler
|
|
10
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
11
|
+
import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
|
|
12
|
+
import {
|
|
13
|
+
ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
|
|
14
|
+
ATTR_SERVICE_NAME
|
|
15
|
+
} from "@opentelemetry/semantic-conventions/incubating";
|
|
16
|
+
|
|
17
|
+
// src/telemetry.ts
|
|
18
|
+
import {
|
|
19
|
+
context,
|
|
20
|
+
propagation,
|
|
21
|
+
SpanStatusCode,
|
|
22
|
+
trace
|
|
23
|
+
} from "@opentelemetry/api";
|
|
24
|
+
import {
|
|
25
|
+
logs,
|
|
26
|
+
SeverityNumber
|
|
27
|
+
} from "@opentelemetry/api-logs";
|
|
28
|
+
|
|
29
|
+
// src/sanitize.ts
|
|
30
|
+
var DEFAULT_REDACT_KEY_PATTERN = /(password|passwd|token|secret|authorization|jwt|api[_-]?key|cookie|email|credit[_-]?card|ssn)/i;
|
|
31
|
+
var DEFAULT_MAX_STRING_LEN = 2048;
|
|
32
|
+
var DEFAULT_MAX_JSON_LEN = 4096;
|
|
33
|
+
function sanitizeAttrs(input, opts = {}) {
|
|
34
|
+
if (!input)
|
|
35
|
+
return {};
|
|
36
|
+
const redactPattern = opts.redactKeyPattern ?? DEFAULT_REDACT_KEY_PATTERN;
|
|
37
|
+
const maxStr = opts.maxStringLen ?? DEFAULT_MAX_STRING_LEN;
|
|
38
|
+
const maxJson = opts.maxJsonLen ?? DEFAULT_MAX_JSON_LEN;
|
|
39
|
+
const out = {};
|
|
40
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
41
|
+
if (redactPattern.test(key))
|
|
42
|
+
continue;
|
|
43
|
+
const value = sanitizeValue(raw, redactPattern, maxStr, maxJson);
|
|
44
|
+
if (value !== undefined)
|
|
45
|
+
out[key] = value;
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
function sanitizeValue(raw, redactPattern, maxStr, maxJson) {
|
|
50
|
+
if (raw === null || raw === undefined)
|
|
51
|
+
return;
|
|
52
|
+
if (typeof raw === "boolean" || typeof raw === "number")
|
|
53
|
+
return raw;
|
|
54
|
+
if (typeof raw === "string") {
|
|
55
|
+
return raw.length > maxStr ? `${raw.slice(0, maxStr)}…(truncated:${raw.length})` : raw;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const filtered = filterRedacted(raw, redactPattern);
|
|
59
|
+
const json = JSON.stringify(filtered);
|
|
60
|
+
if (json === undefined)
|
|
61
|
+
return;
|
|
62
|
+
return json.length > maxJson ? `${json.slice(0, maxJson)}…(truncated:${json.length})` : json;
|
|
63
|
+
} catch {
|
|
64
|
+
return "[unserializable]";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function filterRedacted(node, pattern, seen = new WeakSet) {
|
|
68
|
+
if (node === null || typeof node !== "object")
|
|
69
|
+
return node;
|
|
70
|
+
if (seen.has(node))
|
|
71
|
+
return "[circular]";
|
|
72
|
+
seen.add(node);
|
|
73
|
+
if (Array.isArray(node)) {
|
|
74
|
+
return node.map((v) => filterRedacted(v, pattern, seen));
|
|
75
|
+
}
|
|
76
|
+
const out = {};
|
|
77
|
+
for (const [k, v] of Object.entries(node)) {
|
|
78
|
+
if (pattern.test(k))
|
|
79
|
+
continue;
|
|
80
|
+
out[k] = filterRedacted(v, pattern, seen);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
function redactConnectionString(url) {
|
|
85
|
+
if (!url)
|
|
86
|
+
return "";
|
|
87
|
+
try {
|
|
88
|
+
const u = new URL(url);
|
|
89
|
+
if (u.password)
|
|
90
|
+
u.password = "***";
|
|
91
|
+
return u.toString();
|
|
92
|
+
} catch {
|
|
93
|
+
return "[unparseable]";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/telemetry.ts
|
|
98
|
+
class ArcTelemetry {
|
|
99
|
+
config;
|
|
100
|
+
tracer = null;
|
|
101
|
+
logger = null;
|
|
102
|
+
meter = null;
|
|
103
|
+
histograms = new Map;
|
|
104
|
+
counters = new Map;
|
|
105
|
+
constructor(config) {
|
|
106
|
+
const mode = config.mode ?? "development";
|
|
107
|
+
const enabled = config.enabled ?? mode !== "disabled";
|
|
108
|
+
const sampleRate = config.sampleRate ?? (config.environment === "server" ? 1 : 0.1);
|
|
109
|
+
this.config = {
|
|
110
|
+
...config,
|
|
111
|
+
enabled,
|
|
112
|
+
sampleRate,
|
|
113
|
+
mode,
|
|
114
|
+
debug: config.debug ?? false
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
attach(opts) {
|
|
118
|
+
this.tracer = opts.tracer;
|
|
119
|
+
this.logger = opts.logger ?? null;
|
|
120
|
+
this.meter = opts.meter ?? null;
|
|
121
|
+
}
|
|
122
|
+
get active() {
|
|
123
|
+
return this.config.enabled && this.tracer !== null;
|
|
124
|
+
}
|
|
125
|
+
shouldIncludePayloads() {
|
|
126
|
+
if (this.config.includePayloads !== undefined)
|
|
127
|
+
return this.config.includePayloads;
|
|
128
|
+
return this.config.mode === "development";
|
|
129
|
+
}
|
|
130
|
+
async startSpan(name, fn, options = {}) {
|
|
131
|
+
if (!this.active || !this.tracer) {
|
|
132
|
+
return fn(trace.getActiveSpan() ?? noopSpan());
|
|
133
|
+
}
|
|
134
|
+
const attributes = this.toAttributes(options.attributes, options.unsafeAttrs);
|
|
135
|
+
return this.tracer.startActiveSpan(name, { kind: options.kind, attributes }, async (span) => {
|
|
136
|
+
try {
|
|
137
|
+
const result = await fn(span);
|
|
138
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
139
|
+
return result;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
this.recordError(span, error);
|
|
142
|
+
throw error;
|
|
143
|
+
} finally {
|
|
144
|
+
span.end();
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
createSpan(name, options = {}) {
|
|
149
|
+
if (!this.active || !this.tracer)
|
|
150
|
+
return noopSpan();
|
|
151
|
+
const attributes = this.toAttributes(options.attributes, options.unsafeAttrs);
|
|
152
|
+
return this.tracer.startSpan(name, { kind: options.kind, attributes });
|
|
153
|
+
}
|
|
154
|
+
getCurrentSpan() {
|
|
155
|
+
return trace.getActiveSpan();
|
|
156
|
+
}
|
|
157
|
+
withSpan(parent, fn) {
|
|
158
|
+
return context.with(trace.setSpan(context.active(), parent), fn);
|
|
159
|
+
}
|
|
160
|
+
runWithExtractedContext(carrier, fn) {
|
|
161
|
+
if (!this.active)
|
|
162
|
+
return fn();
|
|
163
|
+
const flat = {};
|
|
164
|
+
if (typeof Headers !== "undefined" && carrier instanceof Headers) {
|
|
165
|
+
carrier.forEach((value, key) => {
|
|
166
|
+
flat[key.toLowerCase()] = value;
|
|
167
|
+
});
|
|
168
|
+
} else if (carrier) {
|
|
169
|
+
for (const [k, v] of Object.entries(carrier)) {
|
|
170
|
+
if (typeof v === "string")
|
|
171
|
+
flat[k.toLowerCase()] = v;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
const parent = propagation.extract(context.active(), flat);
|
|
175
|
+
return context.with(parent, fn);
|
|
176
|
+
}
|
|
177
|
+
recordError(span, error) {
|
|
178
|
+
const err = error instanceof Error ? error : new Error(String(error ?? "unknown error"));
|
|
179
|
+
try {
|
|
180
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
181
|
+
span.recordException(err);
|
|
182
|
+
} catch {}
|
|
183
|
+
}
|
|
184
|
+
addAttributes(attrs, unsafeAttrs = false) {
|
|
185
|
+
const span = this.getCurrentSpan();
|
|
186
|
+
if (!span)
|
|
187
|
+
return;
|
|
188
|
+
span.setAttributes(this.toAttributes(attrs, unsafeAttrs));
|
|
189
|
+
}
|
|
190
|
+
log(level, body, attrs = {}, unsafeAttrs = false) {
|
|
191
|
+
if (!this.active)
|
|
192
|
+
return;
|
|
193
|
+
const logger = this.logger ?? logs.getLogger(this.config.serviceName);
|
|
194
|
+
const record = {
|
|
195
|
+
severityNumber: severityFor(level),
|
|
196
|
+
severityText: level.toUpperCase(),
|
|
197
|
+
body,
|
|
198
|
+
attributes: this.toAttributes(attrs, unsafeAttrs)
|
|
199
|
+
};
|
|
200
|
+
try {
|
|
201
|
+
logger.emit(record);
|
|
202
|
+
} catch {}
|
|
203
|
+
}
|
|
204
|
+
incrementCounter(name, value = 1, attrs = {}) {
|
|
205
|
+
if (!this.active || !this.meter)
|
|
206
|
+
return;
|
|
207
|
+
let counter = this.counters.get(name);
|
|
208
|
+
if (!counter) {
|
|
209
|
+
counter = this.meter.createCounter(name);
|
|
210
|
+
this.counters.set(name, counter);
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
counter.add(value, attrs);
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
recordHistogram(name, value, attrs = {}) {
|
|
217
|
+
if (!this.active || !this.meter)
|
|
218
|
+
return;
|
|
219
|
+
let histogram = this.histograms.get(name);
|
|
220
|
+
if (!histogram) {
|
|
221
|
+
histogram = this.meter.createHistogram(name, { unit: "ms" });
|
|
222
|
+
this.histograms.set(name, histogram);
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
histogram.record(value, attrs);
|
|
226
|
+
} catch {}
|
|
227
|
+
}
|
|
228
|
+
measureSince(name, start, attrs = {}) {
|
|
229
|
+
this.recordHistogram(name, Date.now() - start, attrs);
|
|
230
|
+
}
|
|
231
|
+
toAttributes(raw, unsafe) {
|
|
232
|
+
if (!raw)
|
|
233
|
+
return {};
|
|
234
|
+
if (unsafe)
|
|
235
|
+
return raw;
|
|
236
|
+
return sanitizeAttrs(raw, this.config.sanitize);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function severityFor(level) {
|
|
240
|
+
switch (level) {
|
|
241
|
+
case "debug":
|
|
242
|
+
return SeverityNumber.DEBUG;
|
|
243
|
+
case "info":
|
|
244
|
+
return SeverityNumber.INFO;
|
|
245
|
+
case "warn":
|
|
246
|
+
return SeverityNumber.WARN;
|
|
247
|
+
case "error":
|
|
248
|
+
return SeverityNumber.ERROR;
|
|
249
|
+
default:
|
|
250
|
+
return SeverityNumber.INFO;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function noopSpan() {
|
|
254
|
+
return trace.getActiveSpan() ?? trace.wrapSpanContext({
|
|
255
|
+
traceId: "00000000000000000000000000000000",
|
|
256
|
+
spanId: "0000000000000000",
|
|
257
|
+
traceFlags: 0
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// src/init-browser.ts
|
|
262
|
+
function initBrowserTelemetry(config) {
|
|
263
|
+
const telemetry = new ArcTelemetry({ ...config, environment: "client" });
|
|
264
|
+
if (!telemetry.config.enabled) {
|
|
265
|
+
return { telemetry, flush: async () => {} };
|
|
266
|
+
}
|
|
267
|
+
const endpoint = config.endpoint ?? "/otel";
|
|
268
|
+
const resource = new Resource({
|
|
269
|
+
[ATTR_SERVICE_NAME]: config.serviceName,
|
|
270
|
+
[ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: config.mode ?? "production",
|
|
271
|
+
"browser.user_agent": navigator.userAgent,
|
|
272
|
+
"session.id": getOrCreateSessionId()
|
|
273
|
+
});
|
|
274
|
+
const processor = new BatchSpanProcessor(new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }), {
|
|
275
|
+
maxQueueSize: 200,
|
|
276
|
+
maxExportBatchSize: 20,
|
|
277
|
+
scheduledDelayMillis: 5000
|
|
278
|
+
});
|
|
279
|
+
const provider = new WebTracerProvider({
|
|
280
|
+
resource,
|
|
281
|
+
sampler: new ParentBasedSampler({
|
|
282
|
+
root: new TraceIdRatioBasedSampler(telemetry.config.sampleRate)
|
|
283
|
+
}),
|
|
284
|
+
spanProcessors: [processor]
|
|
285
|
+
});
|
|
286
|
+
provider.register({
|
|
287
|
+
propagator: new W3CTraceContextPropagator
|
|
288
|
+
});
|
|
289
|
+
propagation2.setGlobalPropagator(new W3CTraceContextPropagator);
|
|
290
|
+
telemetry.attach({ tracer: provider.getTracer(config.serviceName) });
|
|
291
|
+
const flushOnExit = () => {
|
|
292
|
+
processor.forceFlush().catch(() => {});
|
|
293
|
+
};
|
|
294
|
+
window.addEventListener("pagehide", flushOnExit);
|
|
295
|
+
window.addEventListener("visibilitychange", () => {
|
|
296
|
+
if (document.visibilityState === "hidden")
|
|
297
|
+
flushOnExit();
|
|
298
|
+
});
|
|
299
|
+
if (telemetry.config.debug) {
|
|
300
|
+
console.log("[arc-otel] browser init", {
|
|
301
|
+
serviceName: config.serviceName,
|
|
302
|
+
endpoint,
|
|
303
|
+
sampleRate: telemetry.config.sampleRate
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
telemetry,
|
|
308
|
+
flush: () => processor.forceFlush()
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
function getOrCreateSessionId() {
|
|
312
|
+
try {
|
|
313
|
+
const KEY = "arc:otel-session-id";
|
|
314
|
+
let id = sessionStorage.getItem(KEY);
|
|
315
|
+
if (!id) {
|
|
316
|
+
id = crypto.randomUUID();
|
|
317
|
+
sessionStorage.setItem(KEY, id);
|
|
318
|
+
}
|
|
319
|
+
return id;
|
|
320
|
+
} catch {
|
|
321
|
+
return "no-storage";
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
export {
|
|
325
|
+
initBrowserTelemetry
|
|
326
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { ArcTelemetry, type TelemetryConfig } from "./telemetry";
|
|
2
|
+
export interface ServerInitResult {
|
|
3
|
+
telemetry: ArcTelemetry;
|
|
4
|
+
/** Flush + shut down all exporters. Call on SIGTERM/SIGINT. Resolves when done. */
|
|
5
|
+
shutdown: () => Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
export declare function initServerTelemetry(config: TelemetryConfig): ServerInitResult;
|
|
8
|
+
//# sourceMappingURL=init-server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init-server.d.ts","sourceRoot":"","sources":["../src/init-server.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,YAAY,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAC;AAUjE,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,YAAY,CAAC;IACxB,mFAAmF;IACnF,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,CA2F7E"}
|
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
// src/init-server.ts
|
|
2
|
+
import { OTLPLogExporter } from "@opentelemetry/exporter-logs-otlp-http";
|
|
3
|
+
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
4
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
5
|
+
import { Resource } from "@opentelemetry/resources";
|
|
6
|
+
import {
|
|
7
|
+
BatchLogRecordProcessor,
|
|
8
|
+
LoggerProvider
|
|
9
|
+
} from "@opentelemetry/sdk-logs";
|
|
10
|
+
import {
|
|
11
|
+
MeterProvider,
|
|
12
|
+
PeriodicExportingMetricReader
|
|
13
|
+
} from "@opentelemetry/sdk-metrics";
|
|
14
|
+
import {
|
|
15
|
+
BatchSpanProcessor,
|
|
16
|
+
ParentBasedSampler,
|
|
17
|
+
TraceIdRatioBasedSampler
|
|
18
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
19
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
20
|
+
import {
|
|
21
|
+
ATTR_DEPLOYMENT_ENVIRONMENT_NAME,
|
|
22
|
+
ATTR_SERVICE_NAME,
|
|
23
|
+
ATTR_SERVICE_VERSION
|
|
24
|
+
} from "@opentelemetry/semantic-conventions/incubating";
|
|
25
|
+
|
|
26
|
+
// src/telemetry.ts
|
|
27
|
+
import {
|
|
28
|
+
context,
|
|
29
|
+
propagation,
|
|
30
|
+
SpanStatusCode,
|
|
31
|
+
trace
|
|
32
|
+
} from "@opentelemetry/api";
|
|
33
|
+
import {
|
|
34
|
+
logs,
|
|
35
|
+
SeverityNumber
|
|
36
|
+
} from "@opentelemetry/api-logs";
|
|
37
|
+
|
|
38
|
+
// src/sanitize.ts
|
|
39
|
+
var DEFAULT_REDACT_KEY_PATTERN = /(password|passwd|token|secret|authorization|jwt|api[_-]?key|cookie|email|credit[_-]?card|ssn)/i;
|
|
40
|
+
var DEFAULT_MAX_STRING_LEN = 2048;
|
|
41
|
+
var DEFAULT_MAX_JSON_LEN = 4096;
|
|
42
|
+
function sanitizeAttrs(input, opts = {}) {
|
|
43
|
+
if (!input)
|
|
44
|
+
return {};
|
|
45
|
+
const redactPattern = opts.redactKeyPattern ?? DEFAULT_REDACT_KEY_PATTERN;
|
|
46
|
+
const maxStr = opts.maxStringLen ?? DEFAULT_MAX_STRING_LEN;
|
|
47
|
+
const maxJson = opts.maxJsonLen ?? DEFAULT_MAX_JSON_LEN;
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const [key, raw] of Object.entries(input)) {
|
|
50
|
+
if (redactPattern.test(key))
|
|
51
|
+
continue;
|
|
52
|
+
const value = sanitizeValue(raw, redactPattern, maxStr, maxJson);
|
|
53
|
+
if (value !== undefined)
|
|
54
|
+
out[key] = value;
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
function sanitizeValue(raw, redactPattern, maxStr, maxJson) {
|
|
59
|
+
if (raw === null || raw === undefined)
|
|
60
|
+
return;
|
|
61
|
+
if (typeof raw === "boolean" || typeof raw === "number")
|
|
62
|
+
return raw;
|
|
63
|
+
if (typeof raw === "string") {
|
|
64
|
+
return raw.length > maxStr ? `${raw.slice(0, maxStr)}…(truncated:${raw.length})` : raw;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const filtered = filterRedacted(raw, redactPattern);
|
|
68
|
+
const json = JSON.stringify(filtered);
|
|
69
|
+
if (json === undefined)
|
|
70
|
+
return;
|
|
71
|
+
return json.length > maxJson ? `${json.slice(0, maxJson)}…(truncated:${json.length})` : json;
|
|
72
|
+
} catch {
|
|
73
|
+
return "[unserializable]";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function filterRedacted(node, pattern, seen = new WeakSet) {
|
|
77
|
+
if (node === null || typeof node !== "object")
|
|
78
|
+
return node;
|
|
79
|
+
if (seen.has(node))
|
|
80
|
+
return "[circular]";
|
|
81
|
+
seen.add(node);
|
|
82
|
+
if (Array.isArray(node)) {
|
|
83
|
+
return node.map((v) => filterRedacted(v, pattern, seen));
|
|
84
|
+
}
|
|
85
|
+
const out = {};
|
|
86
|
+
for (const [k, v] of Object.entries(node)) {
|
|
87
|
+
if (pattern.test(k))
|
|
88
|
+
continue;
|
|
89
|
+
out[k] = filterRedacted(v, pattern, seen);
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
function redactConnectionString(url) {
|
|
94
|
+
if (!url)
|
|
95
|
+
return "";
|
|
96
|
+
try {
|
|
97
|
+
const u = new URL(url);
|
|
98
|
+
if (u.password)
|
|
99
|
+
u.password = "***";
|
|
100
|
+
return u.toString();
|
|
101
|
+
} catch {
|
|
102
|
+
return "[unparseable]";
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// src/telemetry.ts
|
|
107
|
+
class ArcTelemetry {
|
|
108
|
+
config;
|
|
109
|
+
tracer = null;
|
|
110
|
+
logger = null;
|
|
111
|
+
meter = null;
|
|
112
|
+
histograms = new Map;
|
|
113
|
+
counters = new Map;
|
|
114
|
+
constructor(config) {
|
|
115
|
+
const mode = config.mode ?? "development";
|
|
116
|
+
const enabled = config.enabled ?? mode !== "disabled";
|
|
117
|
+
const sampleRate = config.sampleRate ?? (config.environment === "server" ? 1 : 0.1);
|
|
118
|
+
this.config = {
|
|
119
|
+
...config,
|
|
120
|
+
enabled,
|
|
121
|
+
sampleRate,
|
|
122
|
+
mode,
|
|
123
|
+
debug: config.debug ?? false
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
attach(opts) {
|
|
127
|
+
this.tracer = opts.tracer;
|
|
128
|
+
this.logger = opts.logger ?? null;
|
|
129
|
+
this.meter = opts.meter ?? null;
|
|
130
|
+
}
|
|
131
|
+
get active() {
|
|
132
|
+
return this.config.enabled && this.tracer !== null;
|
|
133
|
+
}
|
|
134
|
+
shouldIncludePayloads() {
|
|
135
|
+
if (this.config.includePayloads !== undefined)
|
|
136
|
+
return this.config.includePayloads;
|
|
137
|
+
return this.config.mode === "development";
|
|
138
|
+
}
|
|
139
|
+
async startSpan(name, fn, options = {}) {
|
|
140
|
+
if (!this.active || !this.tracer) {
|
|
141
|
+
return fn(trace.getActiveSpan() ?? noopSpan());
|
|
142
|
+
}
|
|
143
|
+
const attributes = this.toAttributes(options.attributes, options.unsafeAttrs);
|
|
144
|
+
return this.tracer.startActiveSpan(name, { kind: options.kind, attributes }, async (span) => {
|
|
145
|
+
try {
|
|
146
|
+
const result = await fn(span);
|
|
147
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
148
|
+
return result;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
this.recordError(span, error);
|
|
151
|
+
throw error;
|
|
152
|
+
} finally {
|
|
153
|
+
span.end();
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
createSpan(name, options = {}) {
|
|
158
|
+
if (!this.active || !this.tracer)
|
|
159
|
+
return noopSpan();
|
|
160
|
+
const attributes = this.toAttributes(options.attributes, options.unsafeAttrs);
|
|
161
|
+
return this.tracer.startSpan(name, { kind: options.kind, attributes });
|
|
162
|
+
}
|
|
163
|
+
getCurrentSpan() {
|
|
164
|
+
return trace.getActiveSpan();
|
|
165
|
+
}
|
|
166
|
+
withSpan(parent, fn) {
|
|
167
|
+
return context.with(trace.setSpan(context.active(), parent), fn);
|
|
168
|
+
}
|
|
169
|
+
runWithExtractedContext(carrier, fn) {
|
|
170
|
+
if (!this.active)
|
|
171
|
+
return fn();
|
|
172
|
+
const flat = {};
|
|
173
|
+
if (typeof Headers !== "undefined" && carrier instanceof Headers) {
|
|
174
|
+
carrier.forEach((value, key) => {
|
|
175
|
+
flat[key.toLowerCase()] = value;
|
|
176
|
+
});
|
|
177
|
+
} else if (carrier) {
|
|
178
|
+
for (const [k, v] of Object.entries(carrier)) {
|
|
179
|
+
if (typeof v === "string")
|
|
180
|
+
flat[k.toLowerCase()] = v;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const parent = propagation.extract(context.active(), flat);
|
|
184
|
+
return context.with(parent, fn);
|
|
185
|
+
}
|
|
186
|
+
recordError(span, error) {
|
|
187
|
+
const err = error instanceof Error ? error : new Error(String(error ?? "unknown error"));
|
|
188
|
+
try {
|
|
189
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
|
|
190
|
+
span.recordException(err);
|
|
191
|
+
} catch {}
|
|
192
|
+
}
|
|
193
|
+
addAttributes(attrs, unsafeAttrs = false) {
|
|
194
|
+
const span = this.getCurrentSpan();
|
|
195
|
+
if (!span)
|
|
196
|
+
return;
|
|
197
|
+
span.setAttributes(this.toAttributes(attrs, unsafeAttrs));
|
|
198
|
+
}
|
|
199
|
+
log(level, body, attrs = {}, unsafeAttrs = false) {
|
|
200
|
+
if (!this.active)
|
|
201
|
+
return;
|
|
202
|
+
const logger = this.logger ?? logs.getLogger(this.config.serviceName);
|
|
203
|
+
const record = {
|
|
204
|
+
severityNumber: severityFor(level),
|
|
205
|
+
severityText: level.toUpperCase(),
|
|
206
|
+
body,
|
|
207
|
+
attributes: this.toAttributes(attrs, unsafeAttrs)
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
logger.emit(record);
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
incrementCounter(name, value = 1, attrs = {}) {
|
|
214
|
+
if (!this.active || !this.meter)
|
|
215
|
+
return;
|
|
216
|
+
let counter = this.counters.get(name);
|
|
217
|
+
if (!counter) {
|
|
218
|
+
counter = this.meter.createCounter(name);
|
|
219
|
+
this.counters.set(name, counter);
|
|
220
|
+
}
|
|
221
|
+
try {
|
|
222
|
+
counter.add(value, attrs);
|
|
223
|
+
} catch {}
|
|
224
|
+
}
|
|
225
|
+
recordHistogram(name, value, attrs = {}) {
|
|
226
|
+
if (!this.active || !this.meter)
|
|
227
|
+
return;
|
|
228
|
+
let histogram = this.histograms.get(name);
|
|
229
|
+
if (!histogram) {
|
|
230
|
+
histogram = this.meter.createHistogram(name, { unit: "ms" });
|
|
231
|
+
this.histograms.set(name, histogram);
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
histogram.record(value, attrs);
|
|
235
|
+
} catch {}
|
|
236
|
+
}
|
|
237
|
+
measureSince(name, start, attrs = {}) {
|
|
238
|
+
this.recordHistogram(name, Date.now() - start, attrs);
|
|
239
|
+
}
|
|
240
|
+
toAttributes(raw, unsafe) {
|
|
241
|
+
if (!raw)
|
|
242
|
+
return {};
|
|
243
|
+
if (unsafe)
|
|
244
|
+
return raw;
|
|
245
|
+
return sanitizeAttrs(raw, this.config.sanitize);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function severityFor(level) {
|
|
249
|
+
switch (level) {
|
|
250
|
+
case "debug":
|
|
251
|
+
return SeverityNumber.DEBUG;
|
|
252
|
+
case "info":
|
|
253
|
+
return SeverityNumber.INFO;
|
|
254
|
+
case "warn":
|
|
255
|
+
return SeverityNumber.WARN;
|
|
256
|
+
case "error":
|
|
257
|
+
return SeverityNumber.ERROR;
|
|
258
|
+
default:
|
|
259
|
+
return SeverityNumber.INFO;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function noopSpan() {
|
|
263
|
+
return trace.getActiveSpan() ?? trace.wrapSpanContext({
|
|
264
|
+
traceId: "00000000000000000000000000000000",
|
|
265
|
+
spanId: "0000000000000000",
|
|
266
|
+
traceFlags: 0
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/init-server.ts
|
|
271
|
+
function initServerTelemetry(config) {
|
|
272
|
+
const telemetry = new ArcTelemetry({ ...config, environment: "server" });
|
|
273
|
+
if (!telemetry.active && !config.enabled) {
|
|
274
|
+
return { telemetry, shutdown: async () => {} };
|
|
275
|
+
}
|
|
276
|
+
const endpoint = config.endpoint ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT ?? "http://localhost:4318";
|
|
277
|
+
const resource = new Resource({
|
|
278
|
+
[ATTR_SERVICE_NAME]: config.serviceName,
|
|
279
|
+
[ATTR_SERVICE_VERSION]: process.env.ARC_VERSION ?? "unknown",
|
|
280
|
+
[ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: "development"
|
|
281
|
+
});
|
|
282
|
+
const tracerProvider = new NodeTracerProvider({
|
|
283
|
+
resource,
|
|
284
|
+
sampler: new ParentBasedSampler({
|
|
285
|
+
root: new TraceIdRatioBasedSampler(telemetry.config.sampleRate)
|
|
286
|
+
}),
|
|
287
|
+
spanProcessors: [
|
|
288
|
+
new BatchSpanProcessor(new OTLPTraceExporter({ url: `${endpoint}/v1/traces` }), {
|
|
289
|
+
maxQueueSize: 1000,
|
|
290
|
+
maxExportBatchSize: 50,
|
|
291
|
+
scheduledDelayMillis: 5000
|
|
292
|
+
})
|
|
293
|
+
]
|
|
294
|
+
});
|
|
295
|
+
tracerProvider.register();
|
|
296
|
+
const loggerProvider = new LoggerProvider({ resource });
|
|
297
|
+
loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(new OTLPLogExporter({ url: `${endpoint}/v1/logs` }), {
|
|
298
|
+
maxQueueSize: 1000,
|
|
299
|
+
maxExportBatchSize: 50,
|
|
300
|
+
scheduledDelayMillis: 5000
|
|
301
|
+
}));
|
|
302
|
+
const meterProvider = new MeterProvider({
|
|
303
|
+
resource,
|
|
304
|
+
readers: [
|
|
305
|
+
new PeriodicExportingMetricReader({
|
|
306
|
+
exporter: new OTLPMetricExporter({ url: `${endpoint}/v1/metrics` }),
|
|
307
|
+
exportIntervalMillis: 15000
|
|
308
|
+
})
|
|
309
|
+
]
|
|
310
|
+
});
|
|
311
|
+
telemetry.attach({
|
|
312
|
+
tracer: tracerProvider.getTracer(config.serviceName),
|
|
313
|
+
logger: loggerProvider.getLogger(config.serviceName),
|
|
314
|
+
meter: meterProvider.getMeter(config.serviceName)
|
|
315
|
+
});
|
|
316
|
+
if (telemetry.config.debug) {
|
|
317
|
+
console.log("[arc-otel] server init", {
|
|
318
|
+
serviceName: config.serviceName,
|
|
319
|
+
endpoint,
|
|
320
|
+
sampleRate: telemetry.config.sampleRate,
|
|
321
|
+
mode: telemetry.config.mode
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
const shutdown = async () => {
|
|
325
|
+
try {
|
|
326
|
+
await Promise.all([
|
|
327
|
+
tracerProvider.shutdown(),
|
|
328
|
+
loggerProvider.shutdown(),
|
|
329
|
+
meterProvider.shutdown()
|
|
330
|
+
]);
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.error("[arc-otel] shutdown error", err);
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
return { telemetry, shutdown };
|
|
336
|
+
}
|
|
337
|
+
export {
|
|
338
|
+
initServerTelemetry
|
|
339
|
+
};
|