{"version":3,"file":"plugin.mjs","names":[],"sources":["../../../src/nitro-v3/plugin.ts"],"sourcesContent":["import { definePlugin } from 'nitro'\nimport { useRuntimeConfig } from 'nitro/runtime-config'\nimport type { CaptureError } from 'nitro/types'\nimport type { HTTPEvent } from 'nitro/h3'\nimport { parseURL } from 'ufo'\nimport { createRequestLogger, initLogger, isEnabled } from '../logger'\nimport { shouldLog, getServiceForPath, extractErrorStatus } from '../nitro'\nimport type { MxllogConfig } from '../nitro'\nimport type { EnrichContext, RequestLogger, TailSamplingContext, WideEvent } from '../types'\nimport { filterSafeHeaders } from '../utils'\n\n// Nitro v3 doesn't fully export hook types yet\n// https://github.com/nitrojs/nitro/blob/8882bc9e1dbf2d342e73097f22a2156f70f50575/src/types/runtime/nitro.ts#L48-L53\ninterface NitroV3Hooks {\n  close: () => void\n  error: CaptureError\n  request: (event: HTTPEvent) => void | Promise<void>\n  response: (res: Response, event: HTTPEvent) => void | Promise<void>\n  'mxllog:emit:keep': (ctx: TailSamplingContext) => void | Promise<void>\n  'mxllog:enrich': (ctx: EnrichContext) => void | Promise<void>\n  'mxllog:drain': (ctx: { event: WideEvent; request?: { method?: string; path: string; requestId?: string }; headers?: Record<string, string> }) => void | Promise<void>\n}\n\ntype Hooks = {\n  hook: <T extends keyof NitroV3Hooks>(name: T, listener: NitroV3Hooks[T]) => void\n  callHook: <T extends keyof NitroV3Hooks>(name: T, ...args: Parameters<NitroV3Hooks[T]>) => Promise<void>\n}\n\nfunction getContext(event: HTTPEvent): Record<string, unknown> {\n  if (!event.req.context) {\n    event.req.context = {}\n  }\n  return event.req.context\n}\n\nfunction getSafeRequestHeaders(event: HTTPEvent): Record<string, string> {\n  const headers: Record<string, string> = {}\n  event.req.headers.forEach((value, key) => {\n    headers[key] = value\n  })\n  return filterSafeHeaders(headers)\n}\n\nfunction getSafeResponseHeaders(res: Response): Record<string, string> | undefined {\n  const headers: Record<string, string> = {}\n  res.headers.forEach((value, key) => {\n    headers[key] = value\n  })\n  if (Object.keys(headers).length === 0) return undefined\n  return filterSafeHeaders(headers)\n}\n\nfunction buildHookContext(\n  event: HTTPEvent,\n  res?: Response,\n): Omit<EnrichContext, 'event'> {\n  const { pathname } = parseURL(event.req.url)\n  const responseHeaders = res ? getSafeResponseHeaders(res) : undefined\n  return {\n    request: { method: event.req.method, path: pathname },\n    headers: getSafeRequestHeaders(event),\n    response: {\n      status: res?.status ?? 200,\n      headers: responseHeaders,\n    },\n  }\n}\n\nasync function callDrainHook(\n  hooks: Hooks,\n  emittedEvent: WideEvent | null,\n  event: HTTPEvent,\n  hookContext: Omit<EnrichContext, 'event'>,\n): Promise<void> {\n  if (!emittedEvent) return\n\n  let drainPromise: Promise<unknown> | undefined\n  try {\n    const result = hooks.callHook('mxllog:drain', {\n      event: emittedEvent,\n      request: hookContext.request,\n      headers: hookContext.headers,\n    })\n    drainPromise = result?.catch?.((err: unknown) => {\n      console.error('[mxllog] drain failed:', err)\n    })\n  } catch (err) {\n    console.error('[mxllog] drain failed:', err)\n  }\n\n  if (!drainPromise) return\n\n  // Use waitUntil if available (srvx native — Cloudflare Workers, Vercel Edge, etc.)\n  // This keeps the runtime alive for background work without blocking the response\n  if (typeof event.req.waitUntil === 'function') {\n    event.req.waitUntil(drainPromise)\n  } else {\n    // Fallback: await drain to prevent lost logs in serverless environments\n    // (e.g. Vercel Fluid Compute). On the normal path this runs from the\n    // response hook (response already sent); on the error path it may run\n    // before the error response is finalized.\n    await drainPromise\n  }\n}\n\nasync function callEnrichAndDrain(\n  hooks: Hooks,\n  emittedEvent: WideEvent | null,\n  event: HTTPEvent,\n  res?: Response,\n): Promise<void> {\n  if (!emittedEvent) return\n\n  const hookContext = buildHookContext(event, res)\n\n  try {\n    await hooks.callHook('mxllog:enrich', { event: emittedEvent, ...hookContext })\n  } catch (err) {\n    console.error('[mxllog] enrich failed:', err)\n  }\n\n  await callDrainHook(hooks, emittedEvent, event, hookContext)\n}\n\n/**\n * Nitro v3 plugin entry point.\n *\n * Usage in Nitro v3:\n * ```ts\n * // plugins/mxllog.ts\n * export { default } from '@safaricom-mxl/log/nitro/v3'\n * ```\n */\nexport default definePlugin((nitroApp) => {\n  // In production builds the plugin is bundled and useRuntimeConfig()\n  // resolves the virtual module correctly. In dev mode the plugin is\n  // loaded externally so useRuntimeConfig() returns a stub — fall back\n  // to the env var bridge set by the module.\n  const mxllogConfig = (useRuntimeConfig().mxllog ?? (process.env.__EVLOG_CONFIG ? JSON.parse(process.env.__EVLOG_CONFIG) : undefined)) as MxllogConfig | undefined\n\n  initLogger({\n    enabled: mxllogConfig?.enabled,\n    env: mxllogConfig?.env,\n    pretty: mxllogConfig?.pretty,\n    sampling: mxllogConfig?.sampling,\n  })\n\n  if (!isEnabled()) return\n\n  const hooks = nitroApp.hooks as unknown as Hooks\n\n  hooks.hook('request', (event) => {\n    const { pathname } = parseURL(event.req.url)\n\n    // Skip logging for routes not matching include/exclude patterns\n    if (!shouldLog(pathname, mxllogConfig?.include, mxllogConfig?.exclude)) {\n      return\n    }\n\n    const ctx = getContext(event)\n\n    // Store start time for duration calculation in tail sampling\n    ctx._mxllogStartTime = Date.now()\n\n    let requestIdOverride: string | undefined = undefined\n    if (globalThis.navigator?.userAgent === 'Cloudflare-Workers') {\n      const cfRay = event.req.headers.get('cf-ray')\n      if (cfRay) requestIdOverride = cfRay\n    }\n\n    const log = createRequestLogger({\n      method: event.req.method,\n      path: pathname,\n      requestId: requestIdOverride || ctx.requestId as string | undefined || crypto.randomUUID(),\n    })\n\n    // Apply route-based service configuration if a matching route is found\n    const routeService = getServiceForPath(pathname, mxllogConfig?.routes)\n    if (routeService) {\n      log.set({ service: routeService })\n    }\n\n    ctx.log = log\n  })\n\n  hooks.hook('response', async (res, event) => {\n    const ctx = event.req.context\n    // Skip if already emitted by error hook\n    if (ctx?._mxllogEmitted) return\n\n    const log = ctx?.log as RequestLogger | undefined\n    if (!log || !ctx) return\n\n    const { status } = res\n    log.set({ status })\n\n    const startTime = ctx._mxllogStartTime as number | undefined\n    const durationMs = startTime ? Date.now() - startTime : undefined\n\n    const { pathname } = parseURL(event.req.url)\n\n    const tailCtx: TailSamplingContext = {\n      status,\n      duration: durationMs,\n      path: pathname,\n      method: event.req.method,\n      context: log.getContext(),\n      shouldKeep: false,\n    }\n\n    await hooks.callHook('mxllog:emit:keep', tailCtx)\n\n    const emittedEvent = log.emit({ _forceKeep: tailCtx.shouldKeep })\n    await callEnrichAndDrain(hooks, emittedEvent, event, res)\n  })\n\n  hooks.hook('error', async (error, { event }) => {\n    if (!event) return\n    const e = event as HTTPEvent\n\n    const ctx = e.req.context\n    const log = ctx?.log as RequestLogger | undefined\n    if (!log || !ctx) return\n\n    // Check if error.cause is an MxllogError (thrown errors get wrapped in HTTPError by nitro)\n    const actualError = (error.cause as Error)?.name === 'MxllogError' \n      ? error.cause as Error \n      : error as Error\n\n    log.error(actualError)\n\n    const errorStatus = extractErrorStatus(actualError)\n    log.set({ status: errorStatus })\n\n    const { pathname } = parseURL(e.req.url)\n    const startTime = ctx._mxllogStartTime as number | undefined\n    const durationMs = startTime ? Date.now() - startTime : undefined\n\n    const tailCtx: TailSamplingContext = {\n      status: errorStatus,\n      duration: durationMs,\n      path: pathname,\n      method: e.req.method,\n      context: log.getContext(),\n      shouldKeep: false,\n    }\n\n    await hooks.callHook('mxllog:emit:keep', tailCtx)\n\n    ctx._mxllogEmitted = true\n\n    const emittedEvent = log.emit({ _forceKeep: tailCtx.shouldKeep })\n    await callEnrichAndDrain(hooks, emittedEvent, e)\n  })\n})\n"],"mappings":"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"}