hono-webhooks 0.4.1 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/otel.ts CHANGED
@@ -18,6 +18,54 @@ import {
18
18
  /** The `route` label for 404s, which match no webhook. */
19
19
  export const UNMATCHED_ROUTE = '__unmatched__'
20
20
 
21
+ /** A metric `otelWebhooks` records, described for a metric catalog. */
22
+ export interface WebhookMetricDefinition {
23
+ name: string
24
+ kind: 'counter' | 'histogram'
25
+ unit?: string
26
+ description: string
27
+ /** Attribute key to its closed set of values, or `'string'` when open. */
28
+ attributes: Record<string, readonly string[] | 'string'>
29
+ }
30
+
31
+ const metricAttributes = {
32
+ route: 'string',
33
+ status: ['success', 'failed']
34
+ } as const
35
+
36
+ /**
37
+ * The metrics `otelWebhooks` records, as data: list them in a metric
38
+ * catalog without creating a second recorder.
39
+ */
40
+ export const webhookMetrics = [
41
+ {
42
+ name: 'webhook_request_total',
43
+ kind: 'counter',
44
+ description: 'HTTP webhook requests by route template and status',
45
+ attributes: metricAttributes
46
+ },
47
+ {
48
+ name: 'webhook_request_duration_ms',
49
+ kind: 'histogram',
50
+ unit: 'ms',
51
+ description: 'HTTP webhook handler duration by route template and status',
52
+ attributes: metricAttributes
53
+ }
54
+ ] as const satisfies readonly WebhookMetricDefinition[]
55
+
56
+ /** `summarizeEvent(event)` plus what a completion log needs. */
57
+ export interface WebhookCall extends WebhookEventSummary {
58
+ /** `error` for 5xx, `warn` for 4xx, else `info`. */
59
+ level: 'info' | 'warn' | 'error'
60
+ outcome: 'success' | 'failed' | 'validation_failed'
61
+ }
62
+
63
+ /** `console` fits, as does any logger with these three methods. */
64
+ export type WebhookLogger = Record<
65
+ WebhookCall['level'],
66
+ (message: string, fields: Record<string, string | number>) => void
67
+ >
68
+
21
69
  export interface OtelWebhooksOptions<E extends Env = any> {
22
70
  /** @default trace.getTracer('webhooks') */
23
71
  tracer?: Tracer
@@ -31,9 +79,19 @@ export interface OtelWebhooksOptions<E extends Env = any> {
31
79
  * `onEvent`'s.
32
80
  */
33
81
  onWebhookCall?: (
34
- call: WebhookEventSummary,
82
+ call: WebhookCall,
35
83
  event: WebhookEvent<E>
36
84
  ) => void | Promise<void>
85
+ /**
86
+ * Logs one `webhook.completed` event per request at `call.level`, with
87
+ * `route`, `method`, `status_code`, `duration_ms`, `status` (the outcome)
88
+ * and `error_type`. 404s and `ignoreRoutes` are skipped. Called with the
89
+ * request's span, so the logger can attach to it.
90
+ *
91
+ * @example
92
+ * logger: () => console
93
+ */
94
+ logger?: (span: Span) => WebhookLogger
37
95
  }
38
96
 
39
97
  /**
@@ -77,8 +135,17 @@ export function otelWebhooks<E extends Env = any>(
77
135
  )
78
136
  },
79
137
  onEvent: (event) => {
80
- const call = summarizeEvent(event)
81
- const success = call.status < 400
138
+ const summary = summarizeEvent(event)
139
+ const success = summary.status < 400
140
+ const call: WebhookCall = {
141
+ ...summary,
142
+ level: summary.status >= 500 ? 'error' : success ? 'info' : 'warn',
143
+ outcome: summary.validation
144
+ ? 'validation_failed'
145
+ : success
146
+ ? 'success'
147
+ : 'failed'
148
+ }
82
149
 
83
150
  const span = spans.get(event.c)
84
151
  span?.setAttributes({
@@ -96,7 +163,8 @@ export function otelWebhooks<E extends Env = any>(
96
163
  code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR
97
164
  })
98
165
 
99
- if (call.route === null || !ignored.has(call.route)) {
166
+ const isIgnored = call.route !== null && ignored.has(call.route)
167
+ if (!isIgnored) {
100
168
  // Created lazily so a meter provider registered after import still applies.
101
169
  instruments ??= createInstruments(
102
170
  options.meter ?? metrics.getMeter('webhooks')
@@ -109,19 +177,32 @@ export function otelWebhooks<E extends Env = any>(
109
177
  instruments.duration.record(event.durationMs, attributes)
110
178
  }
111
179
 
180
+ if (span && options.logger && call.route !== null && !isIgnored) {
181
+ options.logger(span)[call.level]('webhook.completed', {
182
+ event: 'webhook.completed',
183
+ route: call.route,
184
+ method: call.method,
185
+ status_code: call.status,
186
+ duration_ms: call.durationMs,
187
+ status: call.outcome,
188
+ ...(call.error && { error_type: call.error.name })
189
+ })
190
+ }
191
+
112
192
  return options.onWebhookCall?.(call, event)
113
193
  }
114
194
  }
115
195
  }
116
196
 
117
197
  function createInstruments(meter: Meter) {
198
+ const [total, duration] = webhookMetrics
118
199
  return {
119
- total: meter.createCounter('webhook_request_total', {
120
- description: 'HTTP webhook requests by route template and status'
200
+ total: meter.createCounter(total.name, {
201
+ description: total.description
121
202
  }),
122
- duration: meter.createHistogram('webhook_request_duration_ms', {
123
- unit: 'ms',
124
- description: 'HTTP webhook handler duration by route template and status'
203
+ duration: meter.createHistogram(duration.name, {
204
+ unit: duration.unit,
205
+ description: duration.description
125
206
  })
126
207
  }
127
208
  }