{
  "$schema": "https://hyperjs.ai/schema/registry-item.json",
  "name": "otel",
  "version": "0.1.0",
  "description": "OpenTelemetry tracing + SLO histograms for Hyper.",
  "readme": "# @hyper/otel\n\nOpenTelemetry tracing + SLO histograms for Hyper.\n\n## Install\n\nComponents are installed as source into your repo, not pulled from npm:\n\n```bash\nbunx hyper add otel\n```\n\nWires the alias `@hyper/otel` to `src/hyper/otel/` (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 { otelMiddleware } from \"@hyper/otel\"\n\nexport default new Hyper()\n  .use(otelMiddleware())\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": "otel/index.ts",
      "contents": "/**\n * @hyper/otel — OpenTelemetry-flavored request spans + SLO histograms.\n *\n * The full OTLP exporter is wired via the user's `@opentelemetry/sdk-node`\n * setup (we reuse the global provider). This middleware just emits spans\n * when an `otel.Tracer` is provided, and always pushes duration samples\n * into our built-in SLO recorder.\n */\n\nimport type { Middleware } from \"@hyper/core\"\nimport { SloRecorder } from \"./slo.ts\"\n\nexport { SloRecorder } from \"./slo.ts\"\nexport type { SloTarget } from \"./slo.ts\"\n\nexport interface OtelConfig {\n  readonly tracer?: TracerLike\n  readonly recorder?: SloRecorder\n}\n\ninterface SpanLike {\n  setAttribute(key: string, value: unknown): void\n  setStatus(status: { code: number; message?: string }): void\n  end(): void\n  recordException?(e: unknown): void\n}\n\ninterface TracerLike {\n  startSpan(name: string, options?: { attributes?: Record<string, unknown> }): SpanLike\n}\n\nexport function otel(config: OtelConfig = {}): Middleware {\n  const recorder = config.recorder ?? new SloRecorder()\n  return async ({ req, path, next }) => {\n    const span = config.tracer?.startSpan(`${req.method} ${path}`, {\n      attributes: {\n        \"http.method\": req.method,\n        \"http.route\": path,\n        \"http.url\": req.url,\n      },\n    })\n    const t0 = performance.now()\n    try {\n      const out = await next()\n      const dt = performance.now() - t0\n      recorder.record(path, dt)\n      if (span) {\n        span.setAttribute(\"http.duration_ms\", dt)\n        span.setStatus({ code: 1 })\n        span.end()\n      }\n      return out\n    } catch (error) {\n      const dt = performance.now() - t0\n      recorder.record(path, dt)\n      if (span) {\n        span.setAttribute(\"http.duration_ms\", dt)\n        span.setStatus({ code: 2, message: String(error) })\n        span.recordException?.(error)\n        span.end()\n      }\n      throw error\n    }\n  }\n}\n",
      "sha256": "4e1006ec68ad3aa283cc2defb73d29cb42ceb70bb94e120ee899dd84d6a6719a"
    },
    {
      "path": "otel/slo.ts",
      "contents": "/**\n * SLO histogram recorder + `.slo()` route builder sugar.\n *\n * `.slo({ p99: 200 })` attaches `{ slo: { p99: 200 } }` to `meta`; the\n * `otelPlugin` middleware uses it to produce a per-route histogram and\n * tags spans with `slo.target` / `slo.violation=true`.\n */\n\nexport interface SloTarget {\n  readonly p50?: number\n  readonly p95?: number\n  readonly p99?: number\n}\n\ninterface Sample {\n  readonly durationMs: number\n  readonly route: string\n}\n\nexport class SloRecorder {\n  #samples: Sample[] = []\n\n  record(route: string, durationMs: number): void {\n    this.#samples.push({ route, durationMs })\n    if (this.#samples.length > 10_000) this.#samples.shift()\n  }\n\n  percentile(route: string, p: number): number {\n    const xs = this.#samples\n      .filter((s) => s.route === route)\n      .map((s) => s.durationMs)\n      .sort((a, b) => a - b)\n    if (xs.length === 0) return 0\n    const idx = Math.min(xs.length - 1, Math.floor((xs.length * p) / 100))\n    return xs[idx] ?? 0\n  }\n\n  snapshot(): Record<string, { p50: number; p95: number; p99: number; count: number }> {\n    const byRoute = new Map<string, number[]>()\n    for (const s of this.#samples) {\n      const arr = byRoute.get(s.route) ?? []\n      arr.push(s.durationMs)\n      byRoute.set(s.route, arr)\n    }\n    const out: Record<string, { p50: number; p95: number; p99: number; count: number }> = {}\n    for (const [route, xs] of byRoute) {\n      xs.sort((a, b) => a - b)\n      const at = (p: number) => xs[Math.min(xs.length - 1, Math.floor((xs.length * p) / 100))] ?? 0\n      out[route] = { p50: at(50), p95: at(95), p99: at(99), count: xs.length }\n    }\n    return out\n  }\n}\n",
      "sha256": "43d88404b1c0dd90fd5bb7193c364075dc1c66cf1e99d682c9e58658f3876f39"
    }
  ],
  "subpaths": {}
}