@daloyjs/core 1.1.1 → 1.2.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/README.md +2 -1
- package/dist/adapters/cloudflare.d.ts +8 -1
- package/dist/adapters/cloudflare.js +15 -1
- package/dist/adapters/lambda.js +15 -5
- package/dist/adapters/vercel.d.ts +5 -0
- package/dist/adapters/vercel.js +27 -2
- package/dist/app.d.ts +34 -8
- package/dist/app.js +257 -72
- package/dist/combine.js +14 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/metrics.d.ts +23 -9
- package/dist/metrics.js +37 -10
- package/dist/otlp.d.ts +268 -0
- package/dist/otlp.js +589 -0
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/dist/types.d.ts +18 -2
- package/package.json +5 -1
package/dist/combine.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @since 0.19.0
|
|
7
7
|
*/
|
|
8
|
-
import { _mergePreBodyWithEarlyRejections, EARLY_REJECTION_HOOK_MARKER } from "./middleware.js";
|
|
8
|
+
import { _mergePreBodyWithEarlyRejections, EARLY_REJECTION_HOOK_MARKER, } from "./middleware.js";
|
|
9
9
|
/**
|
|
10
10
|
* Run every supplied {@link Hooks} bundle in order, pipeline-style.
|
|
11
11
|
* Equivalent to passing the bundles to `app.use(...)` one after another,
|
|
@@ -88,7 +88,9 @@ export function some(...layers) {
|
|
|
88
88
|
return [
|
|
89
89
|
async (ctx) => {
|
|
90
90
|
const early = await hooks.preBody(ctx);
|
|
91
|
-
return early instanceof Response
|
|
91
|
+
return early instanceof Response
|
|
92
|
+
? early
|
|
93
|
+
: hooks.beforeHandle(ctx);
|
|
92
94
|
},
|
|
93
95
|
];
|
|
94
96
|
}
|
|
@@ -152,7 +154,9 @@ export function some(...layers) {
|
|
|
152
154
|
*/
|
|
153
155
|
export function except(when, hooks) {
|
|
154
156
|
const earlyRejectionHooks = hooks[EARLY_REJECTION_HOOK_MARKER];
|
|
155
|
-
if (!hooks.preBody &&
|
|
157
|
+
if (!hooks.preBody &&
|
|
158
|
+
!hooks.beforeHandle &&
|
|
159
|
+
!Array.isArray(earlyRejectionHooks))
|
|
156
160
|
return hooks;
|
|
157
161
|
const matches = compileExceptMatcher(when);
|
|
158
162
|
const wrapped = { ...hooks };
|
|
@@ -166,11 +170,11 @@ export function except(when, hooks) {
|
|
|
166
170
|
}
|
|
167
171
|
if (hooks.preBody) {
|
|
168
172
|
const original = hooks.preBody;
|
|
169
|
-
wrapped.preBody = async (ctx) => (
|
|
173
|
+
wrapped.preBody = async (ctx) => (await matches(ctx)) ? undefined : original(ctx);
|
|
170
174
|
}
|
|
171
175
|
if (hooks.beforeHandle) {
|
|
172
176
|
const original = hooks.beforeHandle;
|
|
173
|
-
wrapped.beforeHandle = async (ctx) => (
|
|
177
|
+
wrapped.beforeHandle = async (ctx) => (await matches(ctx)) ? undefined : original(ctx);
|
|
174
178
|
}
|
|
175
179
|
return wrapped;
|
|
176
180
|
}
|
|
@@ -206,7 +210,9 @@ function compilePathPattern(pattern) {
|
|
|
206
210
|
return (path) => regex.test(path);
|
|
207
211
|
}
|
|
208
212
|
function mergeCombineHooks(layers) {
|
|
209
|
-
const pick = (key) => layers
|
|
213
|
+
const pick = (key) => layers
|
|
214
|
+
.map((h) => h[key])
|
|
215
|
+
.filter((f) => typeof f === "function");
|
|
210
216
|
const merged = {};
|
|
211
217
|
const onRequest = pick("onRequest");
|
|
212
218
|
if (onRequest.length > 0) {
|
|
@@ -266,9 +272,9 @@ function mergeCombineHooks(layers) {
|
|
|
266
272
|
}
|
|
267
273
|
const onResponse = pick("onResponse");
|
|
268
274
|
if (onResponse.length > 0) {
|
|
269
|
-
merged.onResponse = async (res) => {
|
|
275
|
+
merged.onResponse = async (res, ctx) => {
|
|
270
276
|
for (const fn of onResponse)
|
|
271
|
-
await fn(res);
|
|
277
|
+
await fn(res, ctx);
|
|
272
278
|
};
|
|
273
279
|
}
|
|
274
280
|
// Forward symbol-keyed security markers (CORS / CSRF / session /
|
package/dist/index.d.ts
CHANGED
|
@@ -95,6 +95,8 @@ export { encodeCursor, decodeCursor, buildLinkHeader, buildPageLinks, pagination
|
|
|
95
95
|
export type { PaginationLink, PageLinkOptions, PageLinks, PaginationQueryOptions, PaginationParams, PaginationQuerySchema, } from "./pagination.js";
|
|
96
96
|
export { MetricsRegistry, Counter, Gauge, Histogram, httpMetrics, DEFAULT_DURATION_BUCKETS, PROMETHEUS_CONTENT_TYPE, } from "./metrics.js";
|
|
97
97
|
export type { MetricLabels, MetricsRegistryOptions, HttpMetricsOptions } from "./metrics.js";
|
|
98
|
+
export { createOtlpLogExporter, createOtlpMetricsExporter, createAppTelemetry, semconvHttpMetrics, HTTP_SERVER_REQUEST_DURATION_BUCKETS, } from "./otlp.js";
|
|
99
|
+
export type { OtlpExporterOptions, OtlpLogExporter, OtlpMetricsExporter, OtlpHistogramOptions, SemconvHttpMetricsOptions, TelemetryOptions, AppTelemetry, } from "./otlp.js";
|
|
98
100
|
export { fileField, multipartObject, isFileFieldSchema, isMultipartObjectSchema, } from "./multipart.js";
|
|
99
101
|
export type { FileFieldSchema, FileFieldOptions, FileMagicBytesOption, FileMagicBytesSignature, MultipartObjectOptions, MultipartShape, UploadedFile, } from "./multipart.js";
|
|
100
102
|
export { otelTracing, TRACING_SPAN_KIND_SERVER, TRACING_SPAN_STATUS_UNSET, TRACING_SPAN_STATUS_OK, TRACING_SPAN_STATUS_ERROR, } from "./tracing.js";
|
package/dist/index.js
CHANGED
|
@@ -48,6 +48,7 @@ export { idempotency, MemoryIdempotencyStore, _resetSharedIdempotencyStoresForTe
|
|
|
48
48
|
export { responseCache, MemoryResponseCacheStore, RESPONSE_CACHE_HOOK_MARKER, _resetSharedResponseCacheStoresForTests, } from "./response-cache.js";
|
|
49
49
|
export { encodeCursor, decodeCursor, buildLinkHeader, buildPageLinks, paginationQuery, MAX_CURSOR_LENGTH, } from "./pagination.js";
|
|
50
50
|
export { MetricsRegistry, Counter, Gauge, Histogram, httpMetrics, DEFAULT_DURATION_BUCKETS, PROMETHEUS_CONTENT_TYPE, } from "./metrics.js";
|
|
51
|
+
export { createOtlpLogExporter, createOtlpMetricsExporter, createAppTelemetry, semconvHttpMetrics, HTTP_SERVER_REQUEST_DURATION_BUCKETS, } from "./otlp.js";
|
|
51
52
|
export { fileField, multipartObject, isFileFieldSchema, isMultipartObjectSchema, } from "./multipart.js";
|
|
52
53
|
export { otelTracing, TRACING_SPAN_KIND_SERVER, TRACING_SPAN_STATUS_UNSET, TRACING_SPAN_STATUS_OK, TRACING_SPAN_STATUS_ERROR, } from "./tracing.js";
|
|
53
54
|
export { tenancy, tenantScope, tenantFromSubdomain, tenantFromHeader, tenantFromPathPrefix, tenantFromClaim, defaultTenantNormalize, TENANCY_HOOK_MARKER, TENANCY_RESOLVED_MARKER, TENANT_UNRESOLVED, } from "./tenancy.js";
|
package/dist/metrics.d.ts
CHANGED
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
* `http_requests_in_flight`) into a registry.
|
|
18
18
|
*
|
|
19
19
|
* Everything is built on Web-standard primitives (plus optional `process.*`
|
|
20
|
-
* gauges guarded for non-Node runtimes)
|
|
21
|
-
* Deno, Cloudflare Workers, and Vercel
|
|
20
|
+
* gauges guarded for non-Node runtimes). The registry and renderer run on
|
|
21
|
+
* Node, Bun, Deno, Cloudflare Workers, and Vercel; **pull scraping
|
|
22
|
+
* (`GET /metrics`) is only a service-wide signal on a long-lived process**.
|
|
23
|
+
* On ephemeral isolates use `new App({ telemetry: true })` (OTLP push)
|
|
24
|
+
* instead of scraping `/metrics`.
|
|
22
25
|
*
|
|
23
26
|
* @module
|
|
24
27
|
* @since 0.37.0
|
|
@@ -94,7 +97,14 @@ export declare class Histogram extends Metric {
|
|
|
94
97
|
}
|
|
95
98
|
/** Options for {@link MetricsRegistry}. */
|
|
96
99
|
export interface MetricsRegistryOptions {
|
|
97
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Prefix applied to every metric name except the framework drop counter.
|
|
102
|
+
* Default `""` so process gauges (`process_resident_memory_bytes`) and HTTP
|
|
103
|
+
* RED series (`http_requests_total`) match the names stock Grafana panels
|
|
104
|
+
* already query. Pass `"daloy_"` (or any other prefix) to namespace them.
|
|
105
|
+
* The cardinality-drop meta-metric stays `daloy_metrics_series_dropped_total`
|
|
106
|
+
* when this is empty, because it is a DaloyJS-specific series.
|
|
107
|
+
*/
|
|
98
108
|
prefix?: string;
|
|
99
109
|
/**
|
|
100
110
|
* Hard cap on the number of distinct series **per metric**. Once reached,
|
|
@@ -172,15 +182,19 @@ export interface HttpMetricsOptions {
|
|
|
172
182
|
registry: MetricsRegistry;
|
|
173
183
|
/**
|
|
174
184
|
* Resolve the low-cardinality `route` label from the request context.
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
185
|
+
* When omitted, the matched route **template** (`ctx.routePath`, e.g.
|
|
186
|
+
* `/books/:id`) is used. That is bounded by the route table, so a hostile
|
|
187
|
+
* client cannot mint series from raw paths. A custom resolver that returns
|
|
188
|
+
* a raw pathname should still keep cardinality bounded; the pathname
|
|
189
|
+
* fallback (no template on the context) is capped by
|
|
190
|
+
* {@link HttpMetricsOptions.maxRouteCardinality}.
|
|
178
191
|
*/
|
|
179
192
|
route?: (ctx: BaseContext<any, any>) => string | undefined;
|
|
180
193
|
/**
|
|
181
|
-
* Maximum distinct values for the
|
|
182
|
-
*
|
|
183
|
-
*
|
|
194
|
+
* Maximum distinct values for the pathname fallback `route` label (used
|
|
195
|
+
* only when {@link HttpMetricsOptions.route} is omitted **and**
|
|
196
|
+
* `ctx.routePath` is missing) before further values collapse to
|
|
197
|
+
* `"<other>"`. Ignored when a custom resolver is supplied. Default `100`.
|
|
184
198
|
*/
|
|
185
199
|
maxRouteCardinality?: number;
|
|
186
200
|
/** Skip instrumentation for matching request paths (e.g. the scrape route). */
|
package/dist/metrics.js
CHANGED
|
@@ -17,8 +17,11 @@
|
|
|
17
17
|
* `http_requests_in_flight`) into a registry.
|
|
18
18
|
*
|
|
19
19
|
* Everything is built on Web-standard primitives (plus optional `process.*`
|
|
20
|
-
* gauges guarded for non-Node runtimes)
|
|
21
|
-
* Deno, Cloudflare Workers, and Vercel
|
|
20
|
+
* gauges guarded for non-Node runtimes). The registry and renderer run on
|
|
21
|
+
* Node, Bun, Deno, Cloudflare Workers, and Vercel; **pull scraping
|
|
22
|
+
* (`GET /metrics`) is only a service-wide signal on a long-lived process**.
|
|
23
|
+
* On ephemeral isolates use `new App({ telemetry: true })` (OTLP push)
|
|
24
|
+
* instead of scraping `/metrics`.
|
|
22
25
|
*
|
|
23
26
|
* @module
|
|
24
27
|
* @since 0.37.0
|
|
@@ -49,7 +52,10 @@ function escapeHelp(value) {
|
|
|
49
52
|
* out of the `{...}` block and injecting forged samples.
|
|
50
53
|
*/
|
|
51
54
|
function escapeLabelValue(value) {
|
|
52
|
-
return value
|
|
55
|
+
return value
|
|
56
|
+
.replace(/\\/g, "\\\\")
|
|
57
|
+
.replace(/"/g, '\\"')
|
|
58
|
+
.replace(/\n/g, "\\n");
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Validate and normalize a label bag: every key must match the Prometheus
|
|
@@ -137,7 +143,10 @@ export class Counter extends Metric {
|
|
|
137
143
|
}
|
|
138
144
|
/** @internal */
|
|
139
145
|
render() {
|
|
140
|
-
const lines = [
|
|
146
|
+
const lines = [
|
|
147
|
+
`# HELP ${this.name} ${escapeHelp(this.help)}`,
|
|
148
|
+
`# TYPE ${this.name} counter`,
|
|
149
|
+
];
|
|
141
150
|
for (const { labels, value } of this.series.values()) {
|
|
142
151
|
lines.push(`${this.name}${renderLabelBlock(labels)} ${value}`);
|
|
143
152
|
}
|
|
@@ -183,7 +192,10 @@ export class Gauge extends Metric {
|
|
|
183
192
|
}
|
|
184
193
|
/** @internal */
|
|
185
194
|
render() {
|
|
186
|
-
const lines = [
|
|
195
|
+
const lines = [
|
|
196
|
+
`# HELP ${this.name} ${escapeHelp(this.help)}`,
|
|
197
|
+
`# TYPE ${this.name} gauge`,
|
|
198
|
+
];
|
|
187
199
|
for (const { labels, value } of this.series.values()) {
|
|
188
200
|
lines.push(`${this.name}${renderLabelBlock(labels)} ${value}`);
|
|
189
201
|
}
|
|
@@ -219,7 +231,12 @@ export class Histogram extends Metric {
|
|
|
219
231
|
if (!s) {
|
|
220
232
|
if (!this.registry._admitSeries(this.series.size))
|
|
221
233
|
return;
|
|
222
|
-
s = {
|
|
234
|
+
s = {
|
|
235
|
+
labels: entries,
|
|
236
|
+
counts: new Array(this.bounds.length).fill(0),
|
|
237
|
+
sum: 0,
|
|
238
|
+
count: 0,
|
|
239
|
+
};
|
|
223
240
|
this.series.set(key, s);
|
|
224
241
|
}
|
|
225
242
|
s.count += 1;
|
|
@@ -231,7 +248,10 @@ export class Histogram extends Metric {
|
|
|
231
248
|
}
|
|
232
249
|
/** @internal */
|
|
233
250
|
render() {
|
|
234
|
-
const lines = [
|
|
251
|
+
const lines = [
|
|
252
|
+
`# HELP ${this.name} ${escapeHelp(this.help)}`,
|
|
253
|
+
`# TYPE ${this.name} histogram`,
|
|
254
|
+
];
|
|
235
255
|
for (const s of this.series.values()) {
|
|
236
256
|
for (let i = 0; i < this.bounds.length; i++) {
|
|
237
257
|
lines.push(`${this.name}_bucket${renderLabelBlock(s.labels, ["le", String(this.bounds[i])])} ${s.counts[i]}`);
|
|
@@ -272,7 +292,7 @@ export class MetricsRegistry {
|
|
|
272
292
|
collectors = [];
|
|
273
293
|
droppedCounter;
|
|
274
294
|
constructor(opts = {}) {
|
|
275
|
-
this.prefix = opts.prefix ?? "
|
|
295
|
+
this.prefix = opts.prefix ?? "";
|
|
276
296
|
this.maxSeries = opts.maxSeries ?? 5000;
|
|
277
297
|
if (!Number.isInteger(this.maxSeries) || this.maxSeries <= 0) {
|
|
278
298
|
throw new Error("MetricsRegistry maxSeries must be a positive integer.");
|
|
@@ -345,7 +365,9 @@ export class MetricsRegistry {
|
|
|
345
365
|
return metric;
|
|
346
366
|
}
|
|
347
367
|
registerDefaultMetrics() {
|
|
348
|
-
this.droppedCounter = this.counter(
|
|
368
|
+
this.droppedCounter = this.counter(this.prefix === ""
|
|
369
|
+
? "daloy_metrics_series_dropped_total"
|
|
370
|
+
: "metrics_series_dropped_total", "Series dropped after hitting the per-metric cardinality cap.");
|
|
349
371
|
if (typeof process === "undefined")
|
|
350
372
|
return;
|
|
351
373
|
const rss = this.gauge("process_resident_memory_bytes", "Resident memory size in bytes.");
|
|
@@ -369,7 +391,8 @@ export class MetricsRegistry {
|
|
|
369
391
|
}
|
|
370
392
|
/** Monotonic clock in milliseconds, falling back to `Date.now` where needed. */
|
|
371
393
|
function nowMs() {
|
|
372
|
-
return typeof performance !== "undefined" &&
|
|
394
|
+
return typeof performance !== "undefined" &&
|
|
395
|
+
typeof performance.now === "function"
|
|
373
396
|
? performance.now()
|
|
374
397
|
: Date.now();
|
|
375
398
|
}
|
|
@@ -401,6 +424,10 @@ export function httpMetrics(opts) {
|
|
|
401
424
|
const routeLabel = (ctx) => {
|
|
402
425
|
if (opts.route)
|
|
403
426
|
return opts.route(ctx) ?? "<unknown>";
|
|
427
|
+
// The router stamps the matched route template on the context; it is
|
|
428
|
+
// low-cardinality by construction, so no cap bookkeeping is needed.
|
|
429
|
+
if (typeof ctx.routePath === "string")
|
|
430
|
+
return ctx.routePath;
|
|
404
431
|
let path = "/";
|
|
405
432
|
try {
|
|
406
433
|
path = new URL(ctx.request.url).pathname;
|
package/dist/otlp.d.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenTelemetry OTLP push export — logs, metrics, and semantic-convention
|
|
3
|
+
* HTTP server instrumentation, with zero runtime dependencies.
|
|
4
|
+
*
|
|
5
|
+
* Many container platforms run an in-cluster OTel collector and expect
|
|
6
|
+
* workloads to **push** telemetry: they inject the standard
|
|
7
|
+
* `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_HEADERS` /
|
|
8
|
+
* `OTEL_RESOURCE_ATTRIBUTES` / `OTEL_SERVICE_NAME` variables into every
|
|
9
|
+
* container and scrape nothing — not stdout, not a `/metrics` route. The
|
|
10
|
+
* OTel Node SDK covers that with monkey-patching auto-instrumentation, but
|
|
11
|
+
* its ESM loader hooks are fragile on modern Node and unavailable on edge
|
|
12
|
+
* runtimes. This module is the framework-native alternative:
|
|
13
|
+
*
|
|
14
|
+
* - {@link createOtlpLogExporter} — batched OTLP/HTTP JSON log export;
|
|
15
|
+
* tee your logger's `write` sink into it.
|
|
16
|
+
* - {@link createOtlpMetricsExporter} — cumulative counters + histograms
|
|
17
|
+
* pushed as OTLP/HTTP JSON.
|
|
18
|
+
* - {@link semconvHttpMetrics} — a `Hooks` bundle emitting
|
|
19
|
+
* `http.server.request.duration` exactly per the OTel HTTP semantic
|
|
20
|
+
* conventions (names, attributes, bucket boundaries), so standard Grafana
|
|
21
|
+
* dashboards work unchanged.
|
|
22
|
+
* - `new App({ telemetry: true })` wires all of the above automatically
|
|
23
|
+
* (see {@link TelemetryOptions}).
|
|
24
|
+
*
|
|
25
|
+
* Everything is transport-portable (`fetch` + web-standard primitives) and
|
|
26
|
+
* **fail-safe by contract**: a dead or misconfigured collector never affects
|
|
27
|
+
* request serving — bounded queues, dropped-batch counters, no retry storms,
|
|
28
|
+
* a timeout on every export POST, and cumulative metric temporality so
|
|
29
|
+
* totals survive failed pushes. Long-lived runtimes flush on an `unref`'d
|
|
30
|
+
* interval plus graceful shutdown; isolate runtimes (Workers, Vercel, Lambda)
|
|
31
|
+
* flush per request via the adapters' `waitUntil` / handler-await path.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
* @since 1.2.0
|
|
35
|
+
*/
|
|
36
|
+
import type { Hooks } from "./types.js";
|
|
37
|
+
/**
|
|
38
|
+
* Shared configuration for {@link createOtlpLogExporter} and
|
|
39
|
+
* {@link createOtlpMetricsExporter}. Every field falls back to the standard
|
|
40
|
+
* `OTEL_*` environment variables, so on platforms that inject them no
|
|
41
|
+
* explicit configuration is needed at all.
|
|
42
|
+
*
|
|
43
|
+
* @since 1.2.0
|
|
44
|
+
*/
|
|
45
|
+
export interface OtlpExporterOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Collector base URL. Defaults to `OTEL_EXPORTER_OTLP_ENDPOINT`. The
|
|
48
|
+
* conventional injected value is the gRPC form (`http://host:4317`); a
|
|
49
|
+
* trailing `:4317` is rewritten to `:4318`, the collector's OTLP/HTTP port.
|
|
50
|
+
* The signal path (`/v1/logs`, `/v1/metrics`) is appended automatically.
|
|
51
|
+
*/
|
|
52
|
+
endpoint?: string;
|
|
53
|
+
/**
|
|
54
|
+
* Extra request headers. Defaults to `OTEL_EXPORTER_OTLP_HEADERS`
|
|
55
|
+
* (`key=value,key2=value2`). Multi-tenant collectors route on these
|
|
56
|
+
* (e.g. `tenant_id=...`), so treat the values as credentials: DaloyJS
|
|
57
|
+
* never logs them and never includes them in errors.
|
|
58
|
+
*/
|
|
59
|
+
headers?: Record<string, string>;
|
|
60
|
+
/**
|
|
61
|
+
* OTLP resource attributes. Defaults to `OTEL_RESOURCE_ATTRIBUTES`
|
|
62
|
+
* (`key=value,...`) plus `service.name` from `OTEL_SERVICE_NAME` when not
|
|
63
|
+
* already present.
|
|
64
|
+
*/
|
|
65
|
+
resourceAttributes?: Record<string, string>;
|
|
66
|
+
/**
|
|
67
|
+
* Flush cadence in ms on runtimes with timers (`unref`'d — never keeps the
|
|
68
|
+
* process alive). Defaults: 5000 (logs), 15000 (metrics). Set `0` to
|
|
69
|
+
* disable the timer and flush manually / on shutdown / via the serverless
|
|
70
|
+
* adapters' per-request flush.
|
|
71
|
+
*/
|
|
72
|
+
flushIntervalMs?: number;
|
|
73
|
+
/**
|
|
74
|
+
* Abort an in-flight export POST after this many ms so a blackholed
|
|
75
|
+
* collector cannot latch `flushing` forever. Default `5000`. Set `0` to
|
|
76
|
+
* wait indefinitely (tests that drive `fetch` themselves).
|
|
77
|
+
*/
|
|
78
|
+
flushTimeoutMs?: number;
|
|
79
|
+
/** Injectable transport for tests. Default `globalThis.fetch`. */
|
|
80
|
+
fetch?: typeof fetch;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Batched OTLP/HTTP JSON **log** exporter returned by
|
|
84
|
+
* {@link createOtlpLogExporter}.
|
|
85
|
+
*
|
|
86
|
+
* @since 1.2.0
|
|
87
|
+
*/
|
|
88
|
+
export interface OtlpLogExporter {
|
|
89
|
+
/**
|
|
90
|
+
* Queue one log line (no trailing newline). JSON lines are decomposed:
|
|
91
|
+
* `msg`/`message`/`event` becomes the record body, `level` maps to the
|
|
92
|
+
* OTLP severity, and every remaining field ships as a string attribute
|
|
93
|
+
* (surfacing as structured metadata in Loki-style backends). Non-JSON
|
|
94
|
+
* lines ship verbatim as the body.
|
|
95
|
+
*/
|
|
96
|
+
pushLine(line: string): void;
|
|
97
|
+
/** Push pending records now. Never rejects; failures count as drops. */
|
|
98
|
+
flush(): Promise<void>;
|
|
99
|
+
/**
|
|
100
|
+
* Mixed drop counter: overflowed log *lines* (one per `shift()` at the
|
|
101
|
+
* queue cap) plus failed export *POSTs* (collector 4xx/5xx, network error,
|
|
102
|
+
* timeout, or disallowed redirect). Not a count of failed HTTP batches
|
|
103
|
+
* alone — compare against queue depth under load, not "POSTs that failed".
|
|
104
|
+
*/
|
|
105
|
+
readonly droppedBatches: number;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Histogram configuration for {@link OtlpMetricsExporter.record}.
|
|
109
|
+
*
|
|
110
|
+
* @since 1.2.0
|
|
111
|
+
*/
|
|
112
|
+
export interface OtlpHistogramOptions {
|
|
113
|
+
/** UCUM unit, e.g. `"s"` or `"{token}"`. */
|
|
114
|
+
unit: string;
|
|
115
|
+
/** Explicit bucket upper bounds, ascending. */
|
|
116
|
+
boundaries: readonly number[];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Cumulative OTLP/HTTP JSON **metrics** exporter returned by
|
|
120
|
+
* {@link createOtlpMetricsExporter}. Counters and histograms use cumulative
|
|
121
|
+
* temporality, so a failed push is self-healing: the totals are retained and
|
|
122
|
+
* the next successful push carries them.
|
|
123
|
+
*
|
|
124
|
+
* @since 1.2.0
|
|
125
|
+
*/
|
|
126
|
+
export interface OtlpMetricsExporter {
|
|
127
|
+
/** Add `value` (default `1`, must be finite and non-negative) to the counter series `name` + `attributes`. */
|
|
128
|
+
count(name: string, attributes: Record<string, string>, value?: number): void;
|
|
129
|
+
/** Record one observation into the histogram series `name` + `attributes`. */
|
|
130
|
+
record(name: string, attributes: Record<string, string>, value: number, options: OtlpHistogramOptions): void;
|
|
131
|
+
/** Push the current state now. Never rejects; failures count as drops. */
|
|
132
|
+
flush(): Promise<void>;
|
|
133
|
+
/**
|
|
134
|
+
* Mixed drop counter: failed export *POSTs* (collector 4xx/5xx, network
|
|
135
|
+
* error, timeout, or disallowed redirect) plus *series* refused at the
|
|
136
|
+
* cardinality cap. A cardinality attack inflates this without any POST
|
|
137
|
+
* failing.
|
|
138
|
+
*/
|
|
139
|
+
readonly droppedBatches: number;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Spec-defined default bucket boundaries for `http.server.request.duration`,
|
|
143
|
+
* in seconds (OTel HTTP semantic conventions).
|
|
144
|
+
*
|
|
145
|
+
* @since 1.2.0
|
|
146
|
+
*/
|
|
147
|
+
export declare const HTTP_SERVER_REQUEST_DURATION_BUCKETS: readonly number[];
|
|
148
|
+
/**
|
|
149
|
+
* Create a batched OTLP/HTTP JSON log exporter, or `null` when no endpoint is
|
|
150
|
+
* configured (local development, tests) so callers can no-op cheaply.
|
|
151
|
+
*
|
|
152
|
+
* Fail-safe by contract: {@link OtlpLogExporter.flush} never rejects, a dead
|
|
153
|
+
* collector cannot grow the queue past its cap (oldest records are dropped
|
|
154
|
+
* and counted), and export work is decoupled from request handling.
|
|
155
|
+
*
|
|
156
|
+
* @param opts - Endpoint/header/resource overrides; defaults from `OTEL_*` env vars.
|
|
157
|
+
* @returns The exporter, or `null` when no endpoint is configured.
|
|
158
|
+
* @since 1.2.0
|
|
159
|
+
*/
|
|
160
|
+
export declare function createOtlpLogExporter(opts?: OtlpExporterOptions): OtlpLogExporter | null;
|
|
161
|
+
/**
|
|
162
|
+
* Create a cumulative OTLP/HTTP JSON metrics exporter, or `null` when no
|
|
163
|
+
* endpoint is configured.
|
|
164
|
+
*
|
|
165
|
+
* Counters export as monotonic cumulative sums and histograms as cumulative
|
|
166
|
+
* explicit-bounds histograms, so state survives failed pushes (the next
|
|
167
|
+
* successful push carries the accumulated totals). Total series are capped
|
|
168
|
+
* ({@link OtlpMetricsExporter.droppedBatches} counts series refused at the
|
|
169
|
+
* cap) and attribute values are length-truncated — both cardinality guards
|
|
170
|
+
* against hostile or buggy attribute sources.
|
|
171
|
+
*
|
|
172
|
+
* @param opts - Endpoint/header/resource overrides; defaults from `OTEL_*` env vars.
|
|
173
|
+
* @returns The exporter, or `null` when no endpoint is configured.
|
|
174
|
+
* @since 1.2.0
|
|
175
|
+
*/
|
|
176
|
+
export declare function createOtlpMetricsExporter(opts?: OtlpExporterOptions): OtlpMetricsExporter | null;
|
|
177
|
+
/** Options for {@link semconvHttpMetrics}. */
|
|
178
|
+
export interface SemconvHttpMetricsOptions {
|
|
179
|
+
/** Skip instrumentation for matching request paths (e.g. a metrics scrape route). */
|
|
180
|
+
exclude?: (path: string) => boolean;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A `Hooks` bundle recording `http.server.request.duration` per the OTel HTTP
|
|
184
|
+
* semantic conventions into an {@link OtlpMetricsExporter}:
|
|
185
|
+
*
|
|
186
|
+
* - unit `s`, spec bucket boundaries
|
|
187
|
+
* ({@link HTTP_SERVER_REQUEST_DURATION_BUCKETS});
|
|
188
|
+
* - attributes `http.request.method` (well-known set, else `_OTHER`),
|
|
189
|
+
* `http.response.status_code`, `url.scheme`, `http.route` (the matched
|
|
190
|
+
* route **template** via `ctx.routePath`), and `error.type` (the status
|
|
191
|
+
* code, on `5xx` only).
|
|
192
|
+
*
|
|
193
|
+
* Only requests that produce a request context are recorded. On the
|
|
194
|
+
* unmatched-404 fast path the framework skips context construction entirely
|
|
195
|
+
* (a deliberate allocation optimization), so unmatched floods record nothing
|
|
196
|
+
* — which is also the strongest possible cardinality guarantee: a hostile
|
|
197
|
+
* client can never mint metric series from raw paths.
|
|
198
|
+
*
|
|
199
|
+
* Install it **before** registering routes (group-hook ordering), or let
|
|
200
|
+
* `new App({ telemetry: true })` install it for you.
|
|
201
|
+
*
|
|
202
|
+
* @param sink - Metrics exporter the duration histogram is recorded into.
|
|
203
|
+
* @param opts - Optional path exclusion.
|
|
204
|
+
* @returns A `Hooks` object for `app.use(...)` or `new App({ hooks })`.
|
|
205
|
+
* @since 1.2.0
|
|
206
|
+
*/
|
|
207
|
+
export declare function semconvHttpMetrics(sink: OtlpMetricsExporter, opts?: SemconvHttpMetricsOptions): Hooks;
|
|
208
|
+
/**
|
|
209
|
+
* Configuration for the `App` `telemetry` option. Passing `true` is
|
|
210
|
+
* equivalent to `{}` — everything defaults on, reading the standard `OTEL_*`
|
|
211
|
+
* environment variables. When no `OTEL_EXPORTER_OTLP_ENDPOINT` is present
|
|
212
|
+
* (and no explicit `exporter.endpoint` is given) the option is a silent
|
|
213
|
+
* no-op, so it is safe to leave enabled in development. A caller-supplied
|
|
214
|
+
* `Logger` instance is not intercepted (it owns its own sink). On isolate
|
|
215
|
+
* runtimes use `toFetchHandler` / `toLambdaHandler` so a per-request flush
|
|
216
|
+
* actually runs; Node/Bun/Deno long-lived processes flush on the interval
|
|
217
|
+
* and on shutdown. The collector endpoint is operator config, so export
|
|
218
|
+
* uses raw `fetch` (not `fetchGuard`) — the same trust class as a database
|
|
219
|
+
* URL.
|
|
220
|
+
*
|
|
221
|
+
* @since 1.2.0
|
|
222
|
+
*/
|
|
223
|
+
export interface TelemetryOptions {
|
|
224
|
+
/**
|
|
225
|
+
* Tee the app logger's output to the collector as OTLP logs. Default
|
|
226
|
+
* `true`. Applies only when the app constructs its own logger (`logger`
|
|
227
|
+
* omitted, `{ level }`, or defaulted) — a caller-supplied `Logger`
|
|
228
|
+
* instance controls its own sink and is not intercepted.
|
|
229
|
+
*/
|
|
230
|
+
logs?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Record `http.server.request.duration` per the OTel HTTP semantic
|
|
233
|
+
* conventions and push it to the collector. Default `true`.
|
|
234
|
+
*/
|
|
235
|
+
metrics?: boolean;
|
|
236
|
+
/** Endpoint/header/resource/interval overrides shared by both signals. */
|
|
237
|
+
exporter?: OtlpExporterOptions;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Internal wiring bundle produced by {@link createAppTelemetry} and consumed
|
|
241
|
+
* by the `App` constructor. Exposed for advanced composition and tests.
|
|
242
|
+
*
|
|
243
|
+
* @since 1.2.0
|
|
244
|
+
*/
|
|
245
|
+
export interface AppTelemetry {
|
|
246
|
+
/** Log exporter, or `null` when logs are disabled or unconfigured. */
|
|
247
|
+
logs: OtlpLogExporter | null;
|
|
248
|
+
/** Metrics exporter, or `null` when metrics are disabled or unconfigured. */
|
|
249
|
+
metrics: OtlpMetricsExporter | null;
|
|
250
|
+
/** Hooks to install when HTTP metrics are active. */
|
|
251
|
+
hooks: Hooks | undefined;
|
|
252
|
+
/** Logger `write` sink that tees to stdout and the log exporter. */
|
|
253
|
+
logWrite: ((line: string) => void) | undefined;
|
|
254
|
+
/** Resolved collector endpoint (no signal path), or `null` when inactive. */
|
|
255
|
+
endpoint: string | null;
|
|
256
|
+
/** Flush both signals. Never rejects. */
|
|
257
|
+
flush(): Promise<void>;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Build the exporters, hooks, and logger sink for the `App` `telemetry`
|
|
261
|
+
* option. Returns an inert bundle (all `null`/`undefined`) when no endpoint
|
|
262
|
+
* is configured, so `telemetry: true` costs nothing in development.
|
|
263
|
+
*
|
|
264
|
+
* @param options - The resolved {@link TelemetryOptions}.
|
|
265
|
+
* @returns The wiring bundle.
|
|
266
|
+
* @since 1.2.0
|
|
267
|
+
*/
|
|
268
|
+
export declare function createAppTelemetry(options: TelemetryOptions): AppTelemetry;
|