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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # midline-agent
2
2
 
3
- **The Node.js SDK for Midline** — automatic request and error monitoring with security and threat detection.
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
- ## Features
12
+ ## How it fits together
13
13
 
14
- - Capture all HTTP requests & responses
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
- ## Getting Started
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
- ### 1. Sign up on Midline Cloud
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
- Visit [https://midline.com](https://midline.com) and create an account.
28
- After login, navigate to your **Dashboard API Keys** and generate a key for your application.
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
- ### 2. Install Package
46
+ ## Install
33
47
 
34
48
  ```bash
35
49
  npm install midline-agent
36
50
  ```
37
51
 
38
- Choose one of the guides below based on your framework.
52
+ Get a project API key from the Midline dashboard (**Project API Keys**).
39
53
 
40
54
  ---
41
55
 
42
- ## Framework Setup
56
+ ## Middleware mode
43
57
 
44
- ### Express.js
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.MIDLINE_KEY!,
65
+ apiKey: process.env.MIDLINE_API_KEY,
59
66
  serviceName: "checkout-api",
60
- environment: "production",
61
- maskFields: ["password", "token", "card_number", "cvv"]
67
+ environment: process.env.NODE_ENV,
62
68
  });
63
69
 
64
- // Parse incoming JSON
70
+ const app = express();
71
+ app.use(midlineMiddleware()); // before your routes
65
72
  app.use(express.json());
66
73
 
67
- // Add request capture middleware BEFORE your routes
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.listen(3000, () => console.log("Server running on port 3000"));
76
+ app.use(midlineErrorHandler()); // after your routes; passes the error on untouched
77
+ app.listen(3000);
82
78
  ```
83
79
 
84
- **Key points:**
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
- ### NestJS
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
- ```bash
95
- npm install midline-agent
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
- **Using with NestJS requires wrapping the Express middleware. Here's the setup:**
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 { NestFactory } from '@nestjs/core';
102
- import { AppModule } from './app.module';
103
- import { MidlineAgent, midlineMiddleware, midlineErrorHandler } from 'midline-agent';
110
+ import http from "http";
111
+ import { MidlineAgent, midlineMiddleware } from "midline-agent";
104
112
 
105
- async function bootstrap() {
106
- const app = await NestFactory.create(AppModule);
113
+ MidlineAgent.init({ apiKey: process.env.MIDLINE_API_KEY, serviceName: "raw-node" });
114
+ const midline = midlineMiddleware();
107
115
 
108
- // Initialize Midline Agent early in bootstrap
109
- MidlineAgent.init({
110
- apiKey: process.env.MIDLINE_KEY!,
111
- serviceName: 'nest-api',
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
- // Apply request capture middleware
117
- app.use(midlineMiddleware());
124
+ ---
118
125
 
119
- // Apply error handler after NestJS is configured
120
- app.use(midlineErrorHandler());
126
+ ## Proxy mode
121
127
 
122
- await app.listen(3000);
123
- }
128
+ Put the agent in front of any API — Node or not — without touching its code:
124
129
 
125
- bootstrap();
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
- **For module-level integration, create a middleware:**
135
+ The same binary, pointed elsewhere:
129
136
 
130
- ```ts
131
- // midline.middleware.ts
132
- import { Injectable, NestMiddleware } from '@nestjs/common';
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
- Then apply it in your module:
142
+ Or from code:
145
143
 
146
144
  ```ts
147
- import { Module, MiddlewareConsumer } from '@nestjs/common';
148
- import { MidlineMiddleware } from './midline.middleware';
149
-
150
- @Module({
151
- imports: [],
152
- controllers: [],
153
- providers: [],
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
- ### Plain Node.js / HTTP Server
173
+ ## TLS
167
174
 
168
- If you're not using Express or NestJS, you can integrate midline-agent by manually wrapping your request handlers:
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
- import http from 'http';
172
- import { MidlineAgent, midlineMiddleware, midlineErrorHandler } from 'midline-agent';
196
+ MidlineAgent.init({ apiKey, endpoint: "https://midline.internal", ca: fs.readFileSync("/etc/ssl/internal-ca.pem") });
197
+ ```
173
198
 
174
- MidlineAgent.init({
175
- apiKey: process.env.MIDLINE_KEY!,
176
- serviceName: 'raw-node-server',
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
- const server = http.createServer((req, res) => {
182
- // Create Express-like req/res wrapper for middleware
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
- server.listen(3000, () => console.log('Server listening on port 3000'));
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 Options
225
+ ## Configuration
204
226
 
205
227
  ```ts
206
228
  MidlineAgent.init({
207
- // Required
208
- apiKey: string; // Get from https://midline.com/dashboard/api-keys
209
- serviceName: string; // Name of your service (e.g., "checkout-api", "auth-service")
210
-
211
- // Optional
212
- maskFields?: string[]; // Field names to mask before sending (passwords, tokens, etc)
213
- environment?: string; // e.g., "production", "staging", "development"
214
- endpoint?: string; // Override default ingest URL (for self-hosted)
215
- host?: string; // Machine/pod name for identifying instances
216
- region?: string; // Deployment region (e.g., "us-east-1", "eu-west-1")
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
- ## Sensitive Field Masking
281
+ ---
223
282
 
224
- By default, `maskFields` removes the following fields before serialization:
225
- - `password`
226
- - `token`
227
- - `apiKey`
228
- - `secret`
229
- - `authorization`
283
+ ## Console output
230
284
 
231
- Add custom fields as needed:
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
- apiKey: process.env.MIDLINE_KEY!,
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
- **Important:** Masking happens in your process before the event is built. Masked values never leave your machine and are never stored on Midline servers.
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
- ## Error Handling
336
+ ## Delivery and failure behaviour
253
337
 
254
- The error handler middleware captures:
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
- **Example with error throwing:**
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
- app.post('/v1/charges', (req, res, next) => {
263
- try {
264
- if (!req.body.amount) {
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
- ## How Delivery Works
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
- - Events are queued in memory and flushed to Midline Cloud every 1.5 seconds
282
- - Request capturing is non-blocking — your response is sent before telemetry is reported
283
- - No network call can slow down your API responses
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
- ## Not using Node.js? Any other backend
375
+ ## Any other backend
293
376
 
294
- This agent does two things: it turns a request or an error into an event, and it POSTs that event to
295
- the Midline ingest API. Both are things your own service can do directly, in whatever language it
296
- is written in.
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
- The API key identifies your organisation and travels in the body; there is no separate auth header.
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 | The route that produced the event, e.g. `/v1/charges` |
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 | Which service reported it |
320
- | `environment` | no | e.g. `production` |
321
- | `timestamp` | no | ISO 8601; defaults to server time |
322
- | `payload` | no | Free-form context — error message, stack, anything you need |
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, before you build the payload. Nothing you do not send can
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 requests, threading
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
- event["apiKey"] = MIDLINE_KEY
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
- type Event struct {
377
- APIKey string `json:"apiKey"`
378
- EventType string `json:"eventType"`
379
- Route string `json:"route"`
380
- Method string `json:"method"`
381
- StatusCode int `json:"statusCode"`
382
- Severity string `json:"severity"`
383
- Service string `json:"service"`
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
- Send events from a goroutine, background thread or queue worker the same rule the Node agent
398
- follows. Telemetry should never be able to slow down or fail a response.
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
- **No code at all**
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
- If you would rather not touch the service, put it behind the Midline gateway instead: point traffic
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