@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.
@@ -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
+ };