@deeptracer/core 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.
@@ -0,0 +1,382 @@
1
+ // src/batcher.ts
2
+ var Batcher = class {
3
+ constructor(config, onFlush) {
4
+ this.onFlush = onFlush;
5
+ this.batchSize = config.batchSize ?? 50;
6
+ this.flushIntervalMs = config.flushIntervalMs ?? 5e3;
7
+ this.startTimer();
8
+ }
9
+ buffer = [];
10
+ timer = null;
11
+ batchSize;
12
+ flushIntervalMs;
13
+ add(entry) {
14
+ this.buffer.push(entry);
15
+ if (this.buffer.length >= this.batchSize) {
16
+ this.flush();
17
+ }
18
+ }
19
+ flush() {
20
+ if (this.buffer.length === 0) return;
21
+ const entries = [...this.buffer];
22
+ this.buffer = [];
23
+ this.onFlush(entries);
24
+ }
25
+ startTimer() {
26
+ this.timer = setInterval(() => this.flush(), this.flushIntervalMs);
27
+ }
28
+ destroy() {
29
+ if (this.timer) clearInterval(this.timer);
30
+ this.flush();
31
+ }
32
+ };
33
+
34
+ // src/transport.ts
35
+ var Transport = class {
36
+ constructor(config) {
37
+ this.config = config;
38
+ }
39
+ async sendLogs(logs) {
40
+ try {
41
+ const res = await fetch(`${this.config.endpoint}/ingest/logs`, {
42
+ method: "POST",
43
+ headers: {
44
+ "Content-Type": "application/json",
45
+ Authorization: `Bearer ${this.config.apiKey}`
46
+ },
47
+ body: JSON.stringify({
48
+ product: this.config.product,
49
+ service: this.config.service,
50
+ environment: this.config.environment,
51
+ logs
52
+ })
53
+ });
54
+ if (!res.ok) {
55
+ console.warn(
56
+ `[@deeptracer/core] Failed to send logs: ${res.status} ${res.statusText}`
57
+ );
58
+ }
59
+ } catch {
60
+ console.warn(
61
+ "[@deeptracer/core] Failed to send logs, falling back to console"
62
+ );
63
+ }
64
+ }
65
+ async sendError(error) {
66
+ try {
67
+ const res = await fetch(`${this.config.endpoint}/ingest/errors`, {
68
+ method: "POST",
69
+ headers: {
70
+ "Content-Type": "application/json",
71
+ Authorization: `Bearer ${this.config.apiKey}`
72
+ },
73
+ body: JSON.stringify({
74
+ ...error,
75
+ product: this.config.product,
76
+ service: this.config.service,
77
+ environment: this.config.environment
78
+ })
79
+ });
80
+ if (!res.ok) {
81
+ console.warn(
82
+ `[@deeptracer/core] Failed to send error: ${res.status} ${res.statusText}`
83
+ );
84
+ }
85
+ } catch {
86
+ console.warn("[@deeptracer/core] Failed to send error report");
87
+ console.error(error.error_message);
88
+ }
89
+ }
90
+ async sendTrace(span) {
91
+ try {
92
+ const res = await fetch(`${this.config.endpoint}/ingest/traces`, {
93
+ method: "POST",
94
+ headers: {
95
+ "Content-Type": "application/json",
96
+ Authorization: `Bearer ${this.config.apiKey}`
97
+ },
98
+ body: JSON.stringify({
99
+ ...span,
100
+ product: this.config.product,
101
+ service: this.config.service,
102
+ environment: this.config.environment
103
+ })
104
+ });
105
+ if (!res.ok) {
106
+ console.warn(
107
+ `[@deeptracer/core] Failed to send trace: ${res.status} ${res.statusText}`
108
+ );
109
+ }
110
+ } catch {
111
+ console.warn("[@deeptracer/core] Failed to send trace span");
112
+ }
113
+ }
114
+ async sendLLMUsage(report) {
115
+ try {
116
+ const res = await fetch(`${this.config.endpoint}/ingest/llm`, {
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ Authorization: `Bearer ${this.config.apiKey}`
121
+ },
122
+ body: JSON.stringify({
123
+ ...report,
124
+ product: this.config.product,
125
+ service: this.config.service,
126
+ environment: this.config.environment
127
+ })
128
+ });
129
+ if (!res.ok) {
130
+ console.warn(
131
+ `[@deeptracer/core] Failed to send LLM usage: ${res.status} ${res.statusText}`
132
+ );
133
+ }
134
+ } catch {
135
+ console.warn("[@deeptracer/core] Failed to send LLM usage report");
136
+ }
137
+ }
138
+ };
139
+
140
+ // src/logger.ts
141
+ function generateId() {
142
+ const bytes = new Uint8Array(8);
143
+ crypto.getRandomValues(bytes);
144
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
145
+ }
146
+ var _originalConsole = {
147
+ log: console.log,
148
+ info: console.info,
149
+ warn: console.warn,
150
+ error: console.error,
151
+ debug: console.debug
152
+ };
153
+ var Logger = class _Logger {
154
+ batcher;
155
+ transport;
156
+ contextName;
157
+ config;
158
+ requestMeta;
159
+ constructor(config, contextName, requestMeta) {
160
+ this.config = config;
161
+ this.contextName = contextName;
162
+ this.requestMeta = requestMeta;
163
+ this.transport = new Transport(config);
164
+ this.batcher = new Batcher(
165
+ { batchSize: config.batchSize, flushIntervalMs: config.flushIntervalMs },
166
+ (entries) => {
167
+ this.transport.sendLogs(entries);
168
+ }
169
+ );
170
+ }
171
+ log(level, message, dataOrError, maybeError) {
172
+ let metadata;
173
+ let error;
174
+ if (dataOrError instanceof Error) {
175
+ error = dataOrError;
176
+ } else if (dataOrError && typeof dataOrError === "object" && !Array.isArray(dataOrError)) {
177
+ metadata = dataOrError;
178
+ error = maybeError;
179
+ } else if (dataOrError !== void 0) {
180
+ error = dataOrError;
181
+ }
182
+ if (error instanceof Error) {
183
+ metadata = {
184
+ ...metadata,
185
+ error: {
186
+ message: error.message,
187
+ name: error.name,
188
+ stack: error.stack
189
+ }
190
+ };
191
+ }
192
+ const entry = {
193
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
194
+ level,
195
+ message,
196
+ metadata,
197
+ context: this.contextName,
198
+ trace_id: this.requestMeta?.trace_id,
199
+ span_id: this.requestMeta?.span_id,
200
+ request_id: this.requestMeta?.request_id,
201
+ vercel_id: this.requestMeta?.vercel_id
202
+ };
203
+ if (this.config.debug) {
204
+ const prefix = this.contextName ? `[${this.contextName}]` : "";
205
+ const lvl = level.toUpperCase().padEnd(5);
206
+ const consoleFn = level === "error" ? _originalConsole.error : level === "warn" ? _originalConsole.warn : level === "debug" ? _originalConsole.debug : _originalConsole.log;
207
+ if (metadata) {
208
+ consoleFn(`${lvl} ${prefix} ${message}`, metadata);
209
+ } else {
210
+ consoleFn(`${lvl} ${prefix} ${message}`);
211
+ }
212
+ }
213
+ this.batcher.add(entry);
214
+ }
215
+ /** Log a debug message. */
216
+ debug(message, dataOrError, error) {
217
+ this.log("debug", message, dataOrError, error);
218
+ }
219
+ /** Log an informational message. */
220
+ info(message, dataOrError, error) {
221
+ this.log("info", message, dataOrError, error);
222
+ }
223
+ /** Log a warning. */
224
+ warn(message, dataOrError, error) {
225
+ this.log("warn", message, dataOrError, error);
226
+ }
227
+ /** Log an error. */
228
+ error(message, dataOrError, error) {
229
+ this.log("error", message, dataOrError, error);
230
+ }
231
+ /** Create a context-scoped logger. All logs include the context name. */
232
+ withContext(name) {
233
+ return new _Logger(this.config, name, this.requestMeta);
234
+ }
235
+ /** Create a request-scoped logger that extracts trace context from headers. */
236
+ forRequest(request) {
237
+ const vercelId = request.headers.get("x-vercel-id") || void 0;
238
+ const requestId = request.headers.get("x-request-id") || void 0;
239
+ const traceId = request.headers.get("x-trace-id") || void 0;
240
+ const spanId = request.headers.get("x-span-id") || void 0;
241
+ return new _Logger(this.config, this.contextName, {
242
+ trace_id: traceId,
243
+ span_id: spanId,
244
+ request_id: requestId || (vercelId ? vercelId.split("::").pop() : void 0),
245
+ vercel_id: vercelId
246
+ });
247
+ }
248
+ /** Capture and report an error immediately (not batched). */
249
+ captureError(error, context) {
250
+ const err = error instanceof Error ? error : new Error(String(error));
251
+ const report = {
252
+ error_message: err.message,
253
+ stack_trace: err.stack || "",
254
+ severity: context?.severity || "medium",
255
+ context: context?.context,
256
+ trace_id: this.requestMeta?.trace_id,
257
+ user_id: context?.userId,
258
+ breadcrumbs: context?.breadcrumbs
259
+ };
260
+ this.transport.sendError(report);
261
+ }
262
+ /** Track LLM usage. Sends to /ingest/llm and logs for visibility. */
263
+ llmUsage(report) {
264
+ this.transport.sendLLMUsage({
265
+ model: report.model,
266
+ provider: report.provider,
267
+ operation: report.operation,
268
+ input_tokens: report.inputTokens,
269
+ output_tokens: report.outputTokens,
270
+ cost_usd: report.costUsd || 0,
271
+ latency_ms: report.latencyMs,
272
+ metadata: report.metadata
273
+ });
274
+ this.log("info", `LLM call: ${report.model} (${report.operation})`, {
275
+ llm_usage: {
276
+ model: report.model,
277
+ provider: report.provider,
278
+ operation: report.operation,
279
+ input_tokens: report.inputTokens,
280
+ output_tokens: report.outputTokens,
281
+ latency_ms: report.latencyMs
282
+ }
283
+ });
284
+ }
285
+ /** Start a span with automatic lifecycle (callback-based, recommended). */
286
+ startSpan(operation, fn) {
287
+ const inactive = this.startInactiveSpan(operation);
288
+ const span = {
289
+ traceId: inactive.traceId,
290
+ spanId: inactive.spanId,
291
+ parentSpanId: inactive.parentSpanId,
292
+ operation: inactive.operation,
293
+ getHeaders: () => inactive.getHeaders()
294
+ };
295
+ let result;
296
+ try {
297
+ result = fn(span);
298
+ } catch (err) {
299
+ inactive.end({ status: "error" });
300
+ throw err;
301
+ }
302
+ if (result instanceof Promise) {
303
+ return result.then(
304
+ (value) => {
305
+ inactive.end({ status: "ok" });
306
+ return value;
307
+ },
308
+ (err) => {
309
+ inactive.end({ status: "error" });
310
+ throw err;
311
+ }
312
+ );
313
+ }
314
+ inactive.end({ status: "ok" });
315
+ return result;
316
+ }
317
+ /** Start a span with manual lifecycle. You must call span.end(). */
318
+ startInactiveSpan(operation) {
319
+ const traceId = this.requestMeta?.trace_id || generateId();
320
+ const parentSpanId = this.requestMeta?.span_id || "";
321
+ const spanId = generateId();
322
+ const startTime = (/* @__PURE__ */ new Date()).toISOString();
323
+ const startMs = Date.now();
324
+ const childMeta = { ...this.requestMeta, trace_id: traceId, span_id: spanId };
325
+ const span = {
326
+ traceId,
327
+ spanId,
328
+ parentSpanId,
329
+ operation,
330
+ end: (options) => {
331
+ const durationMs = Date.now() - startMs;
332
+ const spanData = {
333
+ trace_id: traceId,
334
+ span_id: spanId,
335
+ parent_span_id: parentSpanId,
336
+ operation,
337
+ start_time: startTime,
338
+ duration_ms: durationMs,
339
+ status: options?.status || "ok",
340
+ metadata: options?.metadata
341
+ };
342
+ this.transport.sendTrace(spanData);
343
+ },
344
+ startSpan: (childOp, fn) => {
345
+ const childLogger = new _Logger(this.config, this.contextName, childMeta);
346
+ return childLogger.startSpan(childOp, fn);
347
+ },
348
+ startInactiveSpan: (childOp) => {
349
+ const childLogger = new _Logger(this.config, this.contextName, childMeta);
350
+ return childLogger.startInactiveSpan(childOp);
351
+ },
352
+ getHeaders: () => ({
353
+ "x-trace-id": traceId,
354
+ "x-span-id": spanId
355
+ })
356
+ };
357
+ return span;
358
+ }
359
+ /** Wrap a function with automatic tracing and error capture. */
360
+ wrap(operation, fn) {
361
+ return (...args) => {
362
+ return this.startSpan(operation, () => fn(...args));
363
+ };
364
+ }
365
+ /** Immediately flush all batched log entries. */
366
+ flush() {
367
+ this.batcher.flush();
368
+ }
369
+ /** Stop the batch timer and flush remaining logs. */
370
+ destroy() {
371
+ this.batcher.destroy();
372
+ }
373
+ };
374
+ function createLogger(config) {
375
+ return new Logger(config);
376
+ }
377
+
378
+ export {
379
+ _originalConsole,
380
+ Logger,
381
+ createLogger
382
+ };