evlog 1.7.0 → 1.9.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 (81) hide show
  1. package/README.md +257 -61
  2. package/dist/_http-DVDwNag0.mjs +76 -0
  3. package/dist/_http-DVDwNag0.mjs.map +1 -0
  4. package/dist/_severity-CXfyvxQi.mjs +17 -0
  5. package/dist/_severity-CXfyvxQi.mjs.map +1 -0
  6. package/dist/adapters/axiom.d.mts +1 -0
  7. package/dist/adapters/axiom.d.mts.map +1 -1
  8. package/dist/adapters/axiom.mjs +40 -44
  9. package/dist/adapters/axiom.mjs.map +1 -1
  10. package/dist/adapters/better-stack.d.mts +1 -0
  11. package/dist/adapters/better-stack.d.mts.map +1 -1
  12. package/dist/adapters/better-stack.mjs +34 -45
  13. package/dist/adapters/better-stack.mjs.map +1 -1
  14. package/dist/adapters/otlp.d.mts +1 -0
  15. package/dist/adapters/otlp.d.mts.map +1 -1
  16. package/dist/adapters/otlp.mjs +61 -81
  17. package/dist/adapters/otlp.mjs.map +1 -1
  18. package/dist/adapters/posthog.d.mts +35 -1
  19. package/dist/adapters/posthog.d.mts.map +1 -1
  20. package/dist/adapters/posthog.mjs +91 -45
  21. package/dist/adapters/posthog.mjs.map +1 -1
  22. package/dist/adapters/sentry.d.mts +1 -0
  23. package/dist/adapters/sentry.d.mts.map +1 -1
  24. package/dist/adapters/sentry.mjs +41 -53
  25. package/dist/adapters/sentry.mjs.map +1 -1
  26. package/dist/browser.d.mts +63 -0
  27. package/dist/browser.d.mts.map +1 -0
  28. package/dist/browser.mjs +95 -0
  29. package/dist/browser.mjs.map +1 -0
  30. package/dist/index.d.mts +2 -2
  31. package/dist/index.mjs +2 -2
  32. package/dist/logger.d.mts +6 -2
  33. package/dist/logger.d.mts.map +1 -1
  34. package/dist/logger.mjs +56 -3
  35. package/dist/logger.mjs.map +1 -1
  36. package/dist/nitro/errorHandler.mjs +6 -17
  37. package/dist/nitro/errorHandler.mjs.map +1 -1
  38. package/dist/nitro/module.d.mts +11 -0
  39. package/dist/nitro/module.d.mts.map +1 -0
  40. package/dist/nitro/module.mjs +23 -0
  41. package/dist/nitro/module.mjs.map +1 -0
  42. package/dist/nitro/plugin.mjs +28 -52
  43. package/dist/nitro/plugin.mjs.map +1 -1
  44. package/dist/nitro/v3/errorHandler.d.mts +24 -0
  45. package/dist/nitro/v3/errorHandler.d.mts.map +1 -0
  46. package/dist/nitro/v3/errorHandler.mjs +36 -0
  47. package/dist/nitro/v3/errorHandler.mjs.map +1 -0
  48. package/dist/nitro/v3/index.d.mts +4 -0
  49. package/dist/nitro/v3/index.mjs +4 -0
  50. package/dist/nitro/v3/module.d.mts +10 -0
  51. package/dist/nitro/v3/module.d.mts.map +1 -0
  52. package/dist/nitro/v3/module.mjs +22 -0
  53. package/dist/nitro/v3/module.mjs.map +1 -0
  54. package/dist/nitro/v3/plugin.d.mts +14 -0
  55. package/dist/nitro/v3/plugin.d.mts.map +1 -0
  56. package/dist/nitro/v3/plugin.mjs +157 -0
  57. package/dist/nitro/v3/plugin.mjs.map +1 -0
  58. package/dist/nitro/v3/useLogger.d.mts +24 -0
  59. package/dist/nitro/v3/useLogger.d.mts.map +1 -0
  60. package/dist/nitro/v3/useLogger.mjs +27 -0
  61. package/dist/nitro/v3/useLogger.mjs.map +1 -0
  62. package/dist/nitro-D57TWGyN.mjs +73 -0
  63. package/dist/nitro-D57TWGyN.mjs.map +1 -0
  64. package/dist/nitro-D81NBVPi.d.mts +42 -0
  65. package/dist/nitro-D81NBVPi.d.mts.map +1 -0
  66. package/dist/nuxt/module.d.mts +12 -0
  67. package/dist/nuxt/module.d.mts.map +1 -1
  68. package/dist/nuxt/module.mjs +17 -2
  69. package/dist/nuxt/module.mjs.map +1 -1
  70. package/dist/runtime/client/log.d.mts +5 -2
  71. package/dist/runtime/client/log.d.mts.map +1 -1
  72. package/dist/runtime/client/log.mjs +16 -3
  73. package/dist/runtime/client/log.mjs.map +1 -1
  74. package/dist/runtime/client/plugin.mjs +1 -0
  75. package/dist/runtime/client/plugin.mjs.map +1 -1
  76. package/dist/runtime/server/routes/_evlog/ingest.post.mjs +1 -1
  77. package/dist/types.d.mts +32 -2
  78. package/dist/types.d.mts.map +1 -1
  79. package/package.json +30 -7
  80. package/dist/_utils-DZA9nou3.mjs +0 -23
  81. package/dist/_utils-DZA9nou3.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"axiom.mjs","names":[],"sources":["../../src/adapters/axiom.ts"],"sourcesContent":["import type { DrainContext, WideEvent } from '../types'\nimport { getRuntimeConfig } from './_utils'\n\nexport interface AxiomConfig {\n /** Axiom dataset name */\n dataset: string\n /** Axiom API token */\n token: string\n /** Organization ID (required for Personal Access Tokens) */\n orgId?: string\n /** Base URL for Axiom API. Default: https://api.axiom.co */\n baseUrl?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\n/**\n * Create a drain function for sending logs to Axiom.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createAxiomDrain()\n * 2. runtimeConfig.evlog.axiom\n * 3. runtimeConfig.axiom\n * 4. Environment variables: NUXT_AXIOM_*, AXIOM_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_AXIOM_TOKEN and NUXT_AXIOM_DATASET env vars\n * nitroApp.hooks.hook('evlog:drain', createAxiomDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createAxiomDrain({\n * dataset: 'my-dataset',\n * }))\n * ```\n */\nexport function createAxiomDrain(overrides?: Partial<AxiomConfig>): (ctx: DrainContext | DrainContext[]) => Promise<void> {\n return async (ctx: DrainContext | DrainContext[]) => {\n const contexts = Array.isArray(ctx) ? ctx : [ctx]\n if (contexts.length === 0) return\n\n const runtimeConfig = getRuntimeConfig()\n // Support both runtimeConfig.evlog.axiom and runtimeConfig.axiom\n const evlogAxiom = runtimeConfig?.evlog?.axiom\n const rootAxiom = runtimeConfig?.axiom\n\n // Build config with fallbacks: overrides > evlog.axiom > axiom > env vars (NUXT_AXIOM_* or AXIOM_*)\n const config: Partial<AxiomConfig> = {\n dataset: overrides?.dataset ?? evlogAxiom?.dataset ?? rootAxiom?.dataset ?? process.env.NUXT_AXIOM_DATASET ?? process.env.AXIOM_DATASET,\n token: overrides?.token ?? evlogAxiom?.token ?? rootAxiom?.token ?? process.env.NUXT_AXIOM_TOKEN ?? process.env.AXIOM_TOKEN,\n orgId: overrides?.orgId ?? evlogAxiom?.orgId ?? rootAxiom?.orgId ?? process.env.NUXT_AXIOM_ORG_ID ?? process.env.AXIOM_ORG_ID,\n baseUrl: overrides?.baseUrl ?? evlogAxiom?.baseUrl ?? rootAxiom?.baseUrl ?? process.env.NUXT_AXIOM_URL ?? process.env.AXIOM_URL,\n timeout: overrides?.timeout ?? evlogAxiom?.timeout ?? rootAxiom?.timeout,\n }\n\n if (!config.dataset || !config.token) {\n console.error('[evlog/axiom] Missing dataset or token. Set NUXT_AXIOM_TOKEN/NUXT_AXIOM_DATASET env vars or pass to createAxiomDrain()')\n return\n }\n\n try {\n await sendBatchToAxiom(contexts.map(c => c.event), config as AxiomConfig)\n } catch (error) {\n console.error('[evlog/axiom] Failed to send events to Axiom:', error)\n }\n }\n}\n\n/**\n * Send a single event to Axiom.\n *\n * @example\n * ```ts\n * await sendToAxiom(event, {\n * dataset: 'my-logs',\n * token: process.env.AXIOM_TOKEN!,\n * })\n * ```\n */\nexport async function sendToAxiom(event: WideEvent, config: AxiomConfig): Promise<void> {\n await sendBatchToAxiom([event], config)\n}\n\n/**\n * Send a batch of events to Axiom.\n *\n * @example\n * ```ts\n * await sendBatchToAxiom(events, {\n * dataset: 'my-logs',\n * token: process.env.AXIOM_TOKEN!,\n * })\n * ```\n */\nexport async function sendBatchToAxiom(events: WideEvent[], config: AxiomConfig): Promise<void> {\n const baseUrl = config.baseUrl ?? 'https://api.axiom.co'\n const timeout = config.timeout ?? 5000\n const url = `${baseUrl}/v1/datasets/${encodeURIComponent(config.dataset)}/ingest`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.token}`,\n }\n\n if (config.orgId) {\n headers['X-Axiom-Org-Id'] = config.orgId\n }\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(events),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const text = await response.text().catch(() => 'Unknown error')\n const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text\n throw new Error(`Axiom API error: ${response.status} ${response.statusText} - ${safeText}`)\n }\n } finally {\n clearTimeout(timeoutId)\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,iBAAiB,WAAyF;AACxH,QAAO,OAAO,QAAuC;EACnD,MAAM,WAAW,MAAM,QAAQ,IAAI,GAAG,MAAM,CAAC,IAAI;AACjD,MAAI,SAAS,WAAW,EAAG;EAE3B,MAAM,gBAAgB,kBAAkB;EAExC,MAAM,aAAa,eAAe,OAAO;EACzC,MAAM,YAAY,eAAe;EAGjC,MAAM,SAA+B;GACnC,SAAS,WAAW,WAAW,YAAY,WAAW,WAAW,WAAW,QAAQ,IAAI,sBAAsB,QAAQ,IAAI;GAC1H,OAAO,WAAW,SAAS,YAAY,SAAS,WAAW,SAAS,QAAQ,IAAI,oBAAoB,QAAQ,IAAI;GAChH,OAAO,WAAW,SAAS,YAAY,SAAS,WAAW,SAAS,QAAQ,IAAI,qBAAqB,QAAQ,IAAI;GACjH,SAAS,WAAW,WAAW,YAAY,WAAW,WAAW,WAAW,QAAQ,IAAI,kBAAkB,QAAQ,IAAI;GACtH,SAAS,WAAW,WAAW,YAAY,WAAW,WAAW;GAClE;AAED,MAAI,CAAC,OAAO,WAAW,CAAC,OAAO,OAAO;AACpC,WAAQ,MAAM,yHAAyH;AACvI;;AAGF,MAAI;AACF,SAAM,iBAAiB,SAAS,KAAI,MAAK,EAAE,MAAM,EAAE,OAAsB;WAClE,OAAO;AACd,WAAQ,MAAM,iDAAiD,MAAM;;;;;;;;;;;;;;;AAgB3E,eAAsB,YAAY,OAAkB,QAAoC;AACtF,OAAM,iBAAiB,CAAC,MAAM,EAAE,OAAO;;;;;;;;;;;;;AAczC,eAAsB,iBAAiB,QAAqB,QAAoC;CAC9F,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,MAAM,GAAG,QAAQ,eAAe,mBAAmB,OAAO,QAAQ,CAAC;CAEzE,MAAM,UAAkC;EACtC,gBAAgB;EAChB,iBAAiB,UAAU,OAAO;EACnC;AAED,KAAI,OAAO,MACT,SAAQ,oBAAoB,OAAO;CAGrC,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,QAAQ;AAE/D,KAAI;EACF,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ;GACR;GACA,MAAM,KAAK,UAAU,OAAO;GAC5B,QAAQ,WAAW;GACpB,CAAC;AAEF,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,YAAY,gBAAgB;GAC/D,MAAM,WAAW,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,kBAAkB;AAC7E,SAAM,IAAI,MAAM,oBAAoB,SAAS,OAAO,GAAG,SAAS,WAAW,KAAK,WAAW;;WAErF;AACR,eAAa,UAAU"}
1
+ {"version":3,"file":"axiom.mjs","names":[],"sources":["../../src/adapters/axiom.ts"],"sourcesContent":["import type { WideEvent } from '../types'\nimport type { ConfigField } from './_config'\nimport { resolveAdapterConfig } from './_config'\nimport { defineDrain } from './_drain'\nimport { httpPost } from './_http'\n\nexport interface AxiomConfig {\n /** Axiom dataset name */\n dataset: string\n /** Axiom API token */\n token: string\n /** Organization ID (required for Personal Access Tokens) */\n orgId?: string\n /** Base URL for Axiom API. Default: https://api.axiom.co */\n baseUrl?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\nconst AXIOM_FIELDS: ConfigField<AxiomConfig>[] = [\n { key: 'dataset', env: ['NUXT_AXIOM_DATASET', 'AXIOM_DATASET'] },\n { key: 'token', env: ['NUXT_AXIOM_TOKEN', 'AXIOM_TOKEN'] },\n { key: 'orgId', env: ['NUXT_AXIOM_ORG_ID', 'AXIOM_ORG_ID'] },\n { key: 'baseUrl', env: ['NUXT_AXIOM_URL', 'AXIOM_URL'] },\n { key: 'timeout' },\n]\n\n/**\n * Create a drain function for sending logs to Axiom.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createAxiomDrain()\n * 2. runtimeConfig.evlog.axiom\n * 3. runtimeConfig.axiom\n * 4. Environment variables: NUXT_AXIOM_*, AXIOM_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_AXIOM_TOKEN and NUXT_AXIOM_DATASET env vars\n * nitroApp.hooks.hook('evlog:drain', createAxiomDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createAxiomDrain({\n * dataset: 'my-dataset',\n * }))\n * ```\n */\nexport function createAxiomDrain(overrides?: Partial<AxiomConfig>) {\n return defineDrain<AxiomConfig>({\n name: 'axiom',\n resolve: () => {\n const config = resolveAdapterConfig<AxiomConfig>('axiom', AXIOM_FIELDS, overrides)\n if (!config.dataset || !config.token) {\n console.error('[evlog/axiom] Missing dataset or token. Set NUXT_AXIOM_TOKEN/NUXT_AXIOM_DATASET env vars or pass to createAxiomDrain()')\n return null\n }\n return config as AxiomConfig\n },\n send: sendBatchToAxiom,\n })\n}\n\n/**\n * Send a single event to Axiom.\n *\n * @example\n * ```ts\n * await sendToAxiom(event, {\n * dataset: 'my-logs',\n * token: process.env.AXIOM_TOKEN!,\n * })\n * ```\n */\nexport async function sendToAxiom(event: WideEvent, config: AxiomConfig): Promise<void> {\n await sendBatchToAxiom([event], config)\n}\n\n/**\n * Send a batch of events to Axiom.\n *\n * @example\n * ```ts\n * await sendBatchToAxiom(events, {\n * dataset: 'my-logs',\n * token: process.env.AXIOM_TOKEN!,\n * })\n * ```\n */\nexport async function sendBatchToAxiom(events: WideEvent[], config: AxiomConfig): Promise<void> {\n const baseUrl = config.baseUrl ?? 'https://api.axiom.co'\n const url = `${baseUrl}/v1/datasets/${encodeURIComponent(config.dataset)}/ingest`\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.token}`,\n }\n\n if (config.orgId) {\n headers['X-Axiom-Org-Id'] = config.orgId\n }\n\n await httpPost({\n url,\n headers,\n body: JSON.stringify(events),\n timeout: config.timeout ?? 5000,\n label: 'Axiom',\n })\n}\n"],"mappings":";;;AAmBA,MAAM,eAA2C;CAC/C;EAAE,KAAK;EAAW,KAAK,CAAC,sBAAsB,gBAAgB;EAAE;CAChE;EAAE,KAAK;EAAS,KAAK,CAAC,oBAAoB,cAAc;EAAE;CAC1D;EAAE,KAAK;EAAS,KAAK,CAAC,qBAAqB,eAAe;EAAE;CAC5D;EAAE,KAAK;EAAW,KAAK,CAAC,kBAAkB,YAAY;EAAE;CACxD,EAAE,KAAK,WAAW;CACnB;;;;;;;;;;;;;;;;;;;;;AAsBD,SAAgB,iBAAiB,WAAkC;AACjE,QAAO,YAAyB;EAC9B,MAAM;EACN,eAAe;GACb,MAAM,SAAS,qBAAkC,SAAS,cAAc,UAAU;AAClF,OAAI,CAAC,OAAO,WAAW,CAAC,OAAO,OAAO;AACpC,YAAQ,MAAM,yHAAyH;AACvI,WAAO;;AAET,UAAO;;EAET,MAAM;EACP,CAAC;;;;;;;;;;;;;AAcJ,eAAsB,YAAY,OAAkB,QAAoC;AACtF,OAAM,iBAAiB,CAAC,MAAM,EAAE,OAAO;;;;;;;;;;;;;AAczC,eAAsB,iBAAiB,QAAqB,QAAoC;CAE9F,MAAM,MAAM,GADI,OAAO,WAAW,uBACX,eAAe,mBAAmB,OAAO,QAAQ,CAAC;CAEzE,MAAM,UAAkC;EACtC,gBAAgB;EAChB,iBAAiB,UAAU,OAAO;EACnC;AAED,KAAI,OAAO,MACT,SAAQ,oBAAoB,OAAO;AAGrC,OAAM,SAAS;EACb;EACA;EACA,MAAM,KAAK,UAAU,OAAO;EAC5B,SAAS,OAAO,WAAW;EAC3B,OAAO;EACR,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { DrainContext, WideEvent } from "../types.mjs";
2
+ import "../index.mjs";
2
3
 
3
4
  //#region src/adapters/better-stack.d.ts
4
5
  interface BetterStackConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"better-stack.d.mts","names":[],"sources":["../../src/adapters/better-stack.ts"],"mappings":";;;UAGiB,iBAAA;;EAEf,WAAA;EAFgC;EAIhC,QAAA;EAJgC;EAMhC,OAAA;AAAA;;;;AAOF;iBAAgB,kBAAA,CAAmB,KAAA,EAAO,SAAA,GAAY,MAAA;;;;;;;;AAyBtD;;;;;;;;;;;;;iBAAgB,sBAAA,CAAuB,SAAA,GAAY,OAAA,CAAQ,iBAAA,KAAsB,GAAA,EAAK,YAAA,GAAe,YAAA,OAAmB,OAAA;;;;;;AAsCxH;;;;;iBAAsB,iBAAA,CAAkB,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,iBAAA,GAAoB,OAAA;;;;;;;;;;AActF;iBAAsB,sBAAA,CAAuB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,iBAAA,GAAoB,OAAA"}
1
+ {"version":3,"file":"better-stack.d.mts","names":[],"sources":["../../src/adapters/better-stack.ts"],"mappings":";;;;UAMiB,iBAAA;;EAEf,WAAA;EAFe;EAIf,QAAA;;EAEA,OAAA;AAAA;;;;;iBAac,kBAAA,CAAmB,KAAA,EAAO,SAAA,GAAY,MAAA;;;;;;;;;AAyBtD;;;;;;;;;;;;iBAAgB,sBAAA,CAAuB,SAAA,GAAY,OAAA,CAAQ,iBAAA,KAAkB,GAAA,EAAnB,YAAA,GAAmB,YAAA,OAAA,OAAA;;;;;;;AAyB7E;;;;iBAAsB,iBAAA,CAAkB,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,iBAAA,GAAoB,OAAA;;;;;;;;;;;iBAchE,sBAAA,CAAuB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,iBAAA,GAAoB,OAAA"}
@@ -1,6 +1,17 @@
1
- import { t as getRuntimeConfig } from "../_utils-DZA9nou3.mjs";
1
+ import { n as defineDrain, r as resolveAdapterConfig, t as httpPost } from "../_http-DVDwNag0.mjs";
2
2
 
3
3
  //#region src/adapters/better-stack.ts
4
+ const BETTER_STACK_FIELDS = [
5
+ {
6
+ key: "sourceToken",
7
+ env: ["NUXT_BETTER_STACK_SOURCE_TOKEN", "BETTER_STACK_SOURCE_TOKEN"]
8
+ },
9
+ {
10
+ key: "endpoint",
11
+ env: ["NUXT_BETTER_STACK_ENDPOINT", "BETTER_STACK_ENDPOINT"]
12
+ },
13
+ { key: "timeout" }
14
+ ];
4
15
  /**
5
16
  * Transform an evlog wide event into a Better Stack event.
6
17
  * Maps `timestamp` to `dt` (Better Stack's expected field).
@@ -33,27 +44,18 @@ function toBetterStackEvent(event) {
33
44
  * ```
34
45
  */
35
46
  function createBetterStackDrain(overrides) {
36
- return async (ctx) => {
37
- const contexts = Array.isArray(ctx) ? ctx : [ctx];
38
- if (contexts.length === 0) return;
39
- const runtimeConfig = getRuntimeConfig();
40
- const evlogBetterStack = runtimeConfig?.evlog?.betterStack;
41
- const rootBetterStack = runtimeConfig?.betterStack;
42
- const config = {
43
- sourceToken: overrides?.sourceToken ?? evlogBetterStack?.sourceToken ?? rootBetterStack?.sourceToken ?? process.env.NUXT_BETTER_STACK_SOURCE_TOKEN ?? process.env.BETTER_STACK_SOURCE_TOKEN,
44
- endpoint: overrides?.endpoint ?? evlogBetterStack?.endpoint ?? rootBetterStack?.endpoint ?? process.env.NUXT_BETTER_STACK_ENDPOINT ?? process.env.BETTER_STACK_ENDPOINT,
45
- timeout: overrides?.timeout ?? evlogBetterStack?.timeout ?? rootBetterStack?.timeout
46
- };
47
- if (!config.sourceToken) {
48
- console.error("[evlog/better-stack] Missing source token. Set NUXT_BETTER_STACK_SOURCE_TOKEN env var or pass to createBetterStackDrain()");
49
- return;
50
- }
51
- try {
52
- await sendBatchToBetterStack(contexts.map((c) => c.event), config);
53
- } catch (error) {
54
- console.error("[evlog/better-stack] Failed to send events to Better Stack:", error);
55
- }
56
- };
47
+ return defineDrain({
48
+ name: "better-stack",
49
+ resolve: () => {
50
+ const config = resolveAdapterConfig("betterStack", BETTER_STACK_FIELDS, overrides);
51
+ if (!config.sourceToken) {
52
+ console.error("[evlog/better-stack] Missing source token. Set NUXT_BETTER_STACK_SOURCE_TOKEN env var or pass to createBetterStackDrain()");
53
+ return null;
54
+ }
55
+ return config;
56
+ },
57
+ send: sendBatchToBetterStack
58
+ });
57
59
  }
58
60
  /**
59
61
  * Send a single event to Better Stack.
@@ -79,29 +81,16 @@ async function sendToBetterStack(event, config) {
79
81
  * ```
80
82
  */
81
83
  async function sendBatchToBetterStack(events, config) {
82
- const endpoint = (config.endpoint ?? "https://in.logs.betterstack.com").replace(/\/+$/, "");
83
- const timeout = config.timeout ?? 5e3;
84
- const headers = {
85
- "Content-Type": "application/json",
86
- "Authorization": `Bearer ${config.sourceToken}`
87
- };
88
- const controller = new AbortController();
89
- const timeoutId = setTimeout(() => controller.abort(), timeout);
90
- try {
91
- const response = await fetch(endpoint, {
92
- method: "POST",
93
- headers,
94
- body: JSON.stringify(events.map(toBetterStackEvent)),
95
- signal: controller.signal
96
- });
97
- if (!response.ok) {
98
- const text = await response.text().catch(() => "Unknown error");
99
- const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
100
- throw new Error(`Better Stack API error: ${response.status} ${response.statusText} - ${safeText}`);
101
- }
102
- } finally {
103
- clearTimeout(timeoutId);
104
- }
84
+ await httpPost({
85
+ url: (config.endpoint ?? "https://in.logs.betterstack.com").replace(/\/+$/, ""),
86
+ headers: {
87
+ "Content-Type": "application/json",
88
+ "Authorization": `Bearer ${config.sourceToken}`
89
+ },
90
+ body: JSON.stringify(events.map(toBetterStackEvent)),
91
+ timeout: config.timeout ?? 5e3,
92
+ label: "Better Stack"
93
+ });
105
94
  }
106
95
 
107
96
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"better-stack.mjs","names":[],"sources":["../../src/adapters/better-stack.ts"],"sourcesContent":["import type { DrainContext, WideEvent } from '../types'\nimport { getRuntimeConfig } from './_utils'\n\nexport interface BetterStackConfig {\n /** Better Stack source token */\n sourceToken: string\n /** Logtail ingestion endpoint. Default: https://in.logs.betterstack.com */\n endpoint?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\n/**\n * Transform an evlog wide event into a Better Stack event.\n * Maps `timestamp` to `dt` (Better Stack's expected field).\n */\nexport function toBetterStackEvent(event: WideEvent): Record<string, unknown> {\n const { timestamp, ...rest } = event\n return { ...rest, dt: timestamp }\n}\n\n/**\n * Create a drain function for sending logs to Better Stack.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createBetterStackDrain()\n * 2. runtimeConfig.evlog.betterStack\n * 3. runtimeConfig.betterStack\n * 4. Environment variables: NUXT_BETTER_STACK_*, BETTER_STACK_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_BETTER_STACK_SOURCE_TOKEN env var\n * nitroApp.hooks.hook('evlog:drain', createBetterStackDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createBetterStackDrain({\n * sourceToken: 'my-token',\n * }))\n * ```\n */\nexport function createBetterStackDrain(overrides?: Partial<BetterStackConfig>): (ctx: DrainContext | DrainContext[]) => Promise<void> {\n return async (ctx: DrainContext | DrainContext[]) => {\n const contexts = Array.isArray(ctx) ? ctx : [ctx]\n if (contexts.length === 0) return\n\n const runtimeConfig = getRuntimeConfig()\n const evlogBetterStack = runtimeConfig?.evlog?.betterStack\n const rootBetterStack = runtimeConfig?.betterStack\n\n const config: Partial<BetterStackConfig> = {\n sourceToken: overrides?.sourceToken ?? evlogBetterStack?.sourceToken ?? rootBetterStack?.sourceToken ?? process.env.NUXT_BETTER_STACK_SOURCE_TOKEN ?? process.env.BETTER_STACK_SOURCE_TOKEN,\n endpoint: overrides?.endpoint ?? evlogBetterStack?.endpoint ?? rootBetterStack?.endpoint ?? process.env.NUXT_BETTER_STACK_ENDPOINT ?? process.env.BETTER_STACK_ENDPOINT,\n timeout: overrides?.timeout ?? evlogBetterStack?.timeout ?? rootBetterStack?.timeout,\n }\n\n if (!config.sourceToken) {\n console.error('[evlog/better-stack] Missing source token. Set NUXT_BETTER_STACK_SOURCE_TOKEN env var or pass to createBetterStackDrain()')\n return\n }\n\n try {\n await sendBatchToBetterStack(contexts.map(c => c.event), config as BetterStackConfig)\n } catch (error) {\n console.error('[evlog/better-stack] Failed to send events to Better Stack:', error)\n }\n }\n}\n\n/**\n * Send a single event to Better Stack.\n *\n * @example\n * ```ts\n * await sendToBetterStack(event, {\n * sourceToken: process.env.BETTER_STACK_SOURCE_TOKEN!,\n * })\n * ```\n */\nexport async function sendToBetterStack(event: WideEvent, config: BetterStackConfig): Promise<void> {\n await sendBatchToBetterStack([event], config)\n}\n\n/**\n * Send a batch of events to Better Stack.\n *\n * @example\n * ```ts\n * await sendBatchToBetterStack(events, {\n * sourceToken: process.env.BETTER_STACK_SOURCE_TOKEN!,\n * })\n * ```\n */\nexport async function sendBatchToBetterStack(events: WideEvent[], config: BetterStackConfig): Promise<void> {\n const endpoint = (config.endpoint ?? 'https://in.logs.betterstack.com').replace(/\\/+$/, '')\n const timeout = config.timeout ?? 5000\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.sourceToken}`,\n }\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n const response = await fetch(endpoint, {\n method: 'POST',\n headers,\n body: JSON.stringify(events.map(toBetterStackEvent)),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const text = await response.text().catch(() => 'Unknown error')\n const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text\n throw new Error(`Better Stack API error: ${response.status} ${response.statusText} - ${safeText}`)\n }\n } finally {\n clearTimeout(timeoutId)\n }\n}\n"],"mappings":";;;;;;;AAgBA,SAAgB,mBAAmB,OAA2C;CAC5E,MAAM,EAAE,WAAW,GAAG,SAAS;AAC/B,QAAO;EAAE,GAAG;EAAM,IAAI;EAAW;;;;;;;;;;;;;;;;;;;;;;AAuBnC,SAAgB,uBAAuB,WAA+F;AACpI,QAAO,OAAO,QAAuC;EACnD,MAAM,WAAW,MAAM,QAAQ,IAAI,GAAG,MAAM,CAAC,IAAI;AACjD,MAAI,SAAS,WAAW,EAAG;EAE3B,MAAM,gBAAgB,kBAAkB;EACxC,MAAM,mBAAmB,eAAe,OAAO;EAC/C,MAAM,kBAAkB,eAAe;EAEvC,MAAM,SAAqC;GACzC,aAAa,WAAW,eAAe,kBAAkB,eAAe,iBAAiB,eAAe,QAAQ,IAAI,kCAAkC,QAAQ,IAAI;GAClK,UAAU,WAAW,YAAY,kBAAkB,YAAY,iBAAiB,YAAY,QAAQ,IAAI,8BAA8B,QAAQ,IAAI;GAClJ,SAAS,WAAW,WAAW,kBAAkB,WAAW,iBAAiB;GAC9E;AAED,MAAI,CAAC,OAAO,aAAa;AACvB,WAAQ,MAAM,4HAA4H;AAC1I;;AAGF,MAAI;AACF,SAAM,uBAAuB,SAAS,KAAI,MAAK,EAAE,MAAM,EAAE,OAA4B;WAC9E,OAAO;AACd,WAAQ,MAAM,+DAA+D,MAAM;;;;;;;;;;;;;;AAezF,eAAsB,kBAAkB,OAAkB,QAA0C;AAClG,OAAM,uBAAuB,CAAC,MAAM,EAAE,OAAO;;;;;;;;;;;;AAa/C,eAAsB,uBAAuB,QAAqB,QAA0C;CAC1G,MAAM,YAAY,OAAO,YAAY,mCAAmC,QAAQ,QAAQ,GAAG;CAC3F,MAAM,UAAU,OAAO,WAAW;CAElC,MAAM,UAAkC;EACtC,gBAAgB;EAChB,iBAAiB,UAAU,OAAO;EACnC;CAED,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,QAAQ;AAE/D,KAAI;EACF,MAAM,WAAW,MAAM,MAAM,UAAU;GACrC,QAAQ;GACR;GACA,MAAM,KAAK,UAAU,OAAO,IAAI,mBAAmB,CAAC;GACpD,QAAQ,WAAW;GACpB,CAAC;AAEF,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,YAAY,gBAAgB;GAC/D,MAAM,WAAW,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,kBAAkB;AAC7E,SAAM,IAAI,MAAM,2BAA2B,SAAS,OAAO,GAAG,SAAS,WAAW,KAAK,WAAW;;WAE5F;AACR,eAAa,UAAU"}
1
+ {"version":3,"file":"better-stack.mjs","names":[],"sources":["../../src/adapters/better-stack.ts"],"sourcesContent":["import type { WideEvent } from '../types'\nimport type { ConfigField } from './_config'\nimport { resolveAdapterConfig } from './_config'\nimport { defineDrain } from './_drain'\nimport { httpPost } from './_http'\n\nexport interface BetterStackConfig {\n /** Better Stack source token */\n sourceToken: string\n /** Logtail ingestion endpoint. Default: https://in.logs.betterstack.com */\n endpoint?: string\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\nconst BETTER_STACK_FIELDS: ConfigField<BetterStackConfig>[] = [\n { key: 'sourceToken', env: ['NUXT_BETTER_STACK_SOURCE_TOKEN', 'BETTER_STACK_SOURCE_TOKEN'] },\n { key: 'endpoint', env: ['NUXT_BETTER_STACK_ENDPOINT', 'BETTER_STACK_ENDPOINT'] },\n { key: 'timeout' },\n]\n\n/**\n * Transform an evlog wide event into a Better Stack event.\n * Maps `timestamp` to `dt` (Better Stack's expected field).\n */\nexport function toBetterStackEvent(event: WideEvent): Record<string, unknown> {\n const { timestamp, ...rest } = event\n return { ...rest, dt: timestamp }\n}\n\n/**\n * Create a drain function for sending logs to Better Stack.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createBetterStackDrain()\n * 2. runtimeConfig.evlog.betterStack\n * 3. runtimeConfig.betterStack\n * 4. Environment variables: NUXT_BETTER_STACK_*, BETTER_STACK_*\n *\n * @example\n * ```ts\n * // Zero config - just set NUXT_BETTER_STACK_SOURCE_TOKEN env var\n * nitroApp.hooks.hook('evlog:drain', createBetterStackDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createBetterStackDrain({\n * sourceToken: 'my-token',\n * }))\n * ```\n */\nexport function createBetterStackDrain(overrides?: Partial<BetterStackConfig>) {\n return defineDrain<BetterStackConfig>({\n name: 'better-stack',\n resolve: () => {\n const config = resolveAdapterConfig<BetterStackConfig>('betterStack', BETTER_STACK_FIELDS, overrides)\n if (!config.sourceToken) {\n console.error('[evlog/better-stack] Missing source token. Set NUXT_BETTER_STACK_SOURCE_TOKEN env var or pass to createBetterStackDrain()')\n return null\n }\n return config as BetterStackConfig\n },\n send: sendBatchToBetterStack,\n })\n}\n\n/**\n * Send a single event to Better Stack.\n *\n * @example\n * ```ts\n * await sendToBetterStack(event, {\n * sourceToken: process.env.BETTER_STACK_SOURCE_TOKEN!,\n * })\n * ```\n */\nexport async function sendToBetterStack(event: WideEvent, config: BetterStackConfig): Promise<void> {\n await sendBatchToBetterStack([event], config)\n}\n\n/**\n * Send a batch of events to Better Stack.\n *\n * @example\n * ```ts\n * await sendBatchToBetterStack(events, {\n * sourceToken: process.env.BETTER_STACK_SOURCE_TOKEN!,\n * })\n * ```\n */\nexport async function sendBatchToBetterStack(events: WideEvent[], config: BetterStackConfig): Promise<void> {\n const endpoint = (config.endpoint ?? 'https://in.logs.betterstack.com').replace(/\\/+$/, '')\n\n await httpPost({\n url: endpoint,\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${config.sourceToken}`,\n },\n body: JSON.stringify(events.map(toBetterStackEvent)),\n timeout: config.timeout ?? 5000,\n label: 'Better Stack',\n })\n}\n"],"mappings":";;;AAeA,MAAM,sBAAwD;CAC5D;EAAE,KAAK;EAAe,KAAK,CAAC,kCAAkC,4BAA4B;EAAE;CAC5F;EAAE,KAAK;EAAY,KAAK,CAAC,8BAA8B,wBAAwB;EAAE;CACjF,EAAE,KAAK,WAAW;CACnB;;;;;AAMD,SAAgB,mBAAmB,OAA2C;CAC5E,MAAM,EAAE,WAAW,GAAG,SAAS;AAC/B,QAAO;EAAE,GAAG;EAAM,IAAI;EAAW;;;;;;;;;;;;;;;;;;;;;;AAuBnC,SAAgB,uBAAuB,WAAwC;AAC7E,QAAO,YAA+B;EACpC,MAAM;EACN,eAAe;GACb,MAAM,SAAS,qBAAwC,eAAe,qBAAqB,UAAU;AACrG,OAAI,CAAC,OAAO,aAAa;AACvB,YAAQ,MAAM,4HAA4H;AAC1I,WAAO;;AAET,UAAO;;EAET,MAAM;EACP,CAAC;;;;;;;;;;;;AAaJ,eAAsB,kBAAkB,OAAkB,QAA0C;AAClG,OAAM,uBAAuB,CAAC,MAAM,EAAE,OAAO;;;;;;;;;;;;AAa/C,eAAsB,uBAAuB,QAAqB,QAA0C;AAG1G,OAAM,SAAS;EACb,MAHgB,OAAO,YAAY,mCAAmC,QAAQ,QAAQ,GAAG;EAIzF,SAAS;GACP,gBAAgB;GAChB,iBAAiB,UAAU,OAAO;GACnC;EACD,MAAM,KAAK,UAAU,OAAO,IAAI,mBAAmB,CAAC;EACpD,SAAS,OAAO,WAAW;EAC3B,OAAO;EACR,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { DrainContext, WideEvent } from "../types.mjs";
2
+ import "../index.mjs";
2
3
 
3
4
  //#region src/adapters/otlp.d.ts
4
5
  interface OTLPConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"otlp.d.mts","names":[],"sources":["../../src/adapters/otlp.ts"],"mappings":";;;UAGiB,UAAA;;EAEf,QAAA;EAFyB;EAIzB,WAAA;EAIgB;EAFhB,kBAAA,GAAqB,MAAA;EAFrB;EAIA,OAAA,GAAU,MAAA;EAFW;EAIrB,OAAA;AAAA;;UAIe,aAAA;EACf,YAAA;EACA,cAAA;EACA,YAAA;EACA,IAAA;IAAQ,WAAA;EAAA;EACR,UAAA,EAAY,KAAA;IACV,GAAA;IACA,KAAA;MAAS,WAAA;MAAsB,QAAA;MAAmB,SAAA;IAAA;EAAA;EAEpD,OAAA;EACA,MAAA;AAAA;;;;iBAkEc,eAAA,CAAgB,KAAA,EAAO,SAAA,GAAY,aAAA;AAAnD;;;;;;;;;AA4HA;;;;;;;;;;;AA5HA,iBA4HgB,eAAA,CAAgB,SAAA,GAAY,OAAA,CAAQ,UAAA,KAAe,GAAA,EAAK,YAAA,GAAe,YAAA,OAAmB,OAAA;;;;;;;;AA0E1G;;;iBAAsB,UAAA,CAAW,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,UAAA,GAAa,OAAA;;;;;;;;;;;iBAclD,eAAA,CAAgB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,UAAA,GAAa,OAAA"}
1
+ {"version":3,"file":"otlp.d.mts","names":[],"sources":["../../src/adapters/otlp.ts"],"mappings":";;;;UAOiB,UAAA;;EAEf,QAAA;EAFe;EAIf,WAAA;;EAEA,kBAAA,GAAqB,MAAA;EAJrB;EAMA,OAAA,GAAU,MAAA;EAFV;EAIA,OAAA;AAAA;;UAIe,aAAA;EACf,YAAA;EACA,cAAA;EACA,YAAA;EACA,IAAA;IAAQ,WAAA;EAAA;EACR,UAAA,EAAY,KAAA;IACV,GAAA;IACA,KAAA;MAAS,WAAA;MAAsB,QAAA;MAAmB,SAAA;IAAA;EAAA;EAEpD,OAAA;EACA,MAAA;AAAA;;;;iBAwDc,eAAA,CAAgB,KAAA,EAAO,SAAA,GAAY,aAAA;;AAAnD;;;;;;;;;AA0JA;;;;;;;;;;iBAAgB,eAAA,CAAgB,SAAA,GAAY,OAAA,CAAQ,UAAA,KAAW,GAAA,EAAZ,YAAA,GAAY,YAAA,OAAA,OAAA;;;;;;;;;AA+B/D;;iBAAsB,UAAA,CAAW,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,UAAA,GAAa,OAAA;;;;;;;;;;;iBAclD,eAAA,CAAgB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,UAAA,GAAa,OAAA"}
@@ -1,22 +1,20 @@
1
- import { t as getRuntimeConfig } from "../_utils-DZA9nou3.mjs";
1
+ import { n as defineDrain, r as resolveAdapterConfig, t as httpPost } from "../_http-DVDwNag0.mjs";
2
+ import { n as OTEL_SEVERITY_TEXT, t as OTEL_SEVERITY_NUMBER } from "../_severity-CXfyvxQi.mjs";
2
3
 
3
4
  //#region src/adapters/otlp.ts
4
- /**
5
- * Map evlog levels to OTLP severity numbers.
6
- * Based on OpenTelemetry Logs Data Model specification.
7
- */
8
- const SEVERITY_MAP = {
9
- debug: 5,
10
- info: 9,
11
- warn: 13,
12
- error: 17
13
- };
14
- const SEVERITY_TEXT_MAP = {
15
- debug: "DEBUG",
16
- info: "INFO",
17
- warn: "WARN",
18
- error: "ERROR"
19
- };
5
+ const OTLP_FIELDS = [
6
+ {
7
+ key: "endpoint",
8
+ env: ["NUXT_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_ENDPOINT"]
9
+ },
10
+ {
11
+ key: "serviceName",
12
+ env: ["NUXT_OTLP_SERVICE_NAME", "OTEL_SERVICE_NAME"]
13
+ },
14
+ { key: "headers" },
15
+ { key: "resourceAttributes" },
16
+ { key: "timeout" }
17
+ ];
20
18
  /**
21
19
  * Convert a value to OTLP attribute value format.
22
20
  */
@@ -45,8 +43,8 @@ function toOTLPLogRecord(event) {
45
43
  });
46
44
  const record = {
47
45
  timeUnixNano: String(timestamp),
48
- severityNumber: SEVERITY_MAP[level] ?? 9,
49
- severityText: SEVERITY_TEXT_MAP[level] ?? "INFO",
46
+ severityNumber: OTEL_SEVERITY_NUMBER[level] ?? 9,
47
+ severityText: OTEL_SEVERITY_TEXT[level] ?? "INFO",
50
48
  body: { stringValue: JSON.stringify(event) },
51
49
  attributes
52
50
  };
@@ -86,6 +84,28 @@ function buildResourceAttributes(event, config) {
86
84
  return attributes;
87
85
  }
88
86
  /**
87
+ * Build headers from OTEL env vars.
88
+ * Kept inline as OTLP-specific (parses OTEL_EXPORTER_OTLP_HEADERS=key=val,key=val).
89
+ */
90
+ function getHeadersFromEnv() {
91
+ const headersEnv = process.env.OTEL_EXPORTER_OTLP_HEADERS || process.env.NUXT_OTLP_HEADERS;
92
+ if (headersEnv) {
93
+ const headers = {};
94
+ const decoded = decodeURIComponent(headersEnv);
95
+ for (const pair of decoded.split(",")) {
96
+ const eqIndex = pair.indexOf("=");
97
+ if (eqIndex > 0) {
98
+ const key = pair.slice(0, eqIndex).trim();
99
+ const value = pair.slice(eqIndex + 1).trim();
100
+ if (key && value) headers[key] = value;
101
+ }
102
+ }
103
+ if (Object.keys(headers).length > 0) return headers;
104
+ }
105
+ const auth = process.env.NUXT_OTLP_AUTH;
106
+ if (auth) return { Authorization: auth };
107
+ }
108
+ /**
89
109
  * Create a drain function for sending logs to an OTLP endpoint.
90
110
  *
91
111
  * Configuration priority (highest to lowest):
@@ -106,47 +126,19 @@ function buildResourceAttributes(event, config) {
106
126
  * ```
107
127
  */
108
128
  function createOTLPDrain(overrides) {
109
- return async (ctx) => {
110
- const contexts = Array.isArray(ctx) ? ctx : [ctx];
111
- if (contexts.length === 0) return;
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) headers[key] = value;
126
- }
127
- }
128
- if (Object.keys(headers).length > 0) return headers;
129
+ return defineDrain({
130
+ name: "otlp",
131
+ resolve: () => {
132
+ const config = resolveAdapterConfig("otlp", OTLP_FIELDS, overrides);
133
+ if (!config.headers) config.headers = getHeadersFromEnv();
134
+ if (!config.endpoint) {
135
+ console.error("[evlog/otlp] Missing endpoint. Set NUXT_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT env var, or pass to createOTLPDrain()");
136
+ return null;
129
137
  }
130
- const auth = process.env.NUXT_OTLP_AUTH;
131
- if (auth) return { Authorization: auth };
132
- };
133
- const config = {
134
- endpoint: overrides?.endpoint ?? evlogOtlp?.endpoint ?? rootOtlp?.endpoint ?? process.env.NUXT_OTLP_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
135
- serviceName: overrides?.serviceName ?? evlogOtlp?.serviceName ?? rootOtlp?.serviceName ?? process.env.NUXT_OTLP_SERVICE_NAME ?? process.env.OTEL_SERVICE_NAME,
136
- headers: overrides?.headers ?? evlogOtlp?.headers ?? rootOtlp?.headers ?? getHeadersFromEnv(),
137
- resourceAttributes: overrides?.resourceAttributes ?? evlogOtlp?.resourceAttributes ?? rootOtlp?.resourceAttributes,
138
- timeout: overrides?.timeout ?? evlogOtlp?.timeout ?? rootOtlp?.timeout
139
- };
140
- if (!config.endpoint) {
141
- console.error("[evlog/otlp] Missing endpoint. Set NUXT_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT env var, or pass to createOTLPDrain()");
142
- return;
143
- }
144
- try {
145
- await sendBatchToOTLP(contexts.map((c) => c.event), config);
146
- } catch (error) {
147
- console.error("[evlog/otlp] Failed to send events to OTLP:", error);
148
- }
149
- };
138
+ return config;
139
+ },
140
+ send: sendBatchToOTLP
141
+ });
150
142
  }
151
143
  /**
152
144
  * Send a single event to an OTLP endpoint.
@@ -173,7 +165,6 @@ async function sendToOTLP(event, config) {
173
165
  */
174
166
  async function sendBatchToOTLP(events, config) {
175
167
  if (events.length === 0) return;
176
- const timeout = config.timeout ?? 5e3;
177
168
  const url = `${config.endpoint.replace(/\/$/, "")}/v1/logs`;
178
169
  const [firstEvent] = events;
179
170
  const resourceAttributes = buildResourceAttributes(firstEvent, config);
@@ -188,27 +179,16 @@ async function sendBatchToOTLP(events, config) {
188
179
  logRecords
189
180
  }]
190
181
  }] };
191
- const headers = {
192
- "Content-Type": "application/json",
193
- ...config.headers
194
- };
195
- const controller = new AbortController();
196
- const timeoutId = setTimeout(() => controller.abort(), timeout);
197
- try {
198
- const response = await fetch(url, {
199
- method: "POST",
200
- headers,
201
- body: JSON.stringify(payload),
202
- signal: controller.signal
203
- });
204
- if (!response.ok) {
205
- const text = await response.text().catch(() => "Unknown error");
206
- const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text;
207
- throw new Error(`OTLP API error: ${response.status} ${response.statusText} - ${safeText}`);
208
- }
209
- } finally {
210
- clearTimeout(timeoutId);
211
- }
182
+ await httpPost({
183
+ url,
184
+ headers: {
185
+ "Content-Type": "application/json",
186
+ ...config.headers
187
+ },
188
+ body: JSON.stringify(payload),
189
+ timeout: config.timeout ?? 5e3,
190
+ label: "OTLP"
191
+ });
212
192
  }
213
193
 
214
194
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"otlp.mjs","names":[],"sources":["../../src/adapters/otlp.ts"],"sourcesContent":["import type { DrainContext, LogLevel, WideEvent } from '../types'\nimport { getRuntimeConfig } from './_utils'\n\nexport interface OTLPConfig {\n /** OTLP HTTP endpoint (e.g., http://localhost:4318) */\n endpoint: string\n /** Override service name (defaults to event.service) */\n serviceName?: string\n /** Additional resource attributes */\n resourceAttributes?: Record<string, string | number | boolean>\n /** Custom headers (e.g., for authentication) */\n headers?: Record<string, string>\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\n/** OTLP Log Record structure */\nexport interface OTLPLogRecord {\n timeUnixNano: string\n severityNumber: number\n severityText: string\n body: { stringValue: string }\n attributes: Array<{\n key: string\n value: { stringValue?: string, intValue?: string, boolValue?: boolean }\n }>\n traceId?: string\n spanId?: string\n}\n\n/** OTLP Resource structure */\ninterface OTLPResource {\n attributes: Array<{\n key: string\n value: { stringValue?: string, intValue?: string, boolValue?: boolean }\n }>\n}\n\n/** OTLP Scope structure */\ninterface OTLPScope {\n name: string\n version?: string\n}\n\n/** OTLP ExportLogsServiceRequest structure */\ninterface ExportLogsServiceRequest {\n resourceLogs: Array<{\n resource: OTLPResource\n scopeLogs: Array<{\n scope: OTLPScope\n logRecords: OTLPLogRecord[]\n }>\n }>\n}\n\n/**\n * Map evlog levels to OTLP severity numbers.\n * Based on OpenTelemetry Logs Data Model specification.\n */\nconst SEVERITY_MAP: Record<LogLevel, number> = {\n debug: 5, // DEBUG\n info: 9, // INFO\n warn: 13, // WARN\n error: 17, // ERROR\n}\n\nconst SEVERITY_TEXT_MAP: Record<LogLevel, string> = {\n debug: 'DEBUG',\n info: 'INFO',\n warn: 'WARN',\n error: 'ERROR',\n}\n\n/**\n * Convert a value to OTLP attribute value format.\n */\nfunction toAttributeValue(value: unknown): { stringValue?: string, intValue?: string, boolValue?: boolean } {\n if (typeof value === 'boolean') {\n return { boolValue: value }\n }\n if (typeof value === 'number' && Number.isInteger(value)) {\n return { intValue: String(value) }\n }\n if (typeof value === 'string') {\n return { stringValue: value }\n }\n // For complex types, serialize to JSON string\n return { stringValue: JSON.stringify(value) }\n}\n\n/**\n * Convert an evlog WideEvent to an OTLP LogRecord.\n */\nexport function toOTLPLogRecord(event: WideEvent): OTLPLogRecord {\n const timestamp = new Date(event.timestamp).getTime() * 1_000_000 // Convert to nanoseconds\n\n // Extract known fields, rest goes to attributes\n const { level, traceId, spanId, ...rest } = event\n // Remove base fields from rest (they're handled as resource attributes)\n delete (rest as Record<string, unknown>).timestamp\n delete (rest as Record<string, unknown>).service\n delete (rest as Record<string, unknown>).environment\n delete (rest as Record<string, unknown>).version\n delete (rest as Record<string, unknown>).commitHash\n delete (rest as Record<string, unknown>).region\n\n const attributes: OTLPLogRecord['attributes'] = []\n\n // Add all remaining event fields as attributes\n for (const [key, value] of Object.entries(rest)) {\n if (value !== undefined && value !== null) {\n attributes.push({\n key,\n value: toAttributeValue(value),\n })\n }\n }\n\n const record: OTLPLogRecord = {\n timeUnixNano: String(timestamp),\n severityNumber: SEVERITY_MAP[level] ?? 9,\n severityText: SEVERITY_TEXT_MAP[level] ?? 'INFO',\n body: { stringValue: JSON.stringify(event) },\n attributes,\n }\n\n // Add trace context if present\n if (typeof traceId === 'string') {\n record.traceId = traceId\n }\n if (typeof spanId === 'string') {\n record.spanId = spanId\n }\n\n return record\n}\n\n/**\n * Build OTLP resource attributes from event and config.\n */\nfunction buildResourceAttributes(\n event: WideEvent,\n config: OTLPConfig,\n): OTLPResource['attributes'] {\n const attributes: OTLPResource['attributes'] = []\n\n // Service name\n attributes.push({\n key: 'service.name',\n value: { stringValue: config.serviceName ?? event.service },\n })\n\n // Environment\n if (event.environment) {\n attributes.push({\n key: 'deployment.environment',\n value: { stringValue: event.environment },\n })\n }\n\n // Version\n if (event.version) {\n attributes.push({\n key: 'service.version',\n value: { stringValue: event.version },\n })\n }\n\n // Region\n if (event.region) {\n attributes.push({\n key: 'cloud.region',\n value: { stringValue: event.region },\n })\n }\n\n // Commit hash\n if (event.commitHash) {\n attributes.push({\n key: 'vcs.commit.id',\n value: { stringValue: event.commitHash },\n })\n }\n\n // Custom resource attributes from config\n if (config.resourceAttributes) {\n for (const [key, value] of Object.entries(config.resourceAttributes)) {\n attributes.push({\n key,\n value: toAttributeValue(value),\n })\n }\n }\n\n return attributes\n}\n\n/**\n * Create a drain function for sending logs to an OTLP endpoint.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createOTLPDrain()\n * 2. runtimeConfig.evlog.otlp (NUXT_EVLOG_OTLP_*)\n * 3. runtimeConfig.otlp (NUXT_OTLP_*)\n * 4. Environment variables: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME\n *\n * @example\n * ```ts\n * // Zero config - reads from runtimeConfig or env vars\n * nitroApp.hooks.hook('evlog:drain', createOTLPDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createOTLPDrain({\n * endpoint: 'http://localhost:4318',\n * }))\n * ```\n */\nexport function createOTLPDrain(overrides?: Partial<OTLPConfig>): (ctx: DrainContext | DrainContext[]) => Promise<void> {\n return async (ctx: DrainContext | DrainContext[]) => {\n const contexts = Array.isArray(ctx) ? ctx : [ctx]\n if (contexts.length === 0) return\n\n const runtimeConfig = getRuntimeConfig()\n // Support both runtimeConfig.evlog.otlp and runtimeConfig.otlp\n const evlogOtlp = runtimeConfig?.evlog?.otlp\n const rootOtlp = runtimeConfig?.otlp\n\n // Build headers from env vars (supports multiple formats)\n const getHeadersFromEnv = (): Record<string, string> | undefined => {\n // OTEL standard: OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic xxx\n // or Grafana format: Authorization=Basic%20xxx\n const headersEnv = process.env.OTEL_EXPORTER_OTLP_HEADERS || process.env.NUXT_OTLP_HEADERS\n if (headersEnv) {\n const headers: Record<string, string> = {}\n // Decode URL encoding if present\n const decoded = decodeURIComponent(headersEnv)\n // Parse key=value pairs (comma-separated)\n for (const pair of decoded.split(',')) {\n const eqIndex = pair.indexOf('=')\n if (eqIndex > 0) {\n const key = pair.slice(0, eqIndex).trim()\n const value = pair.slice(eqIndex + 1).trim()\n if (key && value) {\n headers[key] = value\n }\n }\n }\n if (Object.keys(headers).length > 0) return headers\n }\n\n // Simple format: NUXT_OTLP_AUTH=Basic xxx → Authorization: Basic xxx\n const auth = process.env.NUXT_OTLP_AUTH\n if (auth) {\n return { Authorization: auth }\n }\n\n return undefined\n }\n\n // Build config with fallbacks: overrides > evlog.otlp > otlp > env vars (NUXT_OTLP_* or OTEL_*)\n const config: Partial<OTLPConfig> = {\n endpoint: overrides?.endpoint ?? evlogOtlp?.endpoint ?? rootOtlp?.endpoint ?? process.env.NUXT_OTLP_ENDPOINT ?? process.env.OTEL_EXPORTER_OTLP_ENDPOINT,\n serviceName: overrides?.serviceName ?? evlogOtlp?.serviceName ?? rootOtlp?.serviceName ?? process.env.NUXT_OTLP_SERVICE_NAME ?? process.env.OTEL_SERVICE_NAME,\n headers: overrides?.headers ?? evlogOtlp?.headers ?? rootOtlp?.headers ?? getHeadersFromEnv(),\n resourceAttributes: overrides?.resourceAttributes ?? evlogOtlp?.resourceAttributes ?? rootOtlp?.resourceAttributes,\n timeout: overrides?.timeout ?? evlogOtlp?.timeout ?? rootOtlp?.timeout,\n }\n\n if (!config.endpoint) {\n console.error('[evlog/otlp] Missing endpoint. Set NUXT_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT env var, or pass to createOTLPDrain()')\n return\n }\n\n try {\n await sendBatchToOTLP(contexts.map(c => c.event), config as OTLPConfig)\n } catch (error) {\n console.error('[evlog/otlp] Failed to send events to OTLP:', error)\n }\n }\n}\n\n/**\n * Send a single event to an OTLP endpoint.\n *\n * @example\n * ```ts\n * await sendToOTLP(event, {\n * endpoint: 'http://localhost:4318',\n * })\n * ```\n */\nexport async function sendToOTLP(event: WideEvent, config: OTLPConfig): Promise<void> {\n await sendBatchToOTLP([event], config)\n}\n\n/**\n * Send a batch of events to an OTLP endpoint.\n *\n * @example\n * ```ts\n * await sendBatchToOTLP(events, {\n * endpoint: 'http://localhost:4318',\n * })\n * ```\n */\nexport async function sendBatchToOTLP(events: WideEvent[], config: OTLPConfig): Promise<void> {\n if (events.length === 0) return\n\n const timeout = config.timeout ?? 5000\n const url = `${config.endpoint.replace(/\\/$/, '')}/v1/logs`\n\n // Group events by service for proper resource attribution\n // For simplicity, we use the first event's resource attributes\n const [firstEvent] = events\n const resourceAttributes = buildResourceAttributes(firstEvent, config)\n\n const logRecords = events.map(toOTLPLogRecord)\n\n const payload: ExportLogsServiceRequest = {\n resourceLogs: [\n {\n resource: { attributes: resourceAttributes },\n scopeLogs: [\n {\n scope: { name: 'evlog', version: '1.0.0' },\n logRecords,\n },\n ],\n },\n ],\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...config.headers,\n }\n\n const controller = new AbortController()\n const timeoutId = setTimeout(() => controller.abort(), timeout)\n\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers,\n body: JSON.stringify(payload),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const text = await response.text().catch(() => 'Unknown error')\n const safeText = text.length > 200 ? `${text.slice(0, 200)}...[truncated]` : text\n throw new Error(`OTLP API error: ${response.status} ${response.statusText} - ${safeText}`)\n }\n } finally {\n clearTimeout(timeoutId)\n }\n}\n"],"mappings":";;;;;;;AA2DA,MAAM,eAAyC;CAC7C,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR;AAED,MAAM,oBAA8C;CAClD,OAAO;CACP,MAAM;CACN,MAAM;CACN,OAAO;CACR;;;;AAKD,SAAS,iBAAiB,OAAkF;AAC1G,KAAI,OAAO,UAAU,UACnB,QAAO,EAAE,WAAW,OAAO;AAE7B,KAAI,OAAO,UAAU,YAAY,OAAO,UAAU,MAAM,CACtD,QAAO,EAAE,UAAU,OAAO,MAAM,EAAE;AAEpC,KAAI,OAAO,UAAU,SACnB,QAAO,EAAE,aAAa,OAAO;AAG/B,QAAO,EAAE,aAAa,KAAK,UAAU,MAAM,EAAE;;;;;AAM/C,SAAgB,gBAAgB,OAAiC;CAC/D,MAAM,YAAY,IAAI,KAAK,MAAM,UAAU,CAAC,SAAS,GAAG;CAGxD,MAAM,EAAE,OAAO,SAAS,QAAQ,GAAG,SAAS;AAE5C,QAAQ,KAAiC;AACzC,QAAQ,KAAiC;AACzC,QAAQ,KAAiC;AACzC,QAAQ,KAAiC;AACzC,QAAQ,KAAiC;AACzC,QAAQ,KAAiC;CAEzC,MAAM,aAA0C,EAAE;AAGlD,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,KAAK,CAC7C,KAAI,UAAU,UAAa,UAAU,KACnC,YAAW,KAAK;EACd;EACA,OAAO,iBAAiB,MAAM;EAC/B,CAAC;CAIN,MAAM,SAAwB;EAC5B,cAAc,OAAO,UAAU;EAC/B,gBAAgB,aAAa,UAAU;EACvC,cAAc,kBAAkB,UAAU;EAC1C,MAAM,EAAE,aAAa,KAAK,UAAU,MAAM,EAAE;EAC5C;EACD;AAGD,KAAI,OAAO,YAAY,SACrB,QAAO,UAAU;AAEnB,KAAI,OAAO,WAAW,SACpB,QAAO,SAAS;AAGlB,QAAO;;;;;AAMT,SAAS,wBACP,OACA,QAC4B;CAC5B,MAAM,aAAyC,EAAE;AAGjD,YAAW,KAAK;EACd,KAAK;EACL,OAAO,EAAE,aAAa,OAAO,eAAe,MAAM,SAAS;EAC5D,CAAC;AAGF,KAAI,MAAM,YACR,YAAW,KAAK;EACd,KAAK;EACL,OAAO,EAAE,aAAa,MAAM,aAAa;EAC1C,CAAC;AAIJ,KAAI,MAAM,QACR,YAAW,KAAK;EACd,KAAK;EACL,OAAO,EAAE,aAAa,MAAM,SAAS;EACtC,CAAC;AAIJ,KAAI,MAAM,OACR,YAAW,KAAK;EACd,KAAK;EACL,OAAO,EAAE,aAAa,MAAM,QAAQ;EACrC,CAAC;AAIJ,KAAI,MAAM,WACR,YAAW,KAAK;EACd,KAAK;EACL,OAAO,EAAE,aAAa,MAAM,YAAY;EACzC,CAAC;AAIJ,KAAI,OAAO,mBACT,MAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,OAAO,mBAAmB,CAClE,YAAW,KAAK;EACd;EACA,OAAO,iBAAiB,MAAM;EAC/B,CAAC;AAIN,QAAO;;;;;;;;;;;;;;;;;;;;;;AAuBT,SAAgB,gBAAgB,WAAwF;AACtH,QAAO,OAAO,QAAuC;EACnD,MAAM,WAAW,MAAM,QAAQ,IAAI,GAAG,MAAM,CAAC,IAAI;AACjD,MAAI,SAAS,WAAW,EAAG;EAE3B,MAAM,gBAAgB,kBAAkB;EAExC,MAAM,YAAY,eAAe,OAAO;EACxC,MAAM,WAAW,eAAe;EAGhC,MAAM,0BAA8D;GAGlE,MAAM,aAAa,QAAQ,IAAI,8BAA8B,QAAQ,IAAI;AACzE,OAAI,YAAY;IACd,MAAM,UAAkC,EAAE;IAE1C,MAAM,UAAU,mBAAmB,WAAW;AAE9C,SAAK,MAAM,QAAQ,QAAQ,MAAM,IAAI,EAAE;KACrC,MAAM,UAAU,KAAK,QAAQ,IAAI;AACjC,SAAI,UAAU,GAAG;MACf,MAAM,MAAM,KAAK,MAAM,GAAG,QAAQ,CAAC,MAAM;MACzC,MAAM,QAAQ,KAAK,MAAM,UAAU,EAAE,CAAC,MAAM;AAC5C,UAAI,OAAO,MACT,SAAQ,OAAO;;;AAIrB,QAAI,OAAO,KAAK,QAAQ,CAAC,SAAS,EAAG,QAAO;;GAI9C,MAAM,OAAO,QAAQ,IAAI;AACzB,OAAI,KACF,QAAO,EAAE,eAAe,MAAM;;EAOlC,MAAM,SAA8B;GAClC,UAAU,WAAW,YAAY,WAAW,YAAY,UAAU,YAAY,QAAQ,IAAI,sBAAsB,QAAQ,IAAI;GAC5H,aAAa,WAAW,eAAe,WAAW,eAAe,UAAU,eAAe,QAAQ,IAAI,0BAA0B,QAAQ,IAAI;GAC5I,SAAS,WAAW,WAAW,WAAW,WAAW,UAAU,WAAW,mBAAmB;GAC7F,oBAAoB,WAAW,sBAAsB,WAAW,sBAAsB,UAAU;GAChG,SAAS,WAAW,WAAW,WAAW,WAAW,UAAU;GAChE;AAED,MAAI,CAAC,OAAO,UAAU;AACpB,WAAQ,MAAM,6HAA6H;AAC3I;;AAGF,MAAI;AACF,SAAM,gBAAgB,SAAS,KAAI,MAAK,EAAE,MAAM,EAAE,OAAqB;WAChE,OAAO;AACd,WAAQ,MAAM,+CAA+C,MAAM;;;;;;;;;;;;;;AAezE,eAAsB,WAAW,OAAkB,QAAmC;AACpF,OAAM,gBAAgB,CAAC,MAAM,EAAE,OAAO;;;;;;;;;;;;AAaxC,eAAsB,gBAAgB,QAAqB,QAAmC;AAC5F,KAAI,OAAO,WAAW,EAAG;CAEzB,MAAM,UAAU,OAAO,WAAW;CAClC,MAAM,MAAM,GAAG,OAAO,SAAS,QAAQ,OAAO,GAAG,CAAC;CAIlD,MAAM,CAAC,cAAc;CACrB,MAAM,qBAAqB,wBAAwB,YAAY,OAAO;CAEtE,MAAM,aAAa,OAAO,IAAI,gBAAgB;CAE9C,MAAM,UAAoC,EACxC,cAAc,CACZ;EACE,UAAU,EAAE,YAAY,oBAAoB;EAC5C,WAAW,CACT;GACE,OAAO;IAAE,MAAM;IAAS,SAAS;IAAS;GAC1C;GACD,CACF;EACF,CACF,EACF;CAED,MAAM,UAAkC;EACtC,gBAAgB;EAChB,GAAG,OAAO;EACX;CAED,MAAM,aAAa,IAAI,iBAAiB;CACxC,MAAM,YAAY,iBAAiB,WAAW,OAAO,EAAE,QAAQ;AAE/D,KAAI;EACF,MAAM,WAAW,MAAM,MAAM,KAAK;GAChC,QAAQ;GACR;GACA,MAAM,KAAK,UAAU,QAAQ;GAC7B,QAAQ,WAAW;GACpB,CAAC;AAEF,MAAI,CAAC,SAAS,IAAI;GAChB,MAAM,OAAO,MAAM,SAAS,MAAM,CAAC,YAAY,gBAAgB;GAC/D,MAAM,WAAW,KAAK,SAAS,MAAM,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC,kBAAkB;AAC7E,SAAM,IAAI,MAAM,mBAAmB,SAAS,OAAO,GAAG,SAAS,WAAW,KAAK,WAAW;;WAEpF;AACR,eAAa,UAAU"}
1
+ {"version":3,"file":"otlp.mjs","names":[],"sources":["../../src/adapters/otlp.ts"],"sourcesContent":["import type { WideEvent } from '../types'\nimport type { ConfigField } from './_config'\nimport { resolveAdapterConfig } from './_config'\nimport { defineDrain } from './_drain'\nimport { httpPost } from './_http'\nimport { OTEL_SEVERITY_NUMBER, OTEL_SEVERITY_TEXT } from './_severity'\n\nexport interface OTLPConfig {\n /** OTLP HTTP endpoint (e.g., http://localhost:4318) */\n endpoint: string\n /** Override service name (defaults to event.service) */\n serviceName?: string\n /** Additional resource attributes */\n resourceAttributes?: Record<string, string | number | boolean>\n /** Custom headers (e.g., for authentication) */\n headers?: Record<string, string>\n /** Request timeout in milliseconds. Default: 5000 */\n timeout?: number\n}\n\n/** OTLP Log Record structure */\nexport interface OTLPLogRecord {\n timeUnixNano: string\n severityNumber: number\n severityText: string\n body: { stringValue: string }\n attributes: Array<{\n key: string\n value: { stringValue?: string, intValue?: string, boolValue?: boolean }\n }>\n traceId?: string\n spanId?: string\n}\n\n/** OTLP Resource structure */\ninterface OTLPResource {\n attributes: Array<{\n key: string\n value: { stringValue?: string, intValue?: string, boolValue?: boolean }\n }>\n}\n\n/** OTLP Scope structure */\ninterface OTLPScope {\n name: string\n version?: string\n}\n\n/** OTLP ExportLogsServiceRequest structure */\ninterface ExportLogsServiceRequest {\n resourceLogs: Array<{\n resource: OTLPResource\n scopeLogs: Array<{\n scope: OTLPScope\n logRecords: OTLPLogRecord[]\n }>\n }>\n}\n\nconst OTLP_FIELDS: ConfigField<OTLPConfig>[] = [\n { key: 'endpoint', env: ['NUXT_OTLP_ENDPOINT', 'OTEL_EXPORTER_OTLP_ENDPOINT'] },\n { key: 'serviceName', env: ['NUXT_OTLP_SERVICE_NAME', 'OTEL_SERVICE_NAME'] },\n { key: 'headers' },\n { key: 'resourceAttributes' },\n { key: 'timeout' },\n]\n\n/**\n * Convert a value to OTLP attribute value format.\n */\nfunction toAttributeValue(value: unknown): { stringValue?: string, intValue?: string, boolValue?: boolean } {\n if (typeof value === 'boolean') {\n return { boolValue: value }\n }\n if (typeof value === 'number' && Number.isInteger(value)) {\n return { intValue: String(value) }\n }\n if (typeof value === 'string') {\n return { stringValue: value }\n }\n // For complex types, serialize to JSON string\n return { stringValue: JSON.stringify(value) }\n}\n\n/**\n * Convert an evlog WideEvent to an OTLP LogRecord.\n */\nexport function toOTLPLogRecord(event: WideEvent): OTLPLogRecord {\n const timestamp = new Date(event.timestamp).getTime() * 1_000_000 // Convert to nanoseconds\n\n // Extract known fields, rest goes to attributes\n const { level, traceId, spanId, ...rest } = event\n // Remove base fields from rest (they're handled as resource attributes)\n delete (rest as Record<string, unknown>).timestamp\n delete (rest as Record<string, unknown>).service\n delete (rest as Record<string, unknown>).environment\n delete (rest as Record<string, unknown>).version\n delete (rest as Record<string, unknown>).commitHash\n delete (rest as Record<string, unknown>).region\n\n const attributes: OTLPLogRecord['attributes'] = []\n\n // Add all remaining event fields as attributes\n for (const [key, value] of Object.entries(rest)) {\n if (value !== undefined && value !== null) {\n attributes.push({\n key,\n value: toAttributeValue(value),\n })\n }\n }\n\n const record: OTLPLogRecord = {\n timeUnixNano: String(timestamp),\n severityNumber: OTEL_SEVERITY_NUMBER[level] ?? 9,\n severityText: OTEL_SEVERITY_TEXT[level] ?? 'INFO',\n body: { stringValue: JSON.stringify(event) },\n attributes,\n }\n\n // Add trace context if present\n if (typeof traceId === 'string') {\n record.traceId = traceId\n }\n if (typeof spanId === 'string') {\n record.spanId = spanId\n }\n\n return record\n}\n\n/**\n * Build OTLP resource attributes from event and config.\n */\nfunction buildResourceAttributes(\n event: WideEvent,\n config: OTLPConfig,\n): OTLPResource['attributes'] {\n const attributes: OTLPResource['attributes'] = []\n\n // Service name\n attributes.push({\n key: 'service.name',\n value: { stringValue: config.serviceName ?? event.service },\n })\n\n // Environment\n if (event.environment) {\n attributes.push({\n key: 'deployment.environment',\n value: { stringValue: event.environment },\n })\n }\n\n // Version\n if (event.version) {\n attributes.push({\n key: 'service.version',\n value: { stringValue: event.version },\n })\n }\n\n // Region\n if (event.region) {\n attributes.push({\n key: 'cloud.region',\n value: { stringValue: event.region },\n })\n }\n\n // Commit hash\n if (event.commitHash) {\n attributes.push({\n key: 'vcs.commit.id',\n value: { stringValue: event.commitHash },\n })\n }\n\n // Custom resource attributes from config\n if (config.resourceAttributes) {\n for (const [key, value] of Object.entries(config.resourceAttributes)) {\n attributes.push({\n key,\n value: toAttributeValue(value),\n })\n }\n }\n\n return attributes\n}\n\n/**\n * Build headers from OTEL env vars.\n * Kept inline as OTLP-specific (parses OTEL_EXPORTER_OTLP_HEADERS=key=val,key=val).\n */\nfunction getHeadersFromEnv(): Record<string, string> | undefined {\n const headersEnv = process.env.OTEL_EXPORTER_OTLP_HEADERS || process.env.NUXT_OTLP_HEADERS\n if (headersEnv) {\n const headers: Record<string, string> = {}\n const decoded = decodeURIComponent(headersEnv)\n for (const pair of decoded.split(',')) {\n const eqIndex = pair.indexOf('=')\n if (eqIndex > 0) {\n const key = pair.slice(0, eqIndex).trim()\n const value = pair.slice(eqIndex + 1).trim()\n if (key && value) {\n headers[key] = value\n }\n }\n }\n if (Object.keys(headers).length > 0) return headers\n }\n\n const auth = process.env.NUXT_OTLP_AUTH\n if (auth) {\n return { Authorization: auth }\n }\n\n return undefined\n}\n\n/**\n * Create a drain function for sending logs to an OTLP endpoint.\n *\n * Configuration priority (highest to lowest):\n * 1. Overrides passed to createOTLPDrain()\n * 2. runtimeConfig.evlog.otlp (NUXT_EVLOG_OTLP_*)\n * 3. runtimeConfig.otlp (NUXT_OTLP_*)\n * 4. Environment variables: OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_SERVICE_NAME\n *\n * @example\n * ```ts\n * // Zero config - reads from runtimeConfig or env vars\n * nitroApp.hooks.hook('evlog:drain', createOTLPDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('evlog:drain', createOTLPDrain({\n * endpoint: 'http://localhost:4318',\n * }))\n * ```\n */\nexport function createOTLPDrain(overrides?: Partial<OTLPConfig>) {\n return defineDrain<OTLPConfig>({\n name: 'otlp',\n resolve: () => {\n const config = resolveAdapterConfig<OTLPConfig>('otlp', OTLP_FIELDS, overrides)\n\n // OTLP-specific: resolve headers from env if not provided via config\n if (!config.headers) {\n config.headers = getHeadersFromEnv()\n }\n\n if (!config.endpoint) {\n console.error('[evlog/otlp] Missing endpoint. Set NUXT_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_ENDPOINT env var, or pass to createOTLPDrain()')\n return null\n }\n return config as OTLPConfig\n },\n send: sendBatchToOTLP,\n })\n}\n\n/**\n * Send a single event to an OTLP endpoint.\n *\n * @example\n * ```ts\n * await sendToOTLP(event, {\n * endpoint: 'http://localhost:4318',\n * })\n * ```\n */\nexport async function sendToOTLP(event: WideEvent, config: OTLPConfig): Promise<void> {\n await sendBatchToOTLP([event], config)\n}\n\n/**\n * Send a batch of events to an OTLP endpoint.\n *\n * @example\n * ```ts\n * await sendBatchToOTLP(events, {\n * endpoint: 'http://localhost:4318',\n * })\n * ```\n */\nexport async function sendBatchToOTLP(events: WideEvent[], config: OTLPConfig): Promise<void> {\n if (events.length === 0) return\n\n const url = `${config.endpoint.replace(/\\/$/, '')}/v1/logs`\n\n // Group events by service for proper resource attribution\n // For simplicity, we use the first event's resource attributes\n const [firstEvent] = events\n const resourceAttributes = buildResourceAttributes(firstEvent, config)\n\n const logRecords = events.map(toOTLPLogRecord)\n\n const payload: ExportLogsServiceRequest = {\n resourceLogs: [\n {\n resource: { attributes: resourceAttributes },\n scopeLogs: [\n {\n scope: { name: 'evlog', version: '1.0.0' },\n logRecords,\n },\n ],\n },\n ],\n }\n\n const headers: Record<string, string> = {\n 'Content-Type': 'application/json',\n ...config.headers,\n }\n\n await httpPost({\n url,\n headers,\n body: JSON.stringify(payload),\n timeout: config.timeout ?? 5000,\n label: 'OTLP',\n })\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { DrainContext, WideEvent } from "../types.mjs";
2
+ import "../index.mjs";
2
3
 
3
4
  //#region src/adapters/posthog.d.ts
4
5
  interface PostHogConfig {
@@ -68,6 +69,39 @@ declare function sendToPostHog(event: WideEvent, config: PostHogConfig): Promise
68
69
  * ```
69
70
  */
70
71
  declare function sendBatchToPostHog(events: WideEvent[], config: PostHogConfig): Promise<void>;
72
+ interface PostHogLogsConfig {
73
+ /** PostHog project API key */
74
+ apiKey: string;
75
+ /** PostHog host URL. Default: https://us.i.posthog.com */
76
+ host?: string;
77
+ /** Request timeout in milliseconds. Default: 5000 */
78
+ timeout?: number;
79
+ }
80
+ /**
81
+ * Create a drain function for sending logs to PostHog Logs via OTLP.
82
+ *
83
+ * PostHog Logs uses the standard OTLP log format. This drain wraps
84
+ * `sendBatchToOTLP()` with PostHog-specific defaults (endpoint, auth).
85
+ *
86
+ * Configuration priority (highest to lowest):
87
+ * 1. Overrides passed to createPostHogLogsDrain()
88
+ * 2. runtimeConfig.evlog.posthog
89
+ * 3. runtimeConfig.posthog
90
+ * 4. Environment variables: NUXT_POSTHOG_*, POSTHOG_*
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * // Zero config - just set NUXT_POSTHOG_API_KEY env var
95
+ * nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain())
96
+ *
97
+ * // With overrides
98
+ * nitroApp.hooks.hook('evlog:drain', createPostHogLogsDrain({
99
+ * apiKey: 'phc_...',
100
+ * host: 'https://eu.i.posthog.com',
101
+ * }))
102
+ * ```
103
+ */
104
+ declare function createPostHogLogsDrain(overrides?: Partial<PostHogLogsConfig>): (ctx: DrainContext | DrainContext[]) => Promise<void>;
71
105
  //#endregion
72
- export { PostHogConfig, PostHogEvent, createPostHogDrain, sendBatchToPostHog, sendToPostHog, toPostHogEvent };
106
+ export { PostHogConfig, PostHogEvent, PostHogLogsConfig, createPostHogDrain, createPostHogLogsDrain, sendBatchToPostHog, sendToPostHog, toPostHogEvent };
73
107
  //# sourceMappingURL=posthog.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"posthog.d.mts","names":[],"sources":["../../src/adapters/posthog.ts"],"mappings":";;;UAGiB,aAAA;;EAEf,MAAA;EAF4B;EAI5B,IAAA;EAJ4B;EAM5B,SAAA;EAFA;EAIA,UAAA;EAAA;EAEA,OAAA;AAAA;;UAIe,YAAA;EACf,KAAA;EACA,WAAA;EACA,SAAA;EACA,UAAA,EAAY,MAAA;AAAA;;;;iBAME,cAAA,CAAe,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,YAAA;;AAAzE;;;;;;;;;;;;;;;AAoCA;;;;;iBAAgB,kBAAA,CAAmB,SAAA,GAAY,OAAA,CAAQ,aAAA,KAAkB,GAAA,EAAK,YAAA,GAAe,YAAA,OAAmB,OAAA;;;;;;;;;;;iBA0C1F,aAAA,CAAc,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,OAAA;;;AAA9E;;;;;;;;iBAcsB,kBAAA,CAAmB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,aAAA,GAAgB,OAAA"}
1
+ {"version":3,"file":"posthog.d.mts","names":[],"sources":["../../src/adapters/posthog.ts"],"mappings":";;;;UAQiB,aAAA;;EAEf,MAAA;EAFe;EAIf,IAAA;;EAEA,SAAA;EAJA;EAMA,UAAA;EAFA;EAIA,OAAA;AAAA;;UAIe,YAAA;EACf,KAAA;EACA,WAAA;EACA,SAAA;EACA,UAAA,EAAY,MAAA;AAAA;;;;iBAcE,cAAA,CAAe,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,YAAA;;;AAAzE;;;;;;;;;;;;;;;AAoCA;;;;iBAAgB,kBAAA,CAAmB,SAAA,GAAY,OAAA,CAAQ,aAAA,KAAc,GAAA,EAAf,YAAA,GAAe,YAAA,OAAA,OAAA;;;;;;;;;;;iBAyB/C,aAAA,CAAc,KAAA,EAAO,SAAA,EAAW,MAAA,EAAQ,aAAA,GAAgB,OAAA;;;;AAA9E;;;;;;;iBAcsB,kBAAA,CAAmB,MAAA,EAAQ,SAAA,IAAa,MAAA,EAAQ,aAAA,GAAgB,OAAA;AAAA,UAsBrE,iBAAA;EApCmB;EAsClC,MAAA;EAtCoD;EAwCpD,IAAA;EAxCmF;EA0CnF,OAAA;AAAA;;;;;;;;;;;;;;;AANF;;;;;;;;;AAuCA;iBAAgB,sBAAA,CAAuB,SAAA,GAAY,OAAA,CAAQ,iBAAA,KAAkB,GAAA,EAAnB,YAAA,GAAmB,YAAA,OAAA,OAAA"}