UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

186 lines (176 loc) 4.48 kB
/** * 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 } }, }; }