evlog 1.1.0 → 1.3.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/README.md CHANGED
@@ -346,6 +346,49 @@ async function processSyncJob(job: Job) {
346
346
  }
347
347
  ```
348
348
 
349
+ ## Cloudflare Workers
350
+
351
+ Use the Workers adapter for structured logs and correct platform severity.
352
+
353
+ ```typescript
354
+ // src/index.ts
355
+ import { initWorkersLogger, createWorkersLogger } from 'evlog/workers'
356
+
357
+ initWorkersLogger({
358
+ env: { service: 'edge-api' },
359
+ })
360
+
361
+ export default {
362
+ async fetch(request: Request) {
363
+ const log = createWorkersLogger(request)
364
+
365
+ try {
366
+ log.set({ route: 'health' })
367
+ const response = new Response('ok', { status: 200 })
368
+ log.emit({ status: response.status })
369
+ return response
370
+ } catch (error) {
371
+ log.error(error as Error)
372
+ log.emit({ status: 500 })
373
+ throw error
374
+ }
375
+ },
376
+ }
377
+ ```
378
+
379
+ Disable invocation logs to avoid duplicate request logs:
380
+
381
+ ```toml
382
+ # wrangler.toml
383
+ [observability.logs]
384
+ invocation_logs = false
385
+ ```
386
+
387
+ Notes:
388
+ - `requestId` defaults to `cf-ray` when available
389
+ - `request.cf` is included (colo, country, asn) unless disabled
390
+ - Use `headerAllowlist` to avoid logging sensitive headers
391
+
349
392
  ## API Reference
350
393
 
351
394
  ### `initLogger(config)`
@@ -362,6 +405,7 @@ initLogger({
362
405
  region?: string // Deployment region
363
406
  },
364
407
  pretty?: boolean // Pretty print (default: true in dev)
408
+ stringify?: boolean // JSON.stringify output (default: true, false for Workers)
365
409
  include?: string[] // Route patterns to log (glob), e.g. ['/api/**']
366
410
  sampling?: {
367
411
  rates?: { // Head sampling (random per level)
@@ -479,6 +523,34 @@ log.emit() // Emit final event
479
523
  log.getContext() // Get current context
480
524
  ```
481
525
 
526
+ ### `initWorkersLogger(options?)`
527
+
528
+ Initialize evlog for Cloudflare Workers (object logs + correct severity).
529
+
530
+ ```typescript
531
+ import { initWorkersLogger } from 'evlog/workers'
532
+
533
+ initWorkersLogger({
534
+ env: { service: 'edge-api' },
535
+ })
536
+ ```
537
+
538
+ ### `createWorkersLogger(request, options?)`
539
+
540
+ Create a request-scoped logger for Workers. Auto-extracts `cf-ray`, `request.cf`, method, and path.
541
+
542
+ ```typescript
543
+ import { createWorkersLogger } from 'evlog/workers'
544
+
545
+ const log = createWorkersLogger(request, {
546
+ requestId: 'custom-id', // Override cf-ray (default: cf-ray header)
547
+ headers: ['x-request-id'], // Headers to include (default: none)
548
+ })
549
+
550
+ log.set({ user: { id: '123' } })
551
+ log.emit({ status: 200 })
552
+ ```
553
+
482
554
  ### `createError(options)`
483
555
 
484
556
  Create a structured error with HTTP status support. Import from `evlog` directly to avoid conflicts with Nuxt/Nitro's `createError`.
@@ -0,0 +1,62 @@
1
+ import { DrainContext, WideEvent } from '../types.mjs';
2
+
3
+ interface AxiomConfig {
4
+ /** Axiom dataset name */
5
+ dataset: string;
6
+ /** Axiom API token */
7
+ token: string;
8
+ /** Organization ID (required for Personal Access Tokens) */
9
+ orgId?: string;
10
+ /** Base URL for Axiom API. Default: https://api.axiom.co */
11
+ baseUrl?: string;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /**
16
+ * Create a drain function for sending logs to Axiom.
17
+ *
18
+ * Configuration priority (highest to lowest):
19
+ * 1. Overrides passed to createAxiomDrain()
20
+ * 2. runtimeConfig.evlog.axiom
21
+ * 3. runtimeConfig.axiom
22
+ * 4. Environment variables: NUXT_AXIOM_*, AXIOM_*
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // Zero config - just set NUXT_AXIOM_TOKEN and NUXT_AXIOM_DATASET env vars
27
+ * nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
28
+ *
29
+ * // With overrides
30
+ * nitroApp.hooks.hook('evlog:drain', createAxiomDrain({
31
+ * dataset: 'my-dataset',
32
+ * }))
33
+ * ```
34
+ */
35
+ declare function createAxiomDrain(overrides?: Partial<AxiomConfig>): (ctx: DrainContext) => Promise<void>;
36
+ /**
37
+ * Send a single event to Axiom.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * await sendToAxiom(event, {
42
+ * dataset: 'my-logs',
43
+ * token: process.env.AXIOM_TOKEN!,
44
+ * })
45
+ * ```
46
+ */
47
+ declare function sendToAxiom(event: WideEvent, config: AxiomConfig): Promise<void>;
48
+ /**
49
+ * Send a batch of events to Axiom.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * await sendBatchToAxiom(events, {
54
+ * dataset: 'my-logs',
55
+ * token: process.env.AXIOM_TOKEN!,
56
+ * })
57
+ * ```
58
+ */
59
+ declare function sendBatchToAxiom(events: WideEvent[], config: AxiomConfig): Promise<void>;
60
+
61
+ export { createAxiomDrain, sendBatchToAxiom, sendToAxiom };
62
+ export type { AxiomConfig };
@@ -0,0 +1,62 @@
1
+ import { DrainContext, WideEvent } from '../types.js';
2
+
3
+ interface AxiomConfig {
4
+ /** Axiom dataset name */
5
+ dataset: string;
6
+ /** Axiom API token */
7
+ token: string;
8
+ /** Organization ID (required for Personal Access Tokens) */
9
+ orgId?: string;
10
+ /** Base URL for Axiom API. Default: https://api.axiom.co */
11
+ baseUrl?: string;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /**
16
+ * Create a drain function for sending logs to Axiom.
17
+ *
18
+ * Configuration priority (highest to lowest):
19
+ * 1. Overrides passed to createAxiomDrain()
20
+ * 2. runtimeConfig.evlog.axiom
21
+ * 3. runtimeConfig.axiom
22
+ * 4. Environment variables: NUXT_AXIOM_*, AXIOM_*
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * // Zero config - just set NUXT_AXIOM_TOKEN and NUXT_AXIOM_DATASET env vars
27
+ * nitroApp.hooks.hook('evlog:drain', createAxiomDrain())
28
+ *
29
+ * // With overrides
30
+ * nitroApp.hooks.hook('evlog:drain', createAxiomDrain({
31
+ * dataset: 'my-dataset',
32
+ * }))
33
+ * ```
34
+ */
35
+ declare function createAxiomDrain(overrides?: Partial<AxiomConfig>): (ctx: DrainContext) => Promise<void>;
36
+ /**
37
+ * Send a single event to Axiom.
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * await sendToAxiom(event, {
42
+ * dataset: 'my-logs',
43
+ * token: process.env.AXIOM_TOKEN!,
44
+ * })
45
+ * ```
46
+ */
47
+ declare function sendToAxiom(event: WideEvent, config: AxiomConfig): Promise<void>;
48
+ /**
49
+ * Send a batch of events to Axiom.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * await sendBatchToAxiom(events, {
54
+ * dataset: 'my-logs',
55
+ * token: process.env.AXIOM_TOKEN!,
56
+ * })
57
+ * ```
58
+ */
59
+ declare function sendBatchToAxiom(events: WideEvent[], config: AxiomConfig): Promise<void>;
60
+
61
+ export { createAxiomDrain, sendBatchToAxiom, sendToAxiom };
62
+ export type { AxiomConfig };
@@ -0,0 +1,65 @@
1
+ function getRuntimeConfig() {
2
+ try {
3
+ const { useRuntimeConfig } = require("nitropack/runtime");
4
+ return useRuntimeConfig();
5
+ } catch {
6
+ return void 0;
7
+ }
8
+ }
9
+ function createAxiomDrain(overrides) {
10
+ return async (ctx) => {
11
+ const runtimeConfig = getRuntimeConfig();
12
+ const evlogAxiom = runtimeConfig?.evlog?.axiom;
13
+ const rootAxiom = runtimeConfig?.axiom;
14
+ const config = {
15
+ dataset: overrides?.dataset ?? evlogAxiom?.dataset ?? rootAxiom?.dataset ?? process.env.NUXT_AXIOM_DATASET ?? process.env.AXIOM_DATASET,
16
+ token: overrides?.token ?? evlogAxiom?.token ?? rootAxiom?.token ?? process.env.NUXT_AXIOM_TOKEN ?? process.env.AXIOM_TOKEN,
17
+ orgId: overrides?.orgId ?? evlogAxiom?.orgId ?? rootAxiom?.orgId ?? process.env.NUXT_AXIOM_ORG_ID ?? process.env.AXIOM_ORG_ID,
18
+ baseUrl: overrides?.baseUrl ?? evlogAxiom?.baseUrl ?? rootAxiom?.baseUrl ?? process.env.NUXT_AXIOM_URL ?? process.env.AXIOM_URL,
19
+ timeout: overrides?.timeout ?? evlogAxiom?.timeout ?? rootAxiom?.timeout
20
+ };
21
+ if (!config.dataset || !config.token) {
22
+ console.error("[evlog/axiom] Missing dataset or token. Set NUXT_AXIOM_TOKEN/NUXT_AXIOM_DATASET env vars or pass to createAxiomDrain()");
23
+ return;
24
+ }
25
+ try {
26
+ await sendToAxiom(ctx.event, config);
27
+ } catch (error) {
28
+ console.error("[evlog/axiom] Failed to send event:", error);
29
+ }
30
+ };
31
+ }
32
+ async function sendToAxiom(event, config) {
33
+ await sendBatchToAxiom([event], config);
34
+ }
35
+ async function sendBatchToAxiom(events, config) {
36
+ const baseUrl = config.baseUrl ?? "https://api.axiom.co";
37
+ const timeout = config.timeout ?? 5e3;
38
+ const url = `${baseUrl}/v1/datasets/${encodeURIComponent(config.dataset)}/ingest`;
39
+ const headers = {
40
+ "Content-Type": "application/json",
41
+ "Authorization": `Bearer ${config.token}`
42
+ };
43
+ if (config.orgId) {
44
+ headers["X-Axiom-Org-Id"] = config.orgId;
45
+ }
46
+ const controller = new AbortController();
47
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
48
+ try {
49
+ const response = await fetch(url, {
50
+ method: "POST",
51
+ headers,
52
+ body: JSON.stringify(events),
53
+ signal: controller.signal
54
+ });
55
+ if (!response.ok) {
56
+ const text = await response.text().catch(() => "Unknown error");
57
+ const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
58
+ throw new Error(`Axiom API error: ${response.status} ${response.statusText} - ${safeText}`);
59
+ }
60
+ } finally {
61
+ clearTimeout(timeoutId);
62
+ }
63
+ }
64
+
65
+ export { createAxiomDrain, sendBatchToAxiom, sendToAxiom };
@@ -0,0 +1,83 @@
1
+ import { DrainContext, WideEvent } from '../types.mjs';
2
+
3
+ interface OTLPConfig {
4
+ /** OTLP HTTP endpoint (e.g., http://localhost:4318) */
5
+ endpoint: string;
6
+ /** Override service name (defaults to event.service) */
7
+ serviceName?: string;
8
+ /** Additional resource attributes */
9
+ resourceAttributes?: Record<string, string | number | boolean>;
10
+ /** Custom headers (e.g., for authentication) */
11
+ headers?: Record<string, string>;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /** OTLP Log Record structure */
16
+ interface OTLPLogRecord {
17
+ timeUnixNano: string;
18
+ severityNumber: number;
19
+ severityText: string;
20
+ body: {
21
+ stringValue: string;
22
+ };
23
+ attributes: Array<{
24
+ key: string;
25
+ value: {
26
+ stringValue?: string;
27
+ intValue?: string;
28
+ boolValue?: boolean;
29
+ };
30
+ }>;
31
+ traceId?: string;
32
+ spanId?: string;
33
+ }
34
+ /**
35
+ * Convert an evlog WideEvent to an OTLP LogRecord.
36
+ */
37
+ declare function toOTLPLogRecord(event: WideEvent): OTLPLogRecord;
38
+ /**
39
+ * Create a drain function for sending logs to an OTLP endpoint.
40
+ *
41
+ * Configuration priority (highest to lowest):
42
+ * 1. Overrides passed to createOTLPDrain()
43
+ * 2. runtimeConfig.evlog.otlp (NUXT_EVLOG_OTLP_*)
44
+ * 3. runtimeConfig.otlp (NUXT_OTLP_*)
45
+ * 4. Environment variables: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * // Zero config - reads from runtimeConfig or env vars
50
+ * nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
51
+ *
52
+ * // With overrides
53
+ * nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
54
+ * endpoint: 'http://localhost:4318',
55
+ * }))
56
+ * ```
57
+ */
58
+ declare function createOTLPDrain(overrides?: Partial<OTLPConfig>): (ctx: DrainContext) => Promise<void>;
59
+ /**
60
+ * Send a single event to an OTLP endpoint.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * await sendToOTLP(event, {
65
+ * endpoint: 'http://localhost:4318',
66
+ * })
67
+ * ```
68
+ */
69
+ declare function sendToOTLP(event: WideEvent, config: OTLPConfig): Promise<void>;
70
+ /**
71
+ * Send a batch of events to an OTLP endpoint.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * await sendBatchToOTLP(events, {
76
+ * endpoint: 'http://localhost:4318',
77
+ * })
78
+ * ```
79
+ */
80
+ declare function sendBatchToOTLP(events: WideEvent[], config: OTLPConfig): Promise<void>;
81
+
82
+ export { createOTLPDrain, sendBatchToOTLP, sendToOTLP, toOTLPLogRecord };
83
+ export type { OTLPConfig, OTLPLogRecord };
@@ -0,0 +1,83 @@
1
+ import { DrainContext, WideEvent } from '../types.js';
2
+
3
+ interface OTLPConfig {
4
+ /** OTLP HTTP endpoint (e.g., http://localhost:4318) */
5
+ endpoint: string;
6
+ /** Override service name (defaults to event.service) */
7
+ serviceName?: string;
8
+ /** Additional resource attributes */
9
+ resourceAttributes?: Record<string, string | number | boolean>;
10
+ /** Custom headers (e.g., for authentication) */
11
+ headers?: Record<string, string>;
12
+ /** Request timeout in milliseconds. Default: 5000 */
13
+ timeout?: number;
14
+ }
15
+ /** OTLP Log Record structure */
16
+ interface OTLPLogRecord {
17
+ timeUnixNano: string;
18
+ severityNumber: number;
19
+ severityText: string;
20
+ body: {
21
+ stringValue: string;
22
+ };
23
+ attributes: Array<{
24
+ key: string;
25
+ value: {
26
+ stringValue?: string;
27
+ intValue?: string;
28
+ boolValue?: boolean;
29
+ };
30
+ }>;
31
+ traceId?: string;
32
+ spanId?: string;
33
+ }
34
+ /**
35
+ * Convert an evlog WideEvent to an OTLP LogRecord.
36
+ */
37
+ declare function toOTLPLogRecord(event: WideEvent): OTLPLogRecord;
38
+ /**
39
+ * Create a drain function for sending logs to an OTLP endpoint.
40
+ *
41
+ * Configuration priority (highest to lowest):
42
+ * 1. Overrides passed to createOTLPDrain()
43
+ * 2. runtimeConfig.evlog.otlp (NUXT_EVLOG_OTLP_*)
44
+ * 3. runtimeConfig.otlp (NUXT_OTLP_*)
45
+ * 4. Environment variables: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * // Zero config - reads from runtimeConfig or env vars
50
+ * nitroApp.hooks.hook('evlog:drain', createOTLPDrain())
51
+ *
52
+ * // With overrides
53
+ * nitroApp.hooks.hook('evlog:drain', createOTLPDrain({
54
+ * endpoint: 'http://localhost:4318',
55
+ * }))
56
+ * ```
57
+ */
58
+ declare function createOTLPDrain(overrides?: Partial<OTLPConfig>): (ctx: DrainContext) => Promise<void>;
59
+ /**
60
+ * Send a single event to an OTLP endpoint.
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * await sendToOTLP(event, {
65
+ * endpoint: 'http://localhost:4318',
66
+ * })
67
+ * ```
68
+ */
69
+ declare function sendToOTLP(event: WideEvent, config: OTLPConfig): Promise<void>;
70
+ /**
71
+ * Send a batch of events to an OTLP endpoint.
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * await sendBatchToOTLP(events, {
76
+ * endpoint: 'http://localhost:4318',
77
+ * })
78
+ * ```
79
+ */
80
+ declare function sendBatchToOTLP(events: WideEvent[], config: OTLPConfig): Promise<void>;
81
+
82
+ export { createOTLPDrain, sendBatchToOTLP, sendToOTLP, toOTLPLogRecord };
83
+ export type { OTLPConfig, OTLPLogRecord };
@@ -0,0 +1,202 @@
1
+ const SEVERITY_MAP = {
2
+ debug: 5,
3
+ // DEBUG
4
+ info: 9,
5
+ // INFO
6
+ warn: 13,
7
+ // WARN
8
+ error: 17
9
+ // ERROR
10
+ };
11
+ const SEVERITY_TEXT_MAP = {
12
+ debug: "DEBUG",
13
+ info: "INFO",
14
+ warn: "WARN",
15
+ error: "ERROR"
16
+ };
17
+ function getRuntimeConfig() {
18
+ try {
19
+ const { useRuntimeConfig } = require("nitropack/runtime");
20
+ return useRuntimeConfig();
21
+ } catch {
22
+ return void 0;
23
+ }
24
+ }
25
+ function toAttributeValue(value) {
26
+ if (typeof value === "boolean") {
27
+ return { boolValue: value };
28
+ }
29
+ if (typeof value === "number" && Number.isInteger(value)) {
30
+ return { intValue: String(value) };
31
+ }
32
+ if (typeof value === "string") {
33
+ return { stringValue: value };
34
+ }
35
+ return { stringValue: JSON.stringify(value) };
36
+ }
37
+ function toOTLPLogRecord(event) {
38
+ const timestamp = new Date(event.timestamp).getTime() * 1e6;
39
+ const { level, traceId, spanId, ...rest } = event;
40
+ delete rest.timestamp;
41
+ delete rest.service;
42
+ delete rest.environment;
43
+ delete rest.version;
44
+ delete rest.commitHash;
45
+ delete rest.region;
46
+ const attributes = [];
47
+ for (const [key, value] of Object.entries(rest)) {
48
+ if (value !== void 0 && value !== null) {
49
+ attributes.push({
50
+ key,
51
+ value: toAttributeValue(value)
52
+ });
53
+ }
54
+ }
55
+ const record = {
56
+ timeUnixNano: String(timestamp),
57
+ severityNumber: SEVERITY_MAP[level] ?? 9,
58
+ severityText: SEVERITY_TEXT_MAP[level] ?? "INFO",
59
+ body: { stringValue: JSON.stringify(event) },
60
+ attributes
61
+ };
62
+ if (typeof traceId === "string") {
63
+ record.traceId = traceId;
64
+ }
65
+ if (typeof spanId === "string") {
66
+ record.spanId = spanId;
67
+ }
68
+ return record;
69
+ }
70
+ function buildResourceAttributes(event, config) {
71
+ const attributes = [];
72
+ attributes.push({
73
+ key: "service.name",
74
+ value: { stringValue: config.serviceName ?? event.service }
75
+ });
76
+ if (event.environment) {
77
+ attributes.push({
78
+ key: "deployment.environment",
79
+ value: { stringValue: event.environment }
80
+ });
81
+ }
82
+ if (event.version) {
83
+ attributes.push({
84
+ key: "service.version",
85
+ value: { stringValue: event.version }
86
+ });
87
+ }
88
+ if (event.region) {
89
+ attributes.push({
90
+ key: "cloud.region",
91
+ value: { stringValue: event.region }
92
+ });
93
+ }
94
+ if (event.commitHash) {
95
+ attributes.push({
96
+ key: "vcs.commit.id",
97
+ value: { stringValue: event.commitHash }
98
+ });
99
+ }
100
+ if (config.resourceAttributes) {
101
+ for (const [key, value] of Object.entries(config.resourceAttributes)) {
102
+ attributes.push({
103
+ key,
104
+ value: toAttributeValue(value)
105
+ });
106
+ }
107
+ }
108
+ return attributes;
109
+ }
110
+ function createOTLPDrain(overrides) {
111
+ return async (ctx) => {
112
+ const runtimeConfig = getRuntimeConfig();
113
+ const evlogOtlp = runtimeConfig?.evlog?.otlp;
114
+ const rootOtlp = runtimeConfig?.otlp;
115
+ const getHeadersFromEnv = () => {
116
+ const headersEnv = process.env.OTEL_EXPORTER_OTLP_HEADERS || process.env.NUXT_OTLP_HEADERS;
117
+ if (headersEnv) {
118
+ const headers = {};
119
+ const decoded = decodeURIComponent(headersEnv);
120
+ for (const pair of decoded.split(",")) {
121
+ const eqIndex = pair.indexOf("=");
122
+ if (eqIndex > 0) {
123
+ const key = pair.slice(0, eqIndex).trim();
124
+ const value = pair.slice(eqIndex + 1).trim();
125
+ if (key && value) {
126
+ headers[key] = value;
127
+ }
128
+ }
129
+ }
130
+ if (Object.keys(headers).length > 0) return headers;
131
+ }
132
+ const auth = process.env.NUXT_OTLP_AUTH;
133
+ if (auth) {
134
+ return { Authorization: auth };
135
+ }
136
+ return void 0;
137
+ };
138
+ const config = {
139
+ endpoint: overrides?.endpoint ?? evlogOtlp?.endpoint ?? rootOtlp?.endpoint ?? process.env.NUXT_OTLP_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
140
+ serviceName: overrides?.serviceName ?? evlogOtlp?.serviceName ?? rootOtlp?.serviceName ?? process.env.NUXT_OTLP_SERVICE_NAME ?? process.env.OTEL_SERVICE_NAME,
141
+ headers: overrides?.headers ?? evlogOtlp?.headers ?? rootOtlp?.headers ?? getHeadersFromEnv(),
142
+ resourceAttributes: overrides?.resourceAttributes ?? evlogOtlp?.resourceAttributes ?? rootOtlp?.resourceAttributes,
143
+ timeout: overrides?.timeout ?? evlogOtlp?.timeout ?? rootOtlp?.timeout
144
+ };
145
+ if (!config.endpoint) {
146
+ console.error("[evlog/otlp] Missing endpoint. Set NUXT_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT env var, or pass to createOTLPDrain()");
147
+ return;
148
+ }
149
+ try {
150
+ await sendToOTLP(ctx.event, config);
151
+ } catch (error) {
152
+ console.error("[evlog/otlp] Failed to send event:", error);
153
+ }
154
+ };
155
+ }
156
+ async function sendToOTLP(event, config) {
157
+ await sendBatchToOTLP([event], config);
158
+ }
159
+ async function sendBatchToOTLP(events, config) {
160
+ if (events.length === 0) return;
161
+ const timeout = config.timeout ?? 5e3;
162
+ const url = `${config.endpoint.replace(/\/$/, "")}/v1/logs`;
163
+ const [firstEvent] = events;
164
+ const resourceAttributes = buildResourceAttributes(firstEvent, config);
165
+ const logRecords = events.map(toOTLPLogRecord);
166
+ const payload = {
167
+ resourceLogs: [
168
+ {
169
+ resource: { attributes: resourceAttributes },
170
+ scopeLogs: [
171
+ {
172
+ scope: { name: "evlog", version: "1.0.0" },
173
+ logRecords
174
+ }
175
+ ]
176
+ }
177
+ ]
178
+ };
179
+ const headers = {
180
+ "Content-Type": "application/json",
181
+ ...config.headers
182
+ };
183
+ const controller = new AbortController();
184
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
185
+ try {
186
+ const response = await fetch(url, {
187
+ method: "POST",
188
+ headers,
189
+ body: JSON.stringify(payload),
190
+ signal: controller.signal
191
+ });
192
+ if (!response.ok) {
193
+ const text = await response.text().catch(() => "Unknown error");
194
+ const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
195
+ throw new Error(`OTLP API error: ${response.status} ${response.statusText} - ${safeText}`);
196
+ }
197
+ } finally {
198
+ clearTimeout(timeoutId);
199
+ }
200
+ }
201
+
202
+ export { createOTLPDrain, sendBatchToOTLP, sendToOTLP, toOTLPLogRecord };
package/dist/error.d.mts CHANGED
@@ -16,16 +16,23 @@ import { ErrorOptions } from './types.mjs';
16
16
  * ```
17
17
  */
18
18
  declare class EvlogError extends Error {
19
+ /** HTTP status code */
19
20
  readonly status: number;
20
21
  readonly why?: string;
21
22
  readonly fix?: string;
22
23
  readonly link?: string;
23
24
  constructor(options: ErrorOptions | string);
25
+ /** HTTP status text (alias for message) */
26
+ get statusText(): string;
27
+ /** HTTP status code (alias for compatibility) */
24
28
  get statusCode(): number;
29
+ /** HTTP status message (alias for compatibility) */
30
+ get statusMessage(): string;
31
+ /** Structured data for serialization */
25
32
  get data(): {
26
- why: string | undefined;
27
- fix: string | undefined;
28
- link: string | undefined;
33
+ why?: string;
34
+ fix?: string;
35
+ link?: string;
29
36
  } | undefined;
30
37
  toString(): string;
31
38
  toJSON(): Record<string, unknown>;
@@ -34,7 +41,7 @@ declare class EvlogError extends Error {
34
41
  * Create a structured error with context for debugging and user-facing messages.
35
42
  *
36
43
  * @param options - Error message string or full options object
37
- * @returns EvlogError instance compatible with Nitro's error handling
44
+ * @returns EvlogError with HTTP metadata (`status`, `statusText`) and `data`; also includes `statusCode` and `statusMessage` for legacy compatibility
38
45
  *
39
46
  * @example
40
47
  * ```ts
@@ -52,6 +59,5 @@ declare class EvlogError extends Error {
52
59
  * ```
53
60
  */
54
61
  declare function createError(options: ErrorOptions | string): EvlogError;
55
- declare const createEvlogError: typeof createError;
56
62
 
57
- export { EvlogError, createError, createEvlogError };
63
+ export { EvlogError, createError, createError as createEvlogError };