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.
@@ -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
- function midlineErrorHandler() {
7
- return (err, req, res, next) => {
8
- agent_1.MidlineAgent.addEvent({
9
- type: "error",
10
- timestamp: new Date().toISOString(),
11
- path: req.path,
12
- method: req.method,
13
- statusCode: err.status || 500,
14
- message: err.message,
15
- stack: err.stack,
16
- breadcrumbs: (0, breadcrumbs_1.getBreadcrumbs)(),
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 type { Breadcrumb, MidlineConfig, MidlineEvent } from "./types";
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; } });
@@ -1,2 +1,19 @@
1
- import { Request, Response, NextFunction } from "express";
2
- export declare function midlineMiddleware(): (req: Request, res: Response, next: NextFunction) => void;
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;
@@ -3,52 +3,103 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.midlineMiddleware = midlineMiddleware;
4
4
  const agent_1 = require("./agent");
5
5
  const breadcrumbs_1 = require("./breadcrumbs");
6
- function midlineMiddleware() {
7
- return (req, res, next) => {
8
- const start = Date.now();
9
- let logged = false;
10
- const logRequest = () => {
11
- if (logged)
12
- return;
13
- logged = true;
14
- const duration = Date.now() - start;
15
- const statusCode = res.statusCode || 200;
6
+ const config_1 = require("./config");
7
+ const context_1 = require("./context");
8
+ const tap_1 = require("./tap");
9
+ /**
10
+ * Records every request that passes through. Mount it before your routes.
11
+ *
12
+ * It only listens: nothing here reads the request stream, delays `next()`, or
13
+ * changes the response. Recording happens on `finish`/`close`, after the response
14
+ * has been handed to the socket.
15
+ */
16
+ function midlineMiddleware(options = {}) {
17
+ const captureOverride = options.capture ? (0, config_1.resolveCapture)(options.capture) : undefined;
18
+ return function midline(req, res, next) {
19
+ try {
20
+ observe(req, res, options, captureOverride);
21
+ }
22
+ catch {
23
+ // Monitoring must never be the reason a request fails.
24
+ }
25
+ next();
26
+ };
27
+ }
28
+ function observe(req, res, options, captureOverride) {
29
+ const agent = options.agent ?? agent_1.MidlineAgent.current;
30
+ if (!agent || !agent.active || options.ignore?.(req)) {
31
+ return;
32
+ }
33
+ const capture = captureOverride ?? agent.capture;
34
+ const context = (0, context_1.createRequestContext)(req.headers);
35
+ (0, context_1.setRequestContext)(req, context);
36
+ const started = process.hrtime.bigint();
37
+ // originalUrl survives router rewrites of req.url; read it now, not at finish.
38
+ const url = req.originalUrl ?? req.url ?? "/";
39
+ const responseTap = capture.responseBody ? tapResponse(res, capture.maxBodyBytes) : undefined;
40
+ let recorded = false;
41
+ const record = () => {
42
+ if (recorded)
43
+ return;
44
+ recorded = true;
45
+ try {
46
+ const aborted = !res.writableFinished;
47
+ const statusCode = aborted && !res.headersSent ? 499 : res.statusCode;
48
+ const routePath = req.route?.path;
49
+ const encoding = (0, tap_1.headerValue)(res.getHeader("content-encoding"));
16
50
  (0, breadcrumbs_1.addBreadcrumb)({
17
51
  type: "http",
18
- message: `${req.method} ${req.path} -> ${statusCode}`,
52
+ message: `${req.method} ${url.split("?")[0]} -> ${statusCode}`,
19
53
  timestamp: new Date().toISOString(),
20
54
  });
21
- agent_1.MidlineAgent.addEvent({
22
- type: "request",
23
- timestamp: new Date().toISOString(),
24
- path: req.path,
55
+ agent.recordHttp({
56
+ integration: req.originalUrl !== undefined ? "express" : "node-http",
57
+ context,
25
58
  method: req.method,
26
- statusCode: statusCode,
27
- duration,
28
- ip: req.ip || req.socket.remoteAddress,
29
- userAgent: req.headers["user-agent"]
59
+ url,
60
+ statusCode,
61
+ durationMs: Number(process.hrtime.bigint() - started) / 1e6,
62
+ ip: req.ip ?? req.socket?.remoteAddress,
63
+ userAgent: (0, tap_1.headerValue)(req.headers["user-agent"]),
64
+ routeTemplate: typeof routePath === "string" ? `${req.baseUrl ?? ""}${routePath}` : undefined,
65
+ aborted,
66
+ capture,
67
+ request: {
68
+ headers: capture.headers ? req.headers : undefined,
69
+ contentType: (0, tap_1.headerValue)(req.headers["content-type"]),
70
+ // Whatever a body parser already produced. The raw stream is never read here.
71
+ body: capture.requestBody ? req.body : undefined,
72
+ },
73
+ response: {
74
+ headers: capture.headers ? res.getHeaders() : undefined,
75
+ contentType: (0, tap_1.headerValue)(res.getHeader("content-type")),
76
+ body: responseTap && !(0, tap_1.isCompressed)(encoding) ? responseTap.body : undefined,
77
+ bodyBytes: responseTap?.bytes,
78
+ truncated: responseTap?.truncated,
79
+ },
30
80
  });
31
- };
32
- // Override multiple response methods to ensure we catch all responses
33
- const originalSend = res.send.bind(res);
34
- const originalJson = res.json.bind(res);
35
- const originalEnd = res.end.bind(res);
36
- res.send = function (body) {
37
- logRequest();
38
- return originalSend(body);
39
- };
40
- res.json = function (body) {
41
- logRequest();
42
- return originalJson(body);
43
- };
44
- res.end = function (chunk, encoding) {
45
- logRequest();
46
- return originalEnd(chunk, encoding);
47
- };
48
- // Also listen for the 'finish' event as a fallback
49
- res.once('finish', () => {
50
- logRequest();
51
- });
52
- next();
81
+ }
82
+ catch {
83
+ // See above.
84
+ }
85
+ };
86
+ res.once("finish", record);
87
+ res.once("close", record);
88
+ }
89
+ /** Wraps write/end with every argument passed through untouched, callbacks included. */
90
+ function tapResponse(res, limit) {
91
+ const tap = new tap_1.BodyTap(limit);
92
+ const write = res.write;
93
+ const end = res.end;
94
+ res.write = function (...args) {
95
+ tap.push(args[0], typeof args[1] === "string" ? args[1] : undefined);
96
+ return write.apply(this, args);
97
+ };
98
+ res.end = function (...args) {
99
+ if (typeof args[0] !== "function") {
100
+ tap.push(args[0], typeof args[1] === "string" ? args[1] : undefined);
101
+ }
102
+ return end.apply(this, args);
53
103
  };
104
+ return tap;
54
105
  }
@@ -0,0 +1,70 @@
1
+ import * as http from "http";
2
+ import { MidlineAgent } from "./agent";
3
+ import { CaInput, CaptureOptions, MidlineConfig } from "./types";
4
+ export interface MidlineProxyOptions {
5
+ /**
6
+ * The destination API — the thing being monitored. `http://localhost:4000`,
7
+ * `https://staging.example.com`, `https://api.example.com`. Only its origin and
8
+ * path prefix are used; clients can never steer a request to another host.
9
+ * Env fallback: `TARGET_API_URL`.
10
+ */
11
+ target?: string;
12
+ /**
13
+ * Extra CAs to trust for the destination only (a private/internal CA). Added to
14
+ * the default trust store; verification is never switched off. Independent of
15
+ * the Midline server's `ca`. Env fallback: `TARGET_API_CA` (PEM or file path).
16
+ */
17
+ targetCa?: CaInput;
18
+ /** Time allowed for the destination to start responding, in ms. Default 30000. Env: `TARGET_API_TIMEOUT_MS`. */
19
+ timeoutMs?: number;
20
+ /** Time allowed to connect to the destination, in ms. Default 5000. */
21
+ connectTimeoutMs?: number;
22
+ /**
23
+ * Retries for requests that never reached the destination (refused, reset, DNS,
24
+ * connect timeout). Only GET, HEAD and OPTIONS without a body are retried — a
25
+ * write might have been applied. Default 0. Env: `TARGET_API_RETRIES`.
26
+ */
27
+ retries?: number;
28
+ /** Requests with a larger body get 413. Default 10 MiB. */
29
+ maxRequestBodyBytes?: number;
30
+ /** Send the client's Host header upstream instead of the destination's. Default false. */
31
+ preserveHost?: boolean;
32
+ /**
33
+ * Trust `X-Forwarded-*` from the client, e.g. when this proxy itself sits behind a
34
+ * load balancer. Default false, so clients cannot spoof their address.
35
+ */
36
+ trustForwardedHeaders?: boolean;
37
+ /** Overrides the agent's capture settings for proxied traffic. */
38
+ capture?: CaptureOptions;
39
+ /** Where events go. Defaults to `MidlineAgent.current`. Traffic is forwarded either way. */
40
+ agent?: MidlineAgent;
41
+ /** Receives proxy diagnostics. Defaults to console.warn. */
42
+ onError?: (message: string) => void;
43
+ }
44
+ export interface MidlineProxy {
45
+ (req: http.IncomingMessage, res: http.ServerResponse): void;
46
+ readonly target: URL;
47
+ /** Releases pooled upstream connections. */
48
+ close(): void;
49
+ }
50
+ export declare function resolveTarget(target: string | undefined): URL;
51
+ /**
52
+ * A reverse proxy for the destination API: clients call it, it forwards to
53
+ * `target`, and every exchange is reported to Midline out of band.
54
+ *
55
+ * The Midline server is never in the request path. If it is down, slow or
56
+ * misconfigured, traffic keeps flowing and only telemetry is buffered.
57
+ */
58
+ export declare function createMidlineProxy(options?: MidlineProxyOptions): MidlineProxy;
59
+ export interface StartProxyOptions extends MidlineProxyOptions {
60
+ /** Default 8080. Env: `MIDLINE_PROXY_PORT`. */
61
+ port?: number;
62
+ /** Default 127.0.0.1 — set 0.0.0.0 explicitly to accept traffic from other machines. Env: `MIDLINE_PROXY_HOST`. */
63
+ host?: string;
64
+ /** Used to create an agent when `agent` isn't passed and none is initialised. */
65
+ midline?: MidlineConfig;
66
+ }
67
+ export declare function startMidlineProxy(options?: StartProxyOptions): Promise<http.Server & {
68
+ proxy: MidlineProxy;
69
+ }>;
70
+ export declare function proxyAddress(server: http.Server): string;