@agentuity/telemetry 3.0.0-alpha.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.
Files changed (87) hide show
  1. package/dist/console.d.ts +33 -0
  2. package/dist/console.d.ts.map +1 -0
  3. package/dist/console.js +86 -0
  4. package/dist/console.js.map +1 -0
  5. package/dist/exporters/index.d.ts +4 -0
  6. package/dist/exporters/index.d.ts.map +1 -0
  7. package/dist/exporters/index.js +4 -0
  8. package/dist/exporters/index.js.map +1 -0
  9. package/dist/exporters/jsonl-log-exporter.d.ts +36 -0
  10. package/dist/exporters/jsonl-log-exporter.d.ts.map +1 -0
  11. package/dist/exporters/jsonl-log-exporter.js +103 -0
  12. package/dist/exporters/jsonl-log-exporter.js.map +1 -0
  13. package/dist/exporters/jsonl-metric-exporter.d.ts +40 -0
  14. package/dist/exporters/jsonl-metric-exporter.d.ts.map +1 -0
  15. package/dist/exporters/jsonl-metric-exporter.js +104 -0
  16. package/dist/exporters/jsonl-metric-exporter.js.map +1 -0
  17. package/dist/exporters/jsonl-trace-exporter.d.ts +36 -0
  18. package/dist/exporters/jsonl-trace-exporter.d.ts.map +1 -0
  19. package/dist/exporters/jsonl-trace-exporter.js +111 -0
  20. package/dist/exporters/jsonl-trace-exporter.js.map +1 -0
  21. package/dist/fetch.d.ts +12 -0
  22. package/dist/fetch.d.ts.map +1 -0
  23. package/dist/fetch.js +82 -0
  24. package/dist/fetch.js.map +1 -0
  25. package/dist/globals.d.ts +9 -0
  26. package/dist/globals.d.ts.map +1 -0
  27. package/dist/globals.js +13 -0
  28. package/dist/globals.js.map +1 -0
  29. package/dist/http.d.ts +16 -0
  30. package/dist/http.d.ts.map +1 -0
  31. package/dist/http.js +44 -0
  32. package/dist/http.js.map +1 -0
  33. package/dist/index.d.ts +50 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +62 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/logger/console.d.ts +69 -0
  38. package/dist/logger/console.d.ts.map +1 -0
  39. package/dist/logger/console.js +278 -0
  40. package/dist/logger/console.js.map +1 -0
  41. package/dist/logger/index.d.ts +4 -0
  42. package/dist/logger/index.d.ts.map +1 -0
  43. package/dist/logger/index.js +3 -0
  44. package/dist/logger/index.js.map +1 -0
  45. package/dist/logger/internal.d.ts +79 -0
  46. package/dist/logger/internal.d.ts.map +1 -0
  47. package/dist/logger/internal.js +133 -0
  48. package/dist/logger/internal.js.map +1 -0
  49. package/dist/logger/user.d.ts +8 -0
  50. package/dist/logger/user.d.ts.map +1 -0
  51. package/dist/logger/user.js +7 -0
  52. package/dist/logger/user.js.map +1 -0
  53. package/dist/logger/util.d.ts +11 -0
  54. package/dist/logger/util.d.ts.map +1 -0
  55. package/dist/logger/util.js +77 -0
  56. package/dist/logger/util.js.map +1 -0
  57. package/dist/logger.d.ts +40 -0
  58. package/dist/logger.d.ts.map +1 -0
  59. package/dist/logger.js +259 -0
  60. package/dist/logger.js.map +1 -0
  61. package/dist/telemetry.d.ts +71 -0
  62. package/dist/telemetry.d.ts.map +1 -0
  63. package/dist/telemetry.js +274 -0
  64. package/dist/telemetry.js.map +1 -0
  65. package/dist/tracestate.d.ts +44 -0
  66. package/dist/tracestate.d.ts.map +1 -0
  67. package/dist/tracestate.js +84 -0
  68. package/dist/tracestate.js.map +1 -0
  69. package/package.json +58 -0
  70. package/src/console.ts +91 -0
  71. package/src/exporters/README.md +217 -0
  72. package/src/exporters/index.ts +3 -0
  73. package/src/exporters/jsonl-log-exporter.ts +113 -0
  74. package/src/exporters/jsonl-metric-exporter.ts +120 -0
  75. package/src/exporters/jsonl-trace-exporter.ts +121 -0
  76. package/src/fetch.ts +105 -0
  77. package/src/globals.ts +18 -0
  78. package/src/http.ts +53 -0
  79. package/src/index.ts +82 -0
  80. package/src/logger/console.ts +322 -0
  81. package/src/logger/index.ts +3 -0
  82. package/src/logger/internal.ts +165 -0
  83. package/src/logger/user.ts +15 -0
  84. package/src/logger/util.ts +80 -0
  85. package/src/logger.ts +285 -0
  86. package/src/telemetry.ts +403 -0
  87. package/src/tracestate.ts +108 -0
@@ -0,0 +1,120 @@
1
+ import { type ExportResult, ExportResultCode } from '@opentelemetry/core';
2
+ import {
3
+ type PushMetricExporter,
4
+ type ResourceMetrics,
5
+ AggregationTemporality,
6
+ InstrumentType,
7
+ } from '@opentelemetry/sdk-metrics';
8
+ import { existsSync, appendFileSync, mkdirSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import { randomUUID } from 'node:crypto';
11
+
12
+ /**
13
+ * JSONL implementation of the PushMetricExporter interface
14
+ * Writes metrics to a timestamped JSONL file
15
+ */
16
+ export class JSONLMetricExporter implements PushMetricExporter {
17
+ private currentFile: string | null = null;
18
+ private readonly basePath: string;
19
+ private readonly filePrefix: string;
20
+
21
+ /**
22
+ * Creates a new JSONL metric exporter
23
+ * @param basePath - Directory to store the JSONL files
24
+ */
25
+ constructor(basePath: string) {
26
+ this.basePath = basePath;
27
+ this.filePrefix = 'otel-metric';
28
+ this.ensureDirectory();
29
+ }
30
+
31
+ private ensureDirectory(): void {
32
+ if (!existsSync(this.basePath)) {
33
+ mkdirSync(this.basePath, { recursive: true });
34
+ }
35
+ }
36
+
37
+ private getOrCreateFile(): string {
38
+ // If current file exists, use it
39
+ if (this.currentFile && existsSync(this.currentFile)) {
40
+ return this.currentFile;
41
+ }
42
+
43
+ this.currentFile = join(
44
+ this.basePath,
45
+ `${this.filePrefix}-${Date.now()}.${randomUUID()}.jsonl`
46
+ );
47
+ return this.currentFile;
48
+ }
49
+
50
+ /**
51
+ * Exports metrics to a JSONL file
52
+ *
53
+ * @param metrics - The resource metrics to export
54
+ * @param resultCallback - Callback function to report the export result
55
+ */
56
+ export(metrics: ResourceMetrics, resultCallback: (result: ExportResult) => void): void {
57
+ try {
58
+ const file = this.getOrCreateFile();
59
+
60
+ const record = {
61
+ resource: metrics.resource.attributes,
62
+ scopeMetrics: metrics.scopeMetrics.map((sm) => ({
63
+ scope: sm.scope,
64
+ metrics: sm.metrics.map((m) => ({
65
+ descriptor: m.descriptor,
66
+ dataPointType: m.dataPointType,
67
+ dataPoints: m.dataPoints,
68
+ aggregationTemporality: m.aggregationTemporality,
69
+ })),
70
+ })),
71
+ };
72
+
73
+ const line = JSON.stringify(record) + '\n';
74
+ try {
75
+ appendFileSync(file, line, 'utf-8');
76
+ } catch (err) {
77
+ // File may have been deleted, reset and retry once
78
+ const code = (err as NodeJS.ErrnoException).code;
79
+ if (code === 'ENOENT') {
80
+ this.currentFile = null;
81
+ const newFile = this.getOrCreateFile();
82
+ appendFileSync(newFile, line, 'utf-8');
83
+ } else {
84
+ throw err;
85
+ }
86
+ }
87
+
88
+ resultCallback({ code: ExportResultCode.SUCCESS });
89
+ } catch (error) {
90
+ resultCallback({
91
+ code: ExportResultCode.FAILED,
92
+ error: error instanceof Error ? error : new Error(String(error)),
93
+ });
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Shuts down the exporter
99
+ *
100
+ * @returns A promise that resolves when shutdown is complete
101
+ */
102
+ async shutdown(): Promise<void> {
103
+ this.currentFile = null;
104
+ }
105
+
106
+ /**
107
+ * Forces a flush of any pending data
108
+ */
109
+ async forceFlush(): Promise<void> {
110
+ // No-op for file-based exporter as writes are synchronous
111
+ }
112
+
113
+ /**
114
+ * Selects the aggregation temporality for the given instrument type
115
+ */
116
+ selectAggregationTemporality?(_instrumentType: InstrumentType): AggregationTemporality {
117
+ // Default to cumulative temporality
118
+ return AggregationTemporality.CUMULATIVE;
119
+ }
120
+ }
@@ -0,0 +1,121 @@
1
+ import { type ExportResult, ExportResultCode } from '@opentelemetry/core';
2
+ import type { ReadableSpan, SpanExporter } from '@opentelemetry/sdk-trace-base';
3
+ import { existsSync, appendFileSync, mkdirSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+
7
+ /**
8
+ * JSONL implementation of the SpanExporter interface
9
+ * Writes traces to a timestamped JSONL file
10
+ */
11
+ export class JSONLTraceExporter implements SpanExporter {
12
+ private currentFile: string | null = null;
13
+ private readonly basePath: string;
14
+ private readonly filePrefix: string;
15
+
16
+ /**
17
+ * Creates a new JSONL trace exporter
18
+ * @param basePath - Directory to store the JSONL files
19
+ */
20
+ constructor(basePath: string) {
21
+ this.basePath = basePath;
22
+ this.filePrefix = 'otel-trace';
23
+ this.ensureDirectory();
24
+ }
25
+
26
+ private ensureDirectory(): void {
27
+ if (!existsSync(this.basePath)) {
28
+ mkdirSync(this.basePath, { recursive: true });
29
+ }
30
+ }
31
+
32
+ private getOrCreateFile(): string {
33
+ // If current file exists, use it
34
+ if (this.currentFile && existsSync(this.currentFile)) {
35
+ return this.currentFile;
36
+ }
37
+
38
+ this.currentFile = join(
39
+ this.basePath,
40
+ `${this.filePrefix}-${Date.now()}.${randomUUID()}.jsonl`
41
+ );
42
+ return this.currentFile;
43
+ }
44
+
45
+ /**
46
+ * Exports spans to a JSONL file
47
+ *
48
+ * @param spans - The spans to export
49
+ * @param resultCallback - Callback function to report the export result
50
+ */
51
+ export(spans: ReadableSpan[], resultCallback: (result: ExportResult) => void): void {
52
+ try {
53
+ if (spans.length === 0) {
54
+ resultCallback({ code: ExportResultCode.SUCCESS });
55
+ return;
56
+ }
57
+ const file = this.getOrCreateFile();
58
+ const lines: string[] = [];
59
+ for (const span of spans) {
60
+ const record = {
61
+ traceId: span.spanContext().traceId,
62
+ spanId: span.spanContext().spanId,
63
+ traceState: span.spanContext().traceState?.serialize(),
64
+ name: span.name,
65
+ kind: span.kind,
66
+ startTime: span.startTime,
67
+ endTime: span.endTime,
68
+ attributes: span.attributes,
69
+ status: span.status,
70
+ events: span.events,
71
+ links: span.links,
72
+ resource: span.resource.attributes,
73
+ droppedAttributesCount: span.droppedAttributesCount,
74
+ droppedEventsCount: span.droppedEventsCount,
75
+ droppedLinksCount: span.droppedLinksCount,
76
+ duration: span.duration,
77
+ ended: span.ended,
78
+ };
79
+
80
+ lines.push(JSON.stringify(record));
81
+ }
82
+ const payload = `${lines.join('\n')}\n`;
83
+ try {
84
+ appendFileSync(file, payload, 'utf-8');
85
+ } catch (err) {
86
+ // File may have been deleted, reset and retry once
87
+ const code = (err as NodeJS.ErrnoException).code;
88
+ if (code === 'ENOENT') {
89
+ this.currentFile = null;
90
+ const newFile = this.getOrCreateFile();
91
+ appendFileSync(newFile, payload, 'utf-8');
92
+ } else {
93
+ throw err;
94
+ }
95
+ }
96
+
97
+ resultCallback({ code: ExportResultCode.SUCCESS });
98
+ } catch (error) {
99
+ resultCallback({
100
+ code: ExportResultCode.FAILED,
101
+ error: error instanceof Error ? error : new Error(String(error)),
102
+ });
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Shuts down the exporter
108
+ *
109
+ * @returns A promise that resolves when shutdown is complete
110
+ */
111
+ async shutdown(): Promise<void> {
112
+ this.currentFile = null;
113
+ }
114
+
115
+ /**
116
+ * Forces a flush of any pending data
117
+ */
118
+ async forceFlush(): Promise<void> {
119
+ // No-op for file-based exporter as writes are synchronous
120
+ }
121
+ }
package/src/fetch.ts ADDED
@@ -0,0 +1,105 @@
1
+ import { context, propagation, SpanStatusCode, trace } from '@opentelemetry/api';
2
+
3
+ /**
4
+ * Reference to the original fetch function before instrumentation
5
+ */
6
+ export const __originalFetch = fetch; // save the original fetch before we patch it
7
+
8
+ /**
9
+ * Instruments the global fetch function with OpenTelemetry tracing
10
+ *
11
+ * Replaces the global fetch with an instrumented version that creates spans
12
+ * for each HTTP request and propagates trace context in headers
13
+ */
14
+ export function instrumentFetch() {
15
+ const patch = async (
16
+ input: string | Request | URL,
17
+ init: RequestInit | undefined
18
+ ): Promise<Response> => {
19
+ const url =
20
+ typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
21
+
22
+ const method =
23
+ init?.method ||
24
+ (typeof input !== 'string' && !(input instanceof URL) ? input.method || 'GET' : 'GET');
25
+
26
+ // Get the active span if it exists
27
+ const activeSpan = trace.getActiveSpan();
28
+
29
+ // If there's no active span, just call the original fetch
30
+ if (!activeSpan) {
31
+ return __originalFetch(input, init);
32
+ }
33
+
34
+ // Get the current active context
35
+ const currentContext = context.active();
36
+ const _url = new URL(url);
37
+
38
+ // Create a child span using the current context
39
+ const childSpan = trace.getTracer('fetch').startSpan(
40
+ `${method} ${_url.pathname}`,
41
+ {
42
+ attributes: {
43
+ 'http.url': url,
44
+ 'http.path': _url.pathname,
45
+ 'http.method': method,
46
+ host: _url.host,
47
+ },
48
+ },
49
+ currentContext
50
+ );
51
+
52
+ try {
53
+ // Prepare trace context injection
54
+ const carrier: Record<string, string> = {};
55
+
56
+ // Create a new context with the child span
57
+ const newContext = trace.setSpan(currentContext, childSpan);
58
+
59
+ // Use the new context for propagation
60
+ propagation.inject(newContext, carrier);
61
+
62
+ // Preserve original headers and add trace context
63
+ // Handle headers from both Request input and init parameter
64
+ const baseHeaders =
65
+ typeof input !== 'string' && !(input instanceof URL) && input instanceof Request
66
+ ? input.headers
67
+ : undefined;
68
+ const headers = new Headers(baseHeaders ?? init?.headers ?? {});
69
+
70
+ // Add trace context headers (overwriting any already present)
71
+ for (const [key, value] of Object.entries(carrier)) {
72
+ headers.set(key, value);
73
+ }
74
+
75
+ // Create new init object with updated headers
76
+ const newInit = {
77
+ ...init,
78
+ headers,
79
+ };
80
+
81
+ const response = await __originalFetch(input, newInit);
82
+
83
+ // Add response attributes to span
84
+ childSpan.setAttributes({
85
+ 'http.status_code': response.status,
86
+ 'http.user_agent': response.headers.get('user-agent') || '',
87
+ });
88
+
89
+ if (!response.ok) {
90
+ childSpan.setStatus({ code: SpanStatusCode.ERROR });
91
+ } else {
92
+ childSpan.setStatus({ code: SpanStatusCode.OK });
93
+ }
94
+
95
+ return response;
96
+ } catch (error) {
97
+ childSpan.recordException(error as Error);
98
+ childSpan.setStatus({ code: SpanStatusCode.ERROR });
99
+ throw error;
100
+ } finally {
101
+ childSpan.end();
102
+ }
103
+ };
104
+ globalThis.fetch = patch as typeof fetch;
105
+ }
package/src/globals.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Global state for Telemetry instance (survives hot reloads)
3
+ */
4
+
5
+ import type { TelemetryResponse } from './telemetry';
6
+
7
+ const telemetryInstanceKey = Symbol.for('@agentuity/telemetry:instance');
8
+
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ const g = globalThis as any;
11
+
12
+ export const telemetry = {
13
+ get: (): TelemetryResponse | undefined =>
14
+ g[telemetryInstanceKey] as TelemetryResponse | undefined,
15
+ set: (v: TelemetryResponse): void => {
16
+ g[telemetryInstanceKey] = v;
17
+ },
18
+ };
package/src/http.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { context, propagation } from '@opentelemetry/api';
2
+
3
+ /**
4
+ * Injects trace context into response headers using the OpenTelemetry propagation API
5
+ *
6
+ * @param headers - Optional existing headers to include
7
+ * @returns A record of headers with trace context injected
8
+ */
9
+ export function injectTraceContextToHeaders(
10
+ headers: Record<string, string> | Headers = {}
11
+ ): Record<string, string> {
12
+ let _headers: Record<string, string>;
13
+ if (headers instanceof Headers) {
14
+ _headers = {};
15
+ headers.forEach((v, k) => {
16
+ _headers[k] = v;
17
+ });
18
+ } else {
19
+ _headers = { ...headers };
20
+ }
21
+ // Create a carrier object for the headers
22
+ const carrier: Record<string, string> = { ..._headers } as Record<string, string>;
23
+
24
+ // Get the current context
25
+ const currentContext = context.active();
26
+
27
+ // Inject trace context into the carrier
28
+ propagation.inject(currentContext, carrier);
29
+
30
+ return carrier;
31
+ }
32
+
33
+ /**
34
+ * Extracts trace context from Bun Request headers
35
+ *
36
+ * @param req - The Bun Request object
37
+ * @returns The context with trace information
38
+ */
39
+ export function extractTraceContextFromRequest(
40
+ req: Request
41
+ ): ReturnType<typeof propagation.extract> {
42
+ // Create a carrier object from the headers
43
+ const carrier: Record<string, string> = {};
44
+
45
+ // Convert headers to the format expected by the propagator
46
+ req.headers.forEach((value, key) => {
47
+ carrier[key.toLowerCase()] = value;
48
+ });
49
+
50
+ // Extract the context using the global propagator
51
+ const activeContext = context.active();
52
+ return propagation.extract(activeContext, carrier);
53
+ }
package/src/index.ts ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * @agentuity/telemetry - OpenTelemetry telemetry for Agentuity
3
+ *
4
+ * Auto-initializes from environment variables on import (Vercel-style).
5
+ *
6
+ * @example Automatic initialization (recommended)
7
+ * ```typescript
8
+ * // Just import - auto-configures from AGENTUITY_* env vars
9
+ * import '@agentuity/telemetry';
10
+ *
11
+ * // Then access the globals anywhere
12
+ * import { tracer, logger, meter } from '@agentuity/telemetry';
13
+ * ```
14
+ *
15
+ * @example Explicit configuration
16
+ * ```typescript
17
+ * import { register } from '@agentuity/telemetry';
18
+ *
19
+ * register({
20
+ * name: 'my-app',
21
+ * version: '1.0.0',
22
+ * // ... optional overrides
23
+ * });
24
+ * ```
25
+ */
26
+
27
+ // Re-export types
28
+ export type { Logger } from './logger';
29
+
30
+ // Re-export console reference for custom loggers
31
+ export { __originalConsole } from './logger';
32
+ export type { TelemetryConfig, TelemetryResponse } from './telemetry';
33
+
34
+ // Re-export HTTP utilities for trace context propagation
35
+ export { injectTraceContextToHeaders, extractTraceContextFromRequest } from './http';
36
+
37
+ // Re-export trace state utilities
38
+ export {
39
+ enrichContextWithTraceState,
40
+ generateTraceId,
41
+ generateSpanId,
42
+ type TraceStateEntries,
43
+ } from './tracestate';
44
+
45
+ // Core registration function
46
+ export { register, registerTelemetry, getTelemetry, ensureInitialized } from './telemetry';
47
+
48
+ // Lazy-loaded exports - auto-initialized from env vars
49
+ import type { Tracer, Meter } from '@opentelemetry/api';
50
+ import type { Logger } from './logger';
51
+ import { ensureInitialized } from './telemetry';
52
+
53
+ /**
54
+ * Get the OpenTelemetry tracer (auto-initialized)
55
+ */
56
+ export const tracer: Tracer = new Proxy({} as Tracer, {
57
+ get: (_, prop) => ensureInitialized().tracer[prop as keyof Tracer],
58
+ });
59
+
60
+ /**
61
+ * Get the OpenTelemetry meter (auto-initialized)
62
+ */
63
+ export const meter: Meter = new Proxy({} as Meter, {
64
+ get: (_, prop) => ensureInitialized().meter[prop as keyof Meter],
65
+ });
66
+
67
+ /**
68
+ * Get the Logger instance (auto-initialized)
69
+ */
70
+ export const logger: Logger = new Proxy({} as Logger, {
71
+ get: (_, prop) => ensureInitialized().logger[prop as keyof Logger],
72
+ });
73
+
74
+ /**
75
+ * Shutdown telemetry (call on process exit)
76
+ */
77
+ export async function shutdown(): Promise<void> {
78
+ const telemetry = ensureInitialized();
79
+ if (telemetry?.shutdown) {
80
+ await telemetry.shutdown();
81
+ }
82
+ }