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 +72 -0
- package/dist/adapters/axiom.d.mts +62 -0
- package/dist/adapters/axiom.d.ts +62 -0
- package/dist/adapters/axiom.mjs +65 -0
- package/dist/adapters/otlp.d.mts +83 -0
- package/dist/adapters/otlp.d.ts +83 -0
- package/dist/adapters/otlp.mjs +202 -0
- package/dist/error.d.mts +12 -6
- package/dist/error.d.ts +12 -6
- package/dist/error.mjs +21 -8
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +2 -1
- package/dist/logger.mjs +11 -6
- package/dist/nitro/errorHandler.d.mts +13 -0
- package/dist/nitro/errorHandler.d.ts +13 -0
- package/dist/nitro/errorHandler.mjs +41 -0
- package/dist/nitro/plugin.mjs +29 -3
- package/dist/nuxt/module.d.mts +70 -1
- package/dist/nuxt/module.d.ts +70 -1
- package/dist/nuxt/module.mjs +18 -2
- package/dist/runtime/client/log.d.mts +2 -1
- package/dist/runtime/client/log.d.ts +2 -1
- package/dist/runtime/client/log.mjs +25 -0
- package/dist/runtime/client/plugin.mjs +2 -1
- package/dist/runtime/server/routes/_evlog/ingest.post.d.mts +5 -0
- package/dist/runtime/server/routes/_evlog/ingest.post.d.ts +5 -0
- package/dist/runtime/server/routes/_evlog/ingest.post.mjs +87 -0
- package/dist/runtime/utils/parseError.mjs +6 -4
- package/dist/types.d.mts +33 -2
- package/dist/types.d.ts +33 -2
- package/dist/utils.d.mts +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.mjs +8 -4
- package/dist/workers.d.mts +45 -0
- package/dist/workers.d.ts +45 -0
- package/dist/workers.mjs +53 -0
- package/package.json +24 -2
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
|
|
27
|
-
fix
|
|
28
|
-
link
|
|
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
|
|
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 };
|