@checkstack/backend-api 0.16.0 → 0.17.1
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/CHANGELOG.md +52 -0
- package/package.json +7 -7
- package/src/correlation-middleware.test.ts +191 -0
- package/src/rpc.ts +100 -0
- package/src/test-utils.ts +18 -6
- package/src/types.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
# @checkstack/backend-api
|
|
2
2
|
|
|
3
|
+
## 0.17.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [ba07ae2]
|
|
8
|
+
- @checkstack/healthcheck-common@1.2.0
|
|
9
|
+
- @checkstack/cache-api@0.3.5
|
|
10
|
+
- @checkstack/queue-api@0.3.5
|
|
11
|
+
|
|
12
|
+
## 0.17.0
|
|
13
|
+
|
|
14
|
+
### Minor Changes
|
|
15
|
+
|
|
16
|
+
- f23f3c9: Add `correlationMiddleware` to `@checkstack/backend-api` and apply it
|
|
17
|
+
to every plugin/core router so each request carries a stable
|
|
18
|
+
`x-correlation-id` (read from the inbound header, or freshly minted
|
|
19
|
+
via `crypto.randomUUID()` when absent) and an auto-injected child
|
|
20
|
+
logger bound with `{ correlationId, pluginId, userId? }`. The ID is
|
|
21
|
+
echoed back on the response header so the caller can correlate their
|
|
22
|
+
client-side trace to the server logs.
|
|
23
|
+
|
|
24
|
+
The `Logger` interface in `@checkstack/backend-api` now formally
|
|
25
|
+
documents the structured-metadata convention (`logger.info("msg",
|
|
26
|
+
{ ...meta })`) alongside the long-standing varargs shape. Winston's
|
|
27
|
+
splat handling already routes both shapes through the same vararg
|
|
28
|
+
slot, so existing call sites are unaffected. A new optional
|
|
29
|
+
`Logger.child(meta)` method captures the metadata-binding contract the
|
|
30
|
+
new middleware relies on; production loggers always implement it,
|
|
31
|
+
minimal test mocks may omit it (the middleware falls back gracefully).
|
|
32
|
+
|
|
33
|
+
`RpcContext` grew two optional `Headers` bags, `requestHeaders` and
|
|
34
|
+
`responseHeaders`, populated by the outer Hono `/api/*` and `/rest/*`
|
|
35
|
+
handlers in `@checkstack/backend`. They are write-through observation
|
|
36
|
+
points for middleware; an `RpcContext` constructed without them (S2S
|
|
37
|
+
clients, tests) keeps working — the echo is a silent no-op and the ID
|
|
38
|
+
is still bound onto the child logger for server-side correlation.
|
|
39
|
+
|
|
40
|
+
The scaffolding template in `@checkstack/scripts` was updated so any
|
|
41
|
+
new plugin generated via `bun run create` wires the middleware in the
|
|
42
|
+
expected `.use(correlationMiddleware).use(autoAuthMiddleware)` order
|
|
43
|
+
out of the box.
|
|
44
|
+
|
|
45
|
+
### Patch Changes
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [f23f3c9]
|
|
48
|
+
- Updated dependencies [f23f3c9]
|
|
49
|
+
- @checkstack/common@0.11.0
|
|
50
|
+
- @checkstack/healthcheck-common@1.1.2
|
|
51
|
+
- @checkstack/signal-common@0.2.4
|
|
52
|
+
- @checkstack/cache-api@0.3.4
|
|
53
|
+
- @checkstack/queue-api@0.3.4
|
|
54
|
+
|
|
3
55
|
## 0.16.0
|
|
4
56
|
|
|
5
57
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/backend-api",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"license": "Elastic-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -10,11 +10,11 @@
|
|
|
10
10
|
"lint:code": "eslint . --max-warnings 0"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@checkstack/common": "0.
|
|
14
|
-
"@checkstack/healthcheck-common": "1.1.
|
|
15
|
-
"@checkstack/cache-api": "0.3.
|
|
16
|
-
"@checkstack/queue-api": "0.3.
|
|
17
|
-
"@checkstack/signal-common": "0.2.
|
|
13
|
+
"@checkstack/common": "0.11.0",
|
|
14
|
+
"@checkstack/healthcheck-common": "1.1.2",
|
|
15
|
+
"@checkstack/cache-api": "0.3.4",
|
|
16
|
+
"@checkstack/queue-api": "0.3.4",
|
|
17
|
+
"@checkstack/signal-common": "0.2.4",
|
|
18
18
|
"@orpc/client": "^1.13.14",
|
|
19
19
|
"@orpc/contract": "^1.13.14",
|
|
20
20
|
"@orpc/openapi": "^1.13.2",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/bun": "latest",
|
|
30
30
|
"@checkstack/tsconfig": "0.0.7",
|
|
31
|
-
"@checkstack/scripts": "0.3.
|
|
31
|
+
"@checkstack/scripts": "0.3.3"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"hono": "^4.12.14",
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, expect, it, mock, type Mock } from "bun:test";
|
|
2
|
+
import { call, implement } from "@orpc/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { proc } from "@checkstack/common";
|
|
5
|
+
import {
|
|
6
|
+
autoAuthMiddleware,
|
|
7
|
+
CORRELATION_ID_HEADER,
|
|
8
|
+
correlationMiddleware,
|
|
9
|
+
RpcContext,
|
|
10
|
+
} from "./rpc";
|
|
11
|
+
import { createMockRpcContext } from "./test-utils";
|
|
12
|
+
import { Logger } from "./types";
|
|
13
|
+
|
|
14
|
+
const echoLoggerProcedure = proc({
|
|
15
|
+
userType: "anonymous",
|
|
16
|
+
operationType: "query",
|
|
17
|
+
access: [],
|
|
18
|
+
}).output(
|
|
19
|
+
z.object({
|
|
20
|
+
correlationIdFromMeta: z.string().optional(),
|
|
21
|
+
pluginIdFromMeta: z.string().optional(),
|
|
22
|
+
userIdFromMeta: z.string().optional(),
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const testContract = {
|
|
27
|
+
echo: echoLoggerProcedure,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type LastChildMeta = Record<string, unknown> | undefined;
|
|
31
|
+
|
|
32
|
+
function buildRouterCapturingChildMeta(captureRef: { value: LastChildMeta }) {
|
|
33
|
+
const os = implement(testContract)
|
|
34
|
+
.$context<RpcContext>()
|
|
35
|
+
.use(correlationMiddleware)
|
|
36
|
+
.use(autoAuthMiddleware);
|
|
37
|
+
|
|
38
|
+
return os.router({
|
|
39
|
+
echo: os.echo.handler(({ context }) => {
|
|
40
|
+
// The capture happens in the child() mock on the test's logger.
|
|
41
|
+
// We just return the metadata that the middleware should have bound.
|
|
42
|
+
void context.logger;
|
|
43
|
+
return {
|
|
44
|
+
correlationIdFromMeta: captureRef.value?.correlationId as
|
|
45
|
+
| string
|
|
46
|
+
| undefined,
|
|
47
|
+
pluginIdFromMeta: captureRef.value?.pluginId as string | undefined,
|
|
48
|
+
userIdFromMeta: captureRef.value?.userId as string | undefined,
|
|
49
|
+
};
|
|
50
|
+
}),
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildContextWithCapture(
|
|
55
|
+
overrides: Partial<RpcContext> = {},
|
|
56
|
+
): {
|
|
57
|
+
context: RpcContext;
|
|
58
|
+
capture: { value: LastChildMeta };
|
|
59
|
+
childMock: Mock<(meta: Record<string, unknown>) => Logger>;
|
|
60
|
+
} {
|
|
61
|
+
const capture: { value: LastChildMeta } = { value: undefined };
|
|
62
|
+
const childMock = mock((meta: Record<string, unknown>): Logger => {
|
|
63
|
+
capture.value = meta;
|
|
64
|
+
return {
|
|
65
|
+
info: mock(),
|
|
66
|
+
error: mock(),
|
|
67
|
+
warn: mock(),
|
|
68
|
+
debug: mock(),
|
|
69
|
+
};
|
|
70
|
+
});
|
|
71
|
+
const baseContext = createMockRpcContext(overrides);
|
|
72
|
+
// Replace the logger so child() captures the bound metadata.
|
|
73
|
+
baseContext.logger = {
|
|
74
|
+
info: mock(),
|
|
75
|
+
error: mock(),
|
|
76
|
+
warn: mock(),
|
|
77
|
+
debug: mock(),
|
|
78
|
+
child: childMock,
|
|
79
|
+
};
|
|
80
|
+
return { context: baseContext, capture, childMock };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const UUID_V4_REGEX =
|
|
84
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
85
|
+
|
|
86
|
+
describe("correlationMiddleware", () => {
|
|
87
|
+
it("generates a UUID v4 when no x-correlation-id header is present", async () => {
|
|
88
|
+
const { context, capture } = buildContextWithCapture();
|
|
89
|
+
const router = buildRouterCapturingChildMeta(capture);
|
|
90
|
+
|
|
91
|
+
await call(router.echo, {}, { context });
|
|
92
|
+
|
|
93
|
+
expect(typeof capture.value?.correlationId).toBe("string");
|
|
94
|
+
expect(capture.value?.correlationId).toMatch(UUID_V4_REGEX);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("uses the incoming x-correlation-id header when present", async () => {
|
|
98
|
+
const incoming = "11111111-2222-4333-8444-555555555555";
|
|
99
|
+
const { context, capture } = buildContextWithCapture({
|
|
100
|
+
requestHeaders: new Headers({ [CORRELATION_ID_HEADER]: incoming }),
|
|
101
|
+
});
|
|
102
|
+
const router = buildRouterCapturingChildMeta(capture);
|
|
103
|
+
|
|
104
|
+
await call(router.echo, {}, { context });
|
|
105
|
+
|
|
106
|
+
expect(capture.value?.correlationId).toBe(incoming);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("ignores an empty x-correlation-id header and generates a fresh ID", async () => {
|
|
110
|
+
const { context, capture } = buildContextWithCapture({
|
|
111
|
+
requestHeaders: new Headers({ [CORRELATION_ID_HEADER]: "" }),
|
|
112
|
+
});
|
|
113
|
+
const router = buildRouterCapturingChildMeta(capture);
|
|
114
|
+
|
|
115
|
+
await call(router.echo, {}, { context });
|
|
116
|
+
|
|
117
|
+
expect(capture.value?.correlationId).toMatch(UUID_V4_REGEX);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("echoes the correlation ID on responseHeaders when available", async () => {
|
|
121
|
+
const responseHeaders = new Headers();
|
|
122
|
+
const { context, capture } = buildContextWithCapture({
|
|
123
|
+
responseHeaders,
|
|
124
|
+
});
|
|
125
|
+
const router = buildRouterCapturingChildMeta(capture);
|
|
126
|
+
|
|
127
|
+
await call(router.echo, {}, { context });
|
|
128
|
+
|
|
129
|
+
const echoed = responseHeaders.get(CORRELATION_ID_HEADER);
|
|
130
|
+
expect(echoed).not.toBeNull();
|
|
131
|
+
expect(echoed).toBe(capture.value?.correlationId as string);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("binds pluginId from context.pluginMetadata onto the child logger", async () => {
|
|
135
|
+
const { context, capture } = buildContextWithCapture({
|
|
136
|
+
pluginMetadata: { pluginId: "fancy-plugin" },
|
|
137
|
+
});
|
|
138
|
+
const router = buildRouterCapturingChildMeta(capture);
|
|
139
|
+
|
|
140
|
+
await call(router.echo, {}, { context });
|
|
141
|
+
|
|
142
|
+
expect(capture.value?.pluginId).toBe("fancy-plugin");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("binds userId when the user has an id field", async () => {
|
|
146
|
+
const { context, capture } = buildContextWithCapture({
|
|
147
|
+
user: { type: "user", id: "user-abc", accessRules: ["*"] },
|
|
148
|
+
});
|
|
149
|
+
const router = buildRouterCapturingChildMeta(capture);
|
|
150
|
+
|
|
151
|
+
await call(router.echo, {}, { context });
|
|
152
|
+
|
|
153
|
+
expect(capture.value?.userId).toBe("user-abc");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("omits userId when the user is a service (no id field)", async () => {
|
|
157
|
+
const { context, capture } = buildContextWithCapture({
|
|
158
|
+
user: { type: "service", pluginId: "internal-svc" },
|
|
159
|
+
});
|
|
160
|
+
const router = buildRouterCapturingChildMeta(capture);
|
|
161
|
+
|
|
162
|
+
await call(router.echo, {}, { context });
|
|
163
|
+
|
|
164
|
+
expect("userId" in (capture.value ?? {})).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("falls back to the base logger when child() is not implemented", async () => {
|
|
168
|
+
// Reproduce a minimal mock logger without `.child` (e.g. a test mock
|
|
169
|
+
// that hasn't been updated). The middleware must not throw and the
|
|
170
|
+
// request must still complete.
|
|
171
|
+
const context = createMockRpcContext();
|
|
172
|
+
context.logger = {
|
|
173
|
+
info: mock(),
|
|
174
|
+
error: mock(),
|
|
175
|
+
warn: mock(),
|
|
176
|
+
debug: mock(),
|
|
177
|
+
// intentionally no child
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const os = implement(testContract)
|
|
181
|
+
.$context<RpcContext>()
|
|
182
|
+
.use(correlationMiddleware)
|
|
183
|
+
.use(autoAuthMiddleware);
|
|
184
|
+
const router = os.router({
|
|
185
|
+
echo: os.echo.handler(() => ({})),
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const result = await call(router.echo, {}, { context });
|
|
189
|
+
expect(result).toEqual({});
|
|
190
|
+
});
|
|
191
|
+
});
|
package/src/rpc.ts
CHANGED
|
@@ -53,6 +53,21 @@ export interface RpcContext {
|
|
|
53
53
|
cacheManager: CacheManager;
|
|
54
54
|
/** Emit a hook event for cross-plugin communication */
|
|
55
55
|
emitHook: EmitHookFn;
|
|
56
|
+
/**
|
|
57
|
+
* Inbound HTTP request headers (read-only view). Populated by the
|
|
58
|
+
* `/api/*` and `/rest/*` Hono handlers in `core/backend`. Optional
|
|
59
|
+
* because non-HTTP call sites (S2S clients, tests, scheduled queue
|
|
60
|
+
* jobs) can construct an `RpcContext` without a backing request.
|
|
61
|
+
*/
|
|
62
|
+
requestHeaders?: Headers;
|
|
63
|
+
/**
|
|
64
|
+
* Mutable response headers. Middleware (e.g. `correlationMiddleware`)
|
|
65
|
+
* can set headers here, and the Hono handler that drives the oRPC
|
|
66
|
+
* `RPCHandler` / `OpenAPIHandler` merges them onto the actual
|
|
67
|
+
* `Response` after the procedure has run. Optional for the same
|
|
68
|
+
* reason as `requestHeaders`.
|
|
69
|
+
*/
|
|
70
|
+
responseHeaders?: Headers;
|
|
56
71
|
}
|
|
57
72
|
|
|
58
73
|
/** Context with authenticated real user */
|
|
@@ -461,6 +476,91 @@ export const autoAuthMiddleware = os.middleware(
|
|
|
461
476
|
},
|
|
462
477
|
);
|
|
463
478
|
|
|
479
|
+
// =============================================================================
|
|
480
|
+
// CORRELATION ID MIDDLEWARE
|
|
481
|
+
// =============================================================================
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Name of the inbound and outbound HTTP header that carries the correlation
|
|
485
|
+
* ID. Exported so dev tools, integration tests, and front-end fetch wrappers
|
|
486
|
+
* can refer to the canonical value rather than hard-coding the string.
|
|
487
|
+
*/
|
|
488
|
+
export const CORRELATION_ID_HEADER = "x-correlation-id";
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Per-request observability middleware.
|
|
492
|
+
*
|
|
493
|
+
* Behaviour:
|
|
494
|
+
* - Reads `x-correlation-id` from `context.requestHeaders` (populated by the
|
|
495
|
+
* `/api/*` and `/rest/*` Hono handlers in `core/backend`).
|
|
496
|
+
* - Generates a fresh UUID v4 via `crypto.randomUUID()` if absent. This is
|
|
497
|
+
* the ONLY generation site for correlation IDs in the platform — handlers
|
|
498
|
+
* must NOT mint new IDs on their own.
|
|
499
|
+
* - Binds `{ correlationId, pluginId, userId? }` onto a child logger via
|
|
500
|
+
* `ctx.logger.child(...)` so every subsequent log line in the request
|
|
501
|
+
* carries that metadata automatically.
|
|
502
|
+
* - Writes the ID back to `context.responseHeaders` (if available) so the
|
|
503
|
+
* outer Hono handler can echo `x-correlation-id` on the response, letting
|
|
504
|
+
* the caller correlate their own client-side trace to the server log.
|
|
505
|
+
*
|
|
506
|
+
* Note on the echo: oRPC middleware has no direct access to the outgoing
|
|
507
|
+
* `Response` object — the framework constructs it from the procedure's
|
|
508
|
+
* return value AFTER middleware has finished. We use the mutable
|
|
509
|
+
* `responseHeaders` bag on `RpcContext` as a thin write-through: the Hono
|
|
510
|
+
* route handler merges those headers onto the `Response` post-handle. When
|
|
511
|
+
* an `RpcContext` is constructed without `responseHeaders` (S2S clients,
|
|
512
|
+
* tests), the echo silently no-ops; the ID is still bound to the child
|
|
513
|
+
* logger so server-side correlation still works.
|
|
514
|
+
*
|
|
515
|
+
* Order matters: in plugin routers, `.use(correlationMiddleware)` MUST
|
|
516
|
+
* appear BEFORE `.use(autoAuthMiddleware)` so that auth failures still log
|
|
517
|
+
* with the correlation ID attached.
|
|
518
|
+
*
|
|
519
|
+
* Usage:
|
|
520
|
+
*
|
|
521
|
+
* const os = implement(myContract)
|
|
522
|
+
* .$context<RpcContext>()
|
|
523
|
+
* .use(correlationMiddleware)
|
|
524
|
+
* .use(autoAuthMiddleware);
|
|
525
|
+
*/
|
|
526
|
+
export const correlationMiddleware = os.middleware(
|
|
527
|
+
async ({ next, context }) => {
|
|
528
|
+
const incoming = context.requestHeaders?.get(CORRELATION_ID_HEADER);
|
|
529
|
+
const correlationId =
|
|
530
|
+
incoming && incoming.length > 0 ? incoming : crypto.randomUUID();
|
|
531
|
+
|
|
532
|
+
context.responseHeaders?.set(CORRELATION_ID_HEADER, correlationId);
|
|
533
|
+
|
|
534
|
+
const meta: Record<string, unknown> = {
|
|
535
|
+
correlationId,
|
|
536
|
+
pluginId: context.pluginMetadata.pluginId,
|
|
537
|
+
};
|
|
538
|
+
if (context.user && "id" in context.user) {
|
|
539
|
+
meta.userId = context.user.id;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// `child` is optional on the Logger interface so minimal test-mock
|
|
543
|
+
// loggers don't have to implement it. Production Winston loggers
|
|
544
|
+
// always do; gracefully fall back to the base logger otherwise so
|
|
545
|
+
// the middleware never breaks a request just because metadata
|
|
546
|
+
// binding wasn't possible.
|
|
547
|
+
const boundLogger = context.logger.child
|
|
548
|
+
? context.logger.child(meta)
|
|
549
|
+
: context.logger;
|
|
550
|
+
|
|
551
|
+
// Partial-merge style (no `...context` spread): oRPC merges the
|
|
552
|
+
// returned context fields onto the existing context. Spreading
|
|
553
|
+
// would widen TypeScript's inferred chain type and surface
|
|
554
|
+
// TS2883 "inferred type cannot be named" errors in downstream
|
|
555
|
+
// packages whose routers compose this middleware.
|
|
556
|
+
return next({
|
|
557
|
+
context: {
|
|
558
|
+
logger: boundLogger,
|
|
559
|
+
},
|
|
560
|
+
});
|
|
561
|
+
},
|
|
562
|
+
);
|
|
563
|
+
|
|
464
564
|
/**
|
|
465
565
|
* Extract a nested value from an object using dot notation.
|
|
466
566
|
* E.g., getNestedValue({ params: { id: "123" } }, "params.id") => "123"
|
package/src/test-utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { mock } from "bun:test";
|
|
2
2
|
import { RpcContext, EmitHookFn } from "./rpc";
|
|
3
|
+
import { Logger } from "./types";
|
|
3
4
|
import { SafeDatabase } from "./plugin-system";
|
|
4
5
|
import { HealthCheckRegistry } from "./health-check";
|
|
5
6
|
import { CollectorRegistry } from "./collector-registry";
|
|
@@ -9,6 +10,22 @@ import {
|
|
|
9
10
|
CacheManager,
|
|
10
11
|
} from "@checkstack/cache-api";
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Build a mock `Logger` whose `.child(...)` returns another mock logger
|
|
15
|
+
* (recursively). Matches the structural contract that
|
|
16
|
+
* `correlationMiddleware` and other binding sites rely on.
|
|
17
|
+
*/
|
|
18
|
+
function createMockLogger(): Logger {
|
|
19
|
+
const logger: Logger = {
|
|
20
|
+
info: mock(),
|
|
21
|
+
error: mock(),
|
|
22
|
+
warn: mock(),
|
|
23
|
+
debug: mock(),
|
|
24
|
+
child: mock(() => createMockLogger()),
|
|
25
|
+
};
|
|
26
|
+
return logger;
|
|
27
|
+
}
|
|
28
|
+
|
|
12
29
|
/**
|
|
13
30
|
* Creates a mocked oRPC context for testing.
|
|
14
31
|
* By default provides an authenticated user with wildcard access.
|
|
@@ -19,12 +36,7 @@ export function createMockRpcContext(
|
|
|
19
36
|
return {
|
|
20
37
|
pluginMetadata: { pluginId: "test-plugin" },
|
|
21
38
|
db: mock() as unknown as SafeDatabase<Record<string, unknown>>,
|
|
22
|
-
logger:
|
|
23
|
-
info: mock(),
|
|
24
|
-
error: mock(),
|
|
25
|
-
warn: mock(),
|
|
26
|
-
debug: mock(),
|
|
27
|
-
},
|
|
39
|
+
logger: createMockLogger(),
|
|
28
40
|
fetch: {
|
|
29
41
|
fetch: mock(),
|
|
30
42
|
forPlugin: mock().mockReturnValue({
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,45 @@
|
|
|
1
1
|
import { ZodSchema } from "zod";
|
|
2
2
|
import { ClientDefinition, InferClient } from "@checkstack/common";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Backend logger interface used everywhere in the platform via `RpcContext.logger`
|
|
6
|
+
* and the various `coreServices.logger` accessors.
|
|
7
|
+
*
|
|
8
|
+
* Each method accepts a free-form trailing argument list (`...args: unknown[]`)
|
|
9
|
+
* so the long-standing varargs callsites — `logger.error("…", err)` where `err`
|
|
10
|
+
* is an `Error`, or `logger.info("…", value1, value2)` — keep working unchanged.
|
|
11
|
+
*
|
|
12
|
+
* For NEW code, prefer the structured-metadata shape:
|
|
13
|
+
*
|
|
14
|
+
* logger.info("did something", { userId, durationMs });
|
|
15
|
+
*
|
|
16
|
+
* Winston's `splat` handling treats a single trailing plain object as
|
|
17
|
+
* structured metadata (merged into the log entry), and an `Error` instance as
|
|
18
|
+
* a special-cased error (with stack). Either shape lands in the same vararg
|
|
19
|
+
* slot here, so this signature covers both without overload churn.
|
|
20
|
+
*
|
|
21
|
+
* Auto-injected metadata (when the request flows through
|
|
22
|
+
* `correlationMiddleware`): `{ correlationId, pluginId, userId? }`. Do NOT
|
|
23
|
+
* include secrets in the structured-metadata object — it is forwarded
|
|
24
|
+
* verbatim to the log destination.
|
|
25
|
+
*/
|
|
4
26
|
export interface Logger {
|
|
5
27
|
info(message: string, ...args: unknown[]): void;
|
|
6
28
|
error(message: string, ...args: unknown[]): void;
|
|
7
29
|
warn(message: string, ...args: unknown[]): void;
|
|
8
30
|
debug(message: string, ...args: unknown[]): void;
|
|
31
|
+
/**
|
|
32
|
+
* Returns a derived logger with the supplied metadata bound to every
|
|
33
|
+
* subsequent log entry. Used by `correlationMiddleware` to attach
|
|
34
|
+
* `{ correlationId, pluginId, userId? }`, and available to handlers that
|
|
35
|
+
* want a tighter scope (e.g. `ctx.logger.child({ jobId })`).
|
|
36
|
+
*
|
|
37
|
+
* Optional only to keep minimal test-mock logger objects compatible with
|
|
38
|
+
* this interface — production loggers (Winston via `core/backend`) always
|
|
39
|
+
* implement it. Call sites that rely on metadata binding should branch
|
|
40
|
+
* on presence and fall back to the base logger when it is not available.
|
|
41
|
+
*/
|
|
42
|
+
child?(meta: Record<string, unknown>): Logger;
|
|
9
43
|
}
|
|
10
44
|
|
|
11
45
|
export interface Fetch {
|