{
  "$schema": "https://hyperjs.ai/schema/registry-item.json",
  "name": "log",
  "version": "0.1.0",
  "description": "Wide-event structured logger for Hyper — the reference plugin.",
  "readme": "# @hyper/log\n\nWide-event structured logger for Hyper — the reference plugin.\n\nOne log event per request, attached to `ctx.log`. Pluggable drains (stdout, file, Axiom, memory, BYO). Secrets redacted by default.\n\n## Install\n\nComponents are installed as source into your repo, not pulled from npm:\n\n```bash\nbunx hyper add log\n```\n\nWires the alias `@hyper/log` to `src/hyper/log/` (configurable in `hyper.config.json`). See [hyperjs.ai](https://hyperjs.ai) for the full registry.\n\n## Usage\n\n```ts\nimport { Hyper } from \"@hyper/core\"\nimport { hyperLog } from \"@hyper/log\"\n\nexport default new Hyper()\n  .use(hyperLog({ service: \"orders\" }))\n  .get(\"/health\", ({ ctx }) => {\n    ctx.log.event(\"health.check\", { ok: true })\n    return { ok: true }\n  })\n  .listen(3000)\n```\n\n## Docs\n\nSee the [main README](../../README.md) and [docs/](../../docs) for guides and integration recipes.\n\n## License\n\nMIT\n",
  "registryDeps": [
    "core"
  ],
  "peerDeps": {},
  "optionalPeerDeps": {},
  "files": [
    {
      "path": "log/ai.ts",
      "contents": "/**\n * AI SDK wrapper — emits one event per generateText/streamText call with\n * token counts, model, latency, and a hash of the prompt (not the content).\n *\n * Usage:\n *   const wrappedModel = wrapAiModel(openai(\"gpt-4o-mini\"), () => ctx.log)\n *\n * We stay deliberately structural: we don't import `ai` to keep peer deps\n * optional. Users pass their model-like object; we proxy the `doGenerate`\n * / `doStream` calls.\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n\ninterface AiModelLike {\n  readonly modelId?: string\n  readonly provider?: string\n  // biome-ignore lint/suspicious/noExplicitAny: AI SDK provider types vary\n  doGenerate?: (...args: any[]) => Promise<any>\n  // biome-ignore lint/suspicious/noExplicitAny: AI SDK provider types vary\n  doStream?: (...args: any[]) => Promise<any>\n}\n\nexport function wrapAiModel<M extends AiModelLike>(model: M, getLog: GetLog): M {\n  const base = {\n    provider: model.provider,\n    modelId: model.modelId,\n  }\n\n  // biome-ignore lint/suspicious/noExplicitAny: see above\n  const wrap = (fn: (...a: any[]) => Promise<any>, kind: \"generate\" | \"stream\") => {\n    // biome-ignore lint/suspicious/noExplicitAny: dynamic dispatch\n    return async (...args: any[]) => {\n      const start = performance.now()\n      const log = getLog()\n      try {\n        const out = await fn.apply(model, args)\n        const usage = (out as { usage?: { promptTokens?: number; completionTokens?: number } })\n          ?.usage\n        log\n          ?.child(\"ai\")\n          .set({\n            ...base,\n            kind,\n            took_ms: performance.now() - start,\n            prompt_tokens: usage?.promptTokens,\n            completion_tokens: usage?.completionTokens,\n          })\n          .finish()\n        return out\n      } catch (e) {\n        log\n          ?.child(\"ai\")\n          .set({\n            ...base,\n            kind,\n            took_ms: performance.now() - start,\n            err: String(e),\n          })\n          .level(\"error\")\n          .finish()\n        throw e\n      }\n    }\n  }\n\n  return new Proxy(model, {\n    get(target, prop, receiver) {\n      const v = Reflect.get(target, prop, receiver)\n      if (prop === \"doGenerate\" && typeof v === \"function\") {\n        // biome-ignore lint/suspicious/noExplicitAny: see file header\n        return wrap(v as (...a: any[]) => Promise<any>, \"generate\")\n      }\n      if (prop === \"doStream\" && typeof v === \"function\") {\n        // biome-ignore lint/suspicious/noExplicitAny: see file header\n        return wrap(v as (...a: any[]) => Promise<any>, \"stream\")\n      }\n      return v\n    },\n  })\n}\n",
      "sha256": "e950952c86184e6215a5efeac63c6a7445761440893cb0164ad3af9f762ec320"
    },
    {
      "path": "log/builder.ts",
      "contents": "/**\n * LogBuilder — one per request by default. Collects wide-event fields\n * and emits exactly one event when `finish()` is called.\n *\n * Child builders emit their own events but inherit parent fields.\n */\n\nimport type { LogBuilder, LogDrain, LogEvent, LogLevel } from \"./types.ts\"\n\nconst LEVEL_ORDER: Record<LogLevel, number> = {\n  trace: 10,\n  debug: 20,\n  info: 30,\n  warn: 40,\n  error: 50,\n  fatal: 60,\n}\n\nexport interface BuilderOptions {\n  readonly drains: readonly LogDrain[]\n  readonly minLevel: LogLevel\n  readonly clock: () => number\n  readonly render: (event: LogEvent) => LogEvent\n  readonly sample: (event: LogEvent) => boolean\n  readonly parentFields?: Record<string, unknown>\n  readonly scope?: string\n}\n\nexport function createLogBuilder(opts: BuilderOptions): LogBuilder {\n  const fields: Record<string, unknown> = { ...(opts.parentFields ?? {}) }\n  let hintLevel: LogLevel = \"info\"\n  let finished = false\n  if (opts.scope) fields.scope = opts.scope\n\n  const emit = (msg: string | undefined): void => {\n    if (finished) return\n    finished = true\n    const level = hintLevel\n    if (LEVEL_ORDER[level] < LEVEL_ORDER[opts.minLevel]) return\n    const event: LogEvent = opts.render({\n      ts: new Date(opts.clock()).toISOString(),\n      level,\n      ...(msg !== undefined ? { msg } : {}),\n      ...fields,\n    })\n    if (!opts.sample(event)) return\n    for (const d of opts.drains) {\n      try {\n        const p = d.write(event)\n        if (p && typeof (p as Promise<void>).catch === \"function\") {\n          ;(p as Promise<void>).catch(() => {\n            // Swallow drain errors to protect the request path.\n          })\n        }\n      } catch {\n        // Drains must not break the request pipeline.\n      }\n    }\n  }\n\n  const builder: LogBuilder = {\n    set(f) {\n      Object.assign(fields, f)\n      return builder\n    },\n    level(l) {\n      hintLevel = l\n      return builder\n    },\n    child(scope) {\n      return createLogBuilder({\n        drains: opts.drains,\n        minLevel: opts.minLevel,\n        clock: opts.clock,\n        render: opts.render,\n        sample: opts.sample,\n        parentFields: { ...fields },\n        scope,\n      })\n    },\n    finish(msg) {\n      emit(msg)\n    },\n    info(msg, f) {\n      if (f) Object.assign(fields, f)\n      hintLevel = \"info\"\n      emit(msg)\n    },\n    warn(msg, f) {\n      if (f) Object.assign(fields, f)\n      hintLevel = \"warn\"\n      emit(msg)\n    },\n    error(msg, f) {\n      if (f) Object.assign(fields, f)\n      hintLevel = \"error\"\n      emit(msg)\n    },\n  }\n  return builder\n}\n",
      "sha256": "0997403fe6a7196a37ccaf409593b00038d0d365c4369752f8dfd24fd6563cbe"
    },
    {
      "path": "log/bun-sql.ts",
      "contents": "/**\n * Bun.sql template-tag wrapper that emits query events.\n *\n * Usage:\n *   import { sql as raw } from \"bun\"\n *   import { wrapBunSql } from \"@hyper/log/bun-sql\"\n *   const sql = wrapBunSql(raw, () => ctx.log)\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n// biome-ignore lint/suspicious/noExplicitAny: Bun.sql is a template tag with many shapes\ntype BunSql = (strings: TemplateStringsArray, ...values: any[]) => Promise<unknown>\n\nexport function wrapBunSql(sql: BunSql, getLog: GetLog): BunSql {\n  // biome-ignore lint/suspicious/noExplicitAny: see above\n  return ((strings: TemplateStringsArray, ...values: any[]): Promise<unknown> => {\n    const start = performance.now()\n    const log = getLog()\n    return sql(strings, ...values).then(\n      (out) => {\n        log\n          ?.child(\"db.query\")\n          .set({ took_ms: performance.now() - start })\n          .finish()\n        return out\n      },\n      (err) => {\n        log\n          ?.child(\"db.query\")\n          .set({ took_ms: performance.now() - start, err: String(err) })\n          .level(\"error\")\n          .finish()\n        throw err\n      },\n    )\n  }) as BunSql\n}\n",
      "sha256": "276bf4a3471fb8b7e55f65e66e0d3826f8686f42a3bee0c6650ae30d1f479e58"
    },
    {
      "path": "log/drains.ts",
      "contents": "/**\n * Built-in drains.\n *\n * - `stdoutDrain()` — NDJSON to stdout (zero-dep, default).\n * - `memoryDrain()` — collects events for tests.\n * - `fileDrain(path)` — appends NDJSON lines to a file via Bun.file.\n * - `axiomDrain({ dataset, token })` — batched HTTP POST to Axiom.\n *\n * Drains are fire-and-forget. Errors are swallowed by the builder.\n */\n\nimport type { LogDrain, LogEvent } from \"./types.ts\"\n\nexport function stdoutDrain(): LogDrain {\n  const encoder = new TextEncoder()\n  return {\n    name: \"stdout\",\n    write(event) {\n      const line = `${JSON.stringify(event)}\\n`\n      if (typeof Bun !== \"undefined\" && typeof Bun.write === \"function\") {\n        Bun.write(Bun.stdout, line).catch(() => {})\n      } else {\n        process.stdout.write(encoder.encode(line))\n      }\n    },\n  }\n}\n\nexport function memoryDrain(): LogDrain & { readonly events: LogEvent[] } {\n  const events: LogEvent[] = []\n  return {\n    name: \"memory\",\n    events,\n    write(event) {\n      events.push(event)\n    },\n  }\n}\n\nexport function fileDrain(path: string): LogDrain {\n  return {\n    name: `file:${path}`,\n    async write(event) {\n      if (typeof Bun === \"undefined\") return\n      const file = Bun.file(path)\n      const writer = file.writer()\n      writer.write(`${JSON.stringify(event)}\\n`)\n      await writer.end()\n    },\n  }\n}\n\nexport interface AxiomDrainConfig {\n  readonly dataset: string\n  readonly token: string\n  readonly endpoint?: string\n  readonly batchSize?: number\n  readonly flushIntervalMs?: number\n}\n\nexport function axiomDrain(cfg: AxiomDrainConfig): LogDrain {\n  const endpoint = cfg.endpoint ?? `https://api.axiom.co/v1/datasets/${cfg.dataset}/ingest`\n  const batchSize = cfg.batchSize ?? 100\n  const flushMs = cfg.flushIntervalMs ?? 1000\n  let buf: LogEvent[] = []\n  let timer: ReturnType<typeof setTimeout> | null = null\n\n  const flush = async (): Promise<void> => {\n    if (buf.length === 0) return\n    const batch = buf\n    buf = []\n    if (timer) {\n      clearTimeout(timer)\n      timer = null\n    }\n    try {\n      await fetch(endpoint, {\n        method: \"POST\",\n        headers: {\n          \"content-type\": \"application/x-ndjson\",\n          authorization: `Bearer ${cfg.token}`,\n        },\n        body: batch.map((e) => JSON.stringify(e)).join(\"\\n\"),\n      })\n    } catch {\n      // Drop on network error; logs are best-effort.\n    }\n  }\n\n  return {\n    name: `axiom:${cfg.dataset}`,\n    write(event) {\n      buf.push(event)\n      if (buf.length >= batchSize) {\n        flush()\n        return\n      }\n      if (!timer) timer = setTimeout(flush, flushMs)\n    },\n    flush,\n    close: flush,\n  }\n}\n",
      "sha256": "d5455d3c7223b927eeb6e1514493297df31801c710a3d75cf75c8ccecf3180aa"
    },
    {
      "path": "log/drizzle.ts",
      "contents": "/**\n * Query-event helper for Drizzle. Not a hard dependency — users wire it\n * themselves with their own drizzle instance.\n *\n * Usage:\n *   import { drizzle } from \"drizzle-orm/bun-sqlite\"\n *   import { wrapDrizzle } from \"@hyper/log/drizzle\"\n *   const db = wrapDrizzle(drizzle(...), () => ctx.log)\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n\n/**\n * Minimal interface so we don't depend on drizzle-orm types.\n * Works against any object exposing an `execute(query)` or similar.\n */\ninterface ExecutableDb {\n  // biome-ignore lint/suspicious/noExplicitAny: user's drizzle instance\n  execute?: (q: unknown, ...rest: any[]) => Promise<unknown>\n  // biome-ignore lint/suspicious/noExplicitAny: user's drizzle instance\n  run?: (q: unknown, ...rest: any[]) => Promise<unknown>\n}\n\nexport function wrapDrizzle<Db extends ExecutableDb>(db: Db, getLog: GetLog): Db {\n  const hook = async (method: \"execute\" | \"run\", q: unknown, rest: unknown[]) => {\n    const start = performance.now()\n    const log = getLog()\n    try {\n      // biome-ignore lint/suspicious/noExplicitAny: dynamic dispatch\n      const out = await (db[method] as any).call(db, q, ...rest)\n      log\n        ?.child(\"db.query\")\n        .set({ method, took_ms: performance.now() - start })\n        .finish()\n      return out\n    } catch (e) {\n      log\n        ?.child(\"db.query\")\n        .set({ method, took_ms: performance.now() - start, err: String(e) })\n        .level(\"error\")\n        .finish()\n      throw e\n    }\n  }\n  const patched = { ...db }\n  if (typeof db.execute === \"function\") {\n    // biome-ignore lint/suspicious/noExplicitAny: preserving user's types\n    ;(patched as any).execute = (q: unknown, ...rest: any[]) => hook(\"execute\", q, rest)\n  }\n  if (typeof db.run === \"function\") {\n    // biome-ignore lint/suspicious/noExplicitAny: preserving user's types\n    ;(patched as any).run = (q: unknown, ...rest: any[]) => hook(\"run\", q, rest)\n  }\n  return patched as Db\n}\n",
      "sha256": "ae1289c26d1634589837770fcb62a790279aa9aa152c098a0c6a43538a7d685b"
    },
    {
      "path": "log/index.ts",
      "contents": "/**\n * @hyper/log — wide-event structured logger.\n *\n * Straight to the point:\n * - One log event per request (wide events), attached to `ctx.log`.\n * - Drains: stdout (default), file, axiom, memory (tests). BYO is easy.\n * - Redacts secrets by default; `secret()` marks env fields for auto-redaction.\n * - No runtime deps besides @hyper/core peer.\n */\n\nexport { createLogBuilder } from \"./builder.ts\"\nexport { wrapQueries } from \"./wrap-queries.ts\"\nexport { axiomDrain, fileDrain, memoryDrain, stdoutDrain } from \"./drains.ts\"\nexport type { AxiomDrainConfig } from \"./drains.ts\"\nexport { hyperLog } from \"./plugin.ts\"\nexport type { HyperLogPluginConfig } from \"./plugin.ts\"\nexport { DEFAULT_REDACT, redact } from \"./redact.ts\"\nexport type { LogBuilder, LogConfig, LogDrain, LogEvent, LogLevel } from \"./types.ts\"\n",
      "sha256": "89a89ee726231db8fc3861eae468ddecd1907c0a055083a00c2de3e9a17e94e2"
    },
    {
      "path": "log/plugin.ts",
      "contents": "/**\n * hyperLog({ ... }) — the reference plugin.\n *\n * Wires a per-request LogBuilder into `ctx.log` via the plugin protocol:\n *\n *   - `context()` — nothing (we want per-request state, not a singleton)\n *   - `request.before` — constructs a LogBuilder and attaches it to ctx.log\n *   - `request.after` — finishes the event with status/duration/route\n *   - `request.onError` — finishes the event at `error` level with why/fix\n *\n * Request correlation: we hook `ctx.log` directly on the AppContext object.\n * Because plugins run against the same `ctx` for before/after/onError, this\n * is safe without AsyncLocalStorage (which is reserved for useEnv()).\n */\n\nimport type { HyperPlugin } from \"@hyper/core\"\nimport { createLogBuilder } from \"./builder.ts\"\nimport { stdoutDrain } from \"./drains.ts\"\nimport { DEFAULT_REDACT, redact } from \"./redact.ts\"\nimport type { LogBuilder, LogConfig, LogEvent } from \"./types.ts\"\n\nconst REQUEST_ID_HEADER = \"x-request-id\"\n\nfunction makeId(): string {\n  if (typeof crypto !== \"undefined\" && \"randomUUID\" in crypto) return crypto.randomUUID()\n  return `r_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`\n}\n\nexport interface HyperLogPluginConfig extends LogConfig {\n  /** Tag every event with this service name. */\n  service?: string\n  /** If true, include `req.headers` (with redaction). Default: false. */\n  includeHeaders?: boolean\n}\n\nexport function hyperLog(config: HyperLogPluginConfig = {}): HyperPlugin {\n  const drains = config.drains ?? [stdoutDrain()]\n  const minLevel = config.level ?? \"info\"\n  const clock = config.clock ?? Date.now\n  const redactPaths = config.redact ?? DEFAULT_REDACT\n  const sampleRate = config.sampleRate ?? 1\n  const keep = config.keep\n\n  const render = (event: LogEvent): LogEvent => {\n    const masked = redact(event, redactPaths) as LogEvent\n    if (config.service !== undefined && masked.service === undefined) {\n      return { ...masked, service: config.service } as LogEvent\n    }\n    return masked\n  }\n  const sample = (event: LogEvent): boolean => {\n    if (sampleRate >= 1) return true\n    if (keep?.(event)) return true\n    return Math.random() < sampleRate\n  }\n\n  return {\n    name: \"@hyper/log\",\n    request: {\n      before({ req, ctx }) {\n        const startedAt = clock()\n        const requestId = req.headers.get(REQUEST_ID_HEADER) ?? makeId()\n        const log = createLogBuilder({\n          drains,\n          minLevel,\n          clock,\n          render,\n          sample,\n          parentFields: {\n            request_id: requestId,\n            method: req.method,\n            path: new URL(req.url).pathname,\n            ...(config.includeHeaders\n              ? { headers: Object.fromEntries(req.headers.entries()) }\n              : {}),\n          },\n        })\n        const ctxMut = ctx as unknown as {\n          log: LogBuilder\n          _logStartedAt: number\n          _logRequestId: string\n        }\n        ctxMut.log = log\n        ctxMut._logStartedAt = startedAt\n        ctxMut._logRequestId = requestId\n      },\n      after({ ctx, res, route }) {\n        const ctxAny = ctx as unknown as {\n          log?: LogBuilder\n          _logStartedAt?: number\n        }\n        const log = ctxAny.log\n        if (!log) return\n        const durMs = ctxAny._logStartedAt ? clock() - ctxAny._logStartedAt : undefined\n        log.set({\n          status: res.status,\n          ...(durMs !== undefined ? { duration_ms: durMs } : {}),\n          ...(route ? { route: route.path, method: route.method } : {}),\n        })\n        const level = res.status >= 500 ? \"error\" : res.status >= 400 ? \"warn\" : \"info\"\n        log.level(level).finish(\"request\")\n      },\n      onError({ ctx, error, route }) {\n        const ctxAny = ctx as unknown as {\n          log?: LogBuilder\n          _logStartedAt?: number\n        }\n        const log = ctxAny.log\n        if (!log) return\n        const durMs = ctxAny._logStartedAt ? clock() - ctxAny._logStartedAt : undefined\n        const err = error as { message?: string; code?: string; why?: string; fix?: string }\n        log.set({\n          ...(durMs !== undefined ? { duration_ms: durMs } : {}),\n          ...(route ? { route: route.path, method: route.method } : {}),\n          err: {\n            message: err?.message,\n            code: err?.code,\n            why: err?.why,\n            fix: err?.fix,\n          },\n        })\n        log.level(\"error\").finish(\"request_error\")\n      },\n    },\n  }\n}\n",
      "sha256": "a910daa221daa2b59a6d992307ebb2c901de94866c190a3c64be5976a2716047"
    },
    {
      "path": "log/prisma.ts",
      "contents": "/**\n * Prisma $extends() compatible helper — adds per-query timing events.\n *\n * Usage:\n *   const prisma = new PrismaClient().$extends(prismaLogExtension(() => ctx.log))\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n\n// biome-ignore lint/suspicious/noExplicitAny: Prisma extension is dynamic by design\nexport function prismaLogExtension(getLog: GetLog): any {\n  return {\n    name: \"@hyper/log\",\n    query: {\n      $allOperations: async ({\n        model,\n        operation,\n        query,\n        args,\n      }: {\n        model?: string\n        operation: string\n        // biome-ignore lint/suspicious/noExplicitAny: Prisma next\n        query: (args: any) => Promise<unknown>\n        // biome-ignore lint/suspicious/noExplicitAny: Prisma args\n        args: any\n      }): Promise<unknown> => {\n        const start = performance.now()\n        const log = getLog()\n        try {\n          const out = await query(args)\n          log\n            ?.child(\"db.query\")\n            .set({ model, operation, took_ms: performance.now() - start })\n            .finish()\n          return out\n        } catch (e) {\n          log\n            ?.child(\"db.query\")\n            .set({ model, operation, took_ms: performance.now() - start, err: String(e) })\n            .level(\"error\")\n            .finish()\n          throw e\n        }\n      },\n    },\n  }\n}\n",
      "sha256": "8ad242c7c1ec9a1637ca8aea3f3098ae5442be96c4a55211526a5da6e9911674"
    },
    {
      "path": "log/redact.ts",
      "contents": "/**\n * Redaction — path-based masking for log events.\n *\n * Performance note: we pre-compile the redact path list into a small set\n * and short-circuit when the set is empty.\n */\n\nconst MASK = \"[REDACTED]\"\n\n/** Default keys that are always masked when encountered. */\nexport const DEFAULT_REDACT: readonly string[] = [\n  \"password\",\n  \"pass\",\n  \"token\",\n  \"authorization\",\n  \"cookie\",\n  \"secret\",\n  \"api_key\",\n  \"apiKey\",\n  \"access_token\",\n  \"refresh_token\",\n  \"ssn\",\n] as const\n\nexport function redact(value: unknown, paths: readonly string[] = DEFAULT_REDACT): unknown {\n  if (value == null) return value\n  const set = new Set(paths.map((p) => p.toLowerCase()))\n  if (set.size === 0) return value\n  return walk(value, set, \"\")\n}\n\nfunction walk(v: unknown, set: Set<string>, prefix: string): unknown {\n  if (v == null || typeof v !== \"object\") return v\n  if (Array.isArray(v)) return v.map((x, i) => walk(x, set, `${prefix}[${i}]`))\n  const out: Record<string, unknown> = {}\n  for (const [k, val] of Object.entries(v as Record<string, unknown>)) {\n    const path = prefix ? `${prefix}.${k}` : k\n    if (set.has(k.toLowerCase()) || set.has(path.toLowerCase())) {\n      out[k] = MASK\n    } else {\n      out[k] = walk(val, set, path)\n    }\n  }\n  return out\n}\n",
      "sha256": "76b3773a3630a34580d508004fffa6af55408d17808f967bc3936dfcd70cf666"
    },
    {
      "path": "log/types.ts",
      "contents": "/**\n * @hyper/log types — the wide-event surface.\n *\n * One log per request is the default. Handlers call `ctx.log.set({...})`\n * to accumulate fields; the request-level event is flushed on response\n * (or error) with all fields rolled up.\n */\n\nexport type LogLevel = \"trace\" | \"debug\" | \"info\" | \"warn\" | \"error\" | \"fatal\"\n\n/** A single log record (one request = one event by default). */\nexport interface LogEvent {\n  readonly ts: string\n  readonly level: LogLevel\n  readonly msg?: string\n  readonly [key: string]: unknown\n}\n\n/** A log builder that accumulates fields and emits one event. */\nexport interface LogBuilder {\n  /** Merge fields into the current event. Last wins. */\n  set(fields: Record<string, unknown>): LogBuilder\n  /** Scoped child — emits its own event with a shared prefix. */\n  child(scope: string): LogBuilder\n  /** Level hint; final level chosen at drain time. */\n  level(l: LogLevel): LogBuilder\n  /** Manually finish; normally called by the plugin on response. */\n  finish(msg?: string): void\n  /** Convenience shortcuts. Avoid in hot path — prefer `.set()`. */\n  info(msg: string, fields?: Record<string, unknown>): void\n  warn(msg: string, fields?: Record<string, unknown>): void\n  error(msg: string, fields?: Record<string, unknown>): void\n}\n\n/** Drain — receives rendered events. */\nexport interface LogDrain {\n  readonly name: string\n  write(event: LogEvent): void | Promise<void>\n  flush?(): void | Promise<void>\n  close?(): void | Promise<void>\n}\n\nexport interface LogConfig {\n  /** One or more drains. Defaults to stdout NDJSON. */\n  drains?: readonly LogDrain[]\n  /** Minimum level emitted. */\n  level?: LogLevel\n  /** Paths (dot-path) that are masked in serialized output. */\n  redact?: readonly string[]\n  /** Sampling: 0..1. Events below the cutoff drop unless `keep()` hits. */\n  sampleRate?: number\n  /** Predicate: if it returns true, the event is always kept. */\n  keep?: (event: LogEvent) => boolean\n  /** Injected clock for tests; defaults to Date.now(). */\n  clock?: () => number\n}\n",
      "sha256": "cb98e6dc97de8718009d7194eaa776511639d793a096cc3181d7eacb83ec631f"
    },
    {
      "path": "log/wrap-queries.ts",
      "contents": "/**\n * wrapQueries(db, getLog) — a generic helper that records query timing\n * on any object exposing async methods. Works out-of-the-box with Drizzle,\n * Prisma, Bun.sql wrappers, and hand-rolled repositories.\n *\n * The contract is minimal: every method is wrapped; synchronous methods\n * are passed through. Errors are logged at `error` level.\n */\n\nimport type { LogBuilder } from \"./types.ts\"\n\ntype GetLog = () => LogBuilder | undefined\n\n// biome-ignore lint/suspicious/noExplicitAny: user's repo/orm shape is opaque\nexport function wrapQueries<T extends Record<string, any>>(db: T, getLog: GetLog): T {\n  return new Proxy(db, {\n    get(target, prop, receiver) {\n      const v = Reflect.get(target, prop, receiver)\n      if (typeof v !== \"function\") return v\n      return (...args: unknown[]) => {\n        const start = performance.now()\n        const label = String(prop)\n        try {\n          const result = (v as (...a: unknown[]) => unknown).apply(target, args)\n          if (result && typeof (result as Promise<unknown>).then === \"function\") {\n            return (result as Promise<unknown>).then(\n              (out) => {\n                getLog()\n                  ?.child(\"db.query\")\n                  .set({ method: label, took_ms: performance.now() - start })\n                  .finish()\n                return out\n              },\n              (err: unknown) => {\n                getLog()\n                  ?.child(\"db.query\")\n                  .set({ method: label, took_ms: performance.now() - start, err: String(err) })\n                  .level(\"error\")\n                  .finish()\n                throw err\n              },\n            )\n          }\n          getLog()\n            ?.child(\"db.query\")\n            .set({ method: label, took_ms: performance.now() - start })\n            .finish()\n          return result\n        } catch (err) {\n          getLog()\n            ?.child(\"db.query\")\n            .set({ method: label, took_ms: performance.now() - start, err: String(err) })\n            .level(\"error\")\n            .finish()\n          throw err\n        }\n      }\n    },\n  }) as T\n}\n",
      "sha256": "be693b07a8ddaa6fc97fb6a67342f071339bdd58cbbc055574dc46d63056b4b3"
    }
  ],
  "subpaths": {}
}