@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.
@@ -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 (connections, pipelining…). */
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;AAEpB,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,0EAA0E;IAC1E,MAAM,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,gBAAgB,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IAGlF;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,IAAI,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAYD,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAoY3F"}
1
+ {"version":3,"file":"prod-gateway.lib.d.ts","sourceRoot":"","sources":["../../../../../libs/cluster/src/executors/gateway/prod-gateway.lib.ts"],"names":[],"mappings":"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"}