mirror of
https://github.com/SMNETSTUDIO/WeChat-AI.git
synced 2026-08-22 11:03:43 +08:00
119 lines
4.5 KiB
TypeScript
119 lines
4.5 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { LogController } from "fastify";
|
|
import type {
|
|
FastifyInstance,
|
|
FastifyRequest,
|
|
FastifyServerOptions,
|
|
} from "fastify";
|
|
import type { AppConfig } from "./config.js";
|
|
import { describeRequest, logPath } from "./request-log.js";
|
|
|
|
/**
|
|
* Silences Fastify's own "incoming request" / "request completed" pair while
|
|
* leaving every error path intact.
|
|
*
|
|
* The obvious `disableRequestLogging: true` cannot be used: it is routed through
|
|
* `isLogDisabled`, which `defaultErrorLog`, `streamError`, `writeHeadError` and
|
|
* `serializerError` all consult first — so it silences framework error logging
|
|
* as well, which is the single most valuable thing the logger does. (It is also
|
|
* deprecated in Fastify 5 and gone in 6.) Overriding just the two noisy methods
|
|
* gets the quiet request log without giving up error reporting.
|
|
*/
|
|
class QuietRequestLogController extends LogController {
|
|
override incomingRequest(): void {}
|
|
override requestCompleted(): void {}
|
|
}
|
|
|
|
/**
|
|
* Fastify construction options and the request-logging hook.
|
|
*
|
|
* Lives outside index.ts because index.ts self-invokes `main()` and so cannot
|
|
* be imported — and because pino throws at construction on a bad level or
|
|
* redact path, which makes this the one config in the process where a typo is a
|
|
* boot failure. Having it here means a test can build a real instance with it.
|
|
*/
|
|
|
|
export interface ServerOptionsExtras {
|
|
/** Test seam: pino destination. Omit in production to write to stdout. */
|
|
logStream?: NodeJS.WritableStream;
|
|
}
|
|
|
|
export function buildFastifyOptions(
|
|
cfg: AppConfig,
|
|
extras: ServerOptionsExtras = {},
|
|
): FastifyServerOptions {
|
|
return {
|
|
// This used to be `cfg.logLevel === "debug"`, which meant the shipped
|
|
// default of LOG_LEVEL=info produced `logger: false` — no request logs, no
|
|
// latency, and none of Fastify's own error logging. A route that threw left
|
|
// nothing behind but a 500 on the wire.
|
|
logger: {
|
|
level: cfg.logLevel,
|
|
serializers: {
|
|
/**
|
|
* Fastify's built-in `req` serializer emits `url` verbatim — query
|
|
* string and all — and `LogController.defaultErrorLog` logs
|
|
* `{ req, res, err }` on every 5xx. That means a 500 on
|
|
* `/api/v1/auth/callback?code=…&state=…` would write a live single-use
|
|
* OAuth credential to the log, defeating the whole point of logPath().
|
|
* Redacting headers does not help: the built-in serializer never emits
|
|
* headers in the first place. So replace it and strip the query here.
|
|
*/
|
|
req: (req: FastifyRequest) => ({
|
|
method: req.method,
|
|
path: logPath(req.url),
|
|
host: req.host,
|
|
remoteAddress: req.ip,
|
|
}),
|
|
},
|
|
// Belt and braces: the serializer above emits no headers, but any code
|
|
// that logs a header bag explicitly must still not leak credentials.
|
|
redact: {
|
|
paths: [
|
|
"req.headers.cookie",
|
|
"req.headers.authorization",
|
|
'req.headers["x-api-key"]',
|
|
'res.headers["set-cookie"]',
|
|
],
|
|
remove: true,
|
|
},
|
|
...(extras.logStream ? { stream: extras.logStream } : {}),
|
|
},
|
|
// Fastify's own pair of lines per request carries no latency and floods on
|
|
// health probes; registerRequestLogging emits one useful line instead.
|
|
// Errors still log — see QuietRequestLogController.
|
|
logController: new QuietRequestLogController(),
|
|
genReqId: () => randomUUID().slice(0, 8),
|
|
bodyLimit: 1024 * 1024,
|
|
requestTimeout: 60_000,
|
|
connectionTimeout: 30_000,
|
|
// Do NOT enable trustProxy: origins are reachable directly by IP, so a
|
|
// forged X-Forwarded-For would bypass the login and CDN rate limiters.
|
|
// clientIp() reads cf-connecting-ip explicitly instead.
|
|
};
|
|
}
|
|
|
|
/**
|
|
* One line per completed request. Rules (quiet paths, slow-request warnings,
|
|
* and stripping credential-bearing query strings) live in request-log.ts.
|
|
*/
|
|
export function registerRequestLogging(
|
|
app: FastifyInstance,
|
|
cfg: AppConfig,
|
|
): void {
|
|
app.addHook("onResponse", async (req, reply) => {
|
|
const line = describeRequest({
|
|
method: req.method,
|
|
url: req.url,
|
|
status: reply.statusCode,
|
|
elapsedMs: reply.elapsedTime,
|
|
cfConnectingIp: req.headers["cf-connecting-ip"] as string | undefined,
|
|
socketIp: req.ip,
|
|
slowMs: cfg.logSlowRequestMs,
|
|
});
|
|
if (!line) return;
|
|
const { level, ...fields } = line;
|
|
req.log[level](fields, "request");
|
|
});
|
|
}
|