@lib-acr/express 0.0.0-stage → 0.0.2
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/LICENSE +21 -0
- package/README.md +76 -2
- package/dist/cjs/index.js +28 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/middleware.js +174 -0
- package/dist/cjs/middleware.js.map +1 -0
- package/dist/cjs/sdk-info.js +6 -0
- package/dist/cjs/sdk-info.js.map +1 -0
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/middleware.js +166 -0
- package/dist/esm/middleware.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/dist/esm/sdk-info.js +3 -0
- package/dist/esm/sdk-info.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/middleware.d.ts +68 -0
- package/dist/types/sdk-info.d.ts +4 -0
- package/package.json +48 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Application Control Room
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lib-acr/express
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Express middleware: one `http_request` event per request (method, route template, status, duration, body sizes), X-Request-Id correlation, W3C traceparent joining, and an error handler that reports server-side failures.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @lib-acr/express
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import express from 'express';
|
|
15
|
+
import { ACRMiddleware, ACRErrorHandler, acrTraceHeaders } from '@lib-acr/express';
|
|
16
|
+
|
|
17
|
+
import { ACR } from '@lib-acr/node';
|
|
18
|
+
|
|
19
|
+
const acr = new ACR({ apiKey, apiSecret, endpoint }); // one client for the whole process
|
|
20
|
+
const app = express();
|
|
21
|
+
app.use(ACRMiddleware({ client: acr }));
|
|
22
|
+
|
|
23
|
+
app.get('/orders/:id', (req, res) => {
|
|
24
|
+
// req.acr.traceId / requestId are available for logs; acrTraceHeaders(req) for outgoing calls
|
|
25
|
+
res.json({ id: req.params.id });
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
app.use(ACRErrorHandler({ client: acr }));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Passing `client` is recommended. You can pass `options` instead; one client is created per options object, so give
|
|
32
|
+
both handlers the **same** object (`const cfg = {…}; ACRMiddleware({ options: cfg }); ACRErrorHandler({ options: cfg })`).
|
|
33
|
+
Two different objects create two clients with separate buffers.
|
|
34
|
+
|
|
35
|
+
## Notes
|
|
36
|
+
|
|
37
|
+
Route templates are recorded (`/orders/:id`), never concrete URLs. Unmatched paths are recorded as `(unmatched)`. Headers, query strings and bodies are never recorded. Health and readiness paths are ignored by default.
|
|
38
|
+
|
|
39
|
+
## Remote control
|
|
40
|
+
|
|
41
|
+
Opt-in maintenance mode. Register it before your routes:
|
|
42
|
+
|
|
43
|
+
```ts
|
|
44
|
+
import { maintenanceMiddleware } from '@lib-acr/express';
|
|
45
|
+
|
|
46
|
+
app.use(maintenanceMiddleware({
|
|
47
|
+
client: acr,
|
|
48
|
+
getUserId: (req) => req.user?.id, // users in allowUserIds keep access
|
|
49
|
+
exempt: (req) => req.path.startsWith('/webhooks'),
|
|
50
|
+
}));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
While maintenance is on, requests get `503` with `{ "error": "MAINTENANCE", "message": "…", "endsAt": "…" | null }`
|
|
54
|
+
and `Retry-After` (seconds until `endsAt`, otherwise 300). `/health`, `/healthz`, `/ready` and `/readyz` always pass,
|
|
55
|
+
including under a prefix such as `/api/health`.
|
|
56
|
+
|
|
57
|
+
## Feature flags
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
app.get('/checkout', (req, res) => {
|
|
61
|
+
const variant = acr.flag('new-checkout', false, { userId: req.user?.id }) ? 'new' : 'old';
|
|
62
|
+
res.json({ variant });
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Wire protocol
|
|
67
|
+
|
|
68
|
+
Every SDK sends the same protocol: `POST {endpoint}/ingest/{events|logs|errors|heartbeat}` with the `X-ACR-Api-Key`
|
|
69
|
+
header (and `X-ACR-Secret-Key` for server keys). The credential determines the app and environment. The body is
|
|
70
|
+
`{ schemaVersion: 1, sdk, items[1..500] }`.
|
|
71
|
+
Each item has a client `id`, which lets the server drop duplicates when a batch is retried.
|
|
72
|
+
|
|
73
|
+
Retries use exponential backoff with jitter for network errors, 429 and 5xx responses. Other 4xx responses are not retried.
|
|
74
|
+
Telemetry is buffered in a bounded in-memory queue. When it is full, the oldest items are dropped and counted in `stats()`.
|
|
75
|
+
SDK code never throws into the host application.
|
|
76
|
+
|
|
77
|
+
Metrics (`increment`, `gauge`, `histogram`) and traces are not yet available.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.SDK_INFO = exports.resolveClient = exports.maintenanceMiddleware = exports.contextFromRequest = exports.acrTraceHeaders = exports.ACRErrorHandler = exports.ACRMiddleware = void 0;
|
|
18
|
+
var middleware_js_1 = require("./middleware.js");
|
|
19
|
+
Object.defineProperty(exports, "ACRMiddleware", { enumerable: true, get: function () { return middleware_js_1.ACRMiddleware; } });
|
|
20
|
+
Object.defineProperty(exports, "ACRErrorHandler", { enumerable: true, get: function () { return middleware_js_1.ACRErrorHandler; } });
|
|
21
|
+
Object.defineProperty(exports, "acrTraceHeaders", { enumerable: true, get: function () { return middleware_js_1.acrTraceHeaders; } });
|
|
22
|
+
Object.defineProperty(exports, "contextFromRequest", { enumerable: true, get: function () { return middleware_js_1.contextFromRequest; } });
|
|
23
|
+
Object.defineProperty(exports, "maintenanceMiddleware", { enumerable: true, get: function () { return middleware_js_1.maintenanceMiddleware; } });
|
|
24
|
+
Object.defineProperty(exports, "resolveClient", { enumerable: true, get: function () { return middleware_js_1.resolveClient; } });
|
|
25
|
+
__exportStar(require("@lib-acr/node"), exports);
|
|
26
|
+
var sdk_info_js_1 = require("./sdk-info.js");
|
|
27
|
+
Object.defineProperty(exports, "SDK_INFO", { enumerable: true, get: function () { return sdk_info_js_1.SDK_INFO; } });
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;AAAA,iDAA4I;AAAnI,8GAAA,aAAa,OAAA;AAAE,gHAAA,eAAe,OAAA;AAAE,gHAAA,eAAe,OAAA;AAAE,mHAAA,kBAAkB,OAAA;AAAE,sHAAA,qBAAqB,OAAA;AAAE,8GAAA,aAAa,OAAA;AAElH,gDAA8B;AAC9B,6CAAyC;AAAhC,uGAAA,QAAQ,OAAA"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveClient = resolveClient;
|
|
4
|
+
exports.contextFromRequest = contextFromRequest;
|
|
5
|
+
exports.ACRMiddleware = ACRMiddleware;
|
|
6
|
+
exports.ACRErrorHandler = ACRErrorHandler;
|
|
7
|
+
exports.acrTraceHeaders = acrTraceHeaders;
|
|
8
|
+
exports.maintenanceMiddleware = maintenanceMiddleware;
|
|
9
|
+
const core_1 = require("@lib-acr/core");
|
|
10
|
+
const node_1 = require("@lib-acr/node");
|
|
11
|
+
const sdk_info_js_1 = require("./sdk-info.js");
|
|
12
|
+
const DEFAULT_IGNORED = ['/health', '/healthz', '/ready', '/readyz', '/favicon.ico'];
|
|
13
|
+
const REQUEST_ID_RE = /^[\w.-]{1,64}$/;
|
|
14
|
+
/** Clients created from `options`, keyed by the options object, so the middleware and the error handler share one. */
|
|
15
|
+
const clientsByOptions = new WeakMap();
|
|
16
|
+
/**
|
|
17
|
+
* Returns `options.client`, or the client created for `options.options`. The same options object always yields the
|
|
18
|
+
* same client, so ACRMiddleware and ACRErrorHandler configured with it share one buffer and one flush timer.
|
|
19
|
+
*/
|
|
20
|
+
function resolveClient(options) {
|
|
21
|
+
if (options.client)
|
|
22
|
+
return options.client;
|
|
23
|
+
if (!options.options)
|
|
24
|
+
throw new Error('[acr] ExpressAcrOptions needs either client or options');
|
|
25
|
+
const existing = clientsByOptions.get(options.options);
|
|
26
|
+
if (existing)
|
|
27
|
+
return existing;
|
|
28
|
+
const created = new node_1.ACR(options.options, sdk_info_js_1.SDK_INFO);
|
|
29
|
+
clientsByOptions.set(options.options, created);
|
|
30
|
+
return created;
|
|
31
|
+
}
|
|
32
|
+
function isIgnored(path, ignored) {
|
|
33
|
+
return ignored.some((p) => (typeof p === 'string' ? p === path : p.test(path)));
|
|
34
|
+
}
|
|
35
|
+
/** Reads trace headers. The rest of the request headers are never kept. */
|
|
36
|
+
function contextFromRequest(req) {
|
|
37
|
+
const parsed = (0, core_1.parseTraceparent)(req.get('traceparent'));
|
|
38
|
+
const incomingRequestId = req.get('x-request-id');
|
|
39
|
+
const requestId = incomingRequestId && REQUEST_ID_RE.test(incomingRequestId) ? incomingRequestId : (0, core_1.generateId)();
|
|
40
|
+
return {
|
|
41
|
+
traceId: parsed?.traceId ?? (0, core_1.generateTraceId)(),
|
|
42
|
+
spanId: (0, core_1.generateSpanId)(),
|
|
43
|
+
parentSpanId: parsed?.spanId ?? null,
|
|
44
|
+
requestId,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** Route template only (for example /orders/:id), never the concrete URL, so cardinality stays bounded. */
|
|
48
|
+
function routeOf(req) {
|
|
49
|
+
if (!req.route)
|
|
50
|
+
return '(unmatched)';
|
|
51
|
+
const path = typeof req.route.path === 'string' ? req.route.path : '';
|
|
52
|
+
return `${req.baseUrl ?? ''}${path}` || '/';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Records one `http_request` event per request: method, route template, status, duration and body sizes. It sets
|
|
56
|
+
* X-Request-Id on the response and exposes req.acr for logs and errors. Headers, query strings and bodies are not
|
|
57
|
+
* recorded.
|
|
58
|
+
*/
|
|
59
|
+
function ACRMiddleware(options) {
|
|
60
|
+
const client = resolveClient(options);
|
|
61
|
+
const ignored = [...DEFAULT_IGNORED, ...(options.ignorePaths ?? [])];
|
|
62
|
+
const eventName = options.eventName ?? 'http_request';
|
|
63
|
+
return function acrRequestTracing(req, res, next) {
|
|
64
|
+
if (isIgnored(req.path, ignored)) {
|
|
65
|
+
next();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const ctx = contextFromRequest(req);
|
|
69
|
+
req.acr = ctx;
|
|
70
|
+
res.locals.acr = ctx;
|
|
71
|
+
res.setHeader('X-Request-Id', ctx.requestId);
|
|
72
|
+
const started = process.hrtime.bigint();
|
|
73
|
+
let recorded = false;
|
|
74
|
+
const record = (aborted) => {
|
|
75
|
+
if (recorded)
|
|
76
|
+
return;
|
|
77
|
+
recorded = true;
|
|
78
|
+
const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
|
|
79
|
+
const status = aborted ? 499 : res.statusCode;
|
|
80
|
+
const requestBytes = Number(req.get('content-length') ?? 0) || 0;
|
|
81
|
+
const responseBytes = Number(res.getHeader('content-length') ?? 0) || 0;
|
|
82
|
+
client.trackEvent(eventName, {
|
|
83
|
+
method: req.method,
|
|
84
|
+
route: routeOf(req),
|
|
85
|
+
status,
|
|
86
|
+
statusClass: `${Math.floor(status / 100)}xx`,
|
|
87
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
88
|
+
requestBytes,
|
|
89
|
+
responseBytes,
|
|
90
|
+
aborted,
|
|
91
|
+
parentSpanId: ctx.parentSpanId ?? undefined,
|
|
92
|
+
}, {
|
|
93
|
+
traceId: ctx.traceId,
|
|
94
|
+
spanId: ctx.spanId,
|
|
95
|
+
requestId: ctx.requestId,
|
|
96
|
+
userId: options.getUserId?.(req),
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
res.once('finish', () => record(false));
|
|
100
|
+
res.once('close', () => record(!res.writableEnded));
|
|
101
|
+
next();
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Error handler to register after your routes. It reports server-side failures (5xx or an error without a status)
|
|
106
|
+
* and always passes the error on, so the normal Express error response is unchanged. Client errors such as 400 or 404
|
|
107
|
+
* are not reported.
|
|
108
|
+
*/
|
|
109
|
+
function ACRErrorHandler(options) {
|
|
110
|
+
const client = resolveClient(options);
|
|
111
|
+
return function acrErrorReporter(err, req, _res, next) {
|
|
112
|
+
const status = statusOf(err);
|
|
113
|
+
if (status === null || status >= 500) {
|
|
114
|
+
client.captureError(err, {
|
|
115
|
+
handled: false,
|
|
116
|
+
source: 'backend',
|
|
117
|
+
route: routeOf(req),
|
|
118
|
+
traceId: req.acr?.traceId,
|
|
119
|
+
spanId: req.acr?.spanId,
|
|
120
|
+
requestId: req.acr?.requestId,
|
|
121
|
+
userId: options.getUserId?.(req),
|
|
122
|
+
attributes: { method: req.method, status: status ?? 500, framework: 'express' },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
next(err);
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function statusOf(err) {
|
|
129
|
+
const candidate = err;
|
|
130
|
+
const value = candidate?.status ?? candidate?.statusCode;
|
|
131
|
+
return typeof value === 'number' ? value : null;
|
|
132
|
+
}
|
|
133
|
+
/** Headers to add to outgoing calls so downstream services join the same trace. */
|
|
134
|
+
function acrTraceHeaders(req) {
|
|
135
|
+
const ctx = req.acr;
|
|
136
|
+
if (!ctx)
|
|
137
|
+
return {};
|
|
138
|
+
return {
|
|
139
|
+
traceparent: (0, core_1.formatTraceparent)({ traceId: ctx.traceId, spanId: ctx.spanId }),
|
|
140
|
+
'x-request-id': ctx.requestId,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Opt-in maintenance mode for APIs. While the control snapshot has maintenance enabled, requests get `503` with
|
|
145
|
+
* `{ error: 'MAINTENANCE', message, endsAt }` and `Retry-After` (seconds until `endsAt`, else 300). Health and
|
|
146
|
+
* readiness probes (/health, /healthz, /ready, /readyz) always pass, as do users in `allowUserIds` (via `getUserId`)
|
|
147
|
+
* and requests for which `exempt` returns true. Register it before your routes.
|
|
148
|
+
*/
|
|
149
|
+
function maintenanceMiddleware(options) {
|
|
150
|
+
const client = resolveClient(options);
|
|
151
|
+
return function acrMaintenance(req, res, next) {
|
|
152
|
+
if ((0, core_1.isMaintenanceExemptPath)(req.path)) {
|
|
153
|
+
next();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
let blocked = null;
|
|
157
|
+
try {
|
|
158
|
+
blocked = (0, core_1.maintenanceResponse)(client.getControlState(), { userId: options.getUserId?.(req) });
|
|
159
|
+
if (blocked && options.exempt?.(req))
|
|
160
|
+
blocked = null;
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
blocked = null; // a failing exempt/getUserId callback must not take the API down
|
|
164
|
+
}
|
|
165
|
+
if (!blocked) {
|
|
166
|
+
next();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
res.setHeader('Retry-After', String(blocked.retryAfterSeconds));
|
|
170
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
171
|
+
res.status(blocked.status).json(blocked.body);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SDK_INFO = void 0;
|
|
4
|
+
// Generated by scripts/sync-sdk-info.mjs from package.json. Do not edit by hand.
|
|
5
|
+
exports.SDK_INFO = { name: '@lib-acr/express', version: '0.0.2' };
|
|
6
|
+
//# sourceMappingURL=sdk-info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-info.js","sourceRoot":"","sources":["../../src/sdk-info.ts"],"names":[],"mappings":";;;AAAA,iFAAiF;AACpE,QAAA,QAAQ,GAAG,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAW,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE5I,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { formatTraceparent, generateId, generateSpanId, generateTraceId, isMaintenanceExemptPath, maintenanceResponse, parseTraceparent, } from '@lib-acr/core';
|
|
2
|
+
import { ACR } from '@lib-acr/node';
|
|
3
|
+
import { SDK_INFO } from './sdk-info.js';
|
|
4
|
+
const DEFAULT_IGNORED = ['/health', '/healthz', '/ready', '/readyz', '/favicon.ico'];
|
|
5
|
+
const REQUEST_ID_RE = /^[\w.-]{1,64}$/;
|
|
6
|
+
/** Clients created from `options`, keyed by the options object, so the middleware and the error handler share one. */
|
|
7
|
+
const clientsByOptions = new WeakMap();
|
|
8
|
+
/**
|
|
9
|
+
* Returns `options.client`, or the client created for `options.options`. The same options object always yields the
|
|
10
|
+
* same client, so ACRMiddleware and ACRErrorHandler configured with it share one buffer and one flush timer.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveClient(options) {
|
|
13
|
+
if (options.client)
|
|
14
|
+
return options.client;
|
|
15
|
+
if (!options.options)
|
|
16
|
+
throw new Error('[acr] ExpressAcrOptions needs either client or options');
|
|
17
|
+
const existing = clientsByOptions.get(options.options);
|
|
18
|
+
if (existing)
|
|
19
|
+
return existing;
|
|
20
|
+
const created = new ACR(options.options, SDK_INFO);
|
|
21
|
+
clientsByOptions.set(options.options, created);
|
|
22
|
+
return created;
|
|
23
|
+
}
|
|
24
|
+
function isIgnored(path, ignored) {
|
|
25
|
+
return ignored.some((p) => (typeof p === 'string' ? p === path : p.test(path)));
|
|
26
|
+
}
|
|
27
|
+
/** Reads trace headers. The rest of the request headers are never kept. */
|
|
28
|
+
export function contextFromRequest(req) {
|
|
29
|
+
const parsed = parseTraceparent(req.get('traceparent'));
|
|
30
|
+
const incomingRequestId = req.get('x-request-id');
|
|
31
|
+
const requestId = incomingRequestId && REQUEST_ID_RE.test(incomingRequestId) ? incomingRequestId : generateId();
|
|
32
|
+
return {
|
|
33
|
+
traceId: parsed?.traceId ?? generateTraceId(),
|
|
34
|
+
spanId: generateSpanId(),
|
|
35
|
+
parentSpanId: parsed?.spanId ?? null,
|
|
36
|
+
requestId,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Route template only (for example /orders/:id), never the concrete URL, so cardinality stays bounded. */
|
|
40
|
+
function routeOf(req) {
|
|
41
|
+
if (!req.route)
|
|
42
|
+
return '(unmatched)';
|
|
43
|
+
const path = typeof req.route.path === 'string' ? req.route.path : '';
|
|
44
|
+
return `${req.baseUrl ?? ''}${path}` || '/';
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Records one `http_request` event per request: method, route template, status, duration and body sizes. It sets
|
|
48
|
+
* X-Request-Id on the response and exposes req.acr for logs and errors. Headers, query strings and bodies are not
|
|
49
|
+
* recorded.
|
|
50
|
+
*/
|
|
51
|
+
export function ACRMiddleware(options) {
|
|
52
|
+
const client = resolveClient(options);
|
|
53
|
+
const ignored = [...DEFAULT_IGNORED, ...(options.ignorePaths ?? [])];
|
|
54
|
+
const eventName = options.eventName ?? 'http_request';
|
|
55
|
+
return function acrRequestTracing(req, res, next) {
|
|
56
|
+
if (isIgnored(req.path, ignored)) {
|
|
57
|
+
next();
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const ctx = contextFromRequest(req);
|
|
61
|
+
req.acr = ctx;
|
|
62
|
+
res.locals.acr = ctx;
|
|
63
|
+
res.setHeader('X-Request-Id', ctx.requestId);
|
|
64
|
+
const started = process.hrtime.bigint();
|
|
65
|
+
let recorded = false;
|
|
66
|
+
const record = (aborted) => {
|
|
67
|
+
if (recorded)
|
|
68
|
+
return;
|
|
69
|
+
recorded = true;
|
|
70
|
+
const durationMs = Number(process.hrtime.bigint() - started) / 1e6;
|
|
71
|
+
const status = aborted ? 499 : res.statusCode;
|
|
72
|
+
const requestBytes = Number(req.get('content-length') ?? 0) || 0;
|
|
73
|
+
const responseBytes = Number(res.getHeader('content-length') ?? 0) || 0;
|
|
74
|
+
client.trackEvent(eventName, {
|
|
75
|
+
method: req.method,
|
|
76
|
+
route: routeOf(req),
|
|
77
|
+
status,
|
|
78
|
+
statusClass: `${Math.floor(status / 100)}xx`,
|
|
79
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
80
|
+
requestBytes,
|
|
81
|
+
responseBytes,
|
|
82
|
+
aborted,
|
|
83
|
+
parentSpanId: ctx.parentSpanId ?? undefined,
|
|
84
|
+
}, {
|
|
85
|
+
traceId: ctx.traceId,
|
|
86
|
+
spanId: ctx.spanId,
|
|
87
|
+
requestId: ctx.requestId,
|
|
88
|
+
userId: options.getUserId?.(req),
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
res.once('finish', () => record(false));
|
|
92
|
+
res.once('close', () => record(!res.writableEnded));
|
|
93
|
+
next();
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Error handler to register after your routes. It reports server-side failures (5xx or an error without a status)
|
|
98
|
+
* and always passes the error on, so the normal Express error response is unchanged. Client errors such as 400 or 404
|
|
99
|
+
* are not reported.
|
|
100
|
+
*/
|
|
101
|
+
export function ACRErrorHandler(options) {
|
|
102
|
+
const client = resolveClient(options);
|
|
103
|
+
return function acrErrorReporter(err, req, _res, next) {
|
|
104
|
+
const status = statusOf(err);
|
|
105
|
+
if (status === null || status >= 500) {
|
|
106
|
+
client.captureError(err, {
|
|
107
|
+
handled: false,
|
|
108
|
+
source: 'backend',
|
|
109
|
+
route: routeOf(req),
|
|
110
|
+
traceId: req.acr?.traceId,
|
|
111
|
+
spanId: req.acr?.spanId,
|
|
112
|
+
requestId: req.acr?.requestId,
|
|
113
|
+
userId: options.getUserId?.(req),
|
|
114
|
+
attributes: { method: req.method, status: status ?? 500, framework: 'express' },
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
next(err);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function statusOf(err) {
|
|
121
|
+
const candidate = err;
|
|
122
|
+
const value = candidate?.status ?? candidate?.statusCode;
|
|
123
|
+
return typeof value === 'number' ? value : null;
|
|
124
|
+
}
|
|
125
|
+
/** Headers to add to outgoing calls so downstream services join the same trace. */
|
|
126
|
+
export function acrTraceHeaders(req) {
|
|
127
|
+
const ctx = req.acr;
|
|
128
|
+
if (!ctx)
|
|
129
|
+
return {};
|
|
130
|
+
return {
|
|
131
|
+
traceparent: formatTraceparent({ traceId: ctx.traceId, spanId: ctx.spanId }),
|
|
132
|
+
'x-request-id': ctx.requestId,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Opt-in maintenance mode for APIs. While the control snapshot has maintenance enabled, requests get `503` with
|
|
137
|
+
* `{ error: 'MAINTENANCE', message, endsAt }` and `Retry-After` (seconds until `endsAt`, else 300). Health and
|
|
138
|
+
* readiness probes (/health, /healthz, /ready, /readyz) always pass, as do users in `allowUserIds` (via `getUserId`)
|
|
139
|
+
* and requests for which `exempt` returns true. Register it before your routes.
|
|
140
|
+
*/
|
|
141
|
+
export function maintenanceMiddleware(options) {
|
|
142
|
+
const client = resolveClient(options);
|
|
143
|
+
return function acrMaintenance(req, res, next) {
|
|
144
|
+
if (isMaintenanceExemptPath(req.path)) {
|
|
145
|
+
next();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let blocked = null;
|
|
149
|
+
try {
|
|
150
|
+
blocked = maintenanceResponse(client.getControlState(), { userId: options.getUserId?.(req) });
|
|
151
|
+
if (blocked && options.exempt?.(req))
|
|
152
|
+
blocked = null;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
blocked = null; // a failing exempt/getUserId callback must not take the API down
|
|
156
|
+
}
|
|
157
|
+
if (!blocked) {
|
|
158
|
+
next();
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
res.setHeader('Retry-After', String(blocked.retryAfterSeconds));
|
|
162
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
163
|
+
res.status(blocked.status).json(blocked.body);
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
//# sourceMappingURL=middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.js","sourceRoot":"","sources":["../../src/middleware.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sdk-info.js","sourceRoot":"","sources":["../../src/sdk-info.ts"],"names":[],"mappings":"AAAA,iFAAiF;AACjF,MAAM,CAAC,MAAM,QAAQ,GAAG,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,OAAO,EAAW,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { ACRMiddleware, ACRErrorHandler, acrTraceHeaders, contextFromRequest, maintenanceMiddleware, resolveClient } from './middleware.js';
|
|
2
|
+
export type { AcrRequestContext, ExpressAcrOptions, MaintenanceMiddlewareOptions } from './middleware.js';
|
|
3
|
+
export * from '@lib-acr/node';
|
|
4
|
+
export { SDK_INFO } from './sdk-info.js';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { AcrClient, type AcrOptions } from '@lib-acr/core';
|
|
2
|
+
import type { NextFunction, Request, Response } from 'express';
|
|
3
|
+
/** Trace and request identifiers for one incoming request. Available as req.acr and res.locals.acr. */
|
|
4
|
+
export interface AcrRequestContext {
|
|
5
|
+
traceId: string;
|
|
6
|
+
/** Span id of this server operation. */
|
|
7
|
+
spanId: string;
|
|
8
|
+
/** Span id of the caller, when a traceparent header arrived. */
|
|
9
|
+
parentSpanId: string | null;
|
|
10
|
+
requestId: string;
|
|
11
|
+
}
|
|
12
|
+
declare global {
|
|
13
|
+
namespace Express {
|
|
14
|
+
interface Request {
|
|
15
|
+
acr?: AcrRequestContext;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export interface ExpressAcrOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Use an existing client (for example one shared with a NestJS or background-job setup). Recommended: create one
|
|
22
|
+
* `ACR` and pass it to both ACRMiddleware and ACRErrorHandler.
|
|
23
|
+
*/
|
|
24
|
+
client?: AcrClient;
|
|
25
|
+
/**
|
|
26
|
+
* Options used to create a client when `client` is not given. One client is created per options object, so pass
|
|
27
|
+
* the same object to ACRMiddleware and ACRErrorHandler to share it.
|
|
28
|
+
*/
|
|
29
|
+
options?: AcrOptions;
|
|
30
|
+
/** Exact paths or patterns that are not recorded. Default: health and readiness probes. */
|
|
31
|
+
ignorePaths?: Array<string | RegExp>;
|
|
32
|
+
/** Event name for request telemetry. Default `http_request`. */
|
|
33
|
+
eventName?: string;
|
|
34
|
+
/** Returns the authenticated user id for a request, if any. Keep it an opaque id. */
|
|
35
|
+
getUserId?: (req: Request) => string | undefined;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Returns `options.client`, or the client created for `options.options`. The same options object always yields the
|
|
39
|
+
* same client, so ACRMiddleware and ACRErrorHandler configured with it share one buffer and one flush timer.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveClient(options: ExpressAcrOptions): AcrClient;
|
|
42
|
+
/** Reads trace headers. The rest of the request headers are never kept. */
|
|
43
|
+
export declare function contextFromRequest(req: Request): AcrRequestContext;
|
|
44
|
+
/**
|
|
45
|
+
* Records one `http_request` event per request: method, route template, status, duration and body sizes. It sets
|
|
46
|
+
* X-Request-Id on the response and exposes req.acr for logs and errors. Headers, query strings and bodies are not
|
|
47
|
+
* recorded.
|
|
48
|
+
*/
|
|
49
|
+
export declare function ACRMiddleware(options: ExpressAcrOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
50
|
+
/**
|
|
51
|
+
* Error handler to register after your routes. It reports server-side failures (5xx or an error without a status)
|
|
52
|
+
* and always passes the error on, so the normal Express error response is unchanged. Client errors such as 400 or 404
|
|
53
|
+
* are not reported.
|
|
54
|
+
*/
|
|
55
|
+
export declare function ACRErrorHandler(options: ExpressAcrOptions): (err: unknown, req: Request, _res: Response, next: NextFunction) => void;
|
|
56
|
+
/** Headers to add to outgoing calls so downstream services join the same trace. */
|
|
57
|
+
export declare function acrTraceHeaders(req: Request): Record<string, string>;
|
|
58
|
+
export interface MaintenanceMiddlewareOptions extends Pick<ExpressAcrOptions, 'client' | 'options' | 'getUserId'> {
|
|
59
|
+
/** Return true to let a request through during maintenance (for example admin or webhook routes). */
|
|
60
|
+
exempt?: (req: Request) => boolean;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Opt-in maintenance mode for APIs. While the control snapshot has maintenance enabled, requests get `503` with
|
|
64
|
+
* `{ error: 'MAINTENANCE', message, endsAt }` and `Retry-After` (seconds until `endsAt`, else 300). Health and
|
|
65
|
+
* readiness probes (/health, /healthz, /ready, /readyz) always pass, as do users in `allowUserIds` (via `getUserId`)
|
|
66
|
+
* and requests for which `exempt` returns true. Register it before your routes.
|
|
67
|
+
*/
|
|
68
|
+
export declare function maintenanceMiddleware(options: MaintenanceMiddlewareOptions): (req: Request, res: Response, next: NextFunction) => void;
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lib-acr/express",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "ACR middleware for Express: request events with trace correlation, error capture and W3C trace context.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"main": "./dist/cjs/index.js",
|
|
7
|
+
"module": "./dist/esm/index.js",
|
|
8
|
+
"types": "./dist/types/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/types/index.d.ts",
|
|
12
|
+
"import": "./dist/esm/index.js",
|
|
13
|
+
"require": "./dist/cjs/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.17"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@lib-acr/core": "0.0.2",
|
|
27
|
+
"@lib-acr/node": "0.0.2"
|
|
28
|
+
},
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"express": ">=4.18.0 <6"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"express": "^5.1.0",
|
|
34
|
+
"@types/express": "^5.0.6"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "node ../../scripts/build-package.mjs",
|
|
38
|
+
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
39
|
+
"test": "vitest run"
|
|
40
|
+
},
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/application-control-room/application.git",
|
|
44
|
+
"directory": "packages/express"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://github.com/application-control-room/application/tree/main/packages/express#readme",
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
50
|
+
}
|