@daloyjs/core 1.2.0 → 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 -2
- 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 +16 -10
- package/dist/app.js +218 -69
- package/dist/combine.js +14 -8
- package/dist/metrics.d.ts +23 -9
- package/dist/metrics.js +33 -10
- package/dist/otlp.d.ts +31 -5
- package/dist/otlp.js +99 -37
- package/dist/sbom.cdx.json +9 -9
- package/dist/sbom.spdx.json +5 -5
- package/package.json +1 -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/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
|
}
|
package/dist/otlp.d.ts
CHANGED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
* Everything is transport-portable (`fetch` + web-standard primitives) and
|
|
26
26
|
* **fail-safe by contract**: a dead or misconfigured collector never affects
|
|
27
27
|
* request serving — bounded queues, dropped-batch counters, no retry storms,
|
|
28
|
-
* and cumulative metric temporality so
|
|
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.
|
|
29
32
|
*
|
|
30
33
|
* @module
|
|
31
34
|
* @since 1.2.0
|
|
@@ -63,9 +66,16 @@ export interface OtlpExporterOptions {
|
|
|
63
66
|
/**
|
|
64
67
|
* Flush cadence in ms on runtimes with timers (`unref`'d — never keeps the
|
|
65
68
|
* process alive). Defaults: 5000 (logs), 15000 (metrics). Set `0` to
|
|
66
|
-
* disable the timer and flush manually / on shutdown
|
|
69
|
+
* disable the timer and flush manually / on shutdown / via the serverless
|
|
70
|
+
* adapters' per-request flush.
|
|
67
71
|
*/
|
|
68
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;
|
|
69
79
|
/** Injectable transport for tests. Default `globalThis.fetch`. */
|
|
70
80
|
fetch?: typeof fetch;
|
|
71
81
|
}
|
|
@@ -86,7 +96,12 @@ export interface OtlpLogExporter {
|
|
|
86
96
|
pushLine(line: string): void;
|
|
87
97
|
/** Push pending records now. Never rejects; failures count as drops. */
|
|
88
98
|
flush(): Promise<void>;
|
|
89
|
-
/**
|
|
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
|
+
*/
|
|
90
105
|
readonly droppedBatches: number;
|
|
91
106
|
}
|
|
92
107
|
/**
|
|
@@ -115,7 +130,12 @@ export interface OtlpMetricsExporter {
|
|
|
115
130
|
record(name: string, attributes: Record<string, string>, value: number, options: OtlpHistogramOptions): void;
|
|
116
131
|
/** Push the current state now. Never rejects; failures count as drops. */
|
|
117
132
|
flush(): Promise<void>;
|
|
118
|
-
/**
|
|
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
|
+
*/
|
|
119
139
|
readonly droppedBatches: number;
|
|
120
140
|
}
|
|
121
141
|
/**
|
|
@@ -190,7 +210,13 @@ export declare function semconvHttpMetrics(sink: OtlpMetricsExporter, opts?: Sem
|
|
|
190
210
|
* equivalent to `{}` — everything defaults on, reading the standard `OTEL_*`
|
|
191
211
|
* environment variables. When no `OTEL_EXPORTER_OTLP_ENDPOINT` is present
|
|
192
212
|
* (and no explicit `exporter.endpoint` is given) the option is a silent
|
|
193
|
-
* no-op, so it is safe to leave enabled in development.
|
|
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.
|
|
194
220
|
*
|
|
195
221
|
* @since 1.2.0
|
|
196
222
|
*/
|
package/dist/otlp.js
CHANGED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
* Everything is transport-portable (`fetch` + web-standard primitives) and
|
|
26
26
|
* **fail-safe by contract**: a dead or misconfigured collector never affects
|
|
27
27
|
* request serving — bounded queues, dropped-batch counters, no retry storms,
|
|
28
|
-
* and cumulative metric temporality so
|
|
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.
|
|
29
32
|
*
|
|
30
33
|
* @module
|
|
31
34
|
* @since 1.2.0
|
|
@@ -37,6 +40,8 @@ const LOG_FLUSH_INTERVAL_MS = 5_000;
|
|
|
37
40
|
/** Longest log body / attribute value shipped before truncation. */
|
|
38
41
|
const LOG_MAX_FIELD_LENGTH = 8_192;
|
|
39
42
|
const METRICS_FLUSH_INTERVAL_MS = 15_000;
|
|
43
|
+
/** Default abort timeout for an in-flight OTLP POST. */
|
|
44
|
+
const DEFAULT_FLUSH_TIMEOUT_MS = 5_000;
|
|
40
45
|
/**
|
|
41
46
|
* Cardinality guard: total metric series (counter + histogram) before new
|
|
42
47
|
* series are dropped and counted. A hostile client must not be able to mint
|
|
@@ -77,11 +82,23 @@ const KNOWN_METHODS = new Set([
|
|
|
77
82
|
]);
|
|
78
83
|
/** Portable environment lookup (Node/Bun/Deno-with-node-compat; undefined elsewhere). */
|
|
79
84
|
function envVar(name) {
|
|
80
|
-
const env = globalThis.process
|
|
81
|
-
?.env;
|
|
85
|
+
const env = globalThis.process?.env;
|
|
82
86
|
return env?.[name];
|
|
83
87
|
}
|
|
84
|
-
/**
|
|
88
|
+
/** Decode one OTEL env-list component; invalid percent-encoding is kept raw. */
|
|
89
|
+
function decodeOtelComponent(value) {
|
|
90
|
+
try {
|
|
91
|
+
return decodeURIComponent(value);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Parse `key=value,key2=value2` lists (`OTEL_EXPORTER_OTLP_HEADERS` /
|
|
99
|
+
* `OTEL_RESOURCE_ATTRIBUTES`). Spec values are percent-encoded; commas in a
|
|
100
|
+
* value must be `%2C`.
|
|
101
|
+
*/
|
|
85
102
|
function parseKvList(raw) {
|
|
86
103
|
const out = {};
|
|
87
104
|
if (!raw)
|
|
@@ -91,7 +108,10 @@ function parseKvList(raw) {
|
|
|
91
108
|
const idx = trimmed.indexOf("=");
|
|
92
109
|
if (idx <= 0)
|
|
93
110
|
continue;
|
|
94
|
-
|
|
111
|
+
const key = decodeOtelComponent(trimmed.slice(0, idx).trim());
|
|
112
|
+
if (key.length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
out[key] = decodeOtelComponent(trimmed.slice(idx + 1).trim());
|
|
95
115
|
}
|
|
96
116
|
return out;
|
|
97
117
|
}
|
|
@@ -109,7 +129,9 @@ function resolveTransport(signalPath, opts) {
|
|
|
109
129
|
if (!rawEndpoint)
|
|
110
130
|
return null;
|
|
111
131
|
const base = rawEndpoint.replace(/\/+$/, "").replace(/:4317$/, ":4318");
|
|
112
|
-
const headers = {
|
|
132
|
+
const headers = {
|
|
133
|
+
"content-type": "application/json",
|
|
134
|
+
};
|
|
113
135
|
Object.assign(headers, parseKvList(envVar("OTEL_EXPORTER_OTLP_HEADERS")), opts.headers ?? {});
|
|
114
136
|
const resource = parseKvList(envVar("OTEL_RESOURCE_ATTRIBUTES"));
|
|
115
137
|
const serviceName = envVar("OTEL_SERVICE_NAME");
|
|
@@ -131,6 +153,32 @@ function startFlushTimer(intervalMs, flush) {
|
|
|
131
153
|
const timer = setInterval(() => void flush(), intervalMs);
|
|
132
154
|
timer.unref?.();
|
|
133
155
|
}
|
|
156
|
+
/** AbortSignal that fires after `timeoutMs`, or `undefined` when timeout is disabled. */
|
|
157
|
+
function flushSignal(timeoutMs) {
|
|
158
|
+
if (timeoutMs <= 0)
|
|
159
|
+
return undefined;
|
|
160
|
+
if (typeof AbortSignal !== "undefined" &&
|
|
161
|
+
typeof AbortSignal.timeout === "function") {
|
|
162
|
+
return AbortSignal.timeout(timeoutMs);
|
|
163
|
+
}
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
166
|
+
timer.unref?.();
|
|
167
|
+
return controller.signal;
|
|
168
|
+
}
|
|
169
|
+
/** Shared POST init: never follow redirects (tenant headers would leak). */
|
|
170
|
+
function exportRequestInit(headers, body, timeoutMs) {
|
|
171
|
+
const init = {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers,
|
|
174
|
+
body,
|
|
175
|
+
redirect: "error",
|
|
176
|
+
};
|
|
177
|
+
const signal = flushSignal(timeoutMs);
|
|
178
|
+
if (signal !== undefined)
|
|
179
|
+
init.signal = signal;
|
|
180
|
+
return init;
|
|
181
|
+
}
|
|
134
182
|
function toLogRecord(line) {
|
|
135
183
|
let level = "info";
|
|
136
184
|
let body = line;
|
|
@@ -149,7 +197,9 @@ function toLogRecord(line) {
|
|
|
149
197
|
attributes.push({
|
|
150
198
|
key,
|
|
151
199
|
value: {
|
|
152
|
-
stringValue: truncate(typeof value === "string"
|
|
200
|
+
stringValue: truncate(typeof value === "string"
|
|
201
|
+
? value
|
|
202
|
+
: (JSON.stringify(value) ?? "null"), LOG_MAX_FIELD_LENGTH),
|
|
153
203
|
},
|
|
154
204
|
});
|
|
155
205
|
}
|
|
@@ -183,6 +233,7 @@ export function createOtlpLogExporter(opts = {}) {
|
|
|
183
233
|
if (transport === null)
|
|
184
234
|
return null;
|
|
185
235
|
const { url, headers, resourceAttributes, fetchImpl } = transport;
|
|
236
|
+
const timeoutMs = opts.flushTimeoutMs ?? DEFAULT_FLUSH_TIMEOUT_MS;
|
|
186
237
|
const queue = [];
|
|
187
238
|
let dropped = 0;
|
|
188
239
|
let flushing = false;
|
|
@@ -201,11 +252,7 @@ export function createOtlpLogExporter(opts = {}) {
|
|
|
201
252
|
},
|
|
202
253
|
],
|
|
203
254
|
};
|
|
204
|
-
const res = await fetchImpl(url,
|
|
205
|
-
method: "POST",
|
|
206
|
-
headers,
|
|
207
|
-
body: JSON.stringify(payload),
|
|
208
|
-
});
|
|
255
|
+
const res = await fetchImpl(url, exportRequestInit(headers, JSON.stringify(payload), timeoutMs));
|
|
209
256
|
if (!res.ok)
|
|
210
257
|
dropped += 1;
|
|
211
258
|
}
|
|
@@ -253,6 +300,7 @@ export function createOtlpMetricsExporter(opts = {}) {
|
|
|
253
300
|
if (transport === null)
|
|
254
301
|
return null;
|
|
255
302
|
const { url, headers, resourceAttributes, fetchImpl } = transport;
|
|
303
|
+
const timeoutMs = opts.flushTimeoutMs ?? DEFAULT_FLUSH_TIMEOUT_MS;
|
|
256
304
|
const counters = new Map();
|
|
257
305
|
const histograms = new Map();
|
|
258
306
|
let dropped = 0;
|
|
@@ -266,7 +314,10 @@ export function createOtlpMetricsExporter(opts = {}) {
|
|
|
266
314
|
}
|
|
267
315
|
function sortedEntries(attributes) {
|
|
268
316
|
return Object.entries(attributes)
|
|
269
|
-
.map(([k, v]) => [
|
|
317
|
+
.map(([k, v]) => [
|
|
318
|
+
k,
|
|
319
|
+
truncate(v, METRICS_MAX_ATTR_LENGTH),
|
|
320
|
+
])
|
|
270
321
|
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
271
322
|
}
|
|
272
323
|
function atCapacity() {
|
|
@@ -279,6 +330,9 @@ export function createOtlpMetricsExporter(opts = {}) {
|
|
|
279
330
|
if (flushing || !dirty)
|
|
280
331
|
return;
|
|
281
332
|
flushing = true;
|
|
333
|
+
// Clear before the POST so a concurrent count/record keeps the flag true
|
|
334
|
+
// and the next flush carries the in-flight increment.
|
|
335
|
+
dirty = false;
|
|
282
336
|
try {
|
|
283
337
|
const now = (BigInt(Date.now()) * 1000000n).toString();
|
|
284
338
|
const sumsByName = new Map();
|
|
@@ -341,18 +395,15 @@ export function createOtlpMetricsExporter(opts = {}) {
|
|
|
341
395
|
},
|
|
342
396
|
],
|
|
343
397
|
};
|
|
344
|
-
const res = await fetchImpl(url,
|
|
345
|
-
|
|
346
|
-
headers,
|
|
347
|
-
body: JSON.stringify(payload),
|
|
348
|
-
});
|
|
349
|
-
if (res.ok)
|
|
350
|
-
dirty = false;
|
|
351
|
-
else
|
|
398
|
+
const res = await fetchImpl(url, exportRequestInit(headers, JSON.stringify(payload), timeoutMs));
|
|
399
|
+
if (!res.ok) {
|
|
352
400
|
dropped += 1;
|
|
401
|
+
dirty = true;
|
|
402
|
+
}
|
|
353
403
|
}
|
|
354
404
|
catch {
|
|
355
|
-
dropped += 1;
|
|
405
|
+
dropped += 1;
|
|
406
|
+
dirty = true; // totals retained; cumulative temporality self-heals
|
|
356
407
|
}
|
|
357
408
|
finally {
|
|
358
409
|
flushing = false;
|
|
@@ -371,7 +422,10 @@ export function createOtlpMetricsExporter(opts = {}) {
|
|
|
371
422
|
return;
|
|
372
423
|
s = {
|
|
373
424
|
name,
|
|
374
|
-
attributes: entries.map(([k, v]) => ({
|
|
425
|
+
attributes: entries.map(([k, v]) => ({
|
|
426
|
+
key: k,
|
|
427
|
+
value: { stringValue: v },
|
|
428
|
+
})),
|
|
375
429
|
total: 0,
|
|
376
430
|
startTimeUnixNano: (BigInt(Date.now()) * 1000000n).toString(),
|
|
377
431
|
};
|
|
@@ -393,7 +447,10 @@ export function createOtlpMetricsExporter(opts = {}) {
|
|
|
393
447
|
name,
|
|
394
448
|
unit: options.unit,
|
|
395
449
|
boundaries: options.boundaries,
|
|
396
|
-
attributes: entries.map(([k, v]) => ({
|
|
450
|
+
attributes: entries.map(([k, v]) => ({
|
|
451
|
+
key: k,
|
|
452
|
+
value: { stringValue: v },
|
|
453
|
+
})),
|
|
397
454
|
count: 0,
|
|
398
455
|
sum: 0,
|
|
399
456
|
bucketCounts: new Array(options.boundaries.length + 1).fill(0),
|
|
@@ -417,7 +474,8 @@ export function createOtlpMetricsExporter(opts = {}) {
|
|
|
417
474
|
}
|
|
418
475
|
/** Monotonic clock in milliseconds, falling back to `Date.now` where needed. */
|
|
419
476
|
function nowMs() {
|
|
420
|
-
return typeof performance !== "undefined" &&
|
|
477
|
+
return typeof performance !== "undefined" &&
|
|
478
|
+
typeof performance.now === "function"
|
|
421
479
|
? performance.now()
|
|
422
480
|
: Date.now();
|
|
423
481
|
}
|
|
@@ -460,21 +518,25 @@ export function semconvHttpMetrics(sink, opts = {}) {
|
|
|
460
518
|
if (started === undefined)
|
|
461
519
|
return;
|
|
462
520
|
SEMCONV_START_TIMES.delete(request);
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
521
|
+
const url = request.url;
|
|
522
|
+
if (opts.exclude !== undefined) {
|
|
523
|
+
let pathname = "/";
|
|
524
|
+
try {
|
|
525
|
+
pathname = new URL(url).pathname;
|
|
526
|
+
}
|
|
527
|
+
catch {
|
|
528
|
+
/* malformed URL — keep the fallback */
|
|
529
|
+
}
|
|
530
|
+
if (opts.exclude(pathname))
|
|
531
|
+
return;
|
|
472
532
|
}
|
|
473
|
-
|
|
474
|
-
|
|
533
|
+
const colon = url.indexOf(":");
|
|
534
|
+
const scheme = colon > 0 ? url.slice(0, colon) : "http";
|
|
475
535
|
const rawMethod = request.method.toUpperCase();
|
|
476
536
|
const attributes = {
|
|
477
|
-
"http.request.method": KNOWN_METHODS.has(rawMethod)
|
|
537
|
+
"http.request.method": KNOWN_METHODS.has(rawMethod)
|
|
538
|
+
? rawMethod
|
|
539
|
+
: "_OTHER",
|
|
478
540
|
"http.response.status_code": String(res.status),
|
|
479
541
|
"url.scheme": scheme,
|
|
480
542
|
};
|
package/dist/sbom.cdx.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:e489b0cf-2d9d-5f9f-93c0-ff3c851e6f48",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-08-
|
|
7
|
+
"timestamp": "2026-08-20T14:37:11.548Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "1.2.
|
|
12
|
+
"version": "1.2.1"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
],
|
|
20
20
|
"component": {
|
|
21
21
|
"type": "library",
|
|
22
|
-
"bom-ref": "pkg:npm/@daloyjs/core@1.2.
|
|
22
|
+
"bom-ref": "pkg:npm/@daloyjs/core@1.2.1",
|
|
23
23
|
"name": "@daloyjs/core",
|
|
24
|
-
"version": "1.2.
|
|
24
|
+
"version": "1.2.1",
|
|
25
25
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops — distributed via pnpm.",
|
|
26
|
-
"purl": "pkg:npm/@daloyjs/core@1.2.
|
|
26
|
+
"purl": "pkg:npm/@daloyjs/core@1.2.1",
|
|
27
27
|
"licenses": [
|
|
28
28
|
{
|
|
29
29
|
"license": {
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
}
|
|
47
47
|
],
|
|
48
48
|
"swid": {
|
|
49
|
-
"tagId": "swidtag--daloyjs-core-1.2.
|
|
49
|
+
"tagId": "swidtag--daloyjs-core-1.2.1",
|
|
50
50
|
"name": "@daloyjs/core",
|
|
51
|
-
"version": "1.2.
|
|
51
|
+
"version": "1.2.1",
|
|
52
52
|
"tagVersion": 0,
|
|
53
53
|
"patch": false
|
|
54
54
|
}
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
"components": [],
|
|
58
58
|
"dependencies": [
|
|
59
59
|
{
|
|
60
|
-
"ref": "pkg:npm/@daloyjs/core@1.2.
|
|
60
|
+
"ref": "pkg:npm/@daloyjs/core@1.2.1",
|
|
61
61
|
"dependsOn": []
|
|
62
62
|
}
|
|
63
63
|
]
|
package/dist/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "@daloyjs/core-1.2.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.2.
|
|
5
|
+
"name": "@daloyjs/core-1.2.1",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/@daloyjs/core-1.2.1-e489b0cf-2d9d-5f9f-93c0-ff3c851e6f48",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-08-
|
|
8
|
+
"created": "2026-08-20T14:37:11.548Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package--daloyjs-core",
|
|
18
18
|
"name": "@daloyjs/core",
|
|
19
|
-
"versionInfo": "1.2.
|
|
19
|
+
"versionInfo": "1.2.1",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/@daloyjs/core@1.2.
|
|
30
|
+
"referenceLocator": "pkg:npm/@daloyjs/core@1.2.1"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@daloyjs/core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "DaloyJS is a runtime-portable, contract-first TypeScript web framework with built-in OpenAPI (Hey API), typed client generation, large-scale maintainability, and security-first defaults. Hono-grade portability, Elysia-grade DX, FastAPI-grade docs, Fastify-grade ops \u2014 distributed via pnpm.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|