@abbacchio/transport 0.1.4 → 0.2.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/src/client.ts CHANGED
@@ -1,216 +1,574 @@
1
1
  import { encrypt } from "./encrypt.js";
2
2
 
3
+ /**
4
+ * OTLP severity numbers per OpenTelemetry spec.
5
+ * https://opentelemetry.io/docs/specs/otel/logs/data-model/#severity-fields
6
+ */
7
+ const PINO_TO_OTLP_SEVERITY: Record<number, { number: number; text: string }> = {
8
+ 10: { number: 1, text: 'TRACE' },
9
+ 20: { number: 5, text: 'DEBUG' },
10
+ 30: { number: 9, text: 'INFO' },
11
+ 40: { number: 13, text: 'WARN' },
12
+ 50: { number: 17, text: 'ERROR' },
13
+ 60: { number: 21, text: 'FATAL' },
14
+ };
15
+
16
+ /**
17
+ * Convert a flat Record to OTLP KeyValue[] attributes.
18
+ */
19
+ function toOtlpAttributes(obj: Record<string, unknown>): OtlpKeyValue[] {
20
+ const attrs: OtlpKeyValue[] = [];
21
+ for (const [key, val] of Object.entries(obj)) {
22
+ if (val === undefined || val === null) continue;
23
+ attrs.push({ key, value: toOtlpAnyValue(val) });
24
+ }
25
+ return attrs;
26
+ }
27
+
28
+ function toOtlpAnyValue(val: unknown): OtlpAnyValue {
29
+ if (typeof val === 'string') return { stringValue: val };
30
+ if (typeof val === 'number') {
31
+ return Number.isInteger(val) ? { intValue: val } : { doubleValue: val };
32
+ }
33
+ if (typeof val === 'boolean') return { boolValue: val };
34
+ return { stringValue: String(val) };
35
+ }
36
+
37
+ interface OtlpAnyValue {
38
+ stringValue?: string;
39
+ intValue?: number;
40
+ doubleValue?: number;
41
+ boolValue?: boolean;
42
+ }
43
+
44
+ interface OtlpKeyValue {
45
+ key: string;
46
+ value: OtlpAnyValue;
47
+ }
48
+
3
49
  export interface AbbacchioClientOptions {
4
- /** Server URL endpoint */
50
+ /** Base URL of the OTLP server (e.g. "http://localhost:4002"). Logs go to {endpoint}/v1/logs */
51
+ endpoint?: string;
52
+ /** @deprecated Use `endpoint` instead. If set, used as the full URL for log ingestion. */
5
53
  url?: string;
6
- /** Secret key for encryption. If provided, logs will be encrypted before sending */
54
+ /** Secret key for encryption. If provided, payloads will be encrypted before sending */
7
55
  secretKey?: string;
8
- /** Channel/app name for multi-app support. Defaults to 'default' */
9
- channel?: string;
10
- /** Default namespace for all logs. Per-log namespace/name fields take precedence */
11
- namespace?: string;
12
- /** Number of logs to batch before sending. Defaults to 10 */
56
+ /** Service name maps to OTLP resource attribute `service.name`. Defaults to 'default' */
57
+ serviceName?: string;
58
+ /** Additional OTLP resource attributes */
59
+ resourceAttributes?: Record<string, string>;
60
+ /** Number of log records to batch before sending. Defaults to 10 */
13
61
  batchSize?: number;
14
62
  /** Interval in ms between flushes. Defaults to 1000 */
15
63
  interval?: number;
16
64
  /** Additional headers to send with requests */
17
65
  headers?: Record<string, string>;
18
- /** Whether to send logs to the server. Defaults to true */
66
+ /** Whether to send data to the server. Defaults to true */
19
67
  enabled?: boolean;
20
68
  }
21
69
 
22
70
  /**
23
- * Shared HTTP client for all Abbacchio transports.
24
- * Handles batching, encryption, and HTTP communication.
71
+ * A log entry in the format the transports produce before OTLP conversion.
72
+ */
73
+ export interface LogRecord {
74
+ level?: number;
75
+ msg?: string;
76
+ message?: string;
77
+ time?: number;
78
+ [key: string]: unknown;
79
+ }
80
+
81
+ /**
82
+ * A metric data point to send via OTLP.
83
+ */
84
+ export interface MetricRecord {
85
+ name: string;
86
+ description?: string;
87
+ unit?: string;
88
+ type: 'sum' | 'gauge';
89
+ value: number;
90
+ attributes?: Record<string, unknown>;
91
+ isMonotonic?: boolean;
92
+ }
93
+
94
+ /**
95
+ * A histogram data point to send via OTLP.
96
+ *
97
+ * Each record is a single observation. The client accumulates observations
98
+ * into OTLP Histogram data points (with explicit bucket boundaries) on flush.
99
+ */
100
+ export interface HistogramRecord {
101
+ name: string;
102
+ description?: string;
103
+ unit?: string;
104
+ /** The observed value (e.g. request duration in ms) */
105
+ value: number;
106
+ attributes?: Record<string, unknown>;
107
+ /**
108
+ * Explicit bucket boundaries. Defaults to a standard latency distribution:
109
+ * [5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 10000]
110
+ */
111
+ explicitBounds?: number[];
112
+ }
113
+
114
+ /**
115
+ * A trace span to send via OTLP.
116
+ */
117
+ export interface SpanRecord {
118
+ traceId: string;
119
+ spanId: string;
120
+ parentSpanId?: string;
121
+ name: string;
122
+ kind?: number;
123
+ startTimeUnixNano: string;
124
+ endTimeUnixNano: string;
125
+ attributes?: Record<string, unknown>;
126
+ status?: { code: number; message?: string };
127
+ }
128
+
129
+ /**
130
+ * OTLP-native HTTP client for Abbacchio.
131
+ * Sends logs to POST /v1/logs, metrics to POST /v1/metrics, traces to POST /v1/traces.
25
132
  */
26
133
  export class AbbacchioClient {
27
- private url: string;
134
+ private endpoint: string;
28
135
  private secretKey?: string;
29
- private channel?: string;
30
- private namespace?: string;
136
+ private serviceName: string;
137
+ private resourceAttributes: Record<string, string>;
31
138
  private batchSize: number;
32
139
  private interval: number;
33
140
  private headers: Record<string, string>;
34
141
  private enabled: boolean;
35
142
 
36
- private buffer: unknown[] = [];
37
- private timer: ReturnType<typeof setTimeout> | null = null;
143
+ private logBuffer: LogRecord[] = [];
144
+ private metricBuffer: MetricRecord[] = [];
145
+ private histogramBuffer: HistogramRecord[] = [];
146
+ private spanBuffer: SpanRecord[] = [];
147
+ private logTimer: ReturnType<typeof setTimeout> | null = null;
148
+ private metricTimer: ReturnType<typeof setTimeout> | null = null;
149
+ private histogramTimer: ReturnType<typeof setTimeout> | null = null;
150
+ private spanTimer: ReturnType<typeof setTimeout> | null = null;
38
151
 
39
152
  constructor(options: AbbacchioClientOptions = {}) {
40
- this.url = options.url || "http://localhost:4000/api/logs";
153
+ // Support legacy `url` option: if it looks like a full URL with path, strip the path
154
+ if (options.url && !options.endpoint) {
155
+ try {
156
+ const u = new URL(options.url);
157
+ this.endpoint = `${u.protocol}//${u.host}`;
158
+ } catch {
159
+ this.endpoint = options.url;
160
+ }
161
+ } else {
162
+ this.endpoint = options.endpoint || 'http://localhost:4002';
163
+ }
41
164
  this.secretKey = options.secretKey;
42
- this.channel = options.channel;
43
- this.namespace = options.namespace;
165
+ this.serviceName = options.serviceName || 'default';
166
+ this.resourceAttributes = options.resourceAttributes || {};
44
167
  this.batchSize = options.batchSize || 10;
45
168
  this.interval = options.interval || 1000;
46
169
  this.headers = options.headers || {};
47
170
  this.enabled = options.enabled ?? true;
48
171
  }
49
172
 
50
- /**
51
- * Change the channel dynamically after initialization
52
- */
53
- setChannel(channel: string | undefined): void {
54
- this.channel = channel;
173
+ /** Change the service name dynamically */
174
+ setServiceName(name: string): void {
175
+ this.serviceName = name;
55
176
  }
56
177
 
57
- /**
58
- * Get the current channel
59
- */
60
- getChannel(): string | undefined {
61
- return this.channel;
178
+ /** Get the current service name */
179
+ getServiceName(): string {
180
+ return this.serviceName;
62
181
  }
63
182
 
64
- /**
65
- * Change the namespace dynamically after initialization
66
- */
67
- setNamespace(namespace: string | undefined): void {
68
- this.namespace = namespace;
69
- }
70
-
71
- /**
72
- * Get the current namespace
73
- */
74
- getNamespace(): string | undefined {
75
- return this.namespace;
76
- }
77
-
78
- /**
79
- * Enable sending logs to the server
80
- */
183
+ /** Enable sending data to the server */
81
184
  enable(): void {
82
185
  this.enabled = true;
83
186
  }
84
187
 
85
- /**
86
- * Disable sending logs to the server. Logs will be silently dropped.
87
- */
188
+ /** Disable sending data to the server. Data will be silently dropped. */
88
189
  disable(): void {
89
190
  this.enabled = false;
90
191
  }
91
192
 
92
- /**
93
- * Check if the client is currently enabled
94
- */
193
+ /** Check if the client is currently enabled */
95
194
  isEnabled(): boolean {
96
195
  return this.enabled;
97
196
  }
98
197
 
99
- /**
100
- * Process a log entry: inject default namespace, then encrypt if needed
101
- */
102
- private processLog(log: unknown): unknown {
103
- let processed = log;
198
+ // ─── Logs ──────────────────────────────────────────
104
199
 
105
- if (this.namespace && typeof processed === 'object' && processed !== null) {
106
- const obj = processed as Record<string, unknown>;
107
- if (!obj.namespace && !obj.name) {
108
- processed = { ...obj, namespace: this.namespace };
109
- }
200
+ /** Add a log record to the buffer */
201
+ add(log: LogRecord | unknown): void {
202
+ if (!this.enabled) return;
203
+ this.logBuffer.push(log as LogRecord);
204
+
205
+ if (this.logBuffer.length >= this.batchSize) {
206
+ this.flushLogs();
207
+ } else {
208
+ this.scheduleLogFlush();
110
209
  }
210
+ }
111
211
 
112
- if (this.secretKey) {
113
- return { encrypted: encrypt(JSON.stringify(processed), this.secretKey) };
212
+ /** Add multiple log records at once */
213
+ addBatch(logs: (LogRecord | unknown)[]): void {
214
+ if (!this.enabled) return;
215
+ for (const log of logs) {
216
+ this.logBuffer.push(log as LogRecord);
217
+ }
218
+ if (this.logBuffer.length >= this.batchSize) {
219
+ this.flushLogs();
220
+ } else {
221
+ this.scheduleLogFlush();
114
222
  }
115
- return processed;
116
223
  }
117
224
 
118
- /**
119
- * Add a log to the buffer and trigger send if needed
120
- */
121
- add(log: unknown): void {
225
+ /** Send logs immediately without batching */
226
+ async send(logs: (LogRecord | unknown)[]): Promise<void> {
227
+ if (!this.enabled) return;
228
+ await this.sendLogs(logs as LogRecord[]);
229
+ }
230
+
231
+ private scheduleLogFlush(): void {
232
+ if (this.logTimer) return;
233
+ this.logTimer = setTimeout(() => {
234
+ this.logTimer = null;
235
+ this.flushLogs();
236
+ }, this.interval);
237
+ }
238
+
239
+ async flushLogs(): Promise<void> {
240
+ if (this.logBuffer.length === 0) return;
241
+ const batch = this.logBuffer;
242
+ this.logBuffer = [];
243
+ await this.sendLogs(batch);
244
+ }
245
+
246
+ private async sendLogs(logs: LogRecord[]): Promise<void> {
247
+ const logRecords = logs.map((log) => {
248
+ const { level, msg, message, time, ...extra } = log;
249
+ const pinoLevel = typeof level === 'number' ? level : 30;
250
+ const severity = PINO_TO_OTLP_SEVERITY[pinoLevel] || PINO_TO_OTLP_SEVERITY[30];
251
+ const body = msg || message || '';
252
+ const timeNano = String((time || Date.now()) * 1_000_000);
253
+
254
+ return {
255
+ timeUnixNano: timeNano,
256
+ observedTimeUnixNano: timeNano,
257
+ severityNumber: severity.number,
258
+ severityText: severity.text,
259
+ body: { stringValue: String(body) },
260
+ attributes: toOtlpAttributes(extra as Record<string, unknown>),
261
+ };
262
+ });
263
+
264
+ const payload = {
265
+ resourceLogs: [{
266
+ resource: { attributes: this.buildResourceAttributes() },
267
+ scopeLogs: [{
268
+ scope: { name: '@abbacchio/transport' },
269
+ logRecords,
270
+ }],
271
+ }],
272
+ };
273
+
274
+ await this.post('/v1/logs', payload);
275
+ }
276
+
277
+ // ─── Metrics ───────────────────────────────────────
278
+
279
+ /** Add a metric data point to the buffer */
280
+ addMetric(metric: MetricRecord): void {
122
281
  if (!this.enabled) return;
123
- this.buffer.push(this.processLog(log));
282
+ this.metricBuffer.push(metric);
124
283
 
125
- if (this.buffer.length >= this.batchSize) {
126
- this.flush();
284
+ if (this.metricBuffer.length >= this.batchSize) {
285
+ this.flushMetrics();
127
286
  } else {
128
- this.scheduleSend();
287
+ this.scheduleMetricFlush();
129
288
  }
130
289
  }
131
290
 
132
- /**
133
- * Add multiple logs at once
134
- */
135
- addBatch(logs: unknown[]): void {
136
- if (!this.enabled) return;
137
- for (const log of logs) {
138
- this.buffer.push(this.processLog(log));
291
+ private scheduleMetricFlush(): void {
292
+ if (this.metricTimer) return;
293
+ this.metricTimer = setTimeout(() => {
294
+ this.metricTimer = null;
295
+ this.flushMetrics();
296
+ }, this.interval);
297
+ }
298
+
299
+ async flushMetrics(): Promise<void> {
300
+ if (this.metricBuffer.length === 0) return;
301
+ const batch = this.metricBuffer;
302
+ this.metricBuffer = [];
303
+ await this.sendMetrics(batch);
304
+ }
305
+
306
+ private async sendMetrics(metrics: MetricRecord[]): Promise<void> {
307
+ // Group by metric name to build proper OTLP structure
308
+ const byName = new Map<string, MetricRecord[]>();
309
+ for (const m of metrics) {
310
+ const arr = byName.get(m.name) || [];
311
+ arr.push(m);
312
+ byName.set(m.name, arr);
139
313
  }
140
314
 
141
- if (this.buffer.length >= this.batchSize) {
142
- this.flush();
143
- } else {
144
- this.scheduleSend();
315
+ const otlpMetrics: unknown[] = [];
316
+ for (const [name, records] of byName) {
317
+ const first = records[0];
318
+ const dataPoints = records.map((r) => ({
319
+ timeUnixNano: String(Date.now() * 1_000_000),
320
+ asDouble: r.value,
321
+ attributes: r.attributes ? toOtlpAttributes(r.attributes) : [],
322
+ }));
323
+
324
+ const metric: Record<string, unknown> = {
325
+ name,
326
+ description: first.description || '',
327
+ unit: first.unit || '',
328
+ };
329
+
330
+ if (first.type === 'sum') {
331
+ metric.sum = {
332
+ dataPoints,
333
+ aggregationTemporality: 2, // CUMULATIVE
334
+ isMonotonic: first.isMonotonic ?? true,
335
+ };
336
+ } else {
337
+ metric.gauge = { dataPoints };
338
+ }
339
+
340
+ otlpMetrics.push(metric);
145
341
  }
342
+
343
+ const payload = {
344
+ resourceMetrics: [{
345
+ resource: { attributes: this.buildResourceAttributes() },
346
+ scopeMetrics: [{
347
+ scope: { name: '@abbacchio/transport' },
348
+ metrics: otlpMetrics,
349
+ }],
350
+ }],
351
+ };
352
+
353
+ await this.post('/v1/metrics', payload);
146
354
  }
147
355
 
148
- /**
149
- * Send logs immediately without batching
150
- */
151
- async send(logs: unknown[]): Promise<void> {
356
+ // ─── Histograms ─────────────────────────────────────
357
+
358
+ private static DEFAULT_BOUNDS = [5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 10000];
359
+
360
+ /** Add a histogram observation to the buffer */
361
+ addHistogram(record: HistogramRecord): void {
152
362
  if (!this.enabled) return;
153
- const processedLogs = logs.map(log => this.processLog(log));
154
- await this.sendToServer(processedLogs);
363
+ this.histogramBuffer.push(record);
364
+
365
+ if (this.histogramBuffer.length >= this.batchSize) {
366
+ this.flushHistograms();
367
+ } else {
368
+ this.scheduleHistogramFlush();
369
+ }
155
370
  }
156
371
 
157
- /**
158
- * Schedule a send after the interval
159
- */
160
- private scheduleSend(): void {
161
- if (this.timer) return;
162
- this.timer = setTimeout(() => {
163
- this.timer = null;
164
- this.flush();
372
+ private scheduleHistogramFlush(): void {
373
+ if (this.histogramTimer) return;
374
+ this.histogramTimer = setTimeout(() => {
375
+ this.histogramTimer = null;
376
+ this.flushHistograms();
165
377
  }, this.interval);
166
378
  }
167
379
 
168
- /**
169
- * Flush the buffer and send to server
170
- */
171
- async flush(): Promise<void> {
172
- if (this.buffer.length === 0) return;
380
+ async flushHistograms(): Promise<void> {
381
+ if (this.histogramBuffer.length === 0) return;
382
+ const batch = this.histogramBuffer;
383
+ this.histogramBuffer = [];
384
+ await this.sendHistograms(batch);
385
+ }
173
386
 
174
- const toSend = this.buffer;
175
- this.buffer = [];
387
+ private async sendHistograms(records: HistogramRecord[]): Promise<void> {
388
+ // Group by name+attributes key to aggregate observations into OTLP histogram data points
389
+ const groups = new Map<string, { records: HistogramRecord[]; bounds: number[] }>();
390
+ for (const r of records) {
391
+ const key = JSON.stringify({ name: r.name, attributes: r.attributes });
392
+ let group = groups.get(key);
393
+ if (!group) {
394
+ group = { records: [], bounds: r.explicitBounds || AbbacchioClient.DEFAULT_BOUNDS };
395
+ groups.set(key, group);
396
+ }
397
+ group.records.push(r);
398
+ }
176
399
 
177
- await this.sendToServer(toSend);
178
- }
400
+ const otlpMetrics: unknown[] = [];
401
+ for (const [, group] of groups) {
402
+ const first = group.records[0];
403
+ const bounds = group.bounds;
404
+ const bucketCounts = new Array(bounds.length + 1).fill(0);
405
+ let sum = 0;
406
+ let min = Infinity;
407
+ let max = -Infinity;
179
408
 
180
- /**
181
- * Send logs to the Abbacchio server
182
- */
183
- private async sendToServer(logs: unknown[]): Promise<void> {
184
- try {
185
- await fetch(this.url, {
186
- method: "POST",
187
- headers: {
188
- "Content-Type": "application/json",
189
- "X-Encrypted": this.secretKey ? "true" : "false",
190
- ...(this.channel ? { "X-Channel": this.channel } : {}),
191
- ...this.headers,
409
+ for (const r of group.records) {
410
+ sum += r.value;
411
+ if (r.value < min) min = r.value;
412
+ if (r.value > max) max = r.value;
413
+
414
+ // Find the bucket
415
+ let placed = false;
416
+ for (let i = 0; i < bounds.length; i++) {
417
+ if (r.value <= bounds[i]) {
418
+ bucketCounts[i]++;
419
+ placed = true;
420
+ break;
421
+ }
422
+ }
423
+ if (!placed) bucketCounts[bounds.length]++;
424
+ }
425
+
426
+ otlpMetrics.push({
427
+ name: first.name,
428
+ description: first.description || '',
429
+ unit: first.unit || '',
430
+ histogram: {
431
+ dataPoints: [{
432
+ timeUnixNano: String(Date.now() * 1_000_000),
433
+ count: group.records.length,
434
+ sum,
435
+ min: min === Infinity ? 0 : min,
436
+ max: max === -Infinity ? 0 : max,
437
+ bucketCounts: bucketCounts.map(String),
438
+ explicitBounds: bounds,
439
+ attributes: first.attributes ? toOtlpAttributes(first.attributes) : [],
440
+ }],
441
+ aggregationTemporality: 1, // DELTA
192
442
  },
193
- body: JSON.stringify({ logs }),
194
443
  });
195
- } catch {
196
- // Silently fail - don't break the app if Abbacchio server is down
197
444
  }
445
+
446
+ const payload = {
447
+ resourceMetrics: [{
448
+ resource: { attributes: this.buildResourceAttributes() },
449
+ scopeMetrics: [{
450
+ scope: { name: '@abbacchio/transport' },
451
+ metrics: otlpMetrics,
452
+ }],
453
+ }],
454
+ };
455
+
456
+ await this.post('/v1/metrics', payload);
198
457
  }
199
458
 
200
- /**
201
- * Close the client and flush any remaining logs
202
- */
203
- async close(): Promise<void> {
204
- if (this.timer) {
205
- clearTimeout(this.timer);
206
- this.timer = null;
459
+ // ─── Traces ────────────────────────────────────────
460
+
461
+ /** Add a trace span to the buffer */
462
+ addSpan(span: SpanRecord): void {
463
+ if (!this.enabled) return;
464
+ this.spanBuffer.push(span);
465
+
466
+ if (this.spanBuffer.length >= this.batchSize) {
467
+ this.flushSpans();
468
+ } else {
469
+ this.scheduleSpanFlush();
207
470
  }
471
+ }
472
+
473
+ private scheduleSpanFlush(): void {
474
+ if (this.spanTimer) return;
475
+ this.spanTimer = setTimeout(() => {
476
+ this.spanTimer = null;
477
+ this.flushSpans();
478
+ }, this.interval);
479
+ }
480
+
481
+ async flushSpans(): Promise<void> {
482
+ if (this.spanBuffer.length === 0) return;
483
+ const batch = this.spanBuffer;
484
+ this.spanBuffer = [];
485
+ await this.sendSpans(batch);
486
+ }
487
+
488
+ private async sendSpans(spans: SpanRecord[]): Promise<void> {
489
+ const otlpSpans = spans.map((s) => ({
490
+ traceId: s.traceId,
491
+ spanId: s.spanId,
492
+ parentSpanId: s.parentSpanId,
493
+ name: s.name,
494
+ kind: s.kind ?? 1, // SPAN_KIND_INTERNAL
495
+ startTimeUnixNano: s.startTimeUnixNano,
496
+ endTimeUnixNano: s.endTimeUnixNano,
497
+ attributes: s.attributes ? toOtlpAttributes(s.attributes) : [],
498
+ status: s.status,
499
+ }));
500
+
501
+ const payload = {
502
+ resourceSpans: [{
503
+ resource: { attributes: this.buildResourceAttributes() },
504
+ scopeSpans: [{
505
+ scope: { name: '@abbacchio/transport' },
506
+ spans: otlpSpans,
507
+ }],
508
+ }],
509
+ };
510
+
511
+ await this.post('/v1/traces', payload);
512
+ }
513
+
514
+ // ─── Shared ────────────────────────────────────────
515
+
516
+ /** Flush all buffers and close the client */
517
+ async flush(): Promise<void> {
518
+ await Promise.all([
519
+ this.flushLogs(),
520
+ this.flushMetrics(),
521
+ this.flushHistograms(),
522
+ this.flushSpans(),
523
+ ]);
524
+ }
525
+
526
+ /** Close the client and flush any remaining data */
527
+ async close(): Promise<void> {
528
+ if (this.logTimer) { clearTimeout(this.logTimer); this.logTimer = null; }
529
+ if (this.metricTimer) { clearTimeout(this.metricTimer); this.metricTimer = null; }
530
+ if (this.histogramTimer) { clearTimeout(this.histogramTimer); this.histogramTimer = null; }
531
+ if (this.spanTimer) { clearTimeout(this.spanTimer); this.spanTimer = null; }
208
532
  await this.flush();
209
533
  }
534
+
535
+ private buildResourceAttributes(): OtlpKeyValue[] {
536
+ const attrs: OtlpKeyValue[] = [
537
+ { key: 'service.name', value: { stringValue: this.serviceName } },
538
+ ];
539
+ for (const [key, val] of Object.entries(this.resourceAttributes)) {
540
+ attrs.push({ key, value: { stringValue: val } });
541
+ }
542
+ return attrs;
543
+ }
544
+
545
+ private async post(path: string, payload: unknown): Promise<void> {
546
+ try {
547
+ let body = JSON.stringify(payload);
548
+
549
+ const headers: Record<string, string> = {
550
+ 'Content-Type': 'application/json',
551
+ ...this.headers,
552
+ };
553
+
554
+ if (this.secretKey) {
555
+ body = encrypt(JSON.stringify(payload), this.secretKey);
556
+ headers['X-Encrypted'] = 'true';
557
+ }
558
+
559
+ await fetch(`${this.endpoint}${path}`, {
560
+ method: 'POST',
561
+ headers,
562
+ body,
563
+ });
564
+ } catch {
565
+ // Silently fail — don't break the app if the telemetry server is down
566
+ }
567
+ }
210
568
  }
211
569
 
212
570
  /**
213
- * Create a new Abbacchio client instance
571
+ * Create a new Abbacchio OTLP client instance
214
572
  */
215
573
  export function createClient(options?: AbbacchioClientOptions): AbbacchioClient {
216
574
  return new AbbacchioClient(options);