@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/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 ? early : hooks.beforeHandle(ctx);
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 && !hooks.beforeHandle && !Array.isArray(earlyRejectionHooks))
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) => ((await matches(ctx)) ? undefined : original(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) => ((await matches(ctx)) ? undefined : original(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.map((h) => h[key]).filter((f) => typeof f === "function");
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), so it runs unchanged on Node, Bun,
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
- /** Prefix applied to every metric name. Default `"daloy_"`. Pass `""` for none. */
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
- * Strongly recommended: return the route **template** (e.g. `/books/:id`),
176
- * not the raw path, to keep series cardinality bounded. When omitted the
177
- * request pathname is used, capped by {@link HttpMetricsOptions.maxRouteCardinality}.
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 default (pathname-derived) `route` label
182
- * before further values collapse to `"<other>"`. Ignored when a custom
183
- * {@link HttpMetricsOptions.route} resolver is supplied. Default `100`.
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), so it runs unchanged on Node, Bun,
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.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
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 = [`# HELP ${this.name} ${escapeHelp(this.help)}`, `# TYPE ${this.name} counter`];
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 = [`# HELP ${this.name} ${escapeHelp(this.help)}`, `# TYPE ${this.name} gauge`];
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 = { labels: entries, counts: new Array(this.bounds.length).fill(0), sum: 0, count: 0 };
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 = [`# HELP ${this.name} ${escapeHelp(this.help)}`, `# TYPE ${this.name} histogram`];
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 ?? "daloy_";
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("metrics_series_dropped_total", "Series dropped after hitting the per-metric cardinality cap.");
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" && typeof performance.now === "function"
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;