Skip to content

Writing a hook tap

A tap is a passive observer attached to a named hook. It never changes hook output — it just receives a start | end | error callback for every step. Use taps for logging, OTel spans, request metrics, or anything else that should happen alongside the pipeline.

The three phases

ts
import { resolveHook } from "@nwire/forge"

const hook = resolveHook("posts.publish")

hook.tap("audit-observer", (phase, payload) => {
  switch (phase) {
    case "start":
      // payload: { step, ctx }
      break
    case "end":
      // payload: { step, durationMs, result }
      break
    case "error":
      // payload: { step, error, durationMs }
      break
  }
})

Route a tap to your logger

ts
import { resolveHook } from "@nwire/forge"
import { logger } from "@nwire/logger"

resolveHook("posts.publish").tap("log-steps", (phase, payload) => {
  if (phase === "error") {
    logger.error({ step: payload.step, err: payload.error }, "hook step failed")
    return
  }
  if (phase === "end") {
    logger.debug({ step: payload.step, ms: payload.durationMs }, "hook step ok")
  }
})

Send a tap to OTel

ts
import { attachOtelExporter } from "@nwire/telemetry-otel"

// The runtime already streams hook taps as HookStepStart / HookStepEnd /
// HookStepError telemetry kinds; the exporter turns them into spans.
attachOtelExporter(runtime, { tracer })

You usually don't write a manual OTel tap — adopting the exporter wires the full telemetry stream for you. Manual taps are for one-off observers that don't belong in shared infra.

See also

MIT licensed.