@service-bridge/node 0.1.3
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 +854 -0
- package/biome.json +28 -0
- package/bun.lock +249 -0
- package/dist/express.d.ts +51 -0
- package/dist/express.js +129 -0
- package/dist/fastify.d.ts +43 -0
- package/dist/fastify.js +122 -0
- package/dist/index.js +34410 -0
- package/dist/trace.d.ts +19 -0
- package/http/dist/express.d.ts +51 -0
- package/http/dist/express.d.ts.map +1 -0
- package/http/dist/express.test.d.ts +2 -0
- package/http/dist/express.test.d.ts.map +1 -0
- package/http/dist/fastify.d.ts +43 -0
- package/http/dist/fastify.d.ts.map +1 -0
- package/http/dist/fastify.test.d.ts +2 -0
- package/http/dist/fastify.test.d.ts.map +1 -0
- package/http/dist/index.d.ts +7 -0
- package/http/dist/index.d.ts.map +1 -0
- package/http/dist/trace.d.ts +19 -0
- package/http/dist/trace.d.ts.map +1 -0
- package/http/dist/trace.test.d.ts +2 -0
- package/http/dist/trace.test.d.ts.map +1 -0
- package/http/package.json +48 -0
- package/http/src/express.test.ts +125 -0
- package/http/src/express.ts +209 -0
- package/http/src/fastify.test.ts +142 -0
- package/http/src/fastify.ts +159 -0
- package/http/src/index.ts +10 -0
- package/http/src/sdk-augment.d.ts +11 -0
- package/http/src/servicebridge.d.ts +23 -0
- package/http/src/trace.test.ts +97 -0
- package/http/src/trace.ts +56 -0
- package/http/tsconfig.json +17 -0
- package/http/tsconfig.test.json +6 -0
- package/package.json +65 -0
- package/sdk/dist/generated/servicebridge-package-definition.d.ts +4709 -0
- package/sdk/dist/grpc-client.d.ts +304 -0
- package/sdk/dist/grpc-client.test.d.ts +1 -0
- package/sdk/dist/index.d.ts +2 -0
- package/sdk/package.json +30 -0
- package/sdk/scripts/generate-proto.ts +65 -0
- package/sdk/src/generated/servicebridge-package-definition.ts +5198 -0
- package/sdk/src/grpc-client.d.ts +305 -0
- package/sdk/src/grpc-client.d.ts.map +1 -0
- package/sdk/src/grpc-client.test.ts +422 -0
- package/sdk/src/grpc-client.ts +2924 -0
- package/sdk/src/index.d.ts +3 -0
- package/sdk/src/index.d.ts.map +1 -0
- package/sdk/src/index.ts +29 -0
- package/sdk/tsconfig.json +13 -0
package/dist/trace.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of extracting trace context from incoming HTTP headers.
|
|
3
|
+
* traceId — the trace this request belongs to (carry-over or newly generated).
|
|
4
|
+
* parentSpanId — the caller's span ID to use as parent, empty string if unknown.
|
|
5
|
+
*/
|
|
6
|
+
export interface IncomingTraceContext {
|
|
7
|
+
traceId: string;
|
|
8
|
+
parentSpanId: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extract distributed trace context from incoming HTTP request headers.
|
|
12
|
+
*
|
|
13
|
+
* Priority:
|
|
14
|
+
* 1. W3C `traceparent` — full parent info (traceId + parentSpanId)
|
|
15
|
+
* 2. `x-trace-id` — traceId only, no parent span known
|
|
16
|
+
* 3. Nothing — generate a fresh traceId, no parent
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractTraceFromHeaders(headers: Record<string, string | string[] | undefined>): IncomingTraceContext;
|
|
19
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ServiceBridgeService } from "@service-bridge/node";
|
|
2
|
+
import type { Application, NextFunction, Request, Response } from "express";
|
|
3
|
+
import type { IncomingTraceContext } from "./trace";
|
|
4
|
+
import { extractTraceFromHeaders } from "./trace";
|
|
5
|
+
export { extractTraceFromHeaders };
|
|
6
|
+
export type { IncomingTraceContext };
|
|
7
|
+
declare global {
|
|
8
|
+
namespace Express {
|
|
9
|
+
interface Request {
|
|
10
|
+
servicebridge: ServiceBridgeService;
|
|
11
|
+
traceId?: string;
|
|
12
|
+
spanId?: string;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export interface ServiceBridgeExpressOptions {
|
|
17
|
+
/** ServiceBridge client (from servicebridge()). Required. */
|
|
18
|
+
client: ServiceBridgeService;
|
|
19
|
+
/** Paths to skip tracing. Default: [] */
|
|
20
|
+
excludePaths?: string[];
|
|
21
|
+
/** Set x-trace-id response header for propagation. Default: true */
|
|
22
|
+
propagateTraceHeader?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Auto-register route patterns in the ServiceBridge HTTP catalog on first hit.
|
|
25
|
+
* Routes appear in the UI after their first request. Default: true.
|
|
26
|
+
* Use registerExpressRoutes() for eager (pre-traffic) registration.
|
|
27
|
+
*/
|
|
28
|
+
autoRegister?: boolean;
|
|
29
|
+
}
|
|
30
|
+
export declare function servicebridgeMiddleware(options: ServiceBridgeExpressOptions): (req: Request, res: Response, next: NextFunction) => void;
|
|
31
|
+
export interface RegisterExpressRoutesOptions {
|
|
32
|
+
/** Stable identifier for this process instance used in http_instances. */
|
|
33
|
+
instanceId?: string;
|
|
34
|
+
/** Address where this service can be reached, e.g. "http://10.0.0.1:3000" */
|
|
35
|
+
endpoint?: string;
|
|
36
|
+
/** Service names allowed to call these endpoints. Default: [] */
|
|
37
|
+
allowedCallers?: string[];
|
|
38
|
+
/** Path prefixes to exclude from registration. Default: [] */
|
|
39
|
+
excludePaths?: string[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Scan an Express application's route table and register every route pattern
|
|
43
|
+
* in the ServiceBridge HTTP catalog. Safe to call multiple times (idempotent).
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* app.get('/users/:id', handler);
|
|
47
|
+
* app.post('/orders', handler);
|
|
48
|
+
* app.listen(3000, () => registerExpressRoutes(app, sb));
|
|
49
|
+
*/
|
|
50
|
+
export declare function registerExpressRoutes(app: Application, client: ServiceBridgeService, opts?: RegisterExpressRoutesOptions): Promise<void>;
|
|
51
|
+
//# sourceMappingURL=express.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC5E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAElD,OAAO,EAAE,uBAAuB,EAAE,CAAC;AACnC,YAAY,EAAE,oBAAoB,EAAE,CAAC;AAErC,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,OAAO,CAAC;QAChB,UAAU,OAAO;YACf,aAAa,EAAE,oBAAoB,CAAC;YACpC,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,MAAM,CAAC,EAAE,MAAM,CAAC;SACjB;KACF;CACF;AAED,MAAM,WAAW,2BAA2B;IAC1C,6DAA6D;IAC7D,MAAM,EAAE,oBAAoB,CAAC;IAC7B,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,oEAAoE;IACpE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,2BAA2B,IAaxE,KAAK,OAAO,EACZ,KAAK,QAAQ,EACb,MAAM,YAAY,KACjB,IAAI,CA8DR;AAOD,MAAM,WAAW,4BAA4B;IAC3C,0EAA0E;IAC1E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,8DAA8D;IAC9D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;CACzB;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,oBAAoB,EAC5B,IAAI,GAAE,4BAAiC,GACtC,OAAO,CAAC,IAAI,CAAC,CAiBf"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"express.test.d.ts","sourceRoot":"","sources":["../src/express.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { ServiceBridgeService } from "@service-bridge/node";
|
|
2
|
+
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
|
3
|
+
declare module "fastify" {
|
|
4
|
+
interface FastifyRequest {
|
|
5
|
+
servicebridge: ServiceBridgeService;
|
|
6
|
+
traceId?: string;
|
|
7
|
+
spanId?: string;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export interface ServiceBridgeFastifyOptions {
|
|
11
|
+
/** ServiceBridge client (from servicebridge()). Required. */
|
|
12
|
+
client: ServiceBridgeService;
|
|
13
|
+
/** Paths to skip tracing. Default: [] */
|
|
14
|
+
excludePaths?: string[];
|
|
15
|
+
/** Set x-trace-id response header for propagation. Default: true */
|
|
16
|
+
propagateTraceHeader?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Auto-register every route in the ServiceBridge HTTP catalog via Fastify's
|
|
19
|
+
* onRoute hook. Routes appear in the UI as soon as the server starts (before
|
|
20
|
+
* any traffic). Default: true.
|
|
21
|
+
*/
|
|
22
|
+
autoRegister?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Options forwarded to registerHttpEndpoint for each auto-discovered route.
|
|
25
|
+
*/
|
|
26
|
+
register?: {
|
|
27
|
+
/** Stable identifier for this process instance. */
|
|
28
|
+
instanceId?: string;
|
|
29
|
+
/** Address where this service can be reached, e.g. "http://10.0.0.1:3000" */
|
|
30
|
+
endpoint?: string;
|
|
31
|
+
/** Service names allowed to call these endpoints. Default: [] */
|
|
32
|
+
allowedCallers?: string[];
|
|
33
|
+
/** Path prefixes to exclude from registration. Default: [] */
|
|
34
|
+
excludePaths?: string[];
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export declare function servicebridgePlugin(fastify: FastifyInstance, options: ServiceBridgeFastifyOptions): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Wraps a route handler to run inside trace context.
|
|
40
|
+
* Use when the handler calls req.servicebridge.rpc() or event() — ensures trace propagation.
|
|
41
|
+
*/
|
|
42
|
+
export declare function wrapHandler<T>(handler: (request: FastifyRequest, reply: FastifyReply) => T): (request: FastifyRequest, reply: FastifyReply) => T;
|
|
43
|
+
//# sourceMappingURL=fastify.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.d.ts","sourceRoot":"","sources":["../src/fastify.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAEjE,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAI7E,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,aAAa,EAAE,oBAAoB,CAAC;QACpC,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB;CACF;AAED,MAAM,WAAW,2BAA2B;IAC1C,6DAA6D;IAC7D,MAAM,EAAE,oBAAoB,CAAC;IAC7B,yCAAyC;IACzC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,oEAAoE;IACpE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,mDAAmD;QACnD,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,6EAA6E;QAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,iEAAiE;QACjE,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;QAC1B,8DAA8D;QAC9D,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;KACzB,CAAC;CACH;AAED,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,2BAA2B,GACnC,OAAO,CAAC,IAAI,CAAC,CA8Ff;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,CAAC,GAC3D,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,EAAE,YAAY,KAAK,CAAC,CAWrD"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fastify.test.d.ts","sourceRoot":"","sources":["../src/fastify.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type { RegisterExpressRoutesOptions, ServiceBridgeExpressOptions, } from "./express";
|
|
2
|
+
export { registerExpressRoutes, servicebridgeMiddleware } from "./express";
|
|
3
|
+
export type { ServiceBridgeFastifyOptions } from "./fastify";
|
|
4
|
+
export { servicebridgePlugin, wrapHandler } from "./fastify";
|
|
5
|
+
export type { IncomingTraceContext } from "./trace";
|
|
6
|
+
export { extractTraceFromHeaders } from "./trace";
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,4BAA4B,EAC5B,2BAA2B,GAC5B,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,WAAW,CAAC;AAC3E,YAAY,EAAE,2BAA2B,EAAE,MAAM,WAAW,CAAC;AAC7D,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAE7D,YAAY,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,MAAM,SAAS,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Result of extracting trace context from incoming HTTP headers.
|
|
3
|
+
* traceId — the trace this request belongs to (carry-over or newly generated).
|
|
4
|
+
* parentSpanId — the caller's span ID to use as parent, empty string if unknown.
|
|
5
|
+
*/
|
|
6
|
+
export interface IncomingTraceContext {
|
|
7
|
+
traceId: string;
|
|
8
|
+
parentSpanId: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Extract distributed trace context from incoming HTTP request headers.
|
|
12
|
+
*
|
|
13
|
+
* Priority:
|
|
14
|
+
* 1. W3C `traceparent` — full parent info (traceId + parentSpanId)
|
|
15
|
+
* 2. `x-trace-id` — traceId only, no parent span known
|
|
16
|
+
* 3. Nothing — generate a fresh traceId, no parent
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractTraceFromHeaders(headers: Record<string, string | string[] | undefined>): IncomingTraceContext;
|
|
19
|
+
//# sourceMappingURL=trace.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.d.ts","sourceRoot":"","sources":["../src/trace.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;CACtB;AAqBD;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,GACrD,oBAAoB,CAgBtB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"trace.test.d.ts","sourceRoot":"","sources":["../src/trace.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@servicebridge/http-node",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"types": "dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./express": {
|
|
16
|
+
"import": "./dist/express.js",
|
|
17
|
+
"types": "./dist/express.d.ts"
|
|
18
|
+
},
|
|
19
|
+
"./fastify": {
|
|
20
|
+
"import": "./dist/fastify.js",
|
|
21
|
+
"types": "./dist/fastify.d.ts"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "bun build src/index.ts src/express.ts src/fastify.ts --outdir=dist --target=node"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@servicebridge/sdk-node": "^0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"express": ">=4.0.0",
|
|
32
|
+
"fastify": ">=4.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"express": {
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"fastify": {
|
|
39
|
+
"optional": true
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/express": "^4.17.21",
|
|
44
|
+
"@types/node": "^20.0.0",
|
|
45
|
+
"fastify": "^4.29.0",
|
|
46
|
+
"typescript": "^5.3.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, expect, it } from "bun:test";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { getTraceContext } from "@service-bridge/node";
|
|
5
|
+
import { servicebridgeMiddleware } from "./express";
|
|
6
|
+
|
|
7
|
+
class FakeResponse extends EventEmitter {
|
|
8
|
+
headers: Record<string, string> = {};
|
|
9
|
+
statusCode = 200;
|
|
10
|
+
|
|
11
|
+
setHeader(name: string, value: string) {
|
|
12
|
+
this.headers[name.toLowerCase()] = value;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe("servicebridgeMiddleware", () => {
|
|
17
|
+
it("starts span, propagates trace context and ends it once", () => {
|
|
18
|
+
const ended: Array<Record<string, unknown>> = [];
|
|
19
|
+
const client = {
|
|
20
|
+
startHttpSpan(opts: Record<string, unknown>) {
|
|
21
|
+
expect(opts.method).toBe("GET");
|
|
22
|
+
expect(opts.path).toBe("/orders");
|
|
23
|
+
expect(opts.traceId).toBe("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
24
|
+
expect(opts.parentSpanId).toBe("1111111111111111");
|
|
25
|
+
return {
|
|
26
|
+
traceId: "span-trace",
|
|
27
|
+
spanId: "span-id",
|
|
28
|
+
end(opts: Record<string, unknown>) {
|
|
29
|
+
ended.push(opts);
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
const middleware = servicebridgeMiddleware({ client: client as never });
|
|
35
|
+
|
|
36
|
+
const req = {
|
|
37
|
+
method: "GET",
|
|
38
|
+
path: "/orders",
|
|
39
|
+
url: "/orders?limit=10",
|
|
40
|
+
headers: {
|
|
41
|
+
traceparent: "00-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa-1111111111111111-01",
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
const res = new FakeResponse();
|
|
45
|
+
let nextCalls = 0;
|
|
46
|
+
|
|
47
|
+
middleware(req as never, res as never, () => {
|
|
48
|
+
nextCalls++;
|
|
49
|
+
const trace = getTraceContext();
|
|
50
|
+
expect(trace).toEqual({ traceId: "span-trace", spanId: "span-id" });
|
|
51
|
+
expect(req.servicebridge).toBe(client);
|
|
52
|
+
expect(req.traceId).toBe("span-trace");
|
|
53
|
+
expect(req.spanId).toBe("span-id");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
expect(nextCalls).toBe(1);
|
|
57
|
+
expect(res.headers["x-trace-id"]).toBe("span-trace");
|
|
58
|
+
|
|
59
|
+
res.emit("finish");
|
|
60
|
+
res.emit("close");
|
|
61
|
+
|
|
62
|
+
expect(ended).toEqual([{ statusCode: 200, success: true }]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("respects excludePaths and skips tracing", () => {
|
|
66
|
+
let started = 0;
|
|
67
|
+
const client = {
|
|
68
|
+
startHttpSpan() {
|
|
69
|
+
started++;
|
|
70
|
+
throw new Error("must not be called");
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
const middleware = servicebridgeMiddleware({
|
|
74
|
+
client: client as never,
|
|
75
|
+
excludePaths: ["/health"],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const req = {
|
|
79
|
+
method: "GET",
|
|
80
|
+
path: "/health/ready",
|
|
81
|
+
url: "/health/ready",
|
|
82
|
+
headers: {},
|
|
83
|
+
};
|
|
84
|
+
const res = new FakeResponse();
|
|
85
|
+
let nextCalls = 0;
|
|
86
|
+
|
|
87
|
+
middleware(req as never, res as never, () => {
|
|
88
|
+
nextCalls++;
|
|
89
|
+
expect(getTraceContext()).toBeUndefined();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(nextCalls).toBe(1);
|
|
93
|
+
expect(started).toBe(0);
|
|
94
|
+
expect(req.servicebridge).toBe(client);
|
|
95
|
+
expect(res.headers["x-trace-id"]).toBeUndefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("can disable x-trace-id propagation", () => {
|
|
99
|
+
const client = {
|
|
100
|
+
startHttpSpan() {
|
|
101
|
+
return {
|
|
102
|
+
traceId: "trace-id",
|
|
103
|
+
spanId: "span-id",
|
|
104
|
+
end() {},
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
const middleware = servicebridgeMiddleware({
|
|
109
|
+
client: client as never,
|
|
110
|
+
propagateTraceHeader: false,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const req = {
|
|
114
|
+
method: "POST",
|
|
115
|
+
path: "/events",
|
|
116
|
+
url: "/events",
|
|
117
|
+
headers: {},
|
|
118
|
+
};
|
|
119
|
+
const res = new FakeResponse();
|
|
120
|
+
|
|
121
|
+
middleware(req as never, res as never, () => {});
|
|
122
|
+
|
|
123
|
+
expect(res.headers["x-trace-id"]).toBeUndefined();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { ServiceBridgeService } from "@service-bridge/node";
|
|
2
|
+
import { runWithTraceContext } from "@service-bridge/node";
|
|
3
|
+
import type { Application, NextFunction, Request, Response } from "express";
|
|
4
|
+
import type { IncomingTraceContext } from "./trace";
|
|
5
|
+
import { extractTraceFromHeaders } from "./trace";
|
|
6
|
+
|
|
7
|
+
export { extractTraceFromHeaders };
|
|
8
|
+
export type { IncomingTraceContext };
|
|
9
|
+
|
|
10
|
+
declare global {
|
|
11
|
+
namespace Express {
|
|
12
|
+
interface Request {
|
|
13
|
+
servicebridge: ServiceBridgeService;
|
|
14
|
+
traceId?: string;
|
|
15
|
+
spanId?: string;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ServiceBridgeExpressOptions {
|
|
21
|
+
/** ServiceBridge client (from servicebridge()). Required. */
|
|
22
|
+
client: ServiceBridgeService;
|
|
23
|
+
/** Paths to skip tracing. Default: [] */
|
|
24
|
+
excludePaths?: string[];
|
|
25
|
+
/** Set x-trace-id response header for propagation. Default: true */
|
|
26
|
+
propagateTraceHeader?: boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Auto-register route patterns in the ServiceBridge HTTP catalog on first hit.
|
|
29
|
+
* Routes appear in the UI after their first request. Default: true.
|
|
30
|
+
* Use registerExpressRoutes() for eager (pre-traffic) registration.
|
|
31
|
+
*/
|
|
32
|
+
autoRegister?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function servicebridgeMiddleware(options: ServiceBridgeExpressOptions) {
|
|
36
|
+
const {
|
|
37
|
+
client,
|
|
38
|
+
excludePaths = [],
|
|
39
|
+
propagateTraceHeader = true,
|
|
40
|
+
autoRegister = true,
|
|
41
|
+
} = options;
|
|
42
|
+
|
|
43
|
+
// Tracks which "METHOD:pattern" keys have already been registered so we
|
|
44
|
+
// fire the catalog call only once per unique route pattern per process.
|
|
45
|
+
const registeredPatterns = new Set<string>();
|
|
46
|
+
|
|
47
|
+
return function middleware(
|
|
48
|
+
req: Request,
|
|
49
|
+
res: Response,
|
|
50
|
+
next: NextFunction,
|
|
51
|
+
): void {
|
|
52
|
+
const path = req.path ?? req.url?.split("?")[0] ?? "/";
|
|
53
|
+
if (excludePaths.some((p) => path.startsWith(p))) {
|
|
54
|
+
req.servicebridge = client;
|
|
55
|
+
next();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const headers: Record<string, string | string[] | undefined> = {};
|
|
60
|
+
for (const [k, v] of Object.entries(req.headers)) {
|
|
61
|
+
if (v != null) headers[k.toLowerCase()] = v;
|
|
62
|
+
}
|
|
63
|
+
const traceCtx: IncomingTraceContext = extractTraceFromHeaders(headers);
|
|
64
|
+
|
|
65
|
+
const span = client.startHttpSpan({
|
|
66
|
+
method: req.method,
|
|
67
|
+
path,
|
|
68
|
+
traceId: traceCtx.traceId,
|
|
69
|
+
parentSpanId: traceCtx.parentSpanId,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
req.servicebridge = client;
|
|
73
|
+
req.traceId = span.traceId;
|
|
74
|
+
req.spanId = span.spanId;
|
|
75
|
+
|
|
76
|
+
if (propagateTraceHeader) {
|
|
77
|
+
res.setHeader("x-trace-id", span.traceId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const onFinish = () => {
|
|
81
|
+
res.off("finish", onFinish);
|
|
82
|
+
res.off("close", onFinish);
|
|
83
|
+
span.end({
|
|
84
|
+
statusCode: res.statusCode,
|
|
85
|
+
success: res.statusCode < 400,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Auto-register the route pattern in the catalog on first hit.
|
|
89
|
+
if (autoRegister) {
|
|
90
|
+
const routePattern = req.route?.path as string | undefined;
|
|
91
|
+
if (routePattern && typeof routePattern === "string") {
|
|
92
|
+
const key = `${req.method}:${routePattern}`;
|
|
93
|
+
if (!registeredPatterns.has(key)) {
|
|
94
|
+
registeredPatterns.add(key);
|
|
95
|
+
client
|
|
96
|
+
.registerHttpEndpoint({ method: req.method, route: routePattern })
|
|
97
|
+
.catch(() => {
|
|
98
|
+
// Non-fatal: catalog registration is best-effort.
|
|
99
|
+
registeredPatterns.delete(key);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
res.on("finish", onFinish);
|
|
107
|
+
res.on("close", onFinish);
|
|
108
|
+
|
|
109
|
+
runWithTraceContext({ traceId: span.traceId, spanId: span.spanId }, () => {
|
|
110
|
+
next();
|
|
111
|
+
});
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
// Eager registration: scan Express router and register all routes up-front.
|
|
117
|
+
// Call after all app.get/post/... declarations and before (or right after) listen.
|
|
118
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
export interface RegisterExpressRoutesOptions {
|
|
121
|
+
/** Stable identifier for this process instance used in http_instances. */
|
|
122
|
+
instanceId?: string;
|
|
123
|
+
/** Address where this service can be reached, e.g. "http://10.0.0.1:3000" */
|
|
124
|
+
endpoint?: string;
|
|
125
|
+
/** Service names allowed to call these endpoints. Default: [] */
|
|
126
|
+
allowedCallers?: string[];
|
|
127
|
+
/** Path prefixes to exclude from registration. Default: [] */
|
|
128
|
+
excludePaths?: string[];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Scan an Express application's route table and register every route pattern
|
|
133
|
+
* in the ServiceBridge HTTP catalog. Safe to call multiple times (idempotent).
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* app.get('/users/:id', handler);
|
|
137
|
+
* app.post('/orders', handler);
|
|
138
|
+
* app.listen(3000, () => registerExpressRoutes(app, sb));
|
|
139
|
+
*/
|
|
140
|
+
export async function registerExpressRoutes(
|
|
141
|
+
app: Application,
|
|
142
|
+
client: ServiceBridgeService,
|
|
143
|
+
opts: RegisterExpressRoutesOptions = {},
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const { instanceId, endpoint, allowedCallers = [], excludePaths = [] } = opts;
|
|
146
|
+
const routes = extractExpressRoutes(app);
|
|
147
|
+
|
|
148
|
+
await Promise.allSettled(
|
|
149
|
+
routes
|
|
150
|
+
.filter(({ path }) => !excludePaths.some((p) => path.startsWith(p)))
|
|
151
|
+
.map(({ method, path: route }) =>
|
|
152
|
+
client.registerHttpEndpoint({
|
|
153
|
+
method,
|
|
154
|
+
route,
|
|
155
|
+
instanceId,
|
|
156
|
+
endpoint,
|
|
157
|
+
allowedCallers,
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
interface RouteEntry {
|
|
164
|
+
method: string;
|
|
165
|
+
path: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Express internal layer shape (router stack entry). */
|
|
169
|
+
interface ExpressLayer {
|
|
170
|
+
route?: { path?: string; methods?: Record<string, unknown> };
|
|
171
|
+
name?: string;
|
|
172
|
+
handle?: ExpressRouterLike;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** App or Router: has stack of layers. */
|
|
176
|
+
interface ExpressRouterLike {
|
|
177
|
+
_router?: { stack?: ExpressLayer[] };
|
|
178
|
+
router?: { stack?: ExpressLayer[] };
|
|
179
|
+
stack?: ExpressLayer[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function getLayerStack(app: ExpressRouterLike): ExpressLayer[] {
|
|
183
|
+
const r = app._router ?? app.router;
|
|
184
|
+
return r?.stack ?? (app as ExpressRouterLike).stack ?? [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function extractExpressRoutes(
|
|
188
|
+
app: Application | ExpressRouterLike,
|
|
189
|
+
prefix = "",
|
|
190
|
+
): RouteEntry[] {
|
|
191
|
+
const routes: RouteEntry[] = [];
|
|
192
|
+
const stack = getLayerStack(app as ExpressRouterLike);
|
|
193
|
+
|
|
194
|
+
for (const layer of stack) {
|
|
195
|
+
if (layer.route) {
|
|
196
|
+
const routePath = prefix + (layer.route.path ?? "");
|
|
197
|
+
const methods = Object.keys(layer.route.methods ?? {}).filter(
|
|
198
|
+
(m) => m !== "_all",
|
|
199
|
+
);
|
|
200
|
+
for (const method of methods) {
|
|
201
|
+
routes.push({ method: method.toUpperCase(), path: routePath });
|
|
202
|
+
}
|
|
203
|
+
} else if (layer.name === "router" && layer.handle) {
|
|
204
|
+
routes.push(...extractExpressRoutes(layer.handle, prefix));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return routes;
|
|
209
|
+
}
|