{"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 mxllog 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.mxllog.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('mxllog:drain', createOTLPDrain())\n *\n * // With overrides\n * nitroApp.hooks.hook('mxllog: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('[mxllog/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, environment) so each gets correct OTLP resource attributes\n  const grouped = new Map<string, WideEvent[]>()\n  for (const event of events) {\n    const key = `${event.service}::${event.environment}`\n    const group = grouped.get(key)\n    if (group) {\n      group.push(event)\n    } else {\n      grouped.set(key, [event])\n    }\n  }\n\n  const payload: ExportLogsServiceRequest = {\n    resourceLogs: Array.from(grouped.values()).map((groupEvents) => ({\n      resource: { attributes: buildResourceAttributes(groupEvents[0]!, config) },\n      scopeLogs: [\n        {\n          scope: { name: '@safaricom-mxl/log', version: '1.0.0' },\n          logRecords: groupEvents.map(toOTLPLogRecord),\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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"}