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.