midline-agent 0.1.9 → 0.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 +288 -252
- package/dist/agent.d.ts +105 -6
- package/dist/agent.js +663 -102
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +63 -0
- package/dist/config.d.ts +63 -0
- package/dist/config.js +230 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +38 -0
- package/dist/errorHandler.d.ts +11 -2
- package/dist/errorHandler.js +32 -12
- package/dist/index.d.ts +9 -2
- package/dist/index.js +9 -1
- package/dist/middleware.d.ts +19 -2
- package/dist/middleware.js +92 -41
- package/dist/proxy.d.ts +70 -0
- package/dist/proxy.js +383 -0
- package/dist/redact.d.ts +35 -0
- package/dist/redact.js +223 -0
- package/dist/tap.d.ts +18 -0
- package/dist/tap.js +62 -0
- package/dist/transport.d.ts +35 -0
- package/dist/transport.js +133 -0
- package/dist/types.d.ts +107 -5
- package/package.json +13 -8
- package/src/agent.ts +734 -102
- package/src/cli.ts +69 -0
- package/src/config.ts +232 -0
- package/src/context.ts +48 -0
- package/src/errorHandler.ts +36 -13
- package/src/index.ts +19 -2
- package/src/middleware.ts +118 -43
- package/src/proxy.ts +435 -0
- package/src/redact.ts +231 -0
- package/src/tap.ts +55 -0
- package/src/transport.ts +125 -0
- package/src/types.ts +144 -31
- package/test/agent.test.js +353 -0
- package/test/helpers.js +151 -0
- package/test/middleware.test.js +178 -0
- package/test/proxy.test.js +274 -0
- package/test/redact.test.js +105 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const agent_1 = require("./agent");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const proxy_1 = require("./proxy");
|
|
7
|
+
const USAGE = `Usage: midline-agent proxy [--target <url>] [--port <n>] [--host <addr>]
|
|
8
|
+
|
|
9
|
+
Forwards traffic to a destination API and reports every exchange to Midline.
|
|
10
|
+
|
|
11
|
+
--target Destination API (TARGET_API_URL, required)
|
|
12
|
+
--port Port to listen on (MIDLINE_PROXY_PORT, default 8080)
|
|
13
|
+
--host Interface to bind (MIDLINE_PROXY_HOST, default 127.0.0.1)
|
|
14
|
+
|
|
15
|
+
Midline server:
|
|
16
|
+
MIDLINE_API_KEY project API key (without it, traffic is forwarded but not reported)
|
|
17
|
+
MIDLINE_ENDPOINT default https://api.usemidline.com
|
|
18
|
+
MIDLINE_CUSTOM_CA extra CA for the Midline server only (PEM or file path)
|
|
19
|
+
TARGET_API_CA extra CA for the destination only (PEM or file path)
|
|
20
|
+
`;
|
|
21
|
+
function flag(args, name) {
|
|
22
|
+
const index = args.indexOf(`--${name}`);
|
|
23
|
+
if (index !== -1)
|
|
24
|
+
return args[index + 1];
|
|
25
|
+
const inline = args.find((arg) => arg.startsWith(`--${name}=`));
|
|
26
|
+
return inline?.slice(name.length + 3);
|
|
27
|
+
}
|
|
28
|
+
async function main() {
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
if (args[0] !== "proxy" || args.includes("--help") || args.includes("-h")) {
|
|
31
|
+
process.stdout.write(USAGE);
|
|
32
|
+
process.exitCode = args[0] === "proxy" || args.length === 0 ? 0 : 1;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const port = flag(args, "port");
|
|
36
|
+
const target = flag(args, "target") ?? process.env.TARGET_API_URL;
|
|
37
|
+
// Fail on the destination before the agent starts talking about its own config.
|
|
38
|
+
(0, proxy_1.resolveTarget)(target);
|
|
39
|
+
const agent = agent_1.MidlineAgent.init({ serviceName: process.env.MIDLINE_SERVICE_NAME });
|
|
40
|
+
const server = await (0, proxy_1.startMidlineProxy)({
|
|
41
|
+
target,
|
|
42
|
+
port: port ? Number(port) : undefined,
|
|
43
|
+
host: flag(args, "host"),
|
|
44
|
+
agent,
|
|
45
|
+
});
|
|
46
|
+
process.stdout.write(`midline proxy listening on ${(0, proxy_1.proxyAddress)(server)} -> ${server.proxy.target.origin}${server.proxy.target.pathname}` +
|
|
47
|
+
`${agent.active ? "" : " (monitoring off)"}\n`);
|
|
48
|
+
let stopping = false;
|
|
49
|
+
const stop = (signal) => {
|
|
50
|
+
if (stopping)
|
|
51
|
+
return;
|
|
52
|
+
stopping = true;
|
|
53
|
+
process.stdout.write(`midline proxy: ${signal}, draining\n`);
|
|
54
|
+
server.close();
|
|
55
|
+
void agent.shutdown(5000).finally(() => process.exit(0));
|
|
56
|
+
};
|
|
57
|
+
process.on("SIGINT", () => stop("SIGINT"));
|
|
58
|
+
process.on("SIGTERM", () => stop("SIGTERM"));
|
|
59
|
+
}
|
|
60
|
+
main().catch((err) => {
|
|
61
|
+
process.stderr.write(`midline proxy: ${err instanceof config_1.ConfigError ? err.message : err?.stack ?? err}\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
});
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { CaInput, CaptureOptions, MidlineConfig } from "./types";
|
|
2
|
+
export declare const DEFAULT_ENDPOINT = "https://api.usemidline.com";
|
|
3
|
+
export declare const INGEST_PATH = "/api/api-monitor/events";
|
|
4
|
+
/** A setting that cannot work as given. Thrown at construction, never while serving traffic. */
|
|
5
|
+
export declare class ConfigError extends Error {
|
|
6
|
+
constructor(message: string);
|
|
7
|
+
}
|
|
8
|
+
export interface ResolvedCapture {
|
|
9
|
+
headers: boolean;
|
|
10
|
+
query: boolean;
|
|
11
|
+
requestBody: boolean;
|
|
12
|
+
responseBody: boolean;
|
|
13
|
+
maxBodyBytes: number;
|
|
14
|
+
}
|
|
15
|
+
export interface ResolvedConfig {
|
|
16
|
+
apiKey: string;
|
|
17
|
+
serviceName?: string;
|
|
18
|
+
ingestUrl: URL;
|
|
19
|
+
batchUrl: URL;
|
|
20
|
+
/** Full trust store for the Midline endpoint, or undefined for Node's default. */
|
|
21
|
+
ca?: Array<string | Buffer>;
|
|
22
|
+
hasCustomCa: boolean;
|
|
23
|
+
environment?: string;
|
|
24
|
+
host?: string;
|
|
25
|
+
region?: string;
|
|
26
|
+
release?: string;
|
|
27
|
+
redactFields: string[];
|
|
28
|
+
redactHeaders: string[];
|
|
29
|
+
capture: ResolvedCapture;
|
|
30
|
+
onError?: (message: string) => void;
|
|
31
|
+
debug: boolean;
|
|
32
|
+
flushIntervalMs: number;
|
|
33
|
+
timeoutMs: number;
|
|
34
|
+
connectTimeoutMs: number;
|
|
35
|
+
maxBatchSize: number;
|
|
36
|
+
maxQueueSize: number;
|
|
37
|
+
maxEventBytes: number;
|
|
38
|
+
maxRetryDelayMs: number;
|
|
39
|
+
}
|
|
40
|
+
export declare function env(name: string): string | undefined;
|
|
41
|
+
export declare function envFlag(name: string): boolean | undefined;
|
|
42
|
+
export declare function envInt(name: string): number | undefined;
|
|
43
|
+
export declare function isLoopback(hostname: string): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Turns whatever the user configured into the ingest URL.
|
|
46
|
+
*
|
|
47
|
+
* `https://api.usemidline.com`, `https://api.usemidline.com/api/api-monitor/events`
|
|
48
|
+
* and `https://gateway.internal/midline` all work; the last is treated as a prefix.
|
|
49
|
+
*/
|
|
50
|
+
export declare function resolveIngestUrl(endpoint: string): URL;
|
|
51
|
+
/**
|
|
52
|
+
* Loads extra CAs. Every entry must contain at least one parseable certificate, so a
|
|
53
|
+
* typo in a path fails loudly at startup instead of silently trusting nothing.
|
|
54
|
+
*/
|
|
55
|
+
export declare function loadCa(input: CaInput | undefined, label: string): Buffer[] | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Node's `ca` option *replaces* the default trust store. Passing only a private CA
|
|
58
|
+
* would make every public certificate untrusted, so extra CAs are appended to the
|
|
59
|
+
* defaults (including NODE_EXTRA_CA_CERTS where the runtime exposes them).
|
|
60
|
+
*/
|
|
61
|
+
export declare function trustStore(extra: Buffer[] | undefined): Array<string | Buffer> | undefined;
|
|
62
|
+
export declare function resolveCapture(capture: CaptureOptions | undefined): ResolvedCapture;
|
|
63
|
+
export declare function resolveConfig(config: MidlineConfig): ResolvedConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
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 __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.ConfigError = exports.INGEST_PATH = exports.DEFAULT_ENDPOINT = void 0;
|
|
37
|
+
exports.env = env;
|
|
38
|
+
exports.envFlag = envFlag;
|
|
39
|
+
exports.envInt = envInt;
|
|
40
|
+
exports.isLoopback = isLoopback;
|
|
41
|
+
exports.resolveIngestUrl = resolveIngestUrl;
|
|
42
|
+
exports.loadCa = loadCa;
|
|
43
|
+
exports.trustStore = trustStore;
|
|
44
|
+
exports.resolveCapture = resolveCapture;
|
|
45
|
+
exports.resolveConfig = resolveConfig;
|
|
46
|
+
const fs_1 = require("fs");
|
|
47
|
+
const crypto_1 = require("crypto");
|
|
48
|
+
const tls = __importStar(require("tls"));
|
|
49
|
+
exports.DEFAULT_ENDPOINT = "https://api.usemidline.com";
|
|
50
|
+
exports.INGEST_PATH = "/api/api-monitor/events";
|
|
51
|
+
/** A setting that cannot work as given. Thrown at construction, never while serving traffic. */
|
|
52
|
+
class ConfigError extends Error {
|
|
53
|
+
constructor(message) {
|
|
54
|
+
super(message);
|
|
55
|
+
this.name = "ConfigError";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
exports.ConfigError = ConfigError;
|
|
59
|
+
function env(name) {
|
|
60
|
+
if (typeof process === "undefined" || !process.env) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const value = process.env[name];
|
|
64
|
+
return value === undefined || value.trim() === "" ? undefined : value.trim();
|
|
65
|
+
}
|
|
66
|
+
function envFlag(name) {
|
|
67
|
+
const value = env(name)?.toLowerCase();
|
|
68
|
+
if (value === undefined)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (["1", "true", "yes", "on"].includes(value))
|
|
71
|
+
return true;
|
|
72
|
+
if (["0", "false", "no", "off"].includes(value))
|
|
73
|
+
return false;
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
function envInt(name) {
|
|
77
|
+
const value = env(name);
|
|
78
|
+
if (value === undefined)
|
|
79
|
+
return undefined;
|
|
80
|
+
const parsed = Number(value);
|
|
81
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
82
|
+
}
|
|
83
|
+
function isLoopback(hostname) {
|
|
84
|
+
const host = hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
|
85
|
+
return (host === "localhost" ||
|
|
86
|
+
host.endsWith(".localhost") ||
|
|
87
|
+
host === "::1" ||
|
|
88
|
+
/^127(\.\d{1,3}){3}$/.test(host));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Turns whatever the user configured into the ingest URL.
|
|
92
|
+
*
|
|
93
|
+
* `https://api.usemidline.com`, `https://api.usemidline.com/api/api-monitor/events`
|
|
94
|
+
* and `https://gateway.internal/midline` all work; the last is treated as a prefix.
|
|
95
|
+
*/
|
|
96
|
+
function resolveIngestUrl(endpoint) {
|
|
97
|
+
let url;
|
|
98
|
+
try {
|
|
99
|
+
url = new URL(endpoint);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new ConfigError(`endpoint "${endpoint}" is not a valid URL`);
|
|
103
|
+
}
|
|
104
|
+
if (url.protocol !== "https:" && url.protocol !== "http:") {
|
|
105
|
+
throw new ConfigError(`endpoint must use https:// (got ${url.protocol}//)`);
|
|
106
|
+
}
|
|
107
|
+
if (url.protocol === "http:" && !isLoopback(url.hostname)) {
|
|
108
|
+
throw new ConfigError(`refusing to send the API key in cleartext to ${url.origin} — use https://. ` +
|
|
109
|
+
"Plain http:// is only accepted for localhost.");
|
|
110
|
+
}
|
|
111
|
+
if (url.username || url.password) {
|
|
112
|
+
throw new ConfigError("endpoint must not contain credentials; pass the key as apiKey");
|
|
113
|
+
}
|
|
114
|
+
url.search = "";
|
|
115
|
+
url.hash = "";
|
|
116
|
+
const path = url.pathname.replace(/\/+$/, "");
|
|
117
|
+
if (path.endsWith(`${exports.INGEST_PATH}/batch`)) {
|
|
118
|
+
url.pathname = path.slice(0, -"/batch".length);
|
|
119
|
+
}
|
|
120
|
+
else if (path.endsWith(exports.INGEST_PATH)) {
|
|
121
|
+
url.pathname = path;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
url.pathname = `${path}${exports.INGEST_PATH}`;
|
|
125
|
+
}
|
|
126
|
+
return url;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Loads extra CAs. Every entry must contain at least one parseable certificate, so a
|
|
130
|
+
* typo in a path fails loudly at startup instead of silently trusting nothing.
|
|
131
|
+
*/
|
|
132
|
+
function loadCa(input, label) {
|
|
133
|
+
if (input === undefined || input === null || (Array.isArray(input) && input.length === 0)) {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
const entries = Array.isArray(input) ? input : [input];
|
|
137
|
+
return entries.map((entry, index) => {
|
|
138
|
+
let pem;
|
|
139
|
+
if (Buffer.isBuffer(entry)) {
|
|
140
|
+
pem = entry;
|
|
141
|
+
}
|
|
142
|
+
else if (typeof entry === "string" && entry.includes("-----BEGIN")) {
|
|
143
|
+
pem = Buffer.from(entry);
|
|
144
|
+
}
|
|
145
|
+
else if (typeof entry === "string") {
|
|
146
|
+
try {
|
|
147
|
+
pem = (0, fs_1.readFileSync)(entry);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
throw new ConfigError(`${label}: cannot read CA file "${entry}" (${err?.code ?? err?.message})`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
throw new ConfigError(`${label}: entry ${index} must be a PEM string, a Buffer or a file path`);
|
|
155
|
+
}
|
|
156
|
+
if (!pem.toString("utf8").includes("-----BEGIN CERTIFICATE-----")) {
|
|
157
|
+
throw new ConfigError(`${label}: entry ${index} does not contain a PEM certificate`);
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
new crypto_1.X509Certificate(pem);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
throw new ConfigError(`${label}: entry ${index} is not a parseable certificate`);
|
|
164
|
+
}
|
|
165
|
+
return pem;
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Node's `ca` option *replaces* the default trust store. Passing only a private CA
|
|
170
|
+
* would make every public certificate untrusted, so extra CAs are appended to the
|
|
171
|
+
* defaults (including NODE_EXTRA_CA_CERTS where the runtime exposes them).
|
|
172
|
+
*/
|
|
173
|
+
function trustStore(extra) {
|
|
174
|
+
if (!extra || extra.length === 0) {
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
const getCACertificates = tls.getCACertificates;
|
|
178
|
+
const defaults = typeof getCACertificates === "function"
|
|
179
|
+
? getCACertificates("default")
|
|
180
|
+
: [...tls.rootCertificates];
|
|
181
|
+
return [...defaults, ...extra];
|
|
182
|
+
}
|
|
183
|
+
function positiveInt(value, fallback, min, max) {
|
|
184
|
+
if (value === undefined || !Number.isFinite(value)) {
|
|
185
|
+
return fallback;
|
|
186
|
+
}
|
|
187
|
+
return Math.min(Math.max(Math.floor(value), min), max);
|
|
188
|
+
}
|
|
189
|
+
function resolveCapture(capture) {
|
|
190
|
+
return {
|
|
191
|
+
headers: capture?.headers === true,
|
|
192
|
+
query: capture?.query === true,
|
|
193
|
+
requestBody: capture?.requestBody === true,
|
|
194
|
+
responseBody: capture?.responseBody === true,
|
|
195
|
+
maxBodyBytes: positiveInt(capture?.maxBodyBytes, 4096, 0, 1024 * 1024),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function resolveConfig(config) {
|
|
199
|
+
const apiKey = (config.apiKey ?? env("MIDLINE_API_KEY") ?? "").trim();
|
|
200
|
+
const ingestUrl = resolveIngestUrl(config.endpoint || env("MIDLINE_ENDPOINT") || exports.DEFAULT_ENDPOINT);
|
|
201
|
+
const extraCa = loadCa(config.ca ?? env("MIDLINE_CUSTOM_CA"), "ca / MIDLINE_CUSTOM_CA");
|
|
202
|
+
if (extraCa && ingestUrl.protocol !== "https:") {
|
|
203
|
+
throw new ConfigError("ca / MIDLINE_CUSTOM_CA is set but the endpoint is not https://");
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
apiKey,
|
|
207
|
+
serviceName: config.serviceName ?? env("MIDLINE_SERVICE_NAME"),
|
|
208
|
+
ingestUrl,
|
|
209
|
+
batchUrl: new URL(`${ingestUrl.pathname}/batch`, ingestUrl),
|
|
210
|
+
ca: trustStore(extraCa),
|
|
211
|
+
hasCustomCa: Boolean(extraCa),
|
|
212
|
+
environment: config.environment ?? env("MIDLINE_ENVIRONMENT"),
|
|
213
|
+
host: config.host,
|
|
214
|
+
region: config.region,
|
|
215
|
+
release: config.release ?? env("MIDLINE_RELEASE"),
|
|
216
|
+
redactFields: [...(config.redactFields ?? []), ...(config.maskFields ?? [])],
|
|
217
|
+
redactHeaders: config.redactHeaders ?? [],
|
|
218
|
+
capture: resolveCapture(config.capture),
|
|
219
|
+
onError: config.onError,
|
|
220
|
+
debug: config.debug ?? envFlag("MIDLINE_DEBUG") ?? false,
|
|
221
|
+
flushIntervalMs: positiveInt(config.flushIntervalMs, 1500, 50, 60000),
|
|
222
|
+
timeoutMs: positiveInt(config.timeoutMs, 10000, 100, 120000),
|
|
223
|
+
connectTimeoutMs: positiveInt(config.connectTimeoutMs, 5000, 50, 60000),
|
|
224
|
+
maxBatchSize: positiveInt(config.maxBatchSize, 100, 1, 500),
|
|
225
|
+
maxQueueSize: positiveInt(config.maxQueueSize, 1000, 1, 100000),
|
|
226
|
+
// The server caps a payload at 64 KiB; a larger event could never be accepted.
|
|
227
|
+
maxEventBytes: positiveInt(config.maxEventBytes, 64 * 1024, 1024, 64 * 1024),
|
|
228
|
+
maxRetryDelayMs: positiveInt(config.maxRetryDelayMs, 5 * 60000, 1000, 60 * 60000),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { IncomingHttpHeaders } from "http";
|
|
2
|
+
export interface RequestContext {
|
|
3
|
+
/** This hop's id. Taken from `x-request-id` when the caller sent a sane one. */
|
|
4
|
+
requestId: string;
|
|
5
|
+
/** Shared across every service a request touches. Falls back to the request id. */
|
|
6
|
+
correlationId: string;
|
|
7
|
+
traceId?: string;
|
|
8
|
+
spanId?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function createRequestContext(headers: IncomingHttpHeaders): RequestContext;
|
|
11
|
+
export declare function setRequestContext(req: object, context: RequestContext): void;
|
|
12
|
+
/** The ids Midline recorded for this request — useful for stamping your own logs. */
|
|
13
|
+
export declare function getRequestContext(req: object): RequestContext | undefined;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createRequestContext = createRequestContext;
|
|
4
|
+
exports.setRequestContext = setRequestContext;
|
|
5
|
+
exports.getRequestContext = getRequestContext;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
const contexts = new WeakMap();
|
|
8
|
+
/** Ids come from the network, so only short, printable ones are reused. */
|
|
9
|
+
function sanitizeId(value) {
|
|
10
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
11
|
+
if (typeof raw !== "string")
|
|
12
|
+
return undefined;
|
|
13
|
+
const trimmed = raw.trim();
|
|
14
|
+
return /^[A-Za-z0-9._:\/+=-]{1,128}$/.test(trimmed) ? trimmed : undefined;
|
|
15
|
+
}
|
|
16
|
+
/** W3C trace context: `00-<32 hex trace id>-<16 hex parent id>-<2 hex flags>`. */
|
|
17
|
+
function parseTraceparent(value) {
|
|
18
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
19
|
+
const match = typeof raw === "string"
|
|
20
|
+
? /^[\da-f]{2}-([\da-f]{32})-([\da-f]{16})-[\da-f]{2}$/.exec(raw.trim().toLowerCase())
|
|
21
|
+
: null;
|
|
22
|
+
if (!match || /^0+$/.test(match[1]) || /^0+$/.test(match[2])) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
return { traceId: match[1], spanId: match[2] };
|
|
26
|
+
}
|
|
27
|
+
function createRequestContext(headers) {
|
|
28
|
+
const requestId = sanitizeId(headers["x-request-id"]) ?? (0, crypto_1.randomUUID)();
|
|
29
|
+
const correlationId = sanitizeId(headers["x-correlation-id"]) ?? requestId;
|
|
30
|
+
return { requestId, correlationId, ...parseTraceparent(headers["traceparent"]) };
|
|
31
|
+
}
|
|
32
|
+
function setRequestContext(req, context) {
|
|
33
|
+
contexts.set(req, context);
|
|
34
|
+
}
|
|
35
|
+
/** The ids Midline recorded for this request — useful for stamping your own logs. */
|
|
36
|
+
function getRequestContext(req) {
|
|
37
|
+
return contexts.get(req);
|
|
38
|
+
}
|
package/dist/errorHandler.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
2
|
+
import { MidlineAgent } from "./agent";
|
|
3
|
+
export interface MidlineErrorHandlerOptions {
|
|
4
|
+
/** Defaults to the agent created by `MidlineAgent.init()`. */
|
|
5
|
+
agent?: MidlineAgent;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Express error middleware. Records the error, then passes it on unchanged — it
|
|
9
|
+
* never sends a response itself. Register it after your routes.
|
|
10
|
+
*/
|
|
11
|
+
export declare function midlineErrorHandler(options?: MidlineErrorHandlerOptions): (err: any, req: IncomingMessage, _res: ServerResponse, next: (err?: unknown) => void) => void;
|
package/dist/errorHandler.js
CHANGED
|
@@ -3,18 +3,38 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.midlineErrorHandler = midlineErrorHandler;
|
|
4
4
|
const agent_1 = require("./agent");
|
|
5
5
|
const breadcrumbs_1 = require("./breadcrumbs");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
6
|
+
const context_1 = require("./context");
|
|
7
|
+
/**
|
|
8
|
+
* Express error middleware. Records the error, then passes it on unchanged — it
|
|
9
|
+
* never sends a response itself. Register it after your routes.
|
|
10
|
+
*/
|
|
11
|
+
function midlineErrorHandler(options = {}) {
|
|
12
|
+
// Four named parameters: Express recognises error middleware by arity.
|
|
13
|
+
return function midlineError(err, req, _res, next) {
|
|
14
|
+
try {
|
|
15
|
+
const agent = options.agent ?? agent_1.MidlineAgent.current;
|
|
16
|
+
if (agent?.active) {
|
|
17
|
+
const context = (0, context_1.getRequestContext)(req);
|
|
18
|
+
const status = Number(err?.status ?? err?.statusCode);
|
|
19
|
+
agent.addEvent({
|
|
20
|
+
type: "error",
|
|
21
|
+
path: req.originalUrl ?? req.url ?? "/",
|
|
22
|
+
method: req.method,
|
|
23
|
+
statusCode: Number.isInteger(status) && status >= 400 && status <= 599 ? status : 500,
|
|
24
|
+
message: typeof err?.message === "string" ? err.message : String(err),
|
|
25
|
+
stack: typeof err?.stack === "string" ? err.stack : undefined,
|
|
26
|
+
breadcrumbs: (0, breadcrumbs_1.getBreadcrumbs)(),
|
|
27
|
+
requestId: context?.requestId,
|
|
28
|
+
correlationId: context?.correlationId,
|
|
29
|
+
traceId: context?.traceId,
|
|
30
|
+
spanId: context?.spanId,
|
|
31
|
+
integration: "express",
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// Reporting must not replace the application's own error handling.
|
|
37
|
+
}
|
|
18
38
|
next(err);
|
|
19
39
|
};
|
|
20
40
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
export { MidlineAgent } from "./agent";
|
|
1
|
+
export { MidlineAgent, SDK_VERSION } from "./agent";
|
|
2
2
|
export { midlineMiddleware } from "./middleware";
|
|
3
|
+
export type { MidlineMiddlewareOptions } from "./middleware";
|
|
3
4
|
export { midlineErrorHandler } from "./errorHandler";
|
|
5
|
+
export type { MidlineErrorHandlerOptions } from "./errorHandler";
|
|
6
|
+
export { createMidlineProxy, startMidlineProxy } from "./proxy";
|
|
7
|
+
export type { MidlineProxy, MidlineProxyOptions, StartProxyOptions } from "./proxy";
|
|
4
8
|
export { addBreadcrumb } from "./breadcrumbs";
|
|
5
|
-
export
|
|
9
|
+
export { getRequestContext } from "./context";
|
|
10
|
+
export type { RequestContext } from "./context";
|
|
11
|
+
export { ConfigError } from "./config";
|
|
12
|
+
export type { Breadcrumb, CaInput, CapturedMessage, CaptureOptions, EventCategory, EventSeverity, EventType, MidlineConfig, MidlineEvent, } from "./types";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.addBreadcrumb = exports.midlineErrorHandler = exports.midlineMiddleware = exports.MidlineAgent = void 0;
|
|
3
|
+
exports.ConfigError = exports.getRequestContext = exports.addBreadcrumb = exports.startMidlineProxy = exports.createMidlineProxy = exports.midlineErrorHandler = exports.midlineMiddleware = exports.SDK_VERSION = exports.MidlineAgent = void 0;
|
|
4
4
|
var agent_1 = require("./agent");
|
|
5
5
|
Object.defineProperty(exports, "MidlineAgent", { enumerable: true, get: function () { return agent_1.MidlineAgent; } });
|
|
6
|
+
Object.defineProperty(exports, "SDK_VERSION", { enumerable: true, get: function () { return agent_1.SDK_VERSION; } });
|
|
6
7
|
var middleware_1 = require("./middleware");
|
|
7
8
|
Object.defineProperty(exports, "midlineMiddleware", { enumerable: true, get: function () { return middleware_1.midlineMiddleware; } });
|
|
8
9
|
var errorHandler_1 = require("./errorHandler");
|
|
9
10
|
Object.defineProperty(exports, "midlineErrorHandler", { enumerable: true, get: function () { return errorHandler_1.midlineErrorHandler; } });
|
|
11
|
+
var proxy_1 = require("./proxy");
|
|
12
|
+
Object.defineProperty(exports, "createMidlineProxy", { enumerable: true, get: function () { return proxy_1.createMidlineProxy; } });
|
|
13
|
+
Object.defineProperty(exports, "startMidlineProxy", { enumerable: true, get: function () { return proxy_1.startMidlineProxy; } });
|
|
10
14
|
var breadcrumbs_1 = require("./breadcrumbs");
|
|
11
15
|
Object.defineProperty(exports, "addBreadcrumb", { enumerable: true, get: function () { return breadcrumbs_1.addBreadcrumb; } });
|
|
16
|
+
var context_1 = require("./context");
|
|
17
|
+
Object.defineProperty(exports, "getRequestContext", { enumerable: true, get: function () { return context_1.getRequestContext; } });
|
|
18
|
+
var config_1 = require("./config");
|
|
19
|
+
Object.defineProperty(exports, "ConfigError", { enumerable: true, get: function () { return config_1.ConfigError; } });
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,2 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "http";
|
|
2
|
+
import { MidlineAgent } from "./agent";
|
|
3
|
+
import { CaptureOptions } from "./types";
|
|
4
|
+
export interface MidlineMiddlewareOptions {
|
|
5
|
+
/** Defaults to the agent created by `MidlineAgent.init()`, looked up per request. */
|
|
6
|
+
agent?: MidlineAgent;
|
|
7
|
+
/** Overrides the agent's `capture` settings for requests seen by this middleware. */
|
|
8
|
+
capture?: CaptureOptions;
|
|
9
|
+
/** Return true to leave a request out, e.g. health checks. */
|
|
10
|
+
ignore?: (req: IncomingMessage) => boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Records every request that passes through. Mount it before your routes.
|
|
14
|
+
*
|
|
15
|
+
* It only listens: nothing here reads the request stream, delays `next()`, or
|
|
16
|
+
* changes the response. Recording happens on `finish`/`close`, after the response
|
|
17
|
+
* has been handed to the socket.
|
|
18
|
+
*/
|
|
19
|
+
export declare function midlineMiddleware(options?: MidlineMiddlewareOptions): (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void) => void;
|