{"version":3,"sources":["../../src/otel/index.ts"],"sourcesContent":["/**\n * Optional OpenTelemetry adapter (`commitcourier/otel`).\n *\n * Maps CommitCourier's fail-open delivery instrumentation seam (`RelayInit.instrument` /\n * `RelayInit.hooks`, see `../delivery/deliver`) onto OpenTelemetry spans and metrics. This module is\n * the ONLY place `@opentelemetry/api` is referenced: it is an optional peer dependency, so importing\n * the main `commitcourier` entry never pulls OTel into scope, and `core` stays import-zero. Wire the\n * returned `{ instrument, hooks }` into `createRelay({ store, instrument, hooks })`.\n *\n * Each delivery attempt becomes one CLIENT span (started before the request, ended on the terminal\n * outcome) carrying secret-free attributes; the same terminal outcome increments a delivery counter\n * and records a duration histogram, classified by `outcome` (delivered | retry | dead). No secret or\n * high-cardinality id is ever used as a metric attribute.\n */\nimport { SpanKind, SpanStatusCode } from \"@opentelemetry/api\";\nimport type { Span, Tracer, Meter, Counter, Histogram, Attributes } from \"@opentelemetry/api\";\nimport type { DeliveryEvent, DeliveryInstrument, DeliveryHooks } from \"../delivery/deliver\";\n\n/** Options for {@link createOtelInstrumentation}. Both signals are optional and wired independently. */\nexport interface OtelInstrumentationOptions {\n  /** A tracer; when present, every delivery attempt emits one CLIENT span. */\n  tracer?: Tracer;\n  /** A meter; when present, every terminal outcome updates the delivery counter and duration histogram. */\n  meter?: Meter;\n  /** Span name for delivery spans. Defaults to `\"commitcourier.delivery\"`. */\n  spanName?: string;\n}\n\n/** The instrumentation wiring to pass to `createRelay`. */\nexport interface OtelInstrumentation {\n  /** Trace seam for `RelayInit.instrument`. */\n  instrument: DeliveryInstrument;\n  /** Metric callbacks for `RelayInit.hooks`. */\n  hooks: DeliveryHooks;\n}\n\nconst DELIVERIES_COUNTER = \"commitcourier.deliveries\";\nconst DURATION_HISTOGRAM = \"commitcourier.delivery.duration\";\n\n/**\n * Split a `DeliveryEvent.host` (`hostname` / `hostname:port` / `[ipv6]` / `[ipv6]:port`) into the\n * OTel `server.address` (hostname, IPv6 brackets stripped) and `server.port` (number). Using the\n * raw `host` for `server.address` would embed the port, which breaks the OTel semantic convention\n * (port belongs in `server.port`). Returns nulls for an absent/unparseable port.\n */\nfunction splitHost(host: string): { address: string; port: number | null } {\n  if (host.startsWith(\"[\")) {\n    const end = host.indexOf(\"]\");\n    if (end !== -1) {\n      const address = host.slice(1, end);\n      const rest = host.slice(end + 1);\n      const port = rest.startsWith(\":\") ? Number(rest.slice(1)) : Number.NaN;\n      return { address, port: Number.isInteger(port) ? port : null };\n    }\n  }\n  const i = host.lastIndexOf(\":\");\n  if (i !== -1) {\n    const portStr = host.slice(i + 1);\n    if (/^\\d+$/.test(portStr)) return { address: host.slice(0, i), port: Number(portStr) };\n  }\n  return { address: host, port: null };\n}\n\n/** Set `server.address`/`server.port` on a span from a `DeliveryEvent.host` (no-op when null). */\nfunction setServer(span: Span, host: string | null): void {\n  if (host == null) return;\n  const { address, port } = splitHost(host);\n  span.setAttribute(\"server.address\", address);\n  if (port != null) span.setAttribute(\"server.port\", port);\n}\n\n/**\n * Build OpenTelemetry instrumentation from a tracer and/or meter. Pass the result to\n * `createRelay({ store, instrument, hooks })`. Omitting both yields no-op wiring (safe to always wire).\n *\n * @example\n * ```ts\n * import { trace, metrics } from \"@opentelemetry/api\";\n * import { createOtelInstrumentation } from \"commitcourier/otel\";\n * const { instrument, hooks } = createOtelInstrumentation({\n *   tracer: trace.getTracer(\"commitcourier\"),\n *   meter: metrics.getMeter(\"commitcourier\"),\n * });\n * const relay = await createRelay({ store, instrument, hooks });\n * ```\n */\nexport function createOtelInstrumentation(\n  opts: OtelInstrumentationOptions = {},\n): OtelInstrumentation {\n  const { tracer, meter, spanName = \"commitcourier.delivery\" } = opts;\n\n  const counter: Counter | undefined = meter?.createCounter(DELIVERIES_COUNTER, {\n    description: \"Count of webhook delivery attempt outcomes.\",\n  });\n  const histogram: Histogram | undefined = meter?.createHistogram(DURATION_HISTOGRAM, {\n    unit: \"ms\",\n    description: \"Wall-clock duration of webhook delivery attempts.\",\n  });\n\n  const instrument: DeliveryInstrument = (start) => {\n    if (!tracer) return undefined;\n    const span = tracer.startSpan(spanName, { kind: SpanKind.CLIENT });\n    span.setAttribute(\"webhook.id\", start.id);\n    span.setAttribute(\"webhook.event_type\", start.eventType);\n    span.setAttribute(\"webhook.attempt\", start.attempt);\n    if (start.endpointId != null) span.setAttribute(\"endpoint.id\", start.endpointId);\n    setServer(span, start.host);\n    return (event) => {\n      setServer(span, event.host);\n      if (event.status != null) span.setAttribute(\"http.response.status_code\", event.status);\n      if (event.error != null) {\n        span.setAttribute(\"error.type\", event.error);\n        span.setStatus({ code: SpanStatusCode.ERROR, message: event.error });\n      } else {\n        span.setStatus({ code: SpanStatusCode.OK });\n      }\n      span.end();\n    };\n  };\n\n  /** Record the counter/histogram for a terminal outcome. Keeps attributes low-cardinality (no id). */\n  function record(outcome: \"delivered\" | \"retry\" | \"dead\", event: DeliveryEvent): void {\n    if (!counter && !histogram) return;\n    const attrs: Attributes = { outcome, \"webhook.event_type\": event.eventType };\n    if (event.status != null) attrs[\"http.response.status_code\"] = event.status;\n    counter?.add(1, attrs);\n    histogram?.record(event.durationMs, attrs);\n  }\n\n  const hooks: DeliveryHooks = {\n    onDelivered: (event) => {\n      record(\"delivered\", event);\n    },\n    onRetry: (event) => {\n      record(\"retry\", event);\n    },\n    onDead: (event) => {\n      record(\"dead\", event);\n    },\n  };\n\n  return { instrument, hooks };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAcA,iBAAyC;AAsBzC,IAAM,qBAAqB;AAC3B,IAAM,qBAAqB;AAQ3B,SAAS,UAAU,MAAwD;AACzE,MAAI,KAAK,WAAW,GAAG,GAAG;AACxB,UAAM,MAAM,KAAK,QAAQ,GAAG;AAC5B,QAAI,QAAQ,IAAI;AACd,YAAM,UAAU,KAAK,MAAM,GAAG,GAAG;AACjC,YAAM,OAAO,KAAK,MAAM,MAAM,CAAC;AAC/B,YAAM,OAAO,KAAK,WAAW,GAAG,IAAI,OAAO,KAAK,MAAM,CAAC,CAAC,IAAI,OAAO;AACnE,aAAO,EAAE,SAAS,MAAM,OAAO,UAAU,IAAI,IAAI,OAAO,KAAK;AAAA,IAC/D;AAAA,EACF;AACA,QAAM,IAAI,KAAK,YAAY,GAAG;AAC9B,MAAI,MAAM,IAAI;AACZ,UAAM,UAAU,KAAK,MAAM,IAAI,CAAC;AAChC,QAAI,QAAQ,KAAK,OAAO,EAAG,QAAO,EAAE,SAAS,KAAK,MAAM,GAAG,CAAC,GAAG,MAAM,OAAO,OAAO,EAAE;AAAA,EACvF;AACA,SAAO,EAAE,SAAS,MAAM,MAAM,KAAK;AACrC;AAGA,SAAS,UAAU,MAAY,MAA2B;AACxD,MAAI,QAAQ,KAAM;AAClB,QAAM,EAAE,SAAS,KAAK,IAAI,UAAU,IAAI;AACxC,OAAK,aAAa,kBAAkB,OAAO;AAC3C,MAAI,QAAQ,KAAM,MAAK,aAAa,eAAe,IAAI;AACzD;AAiBO,SAAS,0BACd,OAAmC,CAAC,GACf;AACrB,QAAM,EAAE,QAAQ,OAAO,WAAW,yBAAyB,IAAI;AAE/D,QAAM,UAA+B,OAAO,cAAc,oBAAoB;AAAA,IAC5E,aAAa;AAAA,EACf,CAAC;AACD,QAAM,YAAmC,OAAO,gBAAgB,oBAAoB;AAAA,IAClF,MAAM;AAAA,IACN,aAAa;AAAA,EACf,CAAC;AAED,QAAM,aAAiC,CAAC,UAAU;AAChD,QAAI,CAAC,OAAQ,QAAO;AACpB,UAAM,OAAO,OAAO,UAAU,UAAU,EAAE,MAAM,oBAAS,OAAO,CAAC;AACjE,SAAK,aAAa,cAAc,MAAM,EAAE;AACxC,SAAK,aAAa,sBAAsB,MAAM,SAAS;AACvD,SAAK,aAAa,mBAAmB,MAAM,OAAO;AAClD,QAAI,MAAM,cAAc,KAAM,MAAK,aAAa,eAAe,MAAM,UAAU;AAC/E,cAAU,MAAM,MAAM,IAAI;AAC1B,WAAO,CAAC,UAAU;AAChB,gBAAU,MAAM,MAAM,IAAI;AAC1B,UAAI,MAAM,UAAU,KAAM,MAAK,aAAa,6BAA6B,MAAM,MAAM;AACrF,UAAI,MAAM,SAAS,MAAM;AACvB,aAAK,aAAa,cAAc,MAAM,KAAK;AAC3C,aAAK,UAAU,EAAE,MAAM,0BAAe,OAAO,SAAS,MAAM,MAAM,CAAC;AAAA,MACrE,OAAO;AACL,aAAK,UAAU,EAAE,MAAM,0BAAe,GAAG,CAAC;AAAA,MAC5C;AACA,WAAK,IAAI;AAAA,IACX;AAAA,EACF;AAGA,WAAS,OAAO,SAAyC,OAA4B;AACnF,QAAI,CAAC,WAAW,CAAC,UAAW;AAC5B,UAAM,QAAoB,EAAE,SAAS,sBAAsB,MAAM,UAAU;AAC3E,QAAI,MAAM,UAAU,KAAM,OAAM,2BAA2B,IAAI,MAAM;AACrE,aAAS,IAAI,GAAG,KAAK;AACrB,eAAW,OAAO,MAAM,YAAY,KAAK;AAAA,EAC3C;AAEA,QAAM,QAAuB;AAAA,IAC3B,aAAa,CAAC,UAAU;AACtB,aAAO,aAAa,KAAK;AAAA,IAC3B;AAAA,IACA,SAAS,CAAC,UAAU;AAClB,aAAO,SAAS,KAAK;AAAA,IACvB;AAAA,IACA,QAAQ,CAAC,UAAU;AACjB,aAAO,QAAQ,KAAK;AAAA,IACtB;AAAA,EACF;AAEA,SAAO,EAAE,YAAY,MAAM;AAC7B;","names":[]}