@lensmcp/cluster 1.0.0 → 1.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/executors/gateway/gateway.lib.d.ts +32 -0
- package/executors/gateway/gateway.lib.d.ts.map +1 -1
- package/executors/gateway/gateway.lib.js +147 -1
- package/executors/gateway/health-check.d.ts +53 -0
- package/executors/gateway/health-check.d.ts.map +1 -0
- package/executors/gateway/health-check.js +66 -0
- package/executors/gateway/main.prod-gateway.js +219 -8
- package/executors/gateway/manifest.d.ts +7 -2
- package/executors/gateway/manifest.d.ts.map +1 -1
- package/executors/gateway/manifest.js +22 -9
- package/executors/gateway/metrics.d.ts +37 -0
- package/executors/gateway/metrics.d.ts.map +1 -0
- package/executors/gateway/metrics.js +56 -0
- package/executors/gateway/otel-tracing.d.ts +47 -0
- package/executors/gateway/otel-tracing.d.ts.map +1 -0
- package/executors/gateway/otel-tracing.js +73 -0
- package/executors/gateway/prod-gateway.lib.d.ts +87 -1
- package/executors/gateway/prod-gateway.lib.d.ts.map +1 -1
- package/executors/gateway/prod-gateway.lib.js +309 -31
- package/executors/gateway/providers-prod.d.ts.map +1 -1
- package/executors/gateway/providers-prod.js +46 -13
- package/executors/gateway/rate-limit.d.ts +66 -0
- package/executors/gateway/rate-limit.d.ts.map +1 -0
- package/executors/gateway/rate-limit.js +91 -0
- package/executors/gateway/schema.d.ts +8 -0
- package/executors/gateway/schema.json +19 -0
- package/executors.json +8 -8
- package/main.devserver.js +5 -4
- package/package.json +23 -2
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setupTracing = setupTracing;
|
|
4
|
+
/**
|
|
5
|
+
* OpenTelemetry tracing for the production gateway — OPT-IN, cloud-neutral.
|
|
6
|
+
*
|
|
7
|
+
* Makes the gateway a first-class span in a distributed trace: it continues the
|
|
8
|
+
* incoming W3C `traceparent`, emits one SERVER span per request, and INJECTS its
|
|
9
|
+
* context into the forwarded headers so the downstream service's spans parent to
|
|
10
|
+
* the gateway hop. Spans export over OTLP/HTTP — point `OTEL_EXPORTER_OTLP_ENDPOINT`
|
|
11
|
+
* at Cloud Trace (GCP), the OTel Collector / X-Ray (AWS), Tempo, Jaeger, etc. The
|
|
12
|
+
* SAME image traces on any cloud; only the endpoint env changes.
|
|
13
|
+
*
|
|
14
|
+
* All OpenTelemetry imports live HERE. The gateway lib takes a `GatewayTracer`
|
|
15
|
+
* (type-only) and calls these helpers, so it never pulls OTel unless tracing is
|
|
16
|
+
* wired — and when no endpoint is set, `setupTracing` returns undefined and the
|
|
17
|
+
* hot path does zero span work.
|
|
18
|
+
*
|
|
19
|
+
* No global context manager (`provider.register()` is intentionally NOT called):
|
|
20
|
+
* context is threaded explicitly via extract/inject, keeping the proxy hot path
|
|
21
|
+
* synchronous and free of async-hooks overhead.
|
|
22
|
+
*/
|
|
23
|
+
const api_1 = require("@opentelemetry/api");
|
|
24
|
+
const core_1 = require("@opentelemetry/core");
|
|
25
|
+
const sdk_trace_base_1 = require("@opentelemetry/sdk-trace-base");
|
|
26
|
+
const resources_1 = require("@opentelemetry/resources");
|
|
27
|
+
const exporter_trace_otlp_http_1 = require("@opentelemetry/exporter-trace-otlp-http");
|
|
28
|
+
const firstHeader = (h, k) => {
|
|
29
|
+
const v = h[k.toLowerCase()];
|
|
30
|
+
return Array.isArray(v) ? v[0] : v;
|
|
31
|
+
};
|
|
32
|
+
const getter = {
|
|
33
|
+
keys: (carrier) => Object.keys(carrier),
|
|
34
|
+
get: (carrier, key) => firstHeader(carrier, key),
|
|
35
|
+
};
|
|
36
|
+
const setter = {
|
|
37
|
+
set: (carrier, key, value) => { carrier[key.toLowerCase()] = value; },
|
|
38
|
+
};
|
|
39
|
+
/** Build the gateway tracer, or undefined when no OTLP endpoint is configured
|
|
40
|
+
* (default-off → the lib skips all span work, zero hot-path overhead). */
|
|
41
|
+
function setupTracing(opts = {}) {
|
|
42
|
+
const endpoint = opts.endpoint ?? process.env['OTEL_EXPORTER_OTLP_ENDPOINT'];
|
|
43
|
+
if (!endpoint)
|
|
44
|
+
return undefined;
|
|
45
|
+
const base = endpoint.replace(/\/+$/, '');
|
|
46
|
+
const url = /\/v1\/traces$/.test(base) ? base : `${base}/v1/traces`;
|
|
47
|
+
const exporter = new exporter_trace_otlp_http_1.OTLPTraceExporter({ url, ...(opts.headers ? { headers: opts.headers } : {}) });
|
|
48
|
+
const provider = new sdk_trace_base_1.BasicTracerProvider({
|
|
49
|
+
resource: new resources_1.Resource({ 'service.name': opts.serviceName ?? process.env['OTEL_SERVICE_NAME'] ?? 'lensmcp-gateway' }),
|
|
50
|
+
sampler: new sdk_trace_base_1.ParentBasedSampler({ root: new sdk_trace_base_1.TraceIdRatioBasedSampler(opts.sampleRatio ?? 1) }),
|
|
51
|
+
});
|
|
52
|
+
provider.addSpanProcessor(new sdk_trace_base_1.BatchSpanProcessor(exporter));
|
|
53
|
+
const tracer = provider.getTracer('lensmcp-gateway');
|
|
54
|
+
const propagator = new core_1.W3CTraceContextPropagator();
|
|
55
|
+
return {
|
|
56
|
+
startSpan(req, name, attrs) {
|
|
57
|
+
const parent = propagator.extract(api_1.ROOT_CONTEXT, req.headers, getter);
|
|
58
|
+
return tracer.startSpan(name, { kind: api_1.SpanKind.SERVER, attributes: attrs }, parent);
|
|
59
|
+
},
|
|
60
|
+
inject(span, headers) {
|
|
61
|
+
propagator.inject(api_1.trace.setSpan(api_1.ROOT_CONTEXT, span), headers, setter);
|
|
62
|
+
},
|
|
63
|
+
end(span, statusCode, attrs) {
|
|
64
|
+
if (attrs)
|
|
65
|
+
span.setAttributes(attrs);
|
|
66
|
+
span.setAttribute('http.response.status_code', statusCode);
|
|
67
|
+
if (statusCode >= 500 || statusCode === 0)
|
|
68
|
+
span.setStatus({ code: api_1.SpanStatusCode.ERROR });
|
|
69
|
+
span.end();
|
|
70
|
+
},
|
|
71
|
+
shutdown: () => provider.shutdown(),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
import * as http from 'node:http';
|
|
2
2
|
import { type AuthMode, type ManifestProvider, type PodProvider, type Route } from './manifest';
|
|
3
|
+
import type { GatewayTracer } from './otel-tracing';
|
|
4
|
+
import type { HealthChecker } from './health-check';
|
|
5
|
+
import type { RateLimiter } from './rate-limit';
|
|
6
|
+
import type { ProcessMetrics } from './metrics';
|
|
7
|
+
/** Per-service health rollup, derived from observed traffic over a rolling window.
|
|
8
|
+
* `status` is inferred from the recent error rate (passive — not an active probe):
|
|
9
|
+
* healthy <5% err · degraded 5–50% · down ≥50% · idle (no recent traffic). */
|
|
10
|
+
export interface ServiceStatus {
|
|
11
|
+
service: string;
|
|
12
|
+
hosts: string[];
|
|
13
|
+
status: 'healthy' | 'degraded' | 'down' | 'idle';
|
|
14
|
+
requests: number;
|
|
15
|
+
errorRate: number;
|
|
16
|
+
avgMs: number;
|
|
17
|
+
versions: string[];
|
|
18
|
+
lastSeen: number | null;
|
|
19
|
+
}
|
|
3
20
|
export interface ProdGatewayOptions {
|
|
4
21
|
/** Ports to bind. Default `[8080]` (or `[8443]` with TLS). One Fastify
|
|
5
22
|
* instance per port; all share the route table + providers. */
|
|
@@ -23,12 +40,80 @@ export interface ProdGatewayOptions {
|
|
|
23
40
|
/** Request header that PINS a rollout cohort (sticky canary / debugging).
|
|
24
41
|
* Default `x-lensmcp-version`. The chosen version is echoed on the response. */
|
|
25
42
|
versionHeader?: string;
|
|
26
|
-
/** undici pool tuning passed to reply-from (
|
|
43
|
+
/** undici pool tuning passed to reply-from. `bodyTimeout: 0` (default) keeps
|
|
44
|
+
* long-lived SSE / streamable-HTTP MCP responses open — undici's 300s default
|
|
45
|
+
* would otherwise abort an idle stream. `rejectUnauthorized` defaults false
|
|
46
|
+
* (local-CA / self-signed upstreams); set true for verified east-west on GCP.
|
|
47
|
+
* `allowH2` multiplexes many concurrent streams over few HTTP/2 connections
|
|
48
|
+
* (Cloud Run supports h2 end-to-end) — useful when one upstream fans many MCP streams.
|
|
49
|
+
* `cert`/`key` present a client certificate to upstreams (mTLS); `ca` trusts a
|
|
50
|
+
* private CA for the server side. `rejectUnauthorized:false` keeps mTLS while
|
|
51
|
+
* skipping server-cert validation (self-signed internal services). */
|
|
27
52
|
undici?: {
|
|
28
53
|
connections?: number;
|
|
29
54
|
pipelining?: number;
|
|
30
55
|
keepAliveTimeout?: number;
|
|
56
|
+
bodyTimeout?: number;
|
|
57
|
+
headersTimeout?: number;
|
|
58
|
+
rejectUnauthorized?: boolean;
|
|
59
|
+
allowH2?: boolean;
|
|
60
|
+
cert?: string | Buffer;
|
|
61
|
+
key?: string | Buffer;
|
|
62
|
+
ca?: string | Buffer;
|
|
63
|
+
};
|
|
64
|
+
/** Header carrying the AUTHORITATIVE client IP from a trusted front proxy
|
|
65
|
+
* (e.g. `cf-connecting-ip` behind Cloudflare). When set, the `ip` ABAC
|
|
66
|
+
* attribute comes from this header instead of the (multi-proxy, spoofable)
|
|
67
|
+
* X-Forwarded-For chain. Safe only when the origin is locked to that proxy —
|
|
68
|
+
* which Cloudflare Tunnel guarantees (there is no public origin to bypass). */
|
|
69
|
+
clientIpHeader?: string;
|
|
70
|
+
/** CIDRs of trusted front proxies. `clientIpHeader` is honored ONLY when the
|
|
71
|
+
* immediate peer (socket address) is in this set — otherwise the header is
|
|
72
|
+
* ignored and the socket IP is used (prevents spoofing the `ip` ABAC attribute
|
|
73
|
+
* by hitting the origin directly). Default when omitted: loopback only
|
|
74
|
+
* (matches a co-located cloudflared/sidecar). */
|
|
75
|
+
clientIpTrustedProxies?: string[];
|
|
76
|
+
/** OpenTelemetry tracer (from `setupTracing`). When set, each request gets a
|
|
77
|
+
* SERVER span whose context is propagated downstream (W3C `traceparent`), so
|
|
78
|
+
* the gateway hop stitches into the distributed trace. Omit → no span work. */
|
|
79
|
+
tracing?: GatewayTracer;
|
|
80
|
+
/** Expose `GET /statusz` — the per-service health rollup as JSON. Requires
|
|
81
|
+
* `statusToken` too (fail-closed: served only when both are set). Default off (404). */
|
|
82
|
+
statusEndpoint?: boolean;
|
|
83
|
+
/** Token `/statusz` requires (via `?token=` or `x-status-token`), compared in
|
|
84
|
+
* constant time. REQUIRED for the endpoint to serve — no token → /statusz is 404. */
|
|
85
|
+
statusToken?: string;
|
|
86
|
+
/** How many flush windows the rolling status sums over (default 12 → ~60s at the 5s flush). */
|
|
87
|
+
statusWindows?: number;
|
|
88
|
+
/** Called each flush window with the per-service status snapshot — wire it to a
|
|
89
|
+
* Redis `status:*` fan-out / status page. The gateway is the only thing that
|
|
90
|
+
* sees every request, so this is the natural source of observed health. */
|
|
91
|
+
onStatus?: (snapshot: ServiceStatus[]) => void;
|
|
92
|
+
/** Active upstream health-checker (from `createHealthChecker`). When set, the
|
|
93
|
+
* gateway probes upstreams, steers pooled picks toward healthy endpoints, and
|
|
94
|
+
* reflects true liveness in /statusz. Omit → passive drop-on-error only. */
|
|
95
|
+
healthChecker?: HealthChecker;
|
|
96
|
+
/** Opt-in per-client-IP rate limiter — defense-in-depth behind the edge throttle.
|
|
97
|
+
* In-memory + per-instance (`createRateLimiter`) or ONE global budget across all
|
|
98
|
+
* containers (`createRedisRateLimiter`). The check is `await`ed either way; it
|
|
99
|
+
* runs before routing/auth and applies only to proxied traffic (ops endpoints exempt). */
|
|
100
|
+
rateLimiter?: RateLimiter;
|
|
101
|
+
/** Process self-metrics (event-loop lag / rss / cpu / req rates) from `createProcessMetrics`;
|
|
102
|
+
* exposed at the token-gated `/metricsz` for load/stress tests + production monitoring. */
|
|
103
|
+
metrics?: ProcessMetrics;
|
|
104
|
+
/** Server-side HTTP timeouts (ms). `keepAliveTimeout` default 620000 — kept
|
|
105
|
+
* ABOVE the GCP external ALB's 600s backend keep-alive so the gateway never
|
|
106
|
+
* closes a pooled connection the LB still considers live (the classic
|
|
107
|
+
* intermittent-502 race). `requestTimeout` default 0 (disabled) for streaming. */
|
|
108
|
+
server?: {
|
|
109
|
+
keepAliveTimeout?: number;
|
|
110
|
+
requestTimeout?: number;
|
|
111
|
+
connectionTimeout?: number;
|
|
112
|
+
headersTimeout?: number;
|
|
31
113
|
};
|
|
114
|
+
/** Opt-in one-JSON-line-per-request access log to stdout, shaped for Cloud
|
|
115
|
+
* Logging (severity + time + message). Default off (zero overhead). */
|
|
116
|
+
accessLog?: boolean;
|
|
32
117
|
/** Return verified subject attributes (e.g. JWT claims) for ABAC routing.
|
|
33
118
|
* MUST verify (signature/exp) — forged claims must not reach the rules. */
|
|
34
119
|
identify?: (req: http.IncomingMessage) => Record<string, unknown> | undefined;
|
|
@@ -52,6 +137,7 @@ export interface ProdGatewayOptions {
|
|
|
52
137
|
export interface ProdGatewayHandle {
|
|
53
138
|
ports: number[];
|
|
54
139
|
routes: () => Route[];
|
|
140
|
+
status: () => ServiceStatus[];
|
|
55
141
|
stop: () => Promise<void>;
|
|
56
142
|
}
|
|
57
143
|
export declare function startProdGateway(opts: ProdGatewayOptions): Promise<ProdGatewayHandle>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"prod-gateway.lib.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/prod-gateway.lib.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAIlC,OAAO,EAEL,KAAK,QAAQ,EAAc,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAqB,KAAK,KAAK,EAClG,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"prod-gateway.lib.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/prod-gateway.lib.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAIlC,OAAO,EAEL,KAAK,QAAQ,EAAc,KAAK,gBAAgB,EAAE,KAAK,WAAW,EAAqB,KAAK,KAAK,EAClG,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EAAE,aAAa,EAAa,MAAM,gBAAgB,CAAC;AAC/D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD;;+EAE+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,SAAS,GAAG,UAAU,GAAG,MAAM,GAAG,MAAM,CAAC;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IACjC;oEACgE;IAChE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,gBAAgB,CAAC;IAC5B,2EAA2E;IAC3E,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB,4EAA4E;IAC5E,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACrC;qFACiF;IACjF,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,IAAI,CAAC;IACnE,GAAG,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACtD,8EAA8E;IAC9E,IAAI,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAChD,oDAAoD;IACpD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;qFACiF;IACjF,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;;;;;2EAQuE;IACvE,MAAM,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;IACvP;;;;oFAIgF;IAChF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;sDAIkD;IAClD,sBAAsB,CAAC,EAAE,MAAM,EAAE,CAAC;IAClC;;oFAEgF;IAChF,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB;6FACyF;IACzF,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;0FACsF;IACtF,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+FAA+F;IAC/F,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;gFAE4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC;IAC/C;;iFAE6E;IAC7E,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;+FAG2F;IAC3F,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B;gGAC4F;IAC5F,OAAO,CAAC,EAAE,cAAc,CAAC;IACzB;;;uFAGmF;IACnF,MAAM,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAC;QAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrH;4EACwE;IACxE,SAAS,CAAC,EAAE,OAAO,CAAC;IAGpB;gFAC4E;IAC5E,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,eAAe,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;IAC9E,mFAAmF;IACnF,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC5B;2DACuD;IACvD,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;4EACwE;IACxE,MAAM,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAC9E;sEACkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,MAAM,KAAK,EAAE,CAAC;IACtB,MAAM,EAAE,MAAM,aAAa,EAAE,CAAC;IAC9B,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AA+BD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CA0kB3F"}
|