@telorun/http-server 0.16.0 → 0.16.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 CHANGED
@@ -1,5 +1,28 @@
1
1
  # @telorun/http-server
2
2
 
3
+ ## 0.16.1
4
+
5
+ ### Patch Changes
6
+
7
+ - e960991: Fix `Http.Server` failing to boot under Fastify 5, and derive request logging
8
+ from the logging pipeline rather than a per-server flag.
9
+
10
+ The Telo-backed logger adapter was passed to Fastify's `logger:` option, which
11
+ in Fastify 5 only accepts a boolean or config object — a custom instance must go
12
+ through `loggerInstance:`. Booting a server with request logging on threw
13
+ `FST_ERR_LOG_INVALID_LOGGER_CONFIG` at construction. The adapter is now wired
14
+ through `loggerInstance`.
15
+
16
+ The `logger:` manifest field is **removed**. Whether the server instruments
17
+ requests is derived from its resolved logging scope threshold: Fastify's
18
+ per-request access lines are `info`-severity, so they appear whenever the
19
+ server's scope is at `info` or below (the default) and are suppressed by raising
20
+ the http-server import to `level: warn` — which also skips the per-request work
21
+ entirely instead of building a record and discarding it. A boolean toggle only
22
+ duplicated what the threshold already expresses (`warn` keeps server error logs
23
+ while dropping access noise, since they differ in severity). The Server schema is
24
+ open, so an existing `logger:` value validates and is ignored.
25
+
3
26
  ## 0.16.0
4
27
 
5
28
  ### 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>): FastifyLogger;
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 };
@@ -27,7 +27,6 @@ type HttpServerResource = RuntimeResource & {
27
27
  baseUrl?: string;
28
28
  trustForwardedHeaders?: boolean;
29
29
  trustProxy?: boolean | number;
30
- logger?: boolean;
31
30
  cors?: CorsOptions;
32
31
  contentTypeParsers?: Array<{
33
32
  contentType: string;
@@ -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
- // §13.3: replacement, not bridging. Fastify's Pino instance is swapped for
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@telorun/http-server",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Telo HTTP Server module - HTTP server and API resource kinds for Telo manifests.",
5
5
  "keywords": [
6
6
  "telo",
@@ -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: Record<string, unknown>): FastifyLogger;
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 {
@@ -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
- // §13.3: replacement, not bridging. Fastify's Pino instance is swapped for
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
  });
@@ -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
+ resolveChildren: () => ({ 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);