@beignet/core
Version:
Core framework primitives for Beignet
186 lines (176 loc) • 4.48 kB
text/typescript
/**
* Logging hook utilities for @beignet/core/server
*/
import type { HttpContractConfig } from "../../contracts/index.js";
import {
resolveTrustedRequest,
type TrustedRequestInfo,
} from "../trusted-proxy.js";
import type { HttpRequestLike, ServerHook } from "../types.js";
import { getRequestIdFromContext } from "./utils.js";
/**
* Minimal logger shape accepted by `createLoggingHooks(...)`.
*/
export interface Logger {
/**
* Log an informational event.
*/
info: (...args: unknown[]) => void;
/**
* Log an error event.
*/
error: (...args: unknown[]) => void;
/**
* Log a warning event.
*/
warn?: (...args: unknown[]) => void;
/**
* Log a debug event.
*/
debug?: (...args: unknown[]) => void;
}
/**
* Logging hook configuration.
*/
export interface LoggingConfig<Ctx> {
/**
* Logger used when custom lifecycle callbacks are not provided.
*/
logger?: Logger;
/**
* Response header name used to expose `ctx.requestId` when present.
*/
requestIdHeader?: string;
/**
* Custom request-start observer.
*/
onRequestStart?: (args: {
ctx?: Ctx;
req: HttpRequestLike;
requestInfo: TrustedRequestInfo;
contract?: HttpContractConfig;
}) => void;
/**
* Custom request-end observer.
*/
onRequestEnd?: (args: {
ctx?: Ctx;
req: HttpRequestLike;
requestInfo: TrustedRequestInfo;
res: { status: number; headers: Record<string, string> };
durationMs: number;
contract?: HttpContractConfig;
error?: unknown;
}) => void;
}
function loggingRequestInfo(
req: HttpRequestLike,
requestInfo: TrustedRequestInfo | undefined,
): TrustedRequestInfo {
return requestInfo ?? resolveTrustedRequest(req);
}
/**
* Create request logging hooks.
*
* Logging observer errors are intentionally ignored so logging cannot break
* request handling.
*/
export function createLoggingHooks<Ctx>(
config: LoggingConfig<Ctx>,
): ServerHook<Ctx> {
const requestIdHeader = config.requestIdHeader;
return {
name: "logging",
onRequest: ({ req, requestInfo, contract }) => {
if (config.onRequestStart) {
try {
config.onRequestStart({
req,
requestInfo: loggingRequestInfo(req, requestInfo),
contract,
ctx: undefined,
});
} catch {
// Ignore logging errors
}
return undefined;
}
if (!config.logger) return undefined;
try {
config.logger.info(
{
method: req.method,
url: loggingRequestInfo(req, requestInfo).url.pathname,
},
"Request start",
);
} catch {
// Ignore logging errors
}
return undefined;
},
...(requestIdHeader
? {
beforeSend: ({ ctx, response }) => {
const requestId = getRequestIdFromContext(ctx);
if (!requestId) {
return response;
}
return {
...response,
headers: {
...(response.headers ?? {}),
[requestIdHeader]: String(requestId),
},
};
},
}
: {}),
afterSend: ({
ctx,
req,
requestInfo,
response,
durationMs,
contract,
error,
}) => {
const headers = response.headers ?? {};
if (config.onRequestEnd) {
try {
config.onRequestEnd({
ctx,
req,
requestInfo: loggingRequestInfo(req, requestInfo),
res: { status: response.status, headers },
durationMs,
contract,
error,
});
} catch {
// Ignore logging errors
}
return;
}
if (!config.logger) return;
try {
const requestId = getRequestIdFromContext(ctx);
const payload = {
method: req.method,
url: loggingRequestInfo(req, requestInfo).url.pathname,
status: response.status,
durationMs: Math.round(durationMs),
...(requestId !== undefined ? { requestId } : {}),
...(error !== undefined ? { error } : {}),
};
if (error !== undefined) {
config.logger.error(payload, "Request complete with error");
return;
}
config.logger.info(payload, "Request complete");
} catch {
// Ignore logging errors
}
},
};
}