hono-webhooks 0.4.1 → 0.5.0
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/README.md +30 -5
- package/dist/otel.d.ts +51 -2
- package/dist/otel.js +47 -9
- package/package.json +1 -1
- package/src/otel.ts +90 -9
package/README.md
CHANGED
|
@@ -560,12 +560,15 @@ import { otelWebhooks } from 'hono-webhooks/otel'
|
|
|
560
560
|
createWebhooks(webhooks, {
|
|
561
561
|
...otelWebhooks({
|
|
562
562
|
ignoreRoutes: ['/health'],
|
|
563
|
+
// one `webhook.completed` log per request, with the request's span
|
|
564
|
+
logger: (span) => createLogger({ span }),
|
|
563
565
|
onWebhookCall(call, event) {
|
|
564
|
-
// `call` is summarizeEvent(event)
|
|
565
|
-
|
|
566
|
+
// `call` is summarizeEvent(event) plus `level` and `outcome`;
|
|
567
|
+
// the request's span is active here
|
|
568
|
+
storeCall(call)
|
|
566
569
|
}
|
|
567
570
|
}),
|
|
568
|
-
//
|
|
571
|
+
// the completion log carries the error type
|
|
569
572
|
logErrors: false
|
|
570
573
|
})
|
|
571
574
|
```
|
|
@@ -578,7 +581,23 @@ createWebhooks(webhooks, {
|
|
|
578
581
|
- **Metrics** `webhook_request_total` (counter) and
|
|
579
582
|
`webhook_request_duration_ms` (histogram, ms), both by `route` (the pattern,
|
|
580
583
|
`__unmatched__` for a 404) and `status` (`success` below 400, else `failed`).
|
|
581
|
-
- `
|
|
584
|
+
- **Log** `webhook.completed`, when `logger` is set, at `error` for 5xx,
|
|
585
|
+
`warn` for 4xx and `info` otherwise, with `route`, `method`, `status_code`,
|
|
586
|
+
`duration_ms`, `status` (`success`, `failed` or `validation_failed`) and
|
|
587
|
+
`error_type`. `logger` gets the request's span and returns anything with
|
|
588
|
+
`info`, `warn` and `error` methods taking `(message, fields)`, so
|
|
589
|
+
`logger: () => console` works. 404s and `ignoreRoutes` are not logged.
|
|
590
|
+
- `ignoreRoutes` skips metrics and the log; the span and `onWebhookCall` still
|
|
591
|
+
run.
|
|
592
|
+
- `webhookMetrics` describes both metrics (`name`, `kind`, `unit`,
|
|
593
|
+
`description` and `attributes`, where each attribute maps to its values or
|
|
594
|
+
`'string'`), so a metric catalog can list them:
|
|
595
|
+
|
|
596
|
+
```ts
|
|
597
|
+
import { webhookMetrics } from 'hono-webhooks/otel'
|
|
598
|
+
|
|
599
|
+
export const otelMetrics = [...appMetrics, ...webhookMetrics]
|
|
600
|
+
```
|
|
582
601
|
- `tracer` and `meter` default to `trace.getTracer('webhooks')` and
|
|
583
602
|
`metrics.getMeter('webhooks')`. The meter is resolved on the first request,
|
|
584
603
|
so registering the SDK after import works.
|
|
@@ -757,6 +776,7 @@ import {
|
|
|
757
776
|
|
|
758
777
|
import {
|
|
759
778
|
otelWebhooks, // (options?) => { around, onEvent } with spans and metrics
|
|
779
|
+
webhookMetrics, // the two metric definitions, as data
|
|
760
780
|
UNMATCHED_ROUTE // '__unmatched__', the route label for 404s
|
|
761
781
|
} from 'hono-webhooks/otel'
|
|
762
782
|
|
|
@@ -781,7 +801,12 @@ import type {
|
|
|
781
801
|
StandardSchemaV1
|
|
782
802
|
} from 'hono-webhooks'
|
|
783
803
|
|
|
784
|
-
import type {
|
|
804
|
+
import type {
|
|
805
|
+
OtelWebhooksOptions,
|
|
806
|
+
WebhookCall,
|
|
807
|
+
WebhookLogger,
|
|
808
|
+
WebhookMetricDefinition
|
|
809
|
+
} from 'hono-webhooks/otel'
|
|
785
810
|
```
|
|
786
811
|
|
|
787
812
|
Use `Webhook` to type a list: `const all: Webhook<AppEnv>[] = [a, b]`.
|
package/dist/otel.d.ts
CHANGED
|
@@ -1,8 +1,47 @@
|
|
|
1
|
-
import { type Meter, type Tracer } from '@opentelemetry/api';
|
|
1
|
+
import { type Meter, type Span, type Tracer } from '@opentelemetry/api';
|
|
2
2
|
import type { Env } from 'hono';
|
|
3
3
|
import { type WebhookEvent, type WebhookEventSummary, type WebhooksOptions } from 'hono-webhooks';
|
|
4
4
|
/** The `route` label for 404s, which match no webhook. */
|
|
5
5
|
export declare const UNMATCHED_ROUTE = "__unmatched__";
|
|
6
|
+
/** A metric `otelWebhooks` records, described for a metric catalog. */
|
|
7
|
+
export interface WebhookMetricDefinition {
|
|
8
|
+
name: string;
|
|
9
|
+
kind: 'counter' | 'histogram';
|
|
10
|
+
unit?: string;
|
|
11
|
+
description: string;
|
|
12
|
+
/** Attribute key to its closed set of values, or `'string'` when open. */
|
|
13
|
+
attributes: Record<string, readonly string[] | 'string'>;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The metrics `otelWebhooks` records, as data: list them in a metric
|
|
17
|
+
* catalog without creating a second recorder.
|
|
18
|
+
*/
|
|
19
|
+
export declare const webhookMetrics: readonly [{
|
|
20
|
+
readonly name: 'webhook_request_total';
|
|
21
|
+
readonly kind: 'counter';
|
|
22
|
+
readonly description: 'HTTP webhook requests by route template and status';
|
|
23
|
+
readonly attributes: {
|
|
24
|
+
readonly route: 'string';
|
|
25
|
+
readonly status: readonly ['success', 'failed'];
|
|
26
|
+
};
|
|
27
|
+
}, {
|
|
28
|
+
readonly name: 'webhook_request_duration_ms';
|
|
29
|
+
readonly kind: 'histogram';
|
|
30
|
+
readonly unit: 'ms';
|
|
31
|
+
readonly description: 'HTTP webhook handler duration by route template and status';
|
|
32
|
+
readonly attributes: {
|
|
33
|
+
readonly route: 'string';
|
|
34
|
+
readonly status: readonly ['success', 'failed'];
|
|
35
|
+
};
|
|
36
|
+
}];
|
|
37
|
+
/** `summarizeEvent(event)` plus what a completion log needs. */
|
|
38
|
+
export interface WebhookCall extends WebhookEventSummary {
|
|
39
|
+
/** `error` for 5xx, `warn` for 4xx, else `info`. */
|
|
40
|
+
level: 'info' | 'warn' | 'error';
|
|
41
|
+
outcome: 'success' | 'failed' | 'validation_failed';
|
|
42
|
+
}
|
|
43
|
+
/** `console` fits, as does any logger with these three methods. */
|
|
44
|
+
export type WebhookLogger = Record<WebhookCall['level'], (message: string, fields: Record<string, string | number>) => void>;
|
|
6
45
|
export interface OtelWebhooksOptions<E extends Env = any> {
|
|
7
46
|
/** @default trace.getTracer('webhooks') */
|
|
8
47
|
tracer?: Tracer;
|
|
@@ -15,7 +54,17 @@ export interface OtelWebhooksOptions<E extends Env = any> {
|
|
|
15
54
|
* still active: log it, store it. A returned promise is handled like
|
|
16
55
|
* `onEvent`'s.
|
|
17
56
|
*/
|
|
18
|
-
onWebhookCall?: (call:
|
|
57
|
+
onWebhookCall?: (call: WebhookCall, event: WebhookEvent<E>) => void | Promise<void>;
|
|
58
|
+
/**
|
|
59
|
+
* Logs one `webhook.completed` event per request at `call.level`, with
|
|
60
|
+
* `route`, `method`, `status_code`, `duration_ms`, `status` (the outcome)
|
|
61
|
+
* and `error_type`. 404s and `ignoreRoutes` are skipped. Called with the
|
|
62
|
+
* request's span, so the logger can attach to it.
|
|
63
|
+
*
|
|
64
|
+
* @example
|
|
65
|
+
* logger: () => console
|
|
66
|
+
*/
|
|
67
|
+
logger?: (span: Span) => WebhookLogger;
|
|
19
68
|
}
|
|
20
69
|
/**
|
|
21
70
|
* `around` and `onEvent` that put each request in a server span and record
|
package/dist/otel.js
CHANGED
|
@@ -9,6 +9,25 @@ import {
|
|
|
9
9
|
summarizeEvent
|
|
10
10
|
} from "hono-webhooks";
|
|
11
11
|
var UNMATCHED_ROUTE = "__unmatched__";
|
|
12
|
+
var metricAttributes = {
|
|
13
|
+
route: "string",
|
|
14
|
+
status: ["success", "failed"]
|
|
15
|
+
};
|
|
16
|
+
var webhookMetrics = [
|
|
17
|
+
{
|
|
18
|
+
name: "webhook_request_total",
|
|
19
|
+
kind: "counter",
|
|
20
|
+
description: "HTTP webhook requests by route template and status",
|
|
21
|
+
attributes: metricAttributes
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: "webhook_request_duration_ms",
|
|
25
|
+
kind: "histogram",
|
|
26
|
+
unit: "ms",
|
|
27
|
+
description: "HTTP webhook handler duration by route template and status",
|
|
28
|
+
attributes: metricAttributes
|
|
29
|
+
}
|
|
30
|
+
];
|
|
12
31
|
function otelWebhooks(options = {}) {
|
|
13
32
|
const tracer = options.tracer ?? trace.getTracer("webhooks");
|
|
14
33
|
const ignored = new Set(options.ignoreRoutes);
|
|
@@ -30,8 +49,13 @@ function otelWebhooks(options = {}) {
|
|
|
30
49
|
});
|
|
31
50
|
},
|
|
32
51
|
onEvent: (event) => {
|
|
33
|
-
const
|
|
34
|
-
const success =
|
|
52
|
+
const summary = summarizeEvent(event);
|
|
53
|
+
const success = summary.status < 400;
|
|
54
|
+
const call = {
|
|
55
|
+
...summary,
|
|
56
|
+
level: summary.status >= 500 ? "error" : success ? "info" : "warn",
|
|
57
|
+
outcome: summary.validation ? "validation_failed" : success ? "success" : "failed"
|
|
58
|
+
};
|
|
35
59
|
const span = spans.get(event.c);
|
|
36
60
|
span?.setAttributes({
|
|
37
61
|
"http.status_code": call.status,
|
|
@@ -47,7 +71,8 @@ function otelWebhooks(options = {}) {
|
|
|
47
71
|
span?.setStatus({
|
|
48
72
|
code: success ? SpanStatusCode.OK : SpanStatusCode.ERROR
|
|
49
73
|
});
|
|
50
|
-
|
|
74
|
+
const isIgnored = call.route !== null && ignored.has(call.route);
|
|
75
|
+
if (!isIgnored) {
|
|
51
76
|
instruments ??= createInstruments(options.meter ?? metrics.getMeter("webhooks"));
|
|
52
77
|
const attributes = {
|
|
53
78
|
route: call.route ?? UNMATCHED_ROUTE,
|
|
@@ -56,22 +81,35 @@ function otelWebhooks(options = {}) {
|
|
|
56
81
|
instruments.total.add(1, attributes);
|
|
57
82
|
instruments.duration.record(event.durationMs, attributes);
|
|
58
83
|
}
|
|
84
|
+
if (span && options.logger && call.route !== null && !isIgnored) {
|
|
85
|
+
options.logger(span)[call.level]("webhook.completed", {
|
|
86
|
+
event: "webhook.completed",
|
|
87
|
+
route: call.route,
|
|
88
|
+
method: call.method,
|
|
89
|
+
status_code: call.status,
|
|
90
|
+
duration_ms: call.durationMs,
|
|
91
|
+
status: call.outcome,
|
|
92
|
+
...call.error && { error_type: call.error.name }
|
|
93
|
+
});
|
|
94
|
+
}
|
|
59
95
|
return options.onWebhookCall?.(call, event);
|
|
60
96
|
}
|
|
61
97
|
};
|
|
62
98
|
}
|
|
63
99
|
function createInstruments(meter) {
|
|
100
|
+
const [total, duration] = webhookMetrics;
|
|
64
101
|
return {
|
|
65
|
-
total: meter.createCounter(
|
|
66
|
-
description:
|
|
102
|
+
total: meter.createCounter(total.name, {
|
|
103
|
+
description: total.description
|
|
67
104
|
}),
|
|
68
|
-
duration: meter.createHistogram(
|
|
69
|
-
unit:
|
|
70
|
-
description:
|
|
105
|
+
duration: meter.createHistogram(duration.name, {
|
|
106
|
+
unit: duration.unit,
|
|
107
|
+
description: duration.description
|
|
71
108
|
})
|
|
72
109
|
};
|
|
73
110
|
}
|
|
74
111
|
export {
|
|
75
112
|
UNMATCHED_ROUTE,
|
|
76
|
-
otelWebhooks
|
|
113
|
+
otelWebhooks,
|
|
114
|
+
webhookMetrics
|
|
77
115
|
};
|
package/package.json
CHANGED
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:
|
|
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
|
|
81
|
-
const success =
|
|
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
|
-
|
|
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(
|
|
120
|
-
description:
|
|
200
|
+
total: meter.createCounter(total.name, {
|
|
201
|
+
description: total.description
|
|
121
202
|
}),
|
|
122
|
-
duration: meter.createHistogram(
|
|
123
|
-
unit:
|
|
124
|
-
description:
|
|
203
|
+
duration: meter.createHistogram(duration.name, {
|
|
204
|
+
unit: duration.unit,
|
|
205
|
+
description: duration.description
|
|
125
206
|
})
|
|
126
207
|
}
|
|
127
208
|
}
|