@dunx/http 3.1.3 → 3.2.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 +14 -4
- package/dist/{chunk-sz4pvqxy.js → chunk-9x3evk19.js} +70 -1
- package/dist/{chunk-7xrtwbx3.js → chunk-e8a9c6j2.js} +1 -1
- package/dist/{chunk-53cs6qek.js → chunk-y85wcdhw.js} +5 -24
- package/dist/client/options.d.ts +7 -13
- package/dist/client/service.d.ts +2 -2
- package/dist/client.d.ts +1 -1
- package/dist/client.js +9 -16
- package/dist/index.d.ts +2 -2
- package/dist/index.js +180 -40
- package/dist/internal.js +3 -3
- package/dist/server/application.d.ts +8 -0
- package/dist/server/metrics.d.ts +80 -0
- package/dist/server/options-provider.d.ts +2 -0
- package/dist/server/request-logging.d.ts +26 -12
- package/dist/server/trace-context.d.ts +41 -10
- package/package.json +7 -4
- package/dist/chunk-25g22350.js +0 -58
- package/dist/server/request-id.d.ts +0 -22
package/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# @dunx/http
|
|
2
2
|
|
|
3
|
-
`Bun.serve` adapter for [dunx](https://github.com/petarzarkov/dunx)
|
|
4
|
-
controllers **
|
|
5
|
-
router - Bun's native `routes` does path params and per-method
|
|
3
|
+
`Bun.serve` adapter for [dunx](https://github.com/petarzarkov/dunx): class-based
|
|
4
|
+
controllers, **WebSocket gateways**, and standard decorators. There is no
|
|
5
|
+
JavaScript router - Bun's native `routes` does path params and per-method
|
|
6
|
+
dispatch in Zig.
|
|
6
7
|
|
|
7
8
|
`Bun.serve` takes `routes` and `websocket` in one call, so both live here: one
|
|
8
9
|
`listen()`, one server, one port. No `express`, no `ws`, no `socket.io`.
|
|
@@ -62,6 +63,8 @@ The guide is canonical for every row; this table is the index.
|
|
|
62
63
|
| Middleware and guards | One extension point, `@UseGuards`, `@Roles`, `@Public` | [Middleware and guards](../../docs/guide/08-middleware-and-guards.md) |
|
|
63
64
|
| WebSocket gateways | `@Gateway`, handlers, `PubSub`, multi-node relay | [WebSockets](../../docs/guide/09-websockets.md) |
|
|
64
65
|
| Request logging | One structured entry per request, on by default | [Logging](../../docs/guide/13-logging.md) |
|
|
66
|
+
| Trace context | W3C `traceparent` adopted and propagated, on by default | [Logging](../../docs/guide/13-logging.md) |
|
|
67
|
+
| Metrics | Per-route counts and timings, off by default | [Metrics](../../docs/guide/22-metrics.md) |
|
|
65
68
|
| Health and draining | `/health/live`, `/health/ready`, readiness during a rollout | [Health checks](../../docs/guide/20-health-checks.md) |
|
|
66
69
|
| Throttling | `@Throttle`, `@SkipThrottle`, memory and Redis counters | [Middleware and guards](../../docs/guide/08-middleware-and-guards.md) |
|
|
67
70
|
| Static files | `Bun.file` behind a mount, with a cache policy | [Deployment](../../docs/guide/19-deployment.md) |
|
|
@@ -89,7 +92,14 @@ from, and it may change in any release.
|
|
|
89
92
|
- Handlers may return a `Response`, any JSON-serialisable value, or `undefined`
|
|
90
93
|
for a 204.
|
|
91
94
|
- Schemas, parsers and the status resolve at boot into the same closure the
|
|
92
|
-
middleware chain folds into
|
|
95
|
+
middleware chain folds into. A request reads no metadata and does no lookup.
|
|
96
|
+
- Every request adopts W3C Trace Context, so `traceId`, `spanId`, `parentSpanId`
|
|
97
|
+
and `traceFlags` reach every line it writes and `traceresponse` goes out on the
|
|
98
|
+
response. `requestLogging: { trace: false }` removes both;
|
|
99
|
+
`{ traceResponse: false }` keeps the trace and drops the header, which is ~500
|
|
100
|
+
ns. W3C Trace Context is the only correlation id; there is no second one.
|
|
101
|
+
- `metrics: true` adds per-route counts and a nanosecond histogram at +35.2 ns a
|
|
102
|
+
request, folded into the `.then` request logging already allocates.
|
|
93
103
|
|
|
94
104
|
## License
|
|
95
105
|
|
|
@@ -105,4 +105,73 @@ var HttpStatusCode = Object.freeze({
|
|
|
105
105
|
GATEWAY_TIMEOUT: 504
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
// src/server/trace-context.ts
|
|
109
|
+
var TRACEPARENT_HEADER = "traceparent";
|
|
110
|
+
var TRACESTATE_HEADER = "tracestate";
|
|
111
|
+
var TRACERESPONSE_HEADER = "traceresponse";
|
|
112
|
+
var HEX_32 = /^[0-9a-f]{32}$/;
|
|
113
|
+
var HEX_16 = /^[0-9a-f]{16}$/;
|
|
114
|
+
var HEX_2 = /^[0-9a-f]{2}$/;
|
|
115
|
+
var ZERO_TRACE = "0".repeat(32);
|
|
116
|
+
var ZERO_SPAN = "0".repeat(16);
|
|
117
|
+
var SAMPLED = 1;
|
|
118
|
+
var DEFAULT_FLAGS = "01";
|
|
119
|
+
var TRACE = Symbol.for("dunx.http.trace");
|
|
120
|
+
var EXPOSE = Symbol.for("dunx.http.trace.expose");
|
|
121
|
+
var mint = (bytes) => crypto.getRandomValues(new Uint8Array(bytes)).toHex();
|
|
122
|
+
|
|
123
|
+
class TraceContext {
|
|
124
|
+
static adopt(req, expose = true) {
|
|
125
|
+
const inbound = TraceContext.#parse(req.headers.get(TRACEPARENT_HEADER));
|
|
126
|
+
const state = req.headers.get(TRACESTATE_HEADER);
|
|
127
|
+
const trace = inbound === undefined ? { traceId: mint(16), spanId: mint(8), flags: DEFAULT_FLAGS } : {
|
|
128
|
+
traceId: inbound.traceId,
|
|
129
|
+
spanId: mint(8),
|
|
130
|
+
parentSpanId: inbound.spanId,
|
|
131
|
+
flags: inbound.flags,
|
|
132
|
+
...state === null ? {} : { state }
|
|
133
|
+
};
|
|
134
|
+
req[TRACE] = trace;
|
|
135
|
+
if (expose)
|
|
136
|
+
req[EXPOSE] = true;
|
|
137
|
+
return trace;
|
|
138
|
+
}
|
|
139
|
+
static of(req) {
|
|
140
|
+
return req[TRACE];
|
|
141
|
+
}
|
|
142
|
+
static header(trace) {
|
|
143
|
+
return `00-${trace.traceId}-${trace.spanId}-${trace.flags}`;
|
|
144
|
+
}
|
|
145
|
+
static stamp(response, req) {
|
|
146
|
+
const traced = req;
|
|
147
|
+
const trace = traced[TRACE];
|
|
148
|
+
if (trace !== undefined && traced[EXPOSE] === true) {
|
|
149
|
+
response.headers.set(TRACERESPONSE_HEADER, TraceContext.header(trace));
|
|
150
|
+
}
|
|
151
|
+
return response;
|
|
152
|
+
}
|
|
153
|
+
static sampled(trace) {
|
|
154
|
+
return (Number.parseInt(trace.flags, 16) & SAMPLED) === SAMPLED;
|
|
155
|
+
}
|
|
156
|
+
static #parse(header) {
|
|
157
|
+
if (header === null)
|
|
158
|
+
return;
|
|
159
|
+
const parts = header.split("-");
|
|
160
|
+
if (parts.length < 4)
|
|
161
|
+
return;
|
|
162
|
+
const [version, traceId, spanId, flags] = parts;
|
|
163
|
+
if (!HEX_2.test(version) || version === "ff")
|
|
164
|
+
return;
|
|
165
|
+
if (version === "00" && parts.length !== 4)
|
|
166
|
+
return;
|
|
167
|
+
if (!HEX_32.test(traceId) || traceId === ZERO_TRACE)
|
|
168
|
+
return;
|
|
169
|
+
if (!HEX_16.test(spanId) || spanId === ZERO_SPAN)
|
|
170
|
+
return;
|
|
171
|
+
if (!HEX_2.test(flags))
|
|
172
|
+
return;
|
|
173
|
+
return { traceId, spanId, flags };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export { __privateGet, __privateAdd, __decoratorStart, __decoratorMetadata, __runInitializers, __decorateElement, HttpStatusCode, TRACEPARENT_HEADER, TRACESTATE_HEADER, TRACERESPONSE_HEADER, TraceContext };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
HttpStatusCode,
|
|
4
|
+
TraceContext,
|
|
4
5
|
__decorateElement,
|
|
5
6
|
__decoratorMetadata,
|
|
6
7
|
__decoratorStart,
|
|
7
8
|
__privateAdd,
|
|
8
9
|
__privateGet,
|
|
9
10
|
__runInitializers
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-9x3evk19.js";
|
|
11
12
|
|
|
12
13
|
// src/route/marker.ts
|
|
13
14
|
var ROUTE = Symbol.for("dunx.route");
|
|
@@ -570,26 +571,6 @@ class WsRelay {
|
|
|
570
571
|
}
|
|
571
572
|
}
|
|
572
573
|
|
|
573
|
-
// src/server/request-id.ts
|
|
574
|
-
var REQUEST_ID_HEADER = "x-request-id";
|
|
575
|
-
var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
576
|
-
var traceId = (inbound) => inbound !== null && inbound.length === 36 && UUID.test(inbound) ? inbound : crypto.randomUUID();
|
|
577
|
-
var ID = Symbol.for("dunx.http.requestId");
|
|
578
|
-
|
|
579
|
-
class RequestIds {
|
|
580
|
-
static assign(req) {
|
|
581
|
-
const id = traceId(req.headers.get(REQUEST_ID_HEADER));
|
|
582
|
-
req[ID] = id;
|
|
583
|
-
return id;
|
|
584
|
-
}
|
|
585
|
-
static stamp(response, req) {
|
|
586
|
-
const id = req[ID];
|
|
587
|
-
if (id !== undefined)
|
|
588
|
-
response.headers.set(REQUEST_ID_HEADER, id);
|
|
589
|
-
return response;
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
574
|
// src/server/context.ts
|
|
594
575
|
var EMPTY = new Map;
|
|
595
576
|
var buildContext = (route) => {
|
|
@@ -843,7 +824,7 @@ var buildFallback = (middleware = [], onError = defaultErrorMapper, cors, notFou
|
|
|
843
824
|
try {
|
|
844
825
|
return await compose(middleware, unmatchedContext(req, notFound === "public"), miss)(req);
|
|
845
826
|
} catch (error) {
|
|
846
|
-
return
|
|
827
|
+
return TraceContext.stamp(onError(error, req), req);
|
|
847
828
|
}
|
|
848
829
|
};
|
|
849
830
|
return cors ? withCors(cors, run) : run;
|
|
@@ -900,7 +881,7 @@ var buildRoutes = (discovered, middleware = [], onError = defaultErrorMapper, co
|
|
|
900
881
|
try {
|
|
901
882
|
return await chained(req);
|
|
902
883
|
} catch (error) {
|
|
903
|
-
return
|
|
884
|
+
return TraceContext.stamp(onError(error, req), req);
|
|
904
885
|
}
|
|
905
886
|
};
|
|
906
887
|
const byMethod = routes[route.path] ??= {};
|
|
@@ -1313,4 +1294,4 @@ __runInitializers(_init, 1, HiddenHealthController);
|
|
|
1313
1294
|
__decoratorMetadata(_init, HiddenHealthController);
|
|
1314
1295
|
let _HiddenHealthController = HiddenHealthController;
|
|
1315
1296
|
|
|
1316
|
-
export { defaultStatusFor, Controller, Get, Post, Put, Patch, Delete, metaKey, meta, ROLES, PUBLIC, HIDDEN, UNMATCHED, Roles, Public, ApiHidden, UseGuards, guardsOf, metaOf, mergeMeta, HttpError, ValidationError, ErrorFilter, isErrorFilter, toErrorMapper, errorMapper, defaultErrorMapper, joinPath, discoverRoutes, encode, decode, HandlerKind, markHandler, markGateway, isGateway, composeSocket, observe, buildRuntime, buildGateways, buildWebSocket, normalizePath, discoverGateway, discoverGateways, DEFAULT_RELAY_CHANNEL, defaultRelayError, encodeRelay, decodeRelay, WsRelay, RawBody,
|
|
1297
|
+
export { defaultStatusFor, Controller, Get, Post, Put, Patch, Delete, metaKey, meta, ROLES, PUBLIC, HIDDEN, UNMATCHED, Roles, Public, ApiHidden, UseGuards, guardsOf, metaOf, mergeMeta, HttpError, ValidationError, ErrorFilter, isErrorFilter, toErrorMapper, errorMapper, defaultErrorMapper, joinPath, discoverRoutes, encode, decode, HandlerKind, markHandler, markGateway, isGateway, composeSocket, observe, buildRuntime, buildGateways, buildWebSocket, normalizePath, discoverGateway, discoverGateways, DEFAULT_RELAY_CHANNEL, defaultRelayError, encodeRelay, decodeRelay, WsRelay, RawBody, buildContext, withCors, preflight, compose, assertNoCollisions, assertNoGatewayCollisions, withUpgradeRoutes, buildFallback, buildRoutes, StaticOptions, normalizePrefix, negotiate, CompressionEncoding, isCompressibleType, CompressionOptions, defaultRelayUrl, RedisRelay, HEALTH_REPORT_SCHEMA, HealthOptions, HealthRegistry, HealthController, HiddenHealthController };
|
package/dist/client/options.d.ts
CHANGED
|
@@ -16,21 +16,17 @@ export interface HttpClientOptionsInit {
|
|
|
16
16
|
/** Sent on every request, under anything a call sets itself. */
|
|
17
17
|
readonly headers?: Readonly<Record<string, string>>;
|
|
18
18
|
readonly retry?: RetryOptions<unknown>;
|
|
19
|
-
/**
|
|
20
|
-
* Forward the inbound request id to the upstream, so one trace spans both
|
|
21
|
-
* services. `true` uses `x-request-id`; a string names the header. Read from
|
|
22
|
-
* `RequestContext`, so it only carries when there is a request in scope.
|
|
23
|
-
*
|
|
24
|
-
* @default true
|
|
25
|
-
*/
|
|
26
|
-
readonly propagateRequestId?: boolean | string;
|
|
27
19
|
/**
|
|
28
20
|
* Forward W3C Trace Context upstream as `traceparent`, so the callee's spans
|
|
29
|
-
* join this request's trace.
|
|
21
|
+
* join this request's trace and one trace spans both services.
|
|
30
22
|
*
|
|
31
23
|
* Read from `RequestContext`, so it only carries when a trace is in scope -
|
|
32
|
-
* which
|
|
33
|
-
* off there is nothing to send and this costs one
|
|
24
|
+
* which the inbound side puts there unless `requestLogging: { trace: false }`
|
|
25
|
+
* removed it. With that off there is nothing to send and this costs one
|
|
26
|
+
* property read.
|
|
27
|
+
*
|
|
28
|
+
* The caller's `traceFlags` are sent as they arrived, so a trace an upstream
|
|
29
|
+
* sampler declined is not re-sampled at this hop.
|
|
34
30
|
*
|
|
35
31
|
* @default true
|
|
36
32
|
*/
|
|
@@ -67,7 +63,6 @@ export interface HttpClientOptionsInit {
|
|
|
67
63
|
/** How many redirects to follow before rejecting. */
|
|
68
64
|
readonly maxRedirects?: number;
|
|
69
65
|
}
|
|
70
|
-
export declare const DEFAULT_REQUEST_ID_HEADER = "x-request-id";
|
|
71
66
|
/**
|
|
72
67
|
* The resolved options, as a class so it is both the injection token and the type
|
|
73
68
|
* a factory annotates - the same trick `RedisOptions` and `ConfigService` use.
|
|
@@ -77,7 +72,6 @@ export declare class HttpClientOptions {
|
|
|
77
72
|
readonly timeoutMs: number;
|
|
78
73
|
readonly headers: Readonly<Record<string, string>>;
|
|
79
74
|
readonly retry: RetryOptions<unknown>;
|
|
80
|
-
readonly requestIdHeader: string | undefined;
|
|
81
75
|
readonly propagateTrace: boolean;
|
|
82
76
|
readonly name: string | undefined;
|
|
83
77
|
readonly fetchOptions: Readonly<Record<string, unknown>>;
|
package/dist/client/service.d.ts
CHANGED
|
@@ -42,8 +42,8 @@ export interface RequestConfig<TRequest = unknown, TResponse = unknown> {
|
|
|
42
42
|
}
|
|
43
43
|
type BaseOptions<TRequest, TResponse> = Omit<RequestConfig<TRequest, TResponse>, 'method' | 'url' | 'payload'>;
|
|
44
44
|
/**
|
|
45
|
-
* A `fetch` client with a per-request timeout, retry with backoff,
|
|
46
|
-
* propagation and one log line per call. `fetch` and nothing else, so there is no
|
|
45
|
+
* A `fetch` client with a per-request timeout, retry with backoff, W3C Trace
|
|
46
|
+
* Context propagation and one log line per call. `fetch` and nothing else, so there is no
|
|
47
47
|
* client dependency; what it adds is the parts every caller otherwise
|
|
48
48
|
* reimplements - the timeout, the retry policy, `Retry-After`, url building, and
|
|
49
49
|
* a failure that says which call failed.
|
package/dist/client.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* `@dunx/http` does not load any of this.
|
|
7
7
|
*/
|
|
8
8
|
export { FetchError, FetchTransportError } from './client/errors.js';
|
|
9
|
-
export {
|
|
9
|
+
export { HttpClientOptions, type HttpClientOptionsInit, } from './client/options.js';
|
|
10
10
|
export type { BackoffOptions, RetryOptions } from './client/retry.js';
|
|
11
11
|
export { httpClient, HttpModule, type ClientTarget } from './client/module.js';
|
|
12
12
|
export { HttpService, type HeaderFactory, type RequestConfig, type RequestMethod, } from './client/service.js';
|
package/dist/client.js
CHANGED
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import {
|
|
3
|
-
TRACEPARENT_HEADER,
|
|
4
|
-
TraceContext
|
|
5
|
-
} from "./chunk-25g22350.js";
|
|
6
2
|
import {
|
|
7
3
|
FetchError,
|
|
8
4
|
FetchTransportError,
|
|
9
5
|
executeWithRetry,
|
|
10
6
|
isJsonBody,
|
|
11
7
|
safeStringify
|
|
12
|
-
} from "./chunk-
|
|
13
|
-
import
|
|
8
|
+
} from "./chunk-e8a9c6j2.js";
|
|
9
|
+
import {
|
|
10
|
+
TRACEPARENT_HEADER,
|
|
11
|
+
TRACESTATE_HEADER,
|
|
12
|
+
TraceContext
|
|
13
|
+
} from "./chunk-9x3evk19.js";
|
|
14
14
|
// src/client/options.ts
|
|
15
|
-
var DEFAULT_REQUEST_ID_HEADER = "x-request-id";
|
|
16
|
-
|
|
17
15
|
class HttpClientOptions {
|
|
18
16
|
baseUrl;
|
|
19
17
|
timeoutMs;
|
|
20
18
|
headers;
|
|
21
19
|
retry;
|
|
22
|
-
requestIdHeader;
|
|
23
20
|
propagateTrace;
|
|
24
21
|
name;
|
|
25
22
|
fetchOptions;
|
|
@@ -30,8 +27,6 @@ class HttpClientOptions {
|
|
|
30
27
|
this.retry = init.retry ?? {};
|
|
31
28
|
this.name = init.name;
|
|
32
29
|
this.propagateTrace = init.propagateTrace ?? true;
|
|
33
|
-
const propagate = init.propagateRequestId ?? true;
|
|
34
|
-
this.requestIdHeader = propagate === false ? undefined : propagate === true ? DEFAULT_REQUEST_ID_HEADER : propagate;
|
|
35
30
|
this.fetchOptions = Object.fromEntries([
|
|
36
31
|
["proxy", init.proxy],
|
|
37
32
|
["tls", init.tls],
|
|
@@ -213,19 +208,18 @@ class HttpService extends UrlHelper {
|
|
|
213
208
|
return { body: serialised, serialised, json: true };
|
|
214
209
|
}
|
|
215
210
|
async send(config, url, body, serialised, accept = "application/json") {
|
|
216
|
-
const requestId = this.options.requestIdHeader === undefined ? undefined : this.requestContext.getContext().requestId;
|
|
217
211
|
const trace = this.options.propagateTrace ? this.requestContext.getContext() : undefined;
|
|
218
212
|
const headers = {
|
|
219
213
|
accept,
|
|
220
214
|
...serialised === "" ? {} : { "content-type": "application/json" },
|
|
221
215
|
...this.options.headers,
|
|
222
|
-
...requestId === undefined || this.options.requestIdHeader === undefined ? {} : { [this.options.requestIdHeader]: requestId },
|
|
223
216
|
...typeof trace?.traceId === "string" && typeof trace.spanId === "string" ? {
|
|
224
217
|
[TRACEPARENT_HEADER]: TraceContext.header({
|
|
225
218
|
traceId: trace.traceId,
|
|
226
219
|
spanId: trace.spanId,
|
|
227
|
-
flags: "01"
|
|
228
|
-
})
|
|
220
|
+
flags: typeof trace.traceFlags === "string" ? trace.traceFlags : "01"
|
|
221
|
+
}),
|
|
222
|
+
...typeof trace.traceState === "string" ? { [TRACESTATE_HEADER]: trace.traceState } : {}
|
|
229
223
|
} : {},
|
|
230
224
|
...config.headerFactory?.({
|
|
231
225
|
timestamp: Math.floor(Date.now() / 1000),
|
|
@@ -344,7 +338,6 @@ class HttpModule {
|
|
|
344
338
|
}
|
|
345
339
|
}
|
|
346
340
|
export {
|
|
347
|
-
DEFAULT_REQUEST_ID_HEADER,
|
|
348
341
|
FetchError,
|
|
349
342
|
FetchTransportError,
|
|
350
343
|
HttpClientOptions,
|
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export type { CorsOptions, CorsOrigin } from './server/cors.js';
|
|
|
8
8
|
export { defaultErrorMapper, ErrorFilter, errorMapper, HttpError, ValidationError, type ErrorHandler, type ErrorMapper, type HttpErrorOptions, type InputSource, type ValidationIssue, } from './server/errors.js';
|
|
9
9
|
export { HttpFactory, type HttpApp, type HttpOptions, } from './server/factory.js';
|
|
10
10
|
export { DefaultHttpOptions, HttpOptionsProvider, } from './server/options-provider.js';
|
|
11
|
-
export {
|
|
12
|
-
export { TRACEPARENT_HEADER, TRACESTATE_HEADER, TraceContext, type Trace, } from './server/trace-context.js';
|
|
11
|
+
export { TRACEPARENT_HEADER, TRACERESPONSE_HEADER, TRACESTATE_HEADER, TraceContext, type Trace, } from './server/trace-context.js';
|
|
13
12
|
export { RequestLoggingMiddleware, type RequestLoggingOptions, } from './server/request-logging.js';
|
|
13
|
+
export { MetricsMiddleware, RequestMetrics, UNMATCHED_ROUTE, type HttpStatsReport, type RouteStats, } from './server/metrics.js';
|
|
14
14
|
export type { Middleware, Next, RouteHandler } from './server/middleware.js';
|
|
15
15
|
export type { AppSettings } from './server/settings.js';
|
|
16
16
|
export { StaticFiles } from './static/files.js';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,4 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import {
|
|
3
|
-
TRACEPARENT_HEADER,
|
|
4
|
-
TRACESTATE_HEADER,
|
|
5
|
-
TraceContext
|
|
6
|
-
} from "./chunk-25g22350.js";
|
|
7
2
|
import {
|
|
8
3
|
ApiHidden,
|
|
9
4
|
CompressionEncoding,
|
|
@@ -26,11 +21,9 @@ import {
|
|
|
26
21
|
Post,
|
|
27
22
|
Public,
|
|
28
23
|
Put,
|
|
29
|
-
REQUEST_ID_HEADER,
|
|
30
24
|
ROLES,
|
|
31
25
|
RawBody,
|
|
32
26
|
RedisRelay,
|
|
33
|
-
RequestIds,
|
|
34
27
|
Roles,
|
|
35
28
|
StaticOptions,
|
|
36
29
|
UNMATCHED,
|
|
@@ -61,14 +54,18 @@ import {
|
|
|
61
54
|
observe,
|
|
62
55
|
toErrorMapper,
|
|
63
56
|
withUpgradeRoutes
|
|
64
|
-
} from "./chunk-
|
|
57
|
+
} from "./chunk-y85wcdhw.js";
|
|
65
58
|
import {
|
|
66
59
|
HttpStatusCode,
|
|
60
|
+
TRACEPARENT_HEADER,
|
|
61
|
+
TRACERESPONSE_HEADER,
|
|
62
|
+
TRACESTATE_HEADER,
|
|
63
|
+
TraceContext,
|
|
67
64
|
__decorateElement,
|
|
68
65
|
__decoratorMetadata,
|
|
69
66
|
__decoratorStart,
|
|
70
67
|
__runInitializers
|
|
71
|
-
} from "./chunk-
|
|
68
|
+
} from "./chunk-9x3evk19.js";
|
|
72
69
|
// src/server/client-address.ts
|
|
73
70
|
import { AppError } from "@dunx/core";
|
|
74
71
|
var trustedHops = (setting) => {
|
|
@@ -112,6 +109,98 @@ import {
|
|
|
112
109
|
RequestContext as RequestContext3
|
|
113
110
|
} from "@dunx/core";
|
|
114
111
|
|
|
112
|
+
// src/server/metrics.ts
|
|
113
|
+
import { Durations } from "@dunx/core";
|
|
114
|
+
var UNMATCHED_ROUTE = "(unmatched)";
|
|
115
|
+
var seriesFor = (route, method) => ({
|
|
116
|
+
route,
|
|
117
|
+
method,
|
|
118
|
+
count: 0,
|
|
119
|
+
byStatus: {},
|
|
120
|
+
duration: new Durations,
|
|
121
|
+
slowestNs: 0,
|
|
122
|
+
slowestTraceId: undefined
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
class RequestMetrics {
|
|
126
|
+
#series = new Map;
|
|
127
|
+
#unmatched = new Map;
|
|
128
|
+
#since = new Date;
|
|
129
|
+
#server;
|
|
130
|
+
observe(ctx, status, durationNs, traceId) {
|
|
131
|
+
let series = this.#series.get(ctx);
|
|
132
|
+
if (series === undefined) {
|
|
133
|
+
if (ctx.get(UNMATCHED) === true) {
|
|
134
|
+
series = this.#unmatched.get(ctx.method);
|
|
135
|
+
if (series === undefined) {
|
|
136
|
+
series = seriesFor(UNMATCHED_ROUTE, ctx.method);
|
|
137
|
+
this.#unmatched.set(ctx.method, series);
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
series = seriesFor(ctx.path, ctx.method);
|
|
141
|
+
this.#series.set(ctx, series);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
series.count += 1;
|
|
145
|
+
const key = String(status);
|
|
146
|
+
series.byStatus[key] = (series.byStatus[key] ?? 0) + 1;
|
|
147
|
+
series.duration.record(durationNs);
|
|
148
|
+
if (durationNs > series.slowestNs) {
|
|
149
|
+
series.slowestNs = durationNs;
|
|
150
|
+
series.slowestTraceId = traceId;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
snapshot() {
|
|
154
|
+
const routes = [];
|
|
155
|
+
for (const series of [
|
|
156
|
+
...this.#series.values(),
|
|
157
|
+
...this.#unmatched.values()
|
|
158
|
+
]) {
|
|
159
|
+
routes.push({
|
|
160
|
+
route: series.route,
|
|
161
|
+
method: series.method,
|
|
162
|
+
count: series.count,
|
|
163
|
+
byStatus: { ...series.byStatus },
|
|
164
|
+
duration: series.duration.snapshot(),
|
|
165
|
+
...series.slowestTraceId === undefined ? {} : { slowestTraceId: series.slowestTraceId }
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
routes,
|
|
170
|
+
inFlight: this.#server?.pendingRequests ?? 0,
|
|
171
|
+
pendingWebSockets: this.#server?.pendingWebSockets ?? 0,
|
|
172
|
+
since: this.#since.toISOString()
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
reset() {
|
|
176
|
+
this.#series.clear();
|
|
177
|
+
this.#unmatched.clear();
|
|
178
|
+
this.#since = new Date;
|
|
179
|
+
}
|
|
180
|
+
attach(server) {
|
|
181
|
+
this.#server = server;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
var usesMetricsMiddleware = (options) => options.metrics === true && options.requestLogging === false;
|
|
185
|
+
|
|
186
|
+
class MetricsMiddleware {
|
|
187
|
+
metrics;
|
|
188
|
+
constructor(metrics) {
|
|
189
|
+
this.metrics = metrics;
|
|
190
|
+
}
|
|
191
|
+
handle(_req, ctx, next) {
|
|
192
|
+
const started = Bun.nanoseconds();
|
|
193
|
+
return next().then((response) => {
|
|
194
|
+
this.metrics.observe(ctx, response.status, Bun.nanoseconds() - started);
|
|
195
|
+
return response;
|
|
196
|
+
}, (error) => {
|
|
197
|
+
this.metrics.observe(ctx, error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR, Bun.nanoseconds() - started);
|
|
198
|
+
throw error;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
Object.defineProperty(MetricsMiddleware, Symbol.for("dunx.deps"), { value: () => [RequestMetrics] });
|
|
203
|
+
|
|
115
204
|
// src/ws/logging.ts
|
|
116
205
|
import { Logger, LogLevel, RequestContext } from "@dunx/core";
|
|
117
206
|
var LIFECYCLE_LABEL = {
|
|
@@ -378,7 +467,9 @@ class RequestLoggingMiddleware {
|
|
|
378
467
|
#correlateIgnored;
|
|
379
468
|
#correlate;
|
|
380
469
|
#trace;
|
|
381
|
-
|
|
470
|
+
#traceResponse;
|
|
471
|
+
#metrics;
|
|
472
|
+
constructor(logger, context, options = {}, metrics) {
|
|
382
473
|
this.logger = logger;
|
|
383
474
|
this.context = context;
|
|
384
475
|
this.#limit = options.maxBodyLength ?? 2048;
|
|
@@ -388,7 +479,9 @@ class RequestLoggingMiddleware {
|
|
|
388
479
|
this.#ignorePrefix = options.ignorePrefix ?? [];
|
|
389
480
|
this.#correlateIgnored = options.correlateIgnored ?? false;
|
|
390
481
|
this.#correlate = options.correlate ?? true;
|
|
391
|
-
this.#trace = options.trace ??
|
|
482
|
+
this.#trace = options.trace ?? true;
|
|
483
|
+
this.#traceResponse = options.traceResponse ?? true;
|
|
484
|
+
this.#metrics = metrics;
|
|
392
485
|
}
|
|
393
486
|
#ignored(path) {
|
|
394
487
|
if (this.#ignore.size > 0 && this.#ignore.has(path))
|
|
@@ -403,28 +496,32 @@ class RequestLoggingMiddleware {
|
|
|
403
496
|
const mark = from === -1 ? -1 : url.indexOf("?", from);
|
|
404
497
|
const path = from === -1 ? "/" : mark === -1 ? url.slice(from) : url.slice(from, mark);
|
|
405
498
|
if (this.#ignored(path)) {
|
|
499
|
+
if (this.#metrics !== undefined) {
|
|
500
|
+
return this.#ignoredWithMetrics(req, ctx, path, next);
|
|
501
|
+
}
|
|
406
502
|
return this.#correlateIgnored ? this.#correlated(req, ctx, path, next) : next();
|
|
407
503
|
}
|
|
408
504
|
const started = Bun.nanoseconds();
|
|
409
|
-
const requestId = RequestIds.assign(req);
|
|
410
505
|
const scope = {
|
|
411
|
-
requestId,
|
|
412
506
|
method: ctx.method,
|
|
413
507
|
event: path,
|
|
414
508
|
flow: "http",
|
|
415
509
|
context: `${ctx.controller}.${ctx.handler}`
|
|
416
510
|
};
|
|
417
511
|
if (this.#trace) {
|
|
418
|
-
const trace = TraceContext.adopt(req,
|
|
512
|
+
const trace = TraceContext.adopt(req, this.#traceResponse);
|
|
419
513
|
scope.traceId = trace.traceId;
|
|
420
514
|
scope.spanId = trace.spanId;
|
|
515
|
+
scope.traceFlags = trace.flags;
|
|
421
516
|
if (trace.parentSpanId !== undefined) {
|
|
422
517
|
scope.parentSpanId = trace.parentSpanId;
|
|
423
518
|
}
|
|
519
|
+
if (trace.state !== undefined)
|
|
520
|
+
scope.traceState = trace.state;
|
|
424
521
|
}
|
|
425
|
-
return this.#correlate ? this.context.runWithContext(scope, () => this.#begin(req, ctx, url, mark, path,
|
|
522
|
+
return this.#correlate ? this.context.runWithContext(scope, () => this.#begin(req, ctx, url, mark, path, started, next, undefined)) : this.#begin(req, ctx, url, mark, path, started, next, scope);
|
|
426
523
|
}
|
|
427
|
-
#begin(req, ctx, url, mark, path,
|
|
524
|
+
#begin(req, ctx, url, mark, path, started, next, scope) {
|
|
428
525
|
const request = {};
|
|
429
526
|
if (mark !== -1) {
|
|
430
527
|
request["query"] = Object.fromEntries(new URLSearchParams(url.slice(mark + 1)));
|
|
@@ -432,13 +529,13 @@ class RequestLoggingMiddleware {
|
|
|
432
529
|
const body = this.#body(req, ctx);
|
|
433
530
|
if (body === undefined) {
|
|
434
531
|
request["userAgent"] = req.headers.get("user-agent");
|
|
435
|
-
return this.#dispatch(req,
|
|
532
|
+
return this.#dispatch(req, ctx, path, started, request, next, scope);
|
|
436
533
|
}
|
|
437
534
|
return body.then((value) => {
|
|
438
535
|
if (value !== undefined)
|
|
439
536
|
request["body"] = value;
|
|
440
537
|
request["userAgent"] = req.headers.get("user-agent");
|
|
441
|
-
return this.#dispatch(req,
|
|
538
|
+
return this.#dispatch(req, ctx, path, started, request, next, scope);
|
|
442
539
|
});
|
|
443
540
|
}
|
|
444
541
|
#shared(req, request) {
|
|
@@ -453,36 +550,56 @@ class RequestLoggingMiddleware {
|
|
|
453
550
|
if (value !== undefined)
|
|
454
551
|
request["body"] = value;
|
|
455
552
|
}
|
|
456
|
-
#
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
|
|
460
|
-
|
|
553
|
+
#ignoredWithMetrics(req, ctx, path, next) {
|
|
554
|
+
const started = Bun.nanoseconds();
|
|
555
|
+
const failed = (error) => {
|
|
556
|
+
this.#observe(req, ctx, error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR, started);
|
|
557
|
+
throw error;
|
|
461
558
|
};
|
|
559
|
+
let settled;
|
|
560
|
+
try {
|
|
561
|
+
settled = this.#correlateIgnored ? this.#correlated(req, ctx, path, next) : next();
|
|
562
|
+
} catch (error) {
|
|
563
|
+
return failed(error);
|
|
564
|
+
}
|
|
565
|
+
return settled.then((response) => {
|
|
566
|
+
this.#observe(req, ctx, response.status, started);
|
|
567
|
+
return response;
|
|
568
|
+
}, failed);
|
|
569
|
+
}
|
|
570
|
+
#correlated(req, ctx, path, next) {
|
|
571
|
+
const trace = this.#trace ? TraceContext.adopt(req, this.#traceResponse) : undefined;
|
|
572
|
+
const stamp = (response) => trace === undefined ? response : TraceContext.stamp(response, req);
|
|
462
573
|
if (!this.#correlate)
|
|
463
574
|
return next().then(stamp);
|
|
464
575
|
return this.context.runWithContext({
|
|
465
|
-
|
|
576
|
+
...trace === undefined ? {} : {
|
|
577
|
+
traceId: trace.traceId,
|
|
578
|
+
spanId: trace.spanId,
|
|
579
|
+
traceFlags: trace.flags,
|
|
580
|
+
...trace.parentSpanId === undefined ? {} : { parentSpanId: trace.parentSpanId },
|
|
581
|
+
...trace.state === undefined ? {} : { traceState: trace.state }
|
|
582
|
+
},
|
|
466
583
|
method: ctx.method,
|
|
467
584
|
event: path,
|
|
468
585
|
flow: "http",
|
|
469
586
|
context: `${ctx.controller}.${ctx.handler}`
|
|
470
587
|
}, () => next().then(stamp));
|
|
471
588
|
}
|
|
472
|
-
#dispatch(req,
|
|
589
|
+
#dispatch(req, ctx, path, started, request, next, scope) {
|
|
473
590
|
let settled;
|
|
474
591
|
try {
|
|
475
592
|
settled = next();
|
|
476
593
|
} catch (error) {
|
|
477
|
-
this.#failed(req, path, started, request, error, scope);
|
|
594
|
+
this.#failed(req, ctx, path, started, request, error, scope);
|
|
478
595
|
throw error;
|
|
479
596
|
}
|
|
480
|
-
return settled.then((response) => this.#succeeded(req,
|
|
481
|
-
this.#failed(req, path, started, request, error, scope);
|
|
597
|
+
return settled.then((response) => this.#succeeded(req, ctx, path, started, request, response, scope), (error) => {
|
|
598
|
+
this.#failed(req, ctx, path, started, request, error, scope);
|
|
482
599
|
throw error;
|
|
483
600
|
});
|
|
484
601
|
}
|
|
485
|
-
#failed(req, path, started, request, error, scope) {
|
|
602
|
+
#failed(req, ctx, path, started, request, error, scope) {
|
|
486
603
|
this.#shared(req, request);
|
|
487
604
|
const status = error instanceof HttpError ? error.status : HttpStatusCode.INTERNAL_SERVER_ERROR;
|
|
488
605
|
const entry = {
|
|
@@ -492,6 +609,7 @@ class RequestLoggingMiddleware {
|
|
|
492
609
|
statusCode: status,
|
|
493
610
|
elapsedMs: elapsedMs2(started)
|
|
494
611
|
};
|
|
612
|
+
this.#observe(req, ctx, status, started);
|
|
495
613
|
const line = `${req.method} ${path} ${status}`;
|
|
496
614
|
if (status < HttpStatusCode.INTERNAL_SERVER_ERROR) {
|
|
497
615
|
this.logger.warn(line, entry);
|
|
@@ -499,8 +617,9 @@ class RequestLoggingMiddleware {
|
|
|
499
617
|
this.logger.error(line, entry);
|
|
500
618
|
}
|
|
501
619
|
}
|
|
502
|
-
#succeeded(req,
|
|
620
|
+
#succeeded(req, ctx, path, started, request, response, scope) {
|
|
503
621
|
this.#shared(req, request);
|
|
622
|
+
this.#observe(req, ctx, response.status, started);
|
|
504
623
|
const body = this.#responseFields(response);
|
|
505
624
|
if (body === undefined) {
|
|
506
625
|
this.logger.info(`${req.method} ${path} ${response.status}`, {
|
|
@@ -509,8 +628,7 @@ class RequestLoggingMiddleware {
|
|
|
509
628
|
statusCode: response.status,
|
|
510
629
|
elapsedMs: elapsedMs2(started)
|
|
511
630
|
});
|
|
512
|
-
|
|
513
|
-
return response;
|
|
631
|
+
return TraceContext.stamp(response, req);
|
|
514
632
|
}
|
|
515
633
|
return body.then((value) => {
|
|
516
634
|
this.logger.info(`${req.method} ${path} ${response.status}`, {
|
|
@@ -520,10 +638,14 @@ class RequestLoggingMiddleware {
|
|
|
520
638
|
...value === undefined ? {} : { responseBody: value },
|
|
521
639
|
elapsedMs: elapsedMs2(started)
|
|
522
640
|
});
|
|
523
|
-
|
|
524
|
-
return response;
|
|
641
|
+
return TraceContext.stamp(response, req);
|
|
525
642
|
});
|
|
526
643
|
}
|
|
644
|
+
#observe(req, ctx, status, started) {
|
|
645
|
+
if (this.#metrics === undefined)
|
|
646
|
+
return;
|
|
647
|
+
this.#metrics.observe(ctx, status, Bun.nanoseconds() - started, TraceContext.of(req)?.traceId);
|
|
648
|
+
}
|
|
527
649
|
#body(req, ctx) {
|
|
528
650
|
if (!this.#requestBody)
|
|
529
651
|
return;
|
|
@@ -547,7 +669,7 @@ class RequestLoggingMiddleware {
|
|
|
547
669
|
return response.clone().text().then((text) => parse(text, this.#limit));
|
|
548
670
|
}
|
|
549
671
|
}
|
|
550
|
-
Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger2, RequestContext2, { unresolved: "options: RequestLoggingOptions = {}", optional: true }] });
|
|
672
|
+
Object.defineProperty(RequestLoggingMiddleware, Symbol.for("dunx.deps"), { value: () => [Logger2, RequestContext2, { unresolved: "options: RequestLoggingOptions = {}", optional: true }, { unresolved: "metrics?: RequestMetrics", typeOnly: "RequestMetrics" }] });
|
|
551
673
|
|
|
552
674
|
// src/server/settings.ts
|
|
553
675
|
var defaultSettings = () => ({ "trust proxy": false });
|
|
@@ -584,6 +706,7 @@ class HttpApplication {
|
|
|
584
706
|
this.#discovered = discovered;
|
|
585
707
|
this.#middleware = [
|
|
586
708
|
...options.requestLogging === false ? [] : [RequestLoggingMiddleware],
|
|
709
|
+
...usesMetricsMiddleware(options) ? [MetricsMiddleware] : [],
|
|
587
710
|
...options.middleware ?? []
|
|
588
711
|
];
|
|
589
712
|
this.#onError = options.onError === undefined ? errorMapper(app.get(Logger3)) : toErrorMapper(options.onError, (token) => app.get(token, root));
|
|
@@ -666,6 +789,7 @@ class HttpApplication {
|
|
|
666
789
|
server: this.#server,
|
|
667
790
|
trustProxy: this.#settings["trust proxy"]
|
|
668
791
|
});
|
|
792
|
+
this.#app.get(RequestMetrics).attach(this.#server);
|
|
669
793
|
const pubsub = this.#app.get(PubSub);
|
|
670
794
|
pubsub.attach(this.#server);
|
|
671
795
|
if (this.#relay) {
|
|
@@ -776,6 +900,9 @@ class HttpOptionsProvider {
|
|
|
776
900
|
get socketLogging() {
|
|
777
901
|
return true;
|
|
778
902
|
}
|
|
903
|
+
get metrics() {
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
779
906
|
get onError() {
|
|
780
907
|
return;
|
|
781
908
|
}
|
|
@@ -806,6 +933,7 @@ function resolveHttpOptions(settings, given) {
|
|
|
806
933
|
cors: settings.cors,
|
|
807
934
|
requestLogging: settings.requestLogging,
|
|
808
935
|
socketLogging: settings.socketLogging,
|
|
936
|
+
metrics: settings.metrics,
|
|
809
937
|
onError: settings.onError,
|
|
810
938
|
websocket: settings.websocket,
|
|
811
939
|
relay: settings.relay,
|
|
@@ -828,15 +956,24 @@ var pick = (given, fallback) => {
|
|
|
828
956
|
class HttpFactory {
|
|
829
957
|
static async create(root, options = {}) {
|
|
830
958
|
const logging = provide(RequestLoggingMiddleware, {
|
|
831
|
-
useFactory: (logger, context, settings) => new RequestLoggingMiddleware(logger, context, pick(options.requestLogging, settings.requestLogging)),
|
|
832
|
-
inject: [
|
|
959
|
+
useFactory: (logger, context, settings, metrics) => new RequestLoggingMiddleware(logger, context, pick(options.requestLogging, settings.requestLogging), options.metrics ?? settings.metrics ? metrics : undefined),
|
|
960
|
+
inject: [
|
|
961
|
+
Logger4,
|
|
962
|
+
RequestContext3,
|
|
963
|
+
HttpOptionsProvider,
|
|
964
|
+
RequestMetrics
|
|
965
|
+
]
|
|
966
|
+
});
|
|
967
|
+
const metricsMiddleware = provide(MetricsMiddleware, {
|
|
968
|
+
useFactory: (metrics) => new MetricsMiddleware(metrics),
|
|
969
|
+
inject: [RequestMetrics]
|
|
833
970
|
});
|
|
834
971
|
const socketLogging = provide(SocketLoggingMiddleware, {
|
|
835
972
|
useFactory: (logger, context, settings) => new SocketLoggingMiddleware(logger, context, pick(options.socketLogging, settings.socketLogging)),
|
|
836
973
|
inject: [Logger4, RequestContext3, HttpOptionsProvider]
|
|
837
974
|
});
|
|
838
|
-
const services = [PubSub, ClientAddress];
|
|
839
|
-
const providers = [...services, logging, socketLogging];
|
|
975
|
+
const services = [PubSub, ClientAddress, RequestMetrics];
|
|
976
|
+
const providers = [...services, logging, metricsMiddleware, socketLogging];
|
|
840
977
|
const scope = {
|
|
841
978
|
module: HttpModule,
|
|
842
979
|
global: true,
|
|
@@ -1819,6 +1956,7 @@ export {
|
|
|
1819
1956
|
MemoryIndicator,
|
|
1820
1957
|
MemoryOptions,
|
|
1821
1958
|
MemoryThrottleStore,
|
|
1959
|
+
MetricsMiddleware,
|
|
1822
1960
|
OnClose,
|
|
1823
1961
|
OnDrain,
|
|
1824
1962
|
OnMessage,
|
|
@@ -1836,7 +1974,6 @@ export {
|
|
|
1836
1974
|
Public,
|
|
1837
1975
|
Put,
|
|
1838
1976
|
QueryProbe,
|
|
1839
|
-
REQUEST_ID_HEADER,
|
|
1840
1977
|
ROLES,
|
|
1841
1978
|
Readiness,
|
|
1842
1979
|
ReadinessOptions,
|
|
@@ -1845,6 +1982,7 @@ export {
|
|
|
1845
1982
|
RedisThrottleStore,
|
|
1846
1983
|
RelayConnectionOptions,
|
|
1847
1984
|
RequestLoggingMiddleware,
|
|
1985
|
+
RequestMetrics,
|
|
1848
1986
|
Roles,
|
|
1849
1987
|
SKIP_THROTTLE,
|
|
1850
1988
|
SkipThrottle,
|
|
@@ -1854,6 +1992,7 @@ export {
|
|
|
1854
1992
|
StaticOptions,
|
|
1855
1993
|
THROTTLE,
|
|
1856
1994
|
TRACEPARENT_HEADER,
|
|
1995
|
+
TRACERESPONSE_HEADER,
|
|
1857
1996
|
TRACESTATE_HEADER,
|
|
1858
1997
|
Throttle,
|
|
1859
1998
|
ThrottleGuard,
|
|
@@ -1862,6 +2001,7 @@ export {
|
|
|
1862
2001
|
ThrottleStore,
|
|
1863
2002
|
TraceContext,
|
|
1864
2003
|
UNMATCHED,
|
|
2004
|
+
UNMATCHED_ROUTE,
|
|
1865
2005
|
UseGuards,
|
|
1866
2006
|
ValidationError,
|
|
1867
2007
|
WsRelay,
|
package/dist/internal.js
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
toErrorMapper,
|
|
37
37
|
withCors,
|
|
38
38
|
withUpgradeRoutes
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-y85wcdhw.js";
|
|
40
40
|
import {
|
|
41
41
|
backoffDelay,
|
|
42
42
|
executeWithRetry,
|
|
@@ -45,8 +45,8 @@ import {
|
|
|
45
45
|
isRetryableStatus,
|
|
46
46
|
retryAfterMs,
|
|
47
47
|
safeStringify
|
|
48
|
-
} from "./chunk-
|
|
49
|
-
import"./chunk-
|
|
48
|
+
} from "./chunk-e8a9c6j2.js";
|
|
49
|
+
import"./chunk-9x3evk19.js";
|
|
50
50
|
// src/inspect.ts
|
|
51
51
|
import {
|
|
52
52
|
collectModules,
|
|
@@ -50,6 +50,14 @@ export interface HttpOptions extends AppOptions {
|
|
|
50
50
|
* See {@link RequestLoggingMiddleware}.
|
|
51
51
|
*/
|
|
52
52
|
readonly requestLogging?: boolean | RequestLoggingOptions;
|
|
53
|
+
/**
|
|
54
|
+
* Count requests and time them per route, readable through
|
|
55
|
+
* {@link RequestMetrics}. Off by default; `+35.2 ns` per request when
|
|
56
|
+
* `requestLogging` is on, because the entry it already builds shares the
|
|
57
|
+
* timing. With `requestLogging: false` a `MetricsMiddleware` pays for its own
|
|
58
|
+
* `.then` instead, at +175.9 ns.
|
|
59
|
+
*/
|
|
60
|
+
readonly metrics?: boolean;
|
|
53
61
|
/**
|
|
54
62
|
* One entry at `listen()` naming every route and gateway served. On by default,
|
|
55
63
|
* and switched separately from `requestLogging`: one is per process, the other
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type HistogramSnapshot } from '@dunx/core';
|
|
2
|
+
import type { BunRequest, Server } from 'bun';
|
|
3
|
+
import type { RouteContext } from './context.js';
|
|
4
|
+
import type { Middleware, Next } from './middleware.js';
|
|
5
|
+
/** Every path Bun matched nothing for, collapsed into one series. */
|
|
6
|
+
export declare const UNMATCHED_ROUTE = "(unmatched)";
|
|
7
|
+
export interface RouteStats {
|
|
8
|
+
/** The route pattern, so `/users/1` and `/users/2` share one series. */
|
|
9
|
+
readonly route: string;
|
|
10
|
+
readonly method: string;
|
|
11
|
+
readonly count: number;
|
|
12
|
+
/** Keyed by status code as a string, because that is what JSON gives back. */
|
|
13
|
+
readonly byStatus: Readonly<Record<string, number>>;
|
|
14
|
+
/** Nanoseconds. */
|
|
15
|
+
readonly duration: HistogramSnapshot;
|
|
16
|
+
/**
|
|
17
|
+
* The trace of the slowest request on this route so far, which is the only
|
|
18
|
+
* question a p99 provokes: which request was it, and where are its logs.
|
|
19
|
+
*/
|
|
20
|
+
readonly slowestTraceId?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface HttpStatsReport {
|
|
23
|
+
readonly routes: readonly RouteStats[];
|
|
24
|
+
/** Read off `Bun.serve` at 14.7 ns rather than counted, so dunx counts nothing. */
|
|
25
|
+
readonly inFlight: number;
|
|
26
|
+
readonly pendingWebSockets: number;
|
|
27
|
+
/** When the counters were last reset, or boot. */
|
|
28
|
+
readonly since: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One series per route, keyed on the frozen `RouteContext` that `buildContext`
|
|
32
|
+
* makes once at boot. That object identity is the label set: a `Map` lookup on it
|
|
33
|
+
* is 8.8 ns, where building `${method} ${path}` and hashing it is 206.6 ns.
|
|
34
|
+
*
|
|
35
|
+
* Series count is bounded by the handler count, because `ctx.path` is the route
|
|
36
|
+
* pattern rather than the request's path.
|
|
37
|
+
*
|
|
38
|
+
* Bound by `HttpFactory`'s global wrapper, like `PubSub` and `ClientAddress`: an
|
|
39
|
+
* unbound class self-binds into whichever scope asks first, so a second consumer
|
|
40
|
+
* would be a boot error.
|
|
41
|
+
*
|
|
42
|
+
* `observe` takes everything as parameters and reads no ambient store, which is
|
|
43
|
+
* what keeps it at 35.2 ns folded into the `.then` request logging already
|
|
44
|
+
* allocates.
|
|
45
|
+
*/
|
|
46
|
+
export declare class RequestMetrics {
|
|
47
|
+
#private;
|
|
48
|
+
observe(ctx: RouteContext, status: number, durationNs: number, traceId?: string): void;
|
|
49
|
+
snapshot(): HttpStatsReport;
|
|
50
|
+
/**
|
|
51
|
+
* Drops every series rather than zeroing them, so a route that stopped being
|
|
52
|
+
* called stops being reported. A cumulative histogram over a week has a p99
|
|
53
|
+
* reflecting a deploy three days ago; who calls this is the app's decision.
|
|
54
|
+
*/
|
|
55
|
+
reset(): void;
|
|
56
|
+
/** Internal: `listen()` hands the bound server to the resolved singleton. */
|
|
57
|
+
attach(server: Server<unknown>): void;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Whether `MetricsMiddleware` is the thing doing the observing.
|
|
61
|
+
*
|
|
62
|
+
* With request logging on - the default - `RequestLoggingMiddleware` observes
|
|
63
|
+
* from the `.then` it already allocates and this middleware would double-count.
|
|
64
|
+
* Exported because `HttpFactory` binds it and `HttpApplication` installs it, and
|
|
65
|
+
* the two disagreeing would mean either no metrics or twice as many.
|
|
66
|
+
*/
|
|
67
|
+
export declare const usesMetricsMiddleware: (options: {
|
|
68
|
+
readonly metrics?: boolean;
|
|
69
|
+
readonly requestLogging?: unknown;
|
|
70
|
+
}) => boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Installed by `HttpFactory` only when `requestLogging: false`. With logging on -
|
|
73
|
+
* the default - `RequestLoggingMiddleware` calls `observe` from the `.then` it
|
|
74
|
+
* already allocates, at 35.2 ns against this middleware's 175.9 ns standalone.
|
|
75
|
+
*/
|
|
76
|
+
export declare class MetricsMiddleware implements Middleware {
|
|
77
|
+
private readonly metrics;
|
|
78
|
+
constructor(metrics: RequestMetrics);
|
|
79
|
+
handle(_req: BunRequest, ctx: RouteContext, next: Next): Promise<Response>;
|
|
80
|
+
}
|
|
@@ -75,6 +75,8 @@ export declare abstract class HttpOptionsProvider {
|
|
|
75
75
|
/** `false` removes the middleware from the chain; an object tunes it. */
|
|
76
76
|
get requestLogging(): boolean | RequestLoggingOptions;
|
|
77
77
|
get socketLogging(): boolean | SocketLoggingOptions;
|
|
78
|
+
/** Per-route counts and timings. Off by default; see {@link RequestMetrics}. */
|
|
79
|
+
get metrics(): boolean;
|
|
78
80
|
/**
|
|
79
81
|
* Replaces the default mapper. Prefer an `ErrorFilter` class over a bare
|
|
80
82
|
* function: a class is resolved from the container and can inject.
|
|
@@ -2,6 +2,7 @@ import { Logger, RequestContext } from '@dunx/core';
|
|
|
2
2
|
import type { BunRequest } from 'bun';
|
|
3
3
|
import type { RouteContext } from './context.js';
|
|
4
4
|
import type { Middleware, Next } from './middleware.js';
|
|
5
|
+
import type { RequestMetrics } from './metrics.js';
|
|
5
6
|
export interface RequestLoggingOptions {
|
|
6
7
|
/** Bodies past this many characters are logged as a size. Default 2048. `0` omits them. */
|
|
7
8
|
readonly maxBodyLength?: number;
|
|
@@ -17,7 +18,7 @@ export interface RequestLoggingOptions {
|
|
|
17
18
|
* materialised string by the time this clones it. */
|
|
18
19
|
readonly responseBody?: boolean;
|
|
19
20
|
/**
|
|
20
|
-
* Paths to skip entirely: no entry, no `
|
|
21
|
+
* Paths to skip entirely: no entry, no trace, no `traceresponse`, and no
|
|
21
22
|
* `AsyncLocalStorage` scope, so anything the handler logs is uncorrelated.
|
|
22
23
|
* `correlateIgnored` buys the correlation back.
|
|
23
24
|
*/
|
|
@@ -33,24 +34,37 @@ export interface RequestLoggingOptions {
|
|
|
33
34
|
*/
|
|
34
35
|
readonly ignorePrefix?: readonly string[];
|
|
35
36
|
/**
|
|
36
|
-
* Keep the
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
37
|
+
* Keep the trace and the async scope on an `ignore`d path. Default `false`.
|
|
38
|
+
* The path still writes no entry; it gets a `traceresponse` and everything the
|
|
39
|
+
* handler logs carries the trace. Costs ~2.2 us of the ~5.4 us the default path
|
|
40
|
+
* spends.
|
|
40
41
|
*/
|
|
41
42
|
readonly correlateIgnored?: boolean;
|
|
42
43
|
/**
|
|
43
44
|
* Wrap every request in an `AsyncLocalStorage` scope. Default `true`, +0.91 us.
|
|
44
|
-
* It is what lets a service four frames down log `
|
|
45
|
+
* It is what lets a service four frames down log `traceId` without being
|
|
45
46
|
* handed a request. `correlate: false` skips it; this middleware's own entry is
|
|
46
|
-
* unchanged, but every other line the request writes loses its
|
|
47
|
+
* unchanged, but every other line the request writes loses its trace.
|
|
47
48
|
*/
|
|
48
49
|
readonly correlate?: boolean;
|
|
49
50
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* Put `traceresponse` on the response. Default `true`, and ~500 ns of the 4.7 us
|
|
52
|
+
* the path costs, which is the largest thing here that can go without losing a
|
|
53
|
+
* field from a line.
|
|
54
|
+
*
|
|
55
|
+
* `false` keeps the trace on this middleware's own lines, in the async scope and
|
|
56
|
+
* on the metrics exemplar, and withholds the header from every response
|
|
57
|
+
* including a failure's: the error mapper stamps from what `TraceContext.adopt`
|
|
58
|
+
* marked, and this stops it marking.
|
|
59
|
+
*/
|
|
60
|
+
readonly traceResponse?: boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Adopt W3C Trace Context, putting `traceId`, `spanId`, `parentSpanId` and
|
|
63
|
+
* `traceFlags` on every line the request writes and `traceresponse` on its
|
|
64
|
+
* response. Default `true`, at 49.2 ns to mint both ids plus one header read.
|
|
65
|
+
* `@dunx/http/client` sends the adopted trace upstream.
|
|
66
|
+
*
|
|
67
|
+
* `false` removes it, and a request then carries no correlation id at all.
|
|
54
68
|
*/
|
|
55
69
|
readonly trace?: boolean;
|
|
56
70
|
}
|
|
@@ -70,6 +84,6 @@ export declare class RequestLoggingMiddleware implements Middleware {
|
|
|
70
84
|
#private;
|
|
71
85
|
private readonly logger;
|
|
72
86
|
private readonly context;
|
|
73
|
-
constructor(logger: Logger, context: RequestContext, options?: RequestLoggingOptions);
|
|
87
|
+
constructor(logger: Logger, context: RequestContext, options?: RequestLoggingOptions, metrics?: RequestMetrics);
|
|
74
88
|
handle(req: BunRequest, ctx: RouteContext, next: Next): Promise<Response>;
|
|
75
89
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
export declare const TRACEPARENT_HEADER = "traceparent";
|
|
2
2
|
export declare const TRACESTATE_HEADER = "tracestate";
|
|
3
|
+
/**
|
|
4
|
+
* The span that answered, sent back so a caller can record which of the callee's
|
|
5
|
+
* spans its own span points at. Same four fields as `traceparent`, and the
|
|
6
|
+
* the one correlation id a response carries.
|
|
7
|
+
*
|
|
8
|
+
* A W3C Distributed Tracing Working Group proposal rather than a ratified
|
|
9
|
+
* standard: `traceparent` and `tracestate` are the Recommendation, and the
|
|
10
|
+
* published Trace Context Level 2 Candidate Recommendation Draft covers those two
|
|
11
|
+
* request headers and not this response one. The format is specified and stable,
|
|
12
|
+
* and adoption is thin, so treat a caller reading it as a bonus.
|
|
13
|
+
*/
|
|
14
|
+
export declare const TRACERESPONSE_HEADER = "traceresponse";
|
|
3
15
|
export interface Trace {
|
|
4
16
|
/** 32 hex digits, shared by every span in the trace. */
|
|
5
17
|
readonly traceId: string;
|
|
@@ -15,14 +27,18 @@ export interface Trace {
|
|
|
15
27
|
/**
|
|
16
28
|
* W3C Trace Context, propagated across services.
|
|
17
29
|
*
|
|
18
|
-
* The whole of it is one header parsed and
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
30
|
+
* The whole of it is one header parsed and two written. There is no exporter, no
|
|
31
|
+
* sampler and no dependency: every log line a request writes carries the same
|
|
32
|
+
* `traceId` the service upstream logged, so the two join without either of them
|
|
33
|
+
* running a collector.
|
|
34
|
+
*
|
|
35
|
+
* `traceId`, `spanId` and `parentSpanId` are the OpenTelemetry log data model's
|
|
36
|
+
* own fields, so a collector that ingests these lines correlates them with spans
|
|
37
|
+
* emitted by anything else speaking the standard. Bun 1.4.0 runs OpenTelemetry's
|
|
38
|
+
* Node instrumentation, and a trace adopted here is the trace those spans join.
|
|
22
39
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* which is not worth spending in a service that has nothing to correlate with.
|
|
40
|
+
* On by default. `requestLogging: { trace: false }` removes it, at which point a
|
|
41
|
+
* request carries no correlation id at all.
|
|
26
42
|
*/
|
|
27
43
|
export declare class TraceContext {
|
|
28
44
|
#private;
|
|
@@ -32,10 +48,15 @@ export declare class TraceContext {
|
|
|
32
48
|
* higher version keeps its first four fields, so a future format still
|
|
33
49
|
* propagates.
|
|
34
50
|
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
51
|
+
* A trace that arrived is continued with a span of this server's own, and the
|
|
52
|
+
* caller's sampling decision is kept rather than overridden.
|
|
53
|
+
*
|
|
54
|
+
* `expose: false` adopts the trace without marking it for {@link stamp}, so no
|
|
55
|
+
* `traceresponse` is written - by this middleware or by the error mapper, which
|
|
56
|
+
* builds its own `Response` from what was recorded here. Everything inward is
|
|
57
|
+
* unchanged: the scope, the log lines, the metrics exemplar.
|
|
37
58
|
*/
|
|
38
|
-
static adopt(req: Request,
|
|
59
|
+
static adopt(req: Request, expose?: boolean): Trace;
|
|
39
60
|
/** The trace adopted for this request, if one was. */
|
|
40
61
|
static of(req: Request): Trace | undefined;
|
|
41
62
|
/**
|
|
@@ -43,5 +64,15 @@ export declare class TraceContext {
|
|
|
43
64
|
* parent, so the two link without inventing a span nothing logged.
|
|
44
65
|
*/
|
|
45
66
|
static header(trace: Pick<Trace, 'traceId' | 'spanId' | 'flags'>): string;
|
|
67
|
+
/**
|
|
68
|
+
* The response, carrying `traceresponse` if this request adopted a trace.
|
|
69
|
+
*
|
|
70
|
+
* The logging middleware sets the header on a response it returns, and a
|
|
71
|
+
* failure is never one: the error mapper builds a fresh `Response` outside the
|
|
72
|
+
* chain, so a guard's 401, a validation 400 and every unmatched 404 would go out
|
|
73
|
+
* bare. Read back from the request rather than threaded through the mapper,
|
|
74
|
+
* which an app writes its own of.
|
|
75
|
+
*/
|
|
76
|
+
static stamp(response: Response, req: Request): Response;
|
|
46
77
|
static sampled(trace: Pick<Trace, 'flags'>): boolean;
|
|
47
78
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dunx/http",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Bun.serve adapter for the dunx framework: controllers, middleware and WebSocket gateways",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bun",
|
|
@@ -56,13 +56,16 @@
|
|
|
56
56
|
"typecheck": "tsc --noEmit"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@arkv/shared": "
|
|
59
|
+
"@arkv/shared": "0.8.0"
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
|
-
"@dunx/core": "workspace:*"
|
|
62
|
+
"@dunx/core": "workspace:*",
|
|
63
|
+
"@opentelemetry/api": "1.9.1",
|
|
64
|
+
"@opentelemetry/core": "2.11.0",
|
|
65
|
+
"@opentelemetry/sdk-trace-node": "2.11.0"
|
|
63
66
|
},
|
|
64
67
|
"peerDependencies": {
|
|
65
|
-
"@dunx/core": "^3.
|
|
68
|
+
"@dunx/core": "^3.2.0",
|
|
66
69
|
"@types/bun": ">=1.3.0"
|
|
67
70
|
},
|
|
68
71
|
"peerDependenciesMeta": {
|
package/dist/chunk-25g22350.js
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
// @bun
|
|
2
|
-
// src/server/trace-context.ts
|
|
3
|
-
var TRACEPARENT_HEADER = "traceparent";
|
|
4
|
-
var TRACESTATE_HEADER = "tracestate";
|
|
5
|
-
var HEX_32 = /^[0-9a-f]{32}$/;
|
|
6
|
-
var HEX_16 = /^[0-9a-f]{16}$/;
|
|
7
|
-
var HEX_2 = /^[0-9a-f]{2}$/;
|
|
8
|
-
var ZERO_TRACE = "0".repeat(32);
|
|
9
|
-
var ZERO_SPAN = "0".repeat(16);
|
|
10
|
-
var SAMPLED = 1;
|
|
11
|
-
var TRACE = Symbol.for("dunx.http.trace");
|
|
12
|
-
var mintSpanId = () => Buffer.from(crypto.getRandomValues(new Uint8Array(8))).toString("hex");
|
|
13
|
-
|
|
14
|
-
class TraceContext {
|
|
15
|
-
static adopt(req, requestId) {
|
|
16
|
-
const inbound = TraceContext.#parse(req.headers.get(TRACEPARENT_HEADER));
|
|
17
|
-
const state = req.headers.get(TRACESTATE_HEADER);
|
|
18
|
-
const trace = {
|
|
19
|
-
traceId: inbound?.traceId ?? requestId.replaceAll("-", ""),
|
|
20
|
-
spanId: mintSpanId(),
|
|
21
|
-
...inbound === undefined ? {} : { parentSpanId: inbound.spanId },
|
|
22
|
-
flags: inbound?.flags ?? "01",
|
|
23
|
-
...inbound !== undefined && state !== null ? { state } : {}
|
|
24
|
-
};
|
|
25
|
-
req[TRACE] = trace;
|
|
26
|
-
return trace;
|
|
27
|
-
}
|
|
28
|
-
static of(req) {
|
|
29
|
-
return req[TRACE];
|
|
30
|
-
}
|
|
31
|
-
static header(trace) {
|
|
32
|
-
return `00-${trace.traceId}-${trace.spanId}-${trace.flags}`;
|
|
33
|
-
}
|
|
34
|
-
static sampled(trace) {
|
|
35
|
-
return (Number.parseInt(trace.flags, 16) & SAMPLED) === SAMPLED;
|
|
36
|
-
}
|
|
37
|
-
static #parse(header) {
|
|
38
|
-
if (header === null)
|
|
39
|
-
return;
|
|
40
|
-
const parts = header.split("-");
|
|
41
|
-
if (parts.length < 4)
|
|
42
|
-
return;
|
|
43
|
-
const [version, traceId, spanId, flags] = parts;
|
|
44
|
-
if (!HEX_2.test(version) || version === "ff")
|
|
45
|
-
return;
|
|
46
|
-
if (version === "00" && parts.length !== 4)
|
|
47
|
-
return;
|
|
48
|
-
if (!HEX_32.test(traceId) || traceId === ZERO_TRACE)
|
|
49
|
-
return;
|
|
50
|
-
if (!HEX_16.test(spanId) || spanId === ZERO_SPAN)
|
|
51
|
-
return;
|
|
52
|
-
if (!HEX_2.test(flags))
|
|
53
|
-
return;
|
|
54
|
-
return { traceId, spanId, flags };
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export { TRACEPARENT_HEADER, TRACESTATE_HEADER, TraceContext };
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export declare const REQUEST_ID_HEADER = "x-request-id";
|
|
2
|
-
/**
|
|
3
|
-
* The request id, and the only thing that decides a request has one.
|
|
4
|
-
*
|
|
5
|
-
* The logging middleware sets the header on a response it returns, and a failure
|
|
6
|
-
* is never one - the error mapper builds a fresh `Response` outside the chain. So
|
|
7
|
-
* a guard's 401, a validation 400 and every unmatched 404 went out with no id.
|
|
8
|
-
*
|
|
9
|
-
* Recorded against the request rather than threaded through the mapper, which an
|
|
10
|
-
* app writes its own of. {@link stamp} reads back what {@link assign} recorded, so
|
|
11
|
-
* a path nothing minted an id for is still answered without a header.
|
|
12
|
-
*/
|
|
13
|
-
export declare class RequestIds {
|
|
14
|
-
/**
|
|
15
|
-
* Called by `RequestLoggingMiddleware` and by nothing else. Splitting minting
|
|
16
|
-
* from recording would let a second caller invent an id the log line does not
|
|
17
|
-
* carry.
|
|
18
|
-
*/
|
|
19
|
-
static assign(req: Request): string;
|
|
20
|
-
/** The response, with this request's id on it if it was ever given one. */
|
|
21
|
-
static stamp(response: Response, req: Request): Response;
|
|
22
|
-
}
|