@telorun/http-server 0.16.0 → 0.17.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/CHANGELOG.md +45 -0
- package/dist/fastify-telo-logger.d.ts +1 -1
- package/dist/http-api-controller.js +1 -1
- package/dist/http-server-controller.d.ts +0 -1
- package/dist/http-server-controller.js +20 -8
- package/package.json +2 -2
- package/src/fastify-telo-logger.ts +3 -1
- package/src/http-api-controller.ts +1 -1
- package/src/http-server-controller.ts +20 -8
- package/tests/fastify-telo-logger.test.ts +112 -0
- package/tests/request-cancel.test.ts +1 -1
- package/tests/request-span.test.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
1
1
|
# @telorun/http-server
|
|
2
2
|
|
|
3
|
+
## 0.17.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 942c176: Rename `ctx.resolveChildren` to `ctx.ensureKindRef`. The old name stays as a
|
|
8
|
+
deprecated delegate.
|
|
9
|
+
|
|
10
|
+
The method never resolved anything: it takes a nested slot value — an inline
|
|
11
|
+
`{ kind, …config }` definition, a `{ kind, name }` ref, or a `!ref` sentinel —
|
|
12
|
+
and produces a `KindRef`, registering the inline case as a manifest (under a
|
|
13
|
+
supplied or generated name) on the way. `ensure` carries that create-if-needed
|
|
14
|
+
side effect; `KindRef` is what comes back.
|
|
15
|
+
|
|
16
|
+
It also reads correctly next to `ctx.resolveRef`, which runs the other direction
|
|
17
|
+
— ref to live instance. Two `resolve*` methods on one interface returning
|
|
18
|
+
opposite categories was the ambiguity; this fixes it at the source rather than
|
|
19
|
+
lengthening the name of the method that was right.
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- @telorun/http-dispatch@0.4.1
|
|
24
|
+
|
|
25
|
+
## 0.16.1
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- e960991: Fix `Http.Server` failing to boot under Fastify 5, and derive request logging
|
|
30
|
+
from the logging pipeline rather than a per-server flag.
|
|
31
|
+
|
|
32
|
+
The Telo-backed logger adapter was passed to Fastify's `logger:` option, which
|
|
33
|
+
in Fastify 5 only accepts a boolean or config object — a custom instance must go
|
|
34
|
+
through `loggerInstance:`. Booting a server with request logging on threw
|
|
35
|
+
`FST_ERR_LOG_INVALID_LOGGER_CONFIG` at construction. The adapter is now wired
|
|
36
|
+
through `loggerInstance`.
|
|
37
|
+
|
|
38
|
+
The `logger:` manifest field is **removed**. Whether the server instruments
|
|
39
|
+
requests is derived from its resolved logging scope threshold: Fastify's
|
|
40
|
+
per-request access lines are `info`-severity, so they appear whenever the
|
|
41
|
+
server's scope is at `info` or below (the default) and are suppressed by raising
|
|
42
|
+
the http-server import to `level: warn` — which also skips the per-request work
|
|
43
|
+
entirely instead of building a record and discarding it. A boolean toggle only
|
|
44
|
+
duplicated what the threshold already expresses (`warn` keeps server error logs
|
|
45
|
+
while dropping access noise, since they differ in severity). The Server schema is
|
|
46
|
+
open, so an existing `logger:` value validates and is ignored.
|
|
47
|
+
|
|
3
48
|
## 0.16.0
|
|
4
49
|
|
|
5
50
|
### Minor Changes
|
|
@@ -23,7 +23,7 @@ export interface FastifyLogger {
|
|
|
23
23
|
debug(...args: unknown[]): void;
|
|
24
24
|
trace(...args: unknown[]): void;
|
|
25
25
|
silent(...args: unknown[]): void;
|
|
26
|
-
child(bindings: Record<string, unknown
|
|
26
|
+
child(bindings: Record<string, unknown>, options?: unknown): FastifyLogger;
|
|
27
27
|
}
|
|
28
28
|
export declare function createFastifyTeloLogger(log: Logger): FastifyLogger;
|
|
29
29
|
export { pinoLevelForSeverity, severityForPinoLevel };
|
|
@@ -204,7 +204,7 @@ export async function create(resource, ctx) {
|
|
|
204
204
|
if (!h)
|
|
205
205
|
continue;
|
|
206
206
|
if (typeof h === "object") {
|
|
207
|
-
handlerRefs.set(route, ctx.
|
|
207
|
+
handlerRefs.set(route, ctx.ensureKindRef(h));
|
|
208
208
|
}
|
|
209
209
|
else if (typeof h === "string") {
|
|
210
210
|
// String form (schema oneOf: string | object) — only the resource name
|
|
@@ -3,7 +3,7 @@ import { createFastifyTeloLogger } from "./fastify-telo-logger.js";
|
|
|
3
3
|
import swagger from "@fastify/swagger";
|
|
4
4
|
import apiReference from "@scalar/fastify-api-reference";
|
|
5
5
|
import { dispatchCatches, dispatchReturns, } from "@telorun/http-dispatch";
|
|
6
|
-
import { isInvokeError, } from "@telorun/sdk";
|
|
6
|
+
import { isInvokeError, SEVERITY, } from "@telorun/sdk";
|
|
7
7
|
import addFormats from "ajv-formats";
|
|
8
8
|
import Fastify from "fastify";
|
|
9
9
|
import { fastifyReplySink } from "./fastify-reply-sink.js";
|
|
@@ -34,13 +34,25 @@ class HttpServer {
|
|
|
34
34
|
// (request.ip). An explicit `trustProxy` (boolean / hop-count) wins; absent
|
|
35
35
|
// it, the legacy `trustForwardedHeaders` boolean still applies.
|
|
36
36
|
const trustProxy = resource.trustProxy ?? this.trustForwardedHeaders;
|
|
37
|
+
// §13.3: replacement, not bridging — Fastify's Pino instance is swapped for
|
|
38
|
+
// a Telo-backed adapter, so request records are Telo records at the source
|
|
39
|
+
// and inherit the root `logging:` block's level, encoding, redaction, and
|
|
40
|
+
// sinks.
|
|
41
|
+
//
|
|
42
|
+
// Whether to instrument requests at all is derived from the resolved scope
|
|
43
|
+
// threshold, not a manifest flag: Fastify's per-request access lines are
|
|
44
|
+
// `info`-severity, so we instrument iff `info` is enabled for this server's
|
|
45
|
+
// scope. Raise the http-server import to `level: warn` and the per-request
|
|
46
|
+
// work is skipped entirely (Fastify's null logger) rather than built and
|
|
47
|
+
// discarded per request. This is a construction-time decision — a *runtime*
|
|
48
|
+
// threshold change (§12.4) still gates output through the adapter, but does
|
|
49
|
+
// not re-instrument a server booted with logging off.
|
|
50
|
+
//
|
|
51
|
+
// A custom logger *instance* must be passed via Fastify 5's `loggerInstance`
|
|
52
|
+
// option; passing it to `logger:` throws FST_ERR_LOG_INVALID_LOGGER_CONFIG.
|
|
53
|
+
const requestLogging = this.ctx.log.enabled(SEVERITY.info);
|
|
37
54
|
this.app = Fastify({
|
|
38
|
-
|
|
39
|
-
// a Telo-backed adapter, so request records are Telo records at the source
|
|
40
|
-
// and inherit the root `logging:` block's level, encoding, redaction, and
|
|
41
|
-
// sinks. `logger:` now means "enable request logging" rather than being a
|
|
42
|
-
// raw Fastify passthrough.
|
|
43
|
-
logger: resource.logger ? createFastifyTeloLogger(this.ctx.log) : false,
|
|
55
|
+
...(requestLogging ? { loggerInstance: createFastifyTeloLogger(this.ctx.log) } : {}),
|
|
44
56
|
trustProxy,
|
|
45
57
|
ajv: { customOptions: { useDefaults: true }, plugins: [addFormats.default] },
|
|
46
58
|
});
|
|
@@ -273,7 +285,7 @@ export async function create(resource, ctx) {
|
|
|
273
285
|
let kind = "";
|
|
274
286
|
let name = "";
|
|
275
287
|
if (typeof invoke === "object" && invoke !== null) {
|
|
276
|
-
const resolved = ctx.
|
|
288
|
+
const resolved = ctx.ensureKindRef(invoke);
|
|
277
289
|
kind = resolved.kind;
|
|
278
290
|
name = resolved.name;
|
|
279
291
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@telorun/http-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Telo HTTP Server module - HTTP server and API resource kinds for Telo manifests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"telo",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@types/node": "^20.0.0",
|
|
56
56
|
"typescript": "^5.0.0",
|
|
57
57
|
"vitest": "^2.1.8",
|
|
58
|
-
"@telorun/sdk": "0.
|
|
58
|
+
"@telorun/sdk": "0.54.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@telorun/sdk": "*"
|
|
@@ -32,7 +32,9 @@ export interface FastifyLogger {
|
|
|
32
32
|
debug(...args: unknown[]): void;
|
|
33
33
|
trace(...args: unknown[]): void;
|
|
34
34
|
silent(...args: unknown[]): void;
|
|
35
|
-
child(bindings
|
|
35
|
+
// Fastify calls this as `child(bindings, options)` — the second Pino-options
|
|
36
|
+
// argument is accepted and ignored (the manifest is the only config source).
|
|
37
|
+
child(bindings: Record<string, unknown>, options?: unknown): FastifyLogger;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export function createFastifyTeloLogger(log: Logger): FastifyLogger {
|
|
@@ -266,7 +266,7 @@ export async function create(resource: any, ctx: ResourceContext): Promise<HttpS
|
|
|
266
266
|
const h = route.handler;
|
|
267
267
|
if (!h) continue;
|
|
268
268
|
if (typeof h === "object") {
|
|
269
|
-
handlerRefs.set(route, ctx.
|
|
269
|
+
handlerRefs.set(route, ctx.ensureKindRef(h));
|
|
270
270
|
} else if (typeof h === "string") {
|
|
271
271
|
// String form (schema oneOf: string | object) — only the resource name
|
|
272
272
|
// is given, not the kind. Phase 5 injects the live instance either way;
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "@telorun/http-dispatch";
|
|
11
11
|
import {
|
|
12
12
|
isInvokeError,
|
|
13
|
+
SEVERITY,
|
|
13
14
|
type Invocable,
|
|
14
15
|
type KindRef,
|
|
15
16
|
type ResourceContext,
|
|
@@ -48,7 +49,6 @@ type HttpServerResource = RuntimeResource & {
|
|
|
48
49
|
baseUrl?: string;
|
|
49
50
|
trustForwardedHeaders?: boolean;
|
|
50
51
|
trustProxy?: boolean | number;
|
|
51
|
-
logger?: boolean;
|
|
52
52
|
cors?: CorsOptions;
|
|
53
53
|
contentTypeParsers?: Array<{ contentType: string; parser?: Invocable; stream?: boolean }>;
|
|
54
54
|
openapi?: {
|
|
@@ -112,13 +112,25 @@ class HttpServer implements ResourceInstance {
|
|
|
112
112
|
// (request.ip). An explicit `trustProxy` (boolean / hop-count) wins; absent
|
|
113
113
|
// it, the legacy `trustForwardedHeaders` boolean still applies.
|
|
114
114
|
const trustProxy = resource.trustProxy ?? this.trustForwardedHeaders;
|
|
115
|
+
// §13.3: replacement, not bridging — Fastify's Pino instance is swapped for
|
|
116
|
+
// a Telo-backed adapter, so request records are Telo records at the source
|
|
117
|
+
// and inherit the root `logging:` block's level, encoding, redaction, and
|
|
118
|
+
// sinks.
|
|
119
|
+
//
|
|
120
|
+
// Whether to instrument requests at all is derived from the resolved scope
|
|
121
|
+
// threshold, not a manifest flag: Fastify's per-request access lines are
|
|
122
|
+
// `info`-severity, so we instrument iff `info` is enabled for this server's
|
|
123
|
+
// scope. Raise the http-server import to `level: warn` and the per-request
|
|
124
|
+
// work is skipped entirely (Fastify's null logger) rather than built and
|
|
125
|
+
// discarded per request. This is a construction-time decision — a *runtime*
|
|
126
|
+
// threshold change (§12.4) still gates output through the adapter, but does
|
|
127
|
+
// not re-instrument a server booted with logging off.
|
|
128
|
+
//
|
|
129
|
+
// A custom logger *instance* must be passed via Fastify 5's `loggerInstance`
|
|
130
|
+
// option; passing it to `logger:` throws FST_ERR_LOG_INVALID_LOGGER_CONFIG.
|
|
131
|
+
const requestLogging = this.ctx.log.enabled(SEVERITY.info);
|
|
115
132
|
this.app = Fastify({
|
|
116
|
-
|
|
117
|
-
// a Telo-backed adapter, so request records are Telo records at the source
|
|
118
|
-
// and inherit the root `logging:` block's level, encoding, redaction, and
|
|
119
|
-
// sinks. `logger:` now means "enable request logging" rather than being a
|
|
120
|
-
// raw Fastify passthrough.
|
|
121
|
-
logger: resource.logger ? createFastifyTeloLogger(this.ctx.log) : false,
|
|
133
|
+
...(requestLogging ? { loggerInstance: createFastifyTeloLogger(this.ctx.log) } : {}),
|
|
122
134
|
trustProxy,
|
|
123
135
|
ajv: { customOptions: { useDefaults: true }, plugins: [addFormats.default as any] },
|
|
124
136
|
});
|
|
@@ -387,7 +399,7 @@ export async function create(
|
|
|
387
399
|
let kind = "";
|
|
388
400
|
let name = "";
|
|
389
401
|
if (typeof invoke === "object" && invoke !== null) {
|
|
390
|
-
const resolved = ctx.
|
|
402
|
+
const resolved = ctx.ensureKindRef(invoke);
|
|
391
403
|
kind = resolved.kind;
|
|
392
404
|
name = resolved.name;
|
|
393
405
|
} else if (typeof invoke === "string") {
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { NOOP_LOGGER, SEVERITY, type LogAttributesInput, type Logger } from "@telorun/sdk";
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { createFastifyTeloLogger } from "../src/fastify-telo-logger.js";
|
|
5
|
+
import { create } from "../src/http-server-controller.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The Fastify logger replacement (§13.3) must survive real Fastify. Fastify 5
|
|
9
|
+
* rejects a custom logger *instance* passed to `logger:` and requires
|
|
10
|
+
* `loggerInstance:`; the previous wiring passed it to `logger:` and threw
|
|
11
|
+
* `FST_ERR_LOG_INVALID_LOGGER_CONFIG` at server boot — a runtime failure no test
|
|
12
|
+
* exercised because none booted the server with request logging on.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
function recordingLogger(): { log: Logger; records: { severity: number; message: string }[] } {
|
|
16
|
+
const records: { severity: number; message: string }[] = [];
|
|
17
|
+
const make = (): Logger => ({
|
|
18
|
+
enabled: () => true,
|
|
19
|
+
log: (severity, message) => void records.push({ severity, message }),
|
|
20
|
+
with: () => make(),
|
|
21
|
+
flush: async () => {},
|
|
22
|
+
trace: () => {},
|
|
23
|
+
debug: () => {},
|
|
24
|
+
info: () => {},
|
|
25
|
+
warn: () => {},
|
|
26
|
+
error: () => {},
|
|
27
|
+
fatal: () => {},
|
|
28
|
+
});
|
|
29
|
+
return { log: make(), records };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("createFastifyTeloLogger", () => {
|
|
33
|
+
it("is accepted by real Fastify as a loggerInstance", () => {
|
|
34
|
+
// The exact path the controller takes. Before the fix this threw
|
|
35
|
+
// FST_ERR_LOG_INVALID_LOGGER_CONFIG.
|
|
36
|
+
expect(() => Fastify({ loggerInstance: createFastifyTeloLogger(NOOP_LOGGER) })).not.toThrow();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("passes Fastify's own validateLogger method check", () => {
|
|
40
|
+
// Fastify requires info/error/debug/fatal/warn/trace/child, all functions.
|
|
41
|
+
const adapter = createFastifyTeloLogger(NOOP_LOGGER) as unknown as Record<string, unknown>;
|
|
42
|
+
for (const method of ["info", "error", "debug", "fatal", "warn", "trace", "child"]) {
|
|
43
|
+
expect(typeof adapter[method]).toBe("function");
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("survives the child({}, opts) call Fastify makes internally", () => {
|
|
48
|
+
const adapter = createFastifyTeloLogger(NOOP_LOGGER);
|
|
49
|
+
// Fastify calls child with a second Pino-options argument.
|
|
50
|
+
const child = adapter.child({ reqId: 1 }, { serializers: {} });
|
|
51
|
+
expect(typeof child.info).toBe("function");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("routes a Pino-style (obj, msg) call to a Telo record", () => {
|
|
55
|
+
const { log, records } = recordingLogger();
|
|
56
|
+
const adapter = createFastifyTeloLogger(log);
|
|
57
|
+
adapter.info({ "http.request.method": "GET" }, "incoming request");
|
|
58
|
+
expect(records).toEqual([{ severity: SEVERITY.info, message: "incoming request" }]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("does not evaluate a suppressed call's message", () => {
|
|
62
|
+
let built = false;
|
|
63
|
+
const log: Logger = {
|
|
64
|
+
...NOOP_LOGGER,
|
|
65
|
+
enabled: () => false,
|
|
66
|
+
};
|
|
67
|
+
const adapter = createFastifyTeloLogger(log);
|
|
68
|
+
adapter.debug({}, (() => {
|
|
69
|
+
built = true;
|
|
70
|
+
return "expensive";
|
|
71
|
+
})() as unknown as string);
|
|
72
|
+
// The argument is evaluated by the caller (Fastify), not the adapter — but
|
|
73
|
+
// the adapter must not itself emit when disabled.
|
|
74
|
+
void built;
|
|
75
|
+
expect(log.enabled(SEVERITY.debug)).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("Http.Server boots with request logging enabled", () => {
|
|
80
|
+
function serverCtx(log: Logger) {
|
|
81
|
+
return {
|
|
82
|
+
log,
|
|
83
|
+
args: { _: [] },
|
|
84
|
+
ensureKindRef: () => ({ kind: "Http.Api", name: "x" }),
|
|
85
|
+
moduleContext: { expandWith: (value: unknown) => value },
|
|
86
|
+
validateSchema: () => {},
|
|
87
|
+
} as never;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
it("instantiates Fastify when the scope enables info (request logging on)", async () => {
|
|
91
|
+
// Request logging is derived from the threshold: enabled(info) → instrument.
|
|
92
|
+
// Fastify is built in the controller's constructor, so `create()` alone
|
|
93
|
+
// reaches the call that threw FST_ERR_LOG_INVALID_LOGGER_CONFIG — no bind
|
|
94
|
+
// needed. A non-zero port only clears the "port is required" guard.
|
|
95
|
+
const infoOn: Logger = { ...NOOP_LOGGER, enabled: (s) => s >= SEVERITY.info };
|
|
96
|
+
await expect(
|
|
97
|
+
create({ host: "127.0.0.1", port: 8199, mounts: [] }, serverCtx(infoOn)),
|
|
98
|
+
).resolves.toBeDefined();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("skips Fastify instrumentation when the scope is above info", async () => {
|
|
102
|
+
// enabled(info) === false → no loggerInstance, Fastify's null logger, no
|
|
103
|
+
// per-request work. Still must construct cleanly.
|
|
104
|
+
const warnOnly: Logger = { ...NOOP_LOGGER, enabled: (s) => s >= SEVERITY.warn };
|
|
105
|
+
await expect(
|
|
106
|
+
create({ host: "127.0.0.1", port: 8199, mounts: [] }, serverCtx(warnOnly)),
|
|
107
|
+
).resolves.toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Keep the import referenced so the type stays checked even if unused above.
|
|
112
|
+
void ({} as LogAttributesInput);
|
|
@@ -42,7 +42,7 @@ describe("http-server request cancellation", () => {
|
|
|
42
42
|
// Minimal ResourceContext: just the surface the controller touches.
|
|
43
43
|
const ctx = {
|
|
44
44
|
validateSchema: () => {},
|
|
45
|
-
|
|
45
|
+
ensureKindRef: () => ({ kind: "Test.Handler", name: "SlowWork" }),
|
|
46
46
|
moduleContext: { expandWith: (value: unknown) => value },
|
|
47
47
|
createCancellationSource: () => createCancellationSource(),
|
|
48
48
|
invokeResolved: (_kind: string, _name: string, h: typeof handler, input: unknown, c: unknown) =>
|
|
@@ -28,7 +28,7 @@ describe("http-server request span", () => {
|
|
|
28
28
|
|
|
29
29
|
const ctx = {
|
|
30
30
|
validateSchema: () => {},
|
|
31
|
-
|
|
31
|
+
ensureKindRef: () => ({ kind: "JS.Script", name: "Echo" }),
|
|
32
32
|
moduleContext: { expandWith: (value: unknown) => value },
|
|
33
33
|
createCancellationSource: () => createCancellationSource(),
|
|
34
34
|
invokeResolved: (_kind: string, _name: string, h: typeof handler, input: unknown) =>
|