midline-agent 0.1.9 → 0.3.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 +313 -249
- package/dist/agent.d.ts +118 -6
- package/dist/agent.js +746 -99
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +63 -0
- package/dist/config.d.ts +64 -0
- package/dist/config.js +231 -0
- package/dist/console.d.ts +46 -0
- package/dist/console.js +167 -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 +114 -5
- package/package.json +13 -8
- package/src/agent.ts +825 -97
- package/src/cli.ts +69 -0
- package/src/config.ts +234 -0
- package/src/console.ts +182 -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 +151 -31
- package/test/agent.test.js +353 -0
- package/test/console.test.js +182 -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/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# midline-agent
|
|
2
2
|
|
|
3
|
-
**The Node.js SDK for Midline** —
|
|
3
|
+
**The Node.js SDK for Midline** — request and error monitoring with security and threat detection.
|
|
4
4
|
|
|
5
5
|
Midline itself is not tied to Node. Every event lands on the same stream through a plain JSON
|
|
6
6
|
endpoint, so a Django, Rails, Laravel, Spring, Go or .NET service reports exactly what an Express
|
|
@@ -9,328 +9,409 @@ to [Any other backend](#any-other-backend).
|
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## How it fits together
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
- Capture all errors and exceptions
|
|
16
|
-
- Queue and batch events to Midline Cloud
|
|
17
|
-
- Mask sensitive fields (passwords, tokens, etc.)
|
|
18
|
-
- Non-blocking, minimal boilerplate
|
|
19
|
-
- Works with Express, and with anything that accepts Express-style middleware (NestJS, Fastify via `middie`)
|
|
14
|
+
There are two separate things, and the agent never confuses them:
|
|
20
15
|
|
|
21
|
-
|
|
16
|
+
| | What it is | Configured with |
|
|
17
|
+
| --- | --- | --- |
|
|
18
|
+
| **Midline server** (control plane) | Where events, API keys, logs, analytics and security data live. `https://api.usemidline.com` | `endpoint` / `MIDLINE_ENDPOINT`, `apiKey` / `MIDLINE_API_KEY`, `ca` / `MIDLINE_CUSTOM_CA` |
|
|
19
|
+
| **Destination API** (data plane) | The API being monitored — local, staging or production | `target` / `TARGET_API_URL`, `targetCa` / `TARGET_API_CA` (proxy mode only) |
|
|
22
20
|
|
|
23
|
-
|
|
21
|
+
Your traffic never goes through the Midline server. The agent runs next to your API, observes each
|
|
22
|
+
request, and ships a redacted event to Midline in the background:
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
```
|
|
25
|
+
┌──────────────── your infrastructure ────────────────┐
|
|
26
|
+
client ────────► │ midline-agent ──────────────► destination API │
|
|
27
|
+
│ (middleware in your app, or proxy in front of it) │
|
|
28
|
+
└───────┬─────────────────────────────────────────────┘
|
|
29
|
+
│ events, batched, redacted, async (HTTPS, verified)
|
|
30
|
+
▼
|
|
31
|
+
https://api.usemidline.com
|
|
32
|
+
```
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
34
|
+
That is deliberate. If Midline is down, slow or misconfigured, your API keeps serving and only
|
|
35
|
+
telemetry is buffered. A hosted gateway that sits in the request path can't do that, and it can't
|
|
36
|
+
reach `http://localhost:4000` on your laptop at all.
|
|
37
|
+
|
|
38
|
+
Two ways to run the agent:
|
|
39
|
+
|
|
40
|
+
- **Middleware** — inside an Express/NestJS/Node app. No extra hop.
|
|
41
|
+
- **Proxy** — a small reverse proxy in front of any HTTP API, in any language. `TARGET_API_URL`
|
|
42
|
+
decides where requests go.
|
|
29
43
|
|
|
30
44
|
---
|
|
31
45
|
|
|
32
|
-
|
|
46
|
+
## Install
|
|
33
47
|
|
|
34
48
|
```bash
|
|
35
49
|
npm install midline-agent
|
|
36
50
|
```
|
|
37
51
|
|
|
38
|
-
|
|
52
|
+
Get a project API key from the Midline dashboard (**Project → API Keys**).
|
|
39
53
|
|
|
40
54
|
---
|
|
41
55
|
|
|
42
|
-
##
|
|
56
|
+
## Middleware mode
|
|
43
57
|
|
|
44
|
-
### Express
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
npm install midline-agent express
|
|
48
|
-
```
|
|
58
|
+
### Express
|
|
49
59
|
|
|
50
60
|
```ts
|
|
51
61
|
import express from "express";
|
|
52
62
|
import { MidlineAgent, midlineMiddleware, midlineErrorHandler } from "midline-agent";
|
|
53
63
|
|
|
54
|
-
const app = express();
|
|
55
|
-
|
|
56
|
-
// Initialize Midline Agent (do this once, early in your app startup)
|
|
57
64
|
MidlineAgent.init({
|
|
58
|
-
apiKey: process.env.
|
|
65
|
+
apiKey: process.env.MIDLINE_API_KEY,
|
|
59
66
|
serviceName: "checkout-api",
|
|
60
|
-
environment:
|
|
61
|
-
maskFields: ["password", "token", "card_number", "cvv"]
|
|
67
|
+
environment: process.env.NODE_ENV,
|
|
62
68
|
});
|
|
63
69
|
|
|
64
|
-
|
|
70
|
+
const app = express();
|
|
71
|
+
app.use(midlineMiddleware()); // before your routes
|
|
65
72
|
app.use(express.json());
|
|
66
73
|
|
|
67
|
-
|
|
68
|
-
app.use(midlineMiddleware());
|
|
69
|
-
|
|
70
|
-
// Your routes here
|
|
71
|
-
app.get("/health", (req, res) => res.json({ status: "ok" }));
|
|
72
|
-
app.post("/v1/charges", async (req, res) => {
|
|
73
|
-
// Handle payment processing
|
|
74
|
-
res.json({ success: true });
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
// Add error handler middleware AFTER your routes
|
|
78
|
-
// This must be the last middleware registered
|
|
79
|
-
app.use(midlineErrorHandler());
|
|
74
|
+
app.post("/v1/charges", (req, res) => res.json({ ok: true }));
|
|
80
75
|
|
|
81
|
-
app.
|
|
76
|
+
app.use(midlineErrorHandler()); // after your routes; passes the error on untouched
|
|
77
|
+
app.listen(3000);
|
|
82
78
|
```
|
|
83
79
|
|
|
84
|
-
|
|
85
|
-
- `MidlineAgent.init()` must be called once during app startup
|
|
86
|
-
- `midlineMiddleware()` captures all requests and responses
|
|
87
|
-
- `midlineErrorHandler()` must be registered **after** all other middleware and routes
|
|
88
|
-
- Express only routes errors to handlers declared after the code that threw, so order matters
|
|
80
|
+
### NestJS
|
|
89
81
|
|
|
90
|
-
|
|
82
|
+
```ts
|
|
83
|
+
import { NestFactory } from "@nestjs/core";
|
|
84
|
+
import { MidlineAgent, midlineMiddleware } from "midline-agent";
|
|
85
|
+
import { AppModule } from "./app.module";
|
|
91
86
|
|
|
92
|
-
|
|
87
|
+
async function bootstrap() {
|
|
88
|
+
MidlineAgent.init({
|
|
89
|
+
apiKey: process.env.MIDLINE_API_KEY,
|
|
90
|
+
serviceName: "nest-api",
|
|
91
|
+
environment: process.env.NODE_ENV,
|
|
92
|
+
});
|
|
93
93
|
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
const app = await NestFactory.create(AppModule);
|
|
95
|
+
app.use(midlineMiddleware());
|
|
96
|
+
app.enableShutdownHooks();
|
|
97
|
+
await app.listen(3000);
|
|
98
|
+
}
|
|
99
|
+
bootstrap();
|
|
100
|
+
|
|
101
|
+
// On shutdown (e.g. in onApplicationShutdown): await MidlineAgent.shutdown();
|
|
96
102
|
```
|
|
97
103
|
|
|
98
|
-
|
|
104
|
+
Nest handles exceptions in its own filters, so they rarely reach Express error middleware. The
|
|
105
|
+
request middleware still records every 4xx/5xx response.
|
|
106
|
+
|
|
107
|
+
### Plain Node `http`
|
|
99
108
|
|
|
100
109
|
```ts
|
|
101
|
-
import
|
|
102
|
-
import {
|
|
103
|
-
import { MidlineAgent, midlineMiddleware, midlineErrorHandler } from 'midline-agent';
|
|
110
|
+
import http from "http";
|
|
111
|
+
import { MidlineAgent, midlineMiddleware } from "midline-agent";
|
|
104
112
|
|
|
105
|
-
|
|
106
|
-
|
|
113
|
+
MidlineAgent.init({ apiKey: process.env.MIDLINE_API_KEY, serviceName: "raw-node" });
|
|
114
|
+
const midline = midlineMiddleware();
|
|
107
115
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
environment: process.env.NODE_ENV || 'development',
|
|
113
|
-
maskFields: ['password', 'token', 'apiKey', 'secret']
|
|
116
|
+
http.createServer((req, res) => {
|
|
117
|
+
midline(req, res, () => {
|
|
118
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
119
|
+
res.end('{"ok":true}');
|
|
114
120
|
});
|
|
121
|
+
}).listen(3000);
|
|
122
|
+
```
|
|
115
123
|
|
|
116
|
-
|
|
117
|
-
app.use(midlineMiddleware());
|
|
124
|
+
---
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
app.use(midlineErrorHandler());
|
|
126
|
+
## Proxy mode
|
|
121
127
|
|
|
122
|
-
|
|
123
|
-
}
|
|
128
|
+
Put the agent in front of any API — Node or not — without touching its code:
|
|
124
129
|
|
|
125
|
-
|
|
130
|
+
```bash
|
|
131
|
+
MIDLINE_API_KEY=... TARGET_API_URL=http://localhost:4000 npx midline-agent proxy --port 8080
|
|
132
|
+
# clients now call http://localhost:8080 instead of :4000
|
|
126
133
|
```
|
|
127
134
|
|
|
128
|
-
|
|
135
|
+
The same binary, pointed elsewhere:
|
|
129
136
|
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
import { midlineMiddleware, midlineErrorHandler } from 'midline-agent';
|
|
134
|
-
import { Request, Response, NextFunction } from 'express';
|
|
135
|
-
|
|
136
|
-
@Injectable()
|
|
137
|
-
export class MidlineMiddleware implements NestMiddleware {
|
|
138
|
-
use(req: Request, res: Response, next: NextFunction) {
|
|
139
|
-
midlineMiddleware()(req, res, next);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
137
|
+
```bash
|
|
138
|
+
TARGET_API_URL=https://staging.example.com npx midline-agent proxy
|
|
139
|
+
TARGET_API_URL=https://api.example.com npx midline-agent proxy --host 0.0.0.0
|
|
142
140
|
```
|
|
143
141
|
|
|
144
|
-
|
|
142
|
+
Or from code:
|
|
145
143
|
|
|
146
144
|
```ts
|
|
147
|
-
import {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
})
|
|
155
|
-
export class AppModule {
|
|
156
|
-
configure(consumer: MiddlewareConsumer) {
|
|
157
|
-
consumer
|
|
158
|
-
.apply(MidlineMiddleware)
|
|
159
|
-
.forRoutes('*');
|
|
160
|
-
}
|
|
161
|
-
}
|
|
145
|
+
import { startMidlineProxy } from "midline-agent";
|
|
146
|
+
|
|
147
|
+
await startMidlineProxy({
|
|
148
|
+
target: process.env.TARGET_API_URL, // required; never defaulted
|
|
149
|
+
port: 8080,
|
|
150
|
+
midline: { apiKey: process.env.MIDLINE_API_KEY, serviceName: "orders-gateway" },
|
|
151
|
+
});
|
|
162
152
|
```
|
|
163
153
|
|
|
154
|
+
`createMidlineProxy(options)` returns a plain `(req, res)` handler if you want your own server.
|
|
155
|
+
|
|
156
|
+
What the proxy does per request:
|
|
157
|
+
|
|
158
|
+
- Forwards method, path, query, headers and body to the target, streaming both ways
|
|
159
|
+
- Strips hop-by-hop headers; sets `Host` to the target (unless `preserveHost`), adds `X-Forwarded-*`,
|
|
160
|
+
`X-Request-Id` and `X-Correlation-Id`
|
|
161
|
+
- Only accepts origin-form request lines (`GET /path`). It can't be used as an open forward proxy.
|
|
162
|
+
- Answers `502` if the destination is unreachable, `504` if it's too slow (`timeoutMs`, default
|
|
163
|
+
30s), `413` if the body exceeds `maxRequestBodyBytes` (default 10 MiB). Error bodies carry a
|
|
164
|
+
`requestId` and no internal detail.
|
|
165
|
+
- Retries (`retries`, default 0) only `GET`/`HEAD`/`OPTIONS` without a body, and only when the
|
|
166
|
+
request never reached the destination. A write is never replayed.
|
|
167
|
+
- Records the exchange — including destination failures as `infrastructure` errors — to Midline
|
|
168
|
+
|
|
169
|
+
WebSocket upgrades are not proxied.
|
|
170
|
+
|
|
164
171
|
---
|
|
165
172
|
|
|
166
|
-
|
|
173
|
+
## TLS
|
|
167
174
|
|
|
168
|
-
|
|
175
|
+
**Certificate verification is always on**, for both the Midline server and the destination.
|
|
176
|
+
There is no option to turn it off, and the agent never sets `rejectUnauthorized: false`.
|
|
177
|
+
|
|
178
|
+
### The Midline server
|
|
179
|
+
|
|
180
|
+
`https://api.usemidline.com` has a publicly trusted certificate, so nothing needs configuring. If
|
|
181
|
+
verification ever fails, you'll see one line like:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
midline: https://api.usemidline.com presented a self-signed certificate (DEPTH_ZERO_SELF_SIGNED_CERT),
|
|
185
|
+
so the connection was refused. Certificate verification stays on. ...
|
|
186
|
+
Delivery resumes on its own once a trusted certificate is served. 12 event(s) buffered; your application is unaffected.
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
That is a server-side problem, not something to work around in your app. Events stay buffered and
|
|
190
|
+
delivery resumes on its own once the server is fixed — no restart needed.
|
|
191
|
+
|
|
192
|
+
For a **self-hosted** Midline server behind a private CA, trust that CA for the Midline connection
|
|
193
|
+
only:
|
|
169
194
|
|
|
170
195
|
```ts
|
|
171
|
-
|
|
172
|
-
|
|
196
|
+
MidlineAgent.init({ apiKey, endpoint: "https://midline.internal", ca: fs.readFileSync("/etc/ssl/internal-ca.pem") });
|
|
197
|
+
```
|
|
173
198
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
environment: 'production',
|
|
178
|
-
maskFields: ['password', 'token']
|
|
179
|
-
});
|
|
199
|
+
```bash
|
|
200
|
+
MIDLINE_CUSTOM_CA=/etc/ssl/internal-ca.pem # PEM text also works
|
|
201
|
+
```
|
|
180
202
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
const next = (err?: any) => {
|
|
184
|
-
if (err) throw err;
|
|
185
|
-
// Handle your route
|
|
186
|
-
if (req.url === '/health') {
|
|
187
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
188
|
-
res.end(JSON.stringify({ status: 'ok' }));
|
|
189
|
-
} else {
|
|
190
|
-
res.writeHead(404);
|
|
191
|
-
res.end('Not Found');
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
midlineMiddleware()(req, res, next);
|
|
196
|
-
});
|
|
203
|
+
The CA is **added** to Node's default trust store, not substituted for it. Hostname checks still
|
|
204
|
+
apply.
|
|
197
205
|
|
|
198
|
-
|
|
206
|
+
`endpoint` must be `https://`. Plain `http://` is only accepted for `localhost`/`127.0.0.1` (running
|
|
207
|
+
a Midline server on your own machine), so the API key never crosses a network in cleartext.
|
|
208
|
+
|
|
209
|
+
### The destination (proxy mode)
|
|
210
|
+
|
|
211
|
+
A local destination is fine over plain HTTP: `TARGET_API_URL=http://localhost:4000`. That doesn't
|
|
212
|
+
relax anything about the Midline connection.
|
|
213
|
+
|
|
214
|
+
For an HTTPS destination signed by a private/internal CA:
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
TARGET_API_URL=https://orders.internal TARGET_API_CA=/etc/ssl/internal-ca.pem npx midline-agent proxy
|
|
199
218
|
```
|
|
200
219
|
|
|
220
|
+
`TARGET_API_CA` and `MIDLINE_CUSTOM_CA` are independent. Trusting a CA for one never trusts it for
|
|
221
|
+
the other.
|
|
222
|
+
|
|
201
223
|
---
|
|
202
224
|
|
|
203
|
-
## Configuration
|
|
225
|
+
## Configuration
|
|
204
226
|
|
|
205
227
|
```ts
|
|
206
228
|
MidlineAgent.init({
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
//
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
229
|
+
apiKey: string, // MIDLINE_API_KEY
|
|
230
|
+
serviceName?: string, // MIDLINE_SERVICE_NAME
|
|
231
|
+
endpoint?: string, // MIDLINE_ENDPOINT — default https://api.usemidline.com (base or full ingest URL)
|
|
232
|
+
ca?: string | Buffer | Array, // MIDLINE_CUSTOM_CA — extra CA for the Midline server only
|
|
233
|
+
environment?: string, // MIDLINE_ENVIRONMENT
|
|
234
|
+
release?: string, // MIDLINE_RELEASE
|
|
235
|
+
host?: string,
|
|
236
|
+
region?: string,
|
|
237
|
+
enabled?: boolean, // MIDLINE_ENABLED=false keeps the agent inert
|
|
238
|
+
|
|
239
|
+
// What to capture beyond method, path, status and timing. All off by default.
|
|
240
|
+
capture?: {
|
|
241
|
+
headers?: boolean,
|
|
242
|
+
query?: boolean,
|
|
243
|
+
requestBody?: boolean, // whatever your body parser produced (req.body)
|
|
244
|
+
responseBody?: boolean,
|
|
245
|
+
maxBodyBytes?: number, // default 4096
|
|
246
|
+
},
|
|
247
|
+
redactFields?: string[], // added to the built-in list (maskFields still works)
|
|
248
|
+
redactHeaders?: string[],
|
|
249
|
+
captureConsole?: boolean, // MIDLINE_CAPTURE_CONSOLE — also send what the process prints (off by default)
|
|
250
|
+
|
|
251
|
+
// Delivery
|
|
252
|
+
flushIntervalMs?: number, // default 1500
|
|
253
|
+
timeoutMs?: number, // whole request deadline, default 10000
|
|
254
|
+
connectTimeoutMs?: number, // TCP + TLS handshake, default 5000
|
|
255
|
+
maxBatchSize?: number, // default 100
|
|
256
|
+
maxQueueSize?: number, // default 1000; oldest dropped past this
|
|
257
|
+
maxEventBytes?: number, // default 65536; bodies dropped first
|
|
258
|
+
maxRetryDelayMs?: number, // backoff cap, default 300000
|
|
259
|
+
|
|
260
|
+
onError?: (message: string) => void, // route diagnostics to your logger
|
|
261
|
+
debug?: boolean, // MIDLINE_DEBUG — log every diagnostic, not one per fault
|
|
217
262
|
});
|
|
218
263
|
```
|
|
219
264
|
|
|
220
|
-
|
|
265
|
+
### Environment
|
|
266
|
+
|
|
267
|
+
```env
|
|
268
|
+
MIDLINE_API_KEY=...
|
|
269
|
+
MIDLINE_ENDPOINT=https://api.usemidline.com
|
|
270
|
+
# MIDLINE_CAPTURE_CONSOLE=true
|
|
271
|
+
|
|
272
|
+
# proxy mode
|
|
273
|
+
TARGET_API_URL=http://localhost:4000
|
|
274
|
+
# TARGET_API_CA=/etc/ssl/internal-ca.pem
|
|
275
|
+
# TARGET_API_TIMEOUT_MS=30000
|
|
276
|
+
# TARGET_API_RETRIES=0
|
|
277
|
+
# MIDLINE_PROXY_PORT=8080
|
|
278
|
+
# MIDLINE_PROXY_HOST=127.0.0.1
|
|
279
|
+
```
|
|
221
280
|
|
|
222
|
-
|
|
281
|
+
---
|
|
223
282
|
|
|
224
|
-
|
|
225
|
-
- `password`
|
|
226
|
-
- `token`
|
|
227
|
-
- `apiKey`
|
|
228
|
-
- `secret`
|
|
229
|
-
- `authorization`
|
|
283
|
+
## Console output
|
|
230
284
|
|
|
231
|
-
|
|
285
|
+
Set `captureConsole: true` (or `MIDLINE_CAPTURE_CONSOLE=true`) and whatever the process prints
|
|
286
|
+
shows up on the dashboard's Logs page as `console` events, one per line, next to your requests. That
|
|
287
|
+
covers `console.log`, Nest's logger, pino, winston: anything written to stdout or stderr.
|
|
232
288
|
|
|
233
289
|
```ts
|
|
234
|
-
MidlineAgent.init({
|
|
235
|
-
|
|
236
|
-
serviceName: 'checkout-api',
|
|
237
|
-
maskFields: [
|
|
238
|
-
'password',
|
|
239
|
-
'card_number',
|
|
240
|
-
'cvv',
|
|
241
|
-
'ssn',
|
|
242
|
-
'oauth_token',
|
|
243
|
-
'jwt'
|
|
244
|
-
]
|
|
245
|
-
});
|
|
290
|
+
MidlineAgent.init({ apiKey: process.env.MIDLINE_API_KEY, captureConsole: true });
|
|
291
|
+
const app = await NestFactory.create(AppModule); // startup lines are captured from here on
|
|
246
292
|
```
|
|
247
293
|
|
|
248
|
-
|
|
294
|
+
- Initialise the agent before creating the app. Anything printed before `init()` isn't captured.
|
|
295
|
+
- Your terminal output doesn't change. The agent reads each write after it has happened.
|
|
296
|
+
- Colour codes are stripped, each line is capped at 4096 characters, and lines are redacted like any
|
|
297
|
+
other text.
|
|
298
|
+
- Severity comes from the line: `ERROR` or `FATAL` is high, `WARN` is medium, other stderr output is
|
|
299
|
+
medium, and everything else is low.
|
|
300
|
+
- Up to 100 lines a second are sent, with room for a 1,000-line burst such as Nest mapping its routes
|
|
301
|
+
at startup. Lines past that still print but aren't sent.
|
|
302
|
+
- Printed lines don't count towards request totals, error rates or latency.
|
|
303
|
+
- It needs a Midline server that knows the `console` event type. An older server refuses the first
|
|
304
|
+
line; the agent then turns console capture off, says so once, and keeps sending requests and errors.
|
|
305
|
+
- The agent's own diagnostics are never captured.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Sensitive data
|
|
310
|
+
|
|
311
|
+
Redaction happens **in your process, before an event is queued**. What's masked never reaches a
|
|
312
|
+
socket, a log line or the Midline server. The server redacts again on ingest as a second line of
|
|
313
|
+
defence.
|
|
314
|
+
|
|
315
|
+
- **Headers, always masked when captured:** `Authorization`, `Proxy-Authorization`, `Cookie`,
|
|
316
|
+
`Set-Cookie`, `X-API-Key`, `API-Key`, `X-Auth-Token`, `X-Access-Token`, `X-Refresh-Token`,
|
|
317
|
+
`X-CSRF-Token`, `X-XSRF-Token`, `X-Amz-Security-Token`, plus anything matching the field rules below.
|
|
318
|
+
- **Fields, at any depth, in bodies, queries and payloads.** Matched case- and
|
|
319
|
+
punctuation-insensitively, so `api_key`, `apiKey` and `X-API-Key` are the same key. Anything
|
|
320
|
+
containing `password`, `passwd`, `passphrase`, `secret`, `token`, `apikey`, `accesskey`,
|
|
321
|
+
`privatekey`, `authorization`, `cookie`, `session`, `credential`, `csrf`, `xsrf`, `signature`,
|
|
322
|
+
`creditcard`, `cardnumber`, `cvv`, `cvc`, `ssn`, `socialsecurity`; and the exact keys `auth`,
|
|
323
|
+
`pwd`, `pin`, `otp`, `sid`, `jwt`, `bearer`.
|
|
324
|
+
- **Values inside any text** — error messages, stack traces, routes, console lines: bearer/basic credentials,
|
|
325
|
+
JWTs, Midline keys (`ak_…`), Stripe and AWS key formats, PEM private keys, `user:pass@` in URLs,
|
|
326
|
+
and `key=value` / `"key": value` pairs whose key is sensitive.
|
|
327
|
+
- Query strings are never part of `route`.
|
|
328
|
+
- Bodies are only captured if you turn them on. They're capped at `maxBodyBytes`, and compressed or
|
|
329
|
+
binary bodies are skipped.
|
|
330
|
+
|
|
331
|
+
The lists err on the side of masking (`tokenCount` is masked too). Extend them with `redactFields`
|
|
332
|
+
and `redactHeaders`.
|
|
249
333
|
|
|
250
334
|
---
|
|
251
335
|
|
|
252
|
-
##
|
|
336
|
+
## Delivery and failure behaviour
|
|
253
337
|
|
|
254
|
-
The
|
|
255
|
-
- Unhandled errors thrown by your route handlers
|
|
256
|
-
- HTTP error responses
|
|
257
|
-
- Validation errors
|
|
338
|
+
The agent's contract is that the Midline server can never break your application.
|
|
258
339
|
|
|
259
|
-
|
|
340
|
+
- Recording happens after the response is handed to the socket; nothing waits on the network
|
|
341
|
+
- Events are batched (`POST /api/api-monitor/events/batch`) with the key in the `X-API-Key` header
|
|
342
|
+
- **Unreachable, DNS failure, timeout, TLS failure, 5xx, 408, 429:** events stay buffered.
|
|
343
|
+
Retries use exponential backoff with jitter, capped by `maxRetryDelayMs`, and honour `Retry-After`.
|
|
344
|
+
- **401/403:** the key is wrong. The agent logs once and turns itself off — retrying can't fix it.
|
|
345
|
+
- **413:** the batch is split and retried; a single event that's still too large is dropped
|
|
346
|
+
- **Other 4xx:** the batch is re-sent one event at a time, so a malformed event only costs itself
|
|
347
|
+
- **Redirects** are never followed, because that would hand the API key to wherever they point
|
|
348
|
+
- One log line per distinct fault, and one when delivery recovers
|
|
349
|
+
- The buffer is bounded (`maxQueueSize`, 16 MB total); past that, the oldest events go first
|
|
350
|
+
- The flush timer and background sockets are `unref`'d, so telemetry never keeps your process alive
|
|
351
|
+
|
|
352
|
+
Events still in memory are lost if the process crashes. That's fine for operational telemetry, but
|
|
353
|
+
not for billing or audit. On graceful shutdown:
|
|
260
354
|
|
|
261
355
|
```ts
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
throw new Error('Amount is required');
|
|
266
|
-
}
|
|
267
|
-
// Process payment...
|
|
268
|
-
} catch (error) {
|
|
269
|
-
next(error); // Pass to Midline error handler
|
|
270
|
-
}
|
|
356
|
+
process.on("SIGTERM", async () => {
|
|
357
|
+
await MidlineAgent.shutdown(5000); // flush with a deadline, then close
|
|
358
|
+
process.exit(0);
|
|
271
359
|
});
|
|
272
|
-
|
|
273
|
-
// The error handler will capture and report this
|
|
274
|
-
app.use(midlineErrorHandler());
|
|
275
360
|
```
|
|
276
361
|
|
|
277
|
-
|
|
362
|
+
### Request and correlation IDs
|
|
278
363
|
|
|
279
|
-
|
|
364
|
+
Every request gets a `requestId`: an incoming `X-Request-Id` if it's sane, otherwise a UUID. It also
|
|
365
|
+
gets a `correlationId`: `X-Correlation-Id` if present, otherwise the request ID. A W3C
|
|
366
|
+
`traceparent` header becomes `traceId`/`spanId`. Read them in your own code to stamp logs:
|
|
280
367
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
**Important for production:**
|
|
286
|
-
- In-memory events are lost if your process crashes before the flush interval
|
|
287
|
-
- This is acceptable for monitoring telemetry (good for operations, not billing/audit)
|
|
288
|
-
- For high-traffic services, consider using the batch endpoint directly for reliability
|
|
368
|
+
```ts
|
|
369
|
+
import { getRequestContext } from "midline-agent";
|
|
370
|
+
app.get("/x", (req, res) => { logger.info({ requestId: getRequestContext(req)?.requestId }); });
|
|
371
|
+
```
|
|
289
372
|
|
|
290
373
|
---
|
|
291
374
|
|
|
292
|
-
##
|
|
375
|
+
## Any other backend
|
|
293
376
|
|
|
294
|
-
|
|
295
|
-
the Midline ingest API.
|
|
296
|
-
|
|
377
|
+
The agent does two things: it turns a request or an error into an event, and it POSTs that event to
|
|
378
|
+
the Midline ingest API. Your own service can do both directly, in whatever language it's written in
|
|
379
|
+
— or run `midline-agent proxy` in front of it.
|
|
297
380
|
|
|
298
381
|
**Endpoints**
|
|
299
382
|
|
|
300
383
|
| Endpoint | Use |
|
|
301
384
|
| --- | --- |
|
|
302
385
|
| `POST https://api.usemidline.com/api/api-monitor/events` | One event |
|
|
303
|
-
| `POST https://api.usemidline.com/api/api-monitor/events/batch` | Many events — `{ "events": [ … ] }
|
|
386
|
+
| `POST https://api.usemidline.com/api/api-monitor/events/batch` | Many events — `{ "events": [ … ] }`, up to 500 |
|
|
304
387
|
|
|
305
|
-
|
|
388
|
+
Authenticate with the `X-API-Key` header. An `apiKey` field in the body is still accepted.
|
|
306
389
|
|
|
307
390
|
**Event shape**
|
|
308
391
|
|
|
309
392
|
| Field | Required | Notes |
|
|
310
393
|
| --- | --- | --- |
|
|
311
|
-
| `apiKey` | yes | From Dashboard → API Keys |
|
|
312
394
|
| `eventType` | yes | `request` · `error` · `security` · `performance` · `custom` |
|
|
313
|
-
| `route` | yes |
|
|
395
|
+
| `route` | yes | Path only, e.g. `/v1/charges` |
|
|
314
396
|
| `method` | no | HTTP method |
|
|
315
397
|
| `statusCode` | no | 100–599 |
|
|
316
398
|
| `responseTime` | no | Milliseconds |
|
|
317
399
|
| `severity` | no | `low` · `medium` · `high` · `critical` |
|
|
318
400
|
| `category` | no | `application` · `infrastructure` · `security` · `performance` · `business` |
|
|
319
|
-
| `service` | no |
|
|
320
|
-
| `
|
|
321
|
-
| `
|
|
322
|
-
| `payload` | no | Free-form context — error message, stack,
|
|
401
|
+
| `service` / `environment` / `release` | no | Where it came from |
|
|
402
|
+
| `timestamp` | no | ISO 8601; clamped to server time if implausibly far off |
|
|
403
|
+
| `requestId` / `correlationId` / `traceId` | no | For joining events across services |
|
|
404
|
+
| `payload` | no | Free-form context — error message, stack, captured request/response |
|
|
405
|
+
| `metadata` | no | Free-form, e.g. SDK name and version |
|
|
323
406
|
|
|
324
|
-
Mask sensitive fields in your own process
|
|
407
|
+
Mask sensitive fields in your own process before you build the payload. Nothing you don't send can
|
|
325
408
|
be stored.
|
|
326
409
|
|
|
327
|
-
**curl**
|
|
328
|
-
|
|
329
410
|
```bash
|
|
330
411
|
curl -X POST https://api.usemidline.com/api/api-monitor/events \
|
|
331
412
|
-H "Content-Type: application/json" \
|
|
413
|
+
-H "X-API-Key: $MIDLINE_API_KEY" \
|
|
332
414
|
-d '{
|
|
333
|
-
"apiKey": "YOUR_API_KEY",
|
|
334
415
|
"eventType": "error",
|
|
335
416
|
"service": "checkout-api",
|
|
336
417
|
"route": "/v1/charges",
|
|
@@ -343,67 +424,50 @@ curl -X POST https://api.usemidline.com/api/api-monitor/events \
|
|
|
343
424
|
}'
|
|
344
425
|
```
|
|
345
426
|
|
|
346
|
-
**Python (Django / Flask / FastAPI)**
|
|
347
|
-
|
|
348
427
|
```python
|
|
349
|
-
import
|
|
428
|
+
import os, threading, requests
|
|
350
429
|
|
|
351
430
|
INGEST = "https://api.usemidline.com/api/api-monitor/events"
|
|
352
431
|
|
|
353
432
|
def report(event: dict) -> None:
|
|
354
|
-
|
|
355
|
-
event.setdefault("service", "checkout-api")
|
|
356
|
-
# off the request path - never make your user wait on telemetry
|
|
433
|
+
# off the request path - never make a user wait on telemetry
|
|
357
434
|
threading.Thread(
|
|
358
|
-
target=lambda: requests.post(INGEST, json=event, timeout=3),
|
|
435
|
+
target=lambda: requests.post(INGEST, json=event, headers={"X-API-Key": os.environ["MIDLINE_API_KEY"]}, timeout=3),
|
|
359
436
|
daemon=True,
|
|
360
437
|
).start()
|
|
361
|
-
|
|
362
|
-
report({
|
|
363
|
-
"eventType": "error",
|
|
364
|
-
"route": "/v1/charges",
|
|
365
|
-
"method": "POST",
|
|
366
|
-
"statusCode": 500,
|
|
367
|
-
"severity": "critical",
|
|
368
|
-
"category": "application",
|
|
369
|
-
"payload": {"error": str(exc)},
|
|
370
|
-
})
|
|
371
438
|
```
|
|
372
439
|
|
|
373
|
-
**Go**
|
|
374
|
-
|
|
375
440
|
```go
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
func Report(e Event) {
|
|
387
|
-
e.APIKey = os.Getenv("MIDLINE_KEY")
|
|
388
|
-
body, _ := json.Marshal(e)
|
|
389
|
-
go http.Post(
|
|
390
|
-
"https://api.usemidline.com/api/api-monitor/events",
|
|
391
|
-
"application/json",
|
|
392
|
-
bytes.NewReader(body),
|
|
393
|
-
) // fire and forget
|
|
441
|
+
func Report(body []byte) {
|
|
442
|
+
go func() {
|
|
443
|
+
req, _ := http.NewRequest("POST", "https://api.usemidline.com/api/api-monitor/events", bytes.NewReader(body))
|
|
444
|
+
req.Header.Set("Content-Type", "application/json")
|
|
445
|
+
req.Header.Set("X-API-Key", os.Getenv("MIDLINE_API_KEY"))
|
|
446
|
+
client := &http.Client{Timeout: 3 * time.Second}
|
|
447
|
+
if resp, err := client.Do(req); err == nil {
|
|
448
|
+
resp.Body.Close()
|
|
449
|
+
}
|
|
450
|
+
}()
|
|
394
451
|
}
|
|
395
452
|
```
|
|
396
453
|
|
|
397
|
-
|
|
398
|
-
|
|
454
|
+
Same rule everywhere: send events from a goroutine, a background thread or a queue worker. Keep TLS
|
|
455
|
+
verification on. Telemetry should never be able to slow down or fail a response.
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Upgrading from 0.1.x
|
|
399
460
|
|
|
400
|
-
|
|
461
|
+
- `insecureTLS` / `MIDLINE_INSECURE_TLS` are gone (they were never published). Fix the server's
|
|
462
|
+
certificate, or pass the private CA with `ca` / `MIDLINE_CUSTOM_CA`.
|
|
463
|
+
- `endpoint` can now be just `https://api.usemidline.com`; full ingest URLs still work.
|
|
464
|
+
- `maskFields` used to be accepted and ignored. It's now honoured, as an alias for `redactFields`.
|
|
465
|
+
- `serviceName` is optional; `MIDLINE_API_KEY` / `MIDLINE_ENDPOINT` are read if not passed.
|
|
466
|
+
- `cross-fetch` is no longer a dependency; Node 18+ is required.
|
|
467
|
+
- New: `MidlineAgent.shutdown(timeoutMs)` flushes with a deadline and stops the agent.
|
|
468
|
+
- The agent sends the key both in the `X-API-Key` header and in each event body. That keeps it
|
|
469
|
+
compatible with Midline servers that predate header auth.
|
|
401
470
|
|
|
402
|
-
|
|
403
|
-
at your fence URL and every request is authenticated, scanned, logged and forwarded upstream
|
|
404
|
-
unchanged. That path is language-agnostic by construction — the gateway never knows or cares what
|
|
405
|
-
your backend is written in.
|
|
471
|
+
## License
|
|
406
472
|
|
|
407
|
-
### License
|
|
408
473
|
MIT
|
|
409
|
-
# Midline-agent
|