UNPKG

@neodx/log

Version:

A lightweight universal logging framework

1 lines 12.6 kB
{"version":3,"file":"index.cjs","sources":["../../src/http/index.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-explicit-any */\nimport type { Colors } from '@neodx/colors';\nimport { colors as defaultColors } from '@neodx/colors';\nimport { identity } from '@neodx/std';\nimport type { IncomingMessage, OutgoingMessage } from 'http';\nimport type { Logger } from '../core/types';\nimport { createLogger } from '../node';\nimport {\n createRequestIdGenerator,\n formatIncomingMessage,\n formatOutgoingMessageStatus,\n formatResponseTime,\n HTTP_LOG_START_TIME_SYMBOL\n} from './utils';\n\nexport interface HttpLoggerParams<\n Req extends IncomingMessage = IncomingMessage,\n Res extends OutgoingMessage = OutgoingMessage\n> {\n /**\n * Custom logger instance.\n * @default createLogger()\n */\n logger?: Logger<HttpLogLevels>;\n /**\n * Custom colors instance\n * @see `@neodx/colors`\n */\n colors?: Colors;\n /**\n * If `true`, the logger will only log the pre-formatted message without any additional metadata.\n * @default process.env.NODE_ENV === 'development'\n */\n simple?: boolean;\n /**\n * Optional function to extract/create request ID.\n * @default built-in simple safe number counter\n */\n getRequestId?: (req: Req, res: Res) => string | number;\n\n // ===\n // Metadata and formatting\n // ===\n\n /**\n * Extract shared metadata for every produced log\n */\n getMeta?: (req: Req, res: Res) => Record<string, unknown>;\n /**\n * Extract metadata for request logs\n */\n getRequestMeta?: (ctx: HttpResponseContext<Req, Res>) => Record<string, unknown>;\n /**\n * Custom incoming request message formatter\n */\n getRequestMessage?: (ctx: HttpResponseContext<Req, Res>) => string;\n /**\n * Extract metadata for success response logs\n */\n getResponseMeta?: (ctx: HttpResponseContext<Req, Res>) => Record<string, unknown>;\n /**\n * Custom success response message formatter\n */\n getResponseMessage?: (ctx: HttpResponseContext<Req, Res>) => string;\n /**\n * Extract metadata for error response logs\n */\n getErrorMeta?: (ctx: HttpResponseContext<Req, Res>) => Record<string, unknown>;\n /**\n * Custom error response message formatter\n */\n getErrorMessage?: (ctx: HttpResponseContext<Req, Res>) => string;\n\n // ===\n // Control logging behavior\n // ===\n\n /**\n * Whether to log anything at all.\n * @default true\n */\n shouldLog?: boolean | ((req: Req, res: Res) => boolean);\n /**\n * Prevents logging of errors.\n * @default true\n */\n shouldLogError?: boolean | ((ctx: HttpResponseContext<Req, Res>) => boolean);\n /**\n * Prevents built-in logging of requests.\n * DISABLED BY DEFAULT, because it can be very verbose.\n * @default false\n */\n shouldLogRequest?: boolean | ((ctx: HttpResponseContext<Req, Res>) => boolean);\n /**\n * Prevents built-in logging of responses.\n * @default true\n */\n shouldLogResponse?: boolean | ((ctx: HttpResponseContext<Req, Res>) => boolean);\n}\n\nexport interface HttpLoggerMetaKeys {\n req: string;\n res: string;\n err: string;\n requestId: string;\n responseTime: string;\n}\n\nexport interface HttpResponseContext<\n Req extends IncomingMessage = IncomingMessage,\n Res extends OutgoingMessage = OutgoingMessage\n> {\n req: Req;\n res: Res;\n error?: Error;\n logger: Logger<HttpLogLevels>;\n colors: Colors;\n responseTime: number;\n}\n\nexport type HttpRequestId = string | number;\nexport type HttpLogLevels = 'debug' | 'error' | 'info' | 'done';\n\ndeclare module 'http' {\n interface IncomingMessage {\n id: HttpRequestId;\n log: Logger<HttpLogLevels>;\n }\n\n interface ServerResponse {\n err?: Error | undefined;\n }\n\n interface OutgoingMessage {\n [HTTP_LOG_START_TIME_SYMBOL]: number;\n }\n}\n\nexport function createHttpLogger<\n Req extends IncomingMessage = IncomingMessage,\n Res extends OutgoingMessage = OutgoingMessage\n>({\n simple = process.env.NODE_ENV === 'development',\n colors = defaultColors,\n logger: rootLogger = createLogger(),\n getRequestId = createRequestIdGenerator(),\n\n getMeta,\n getErrorMeta,\n getErrorMessage = simple ? simpleErrorMessage : defaultErrorMessage,\n getRequestMeta,\n getRequestMessage = defaultRequestMessage,\n getResponseMeta,\n getResponseMessage = defaultResponseMessage,\n\n shouldLog = true,\n shouldLogError = true,\n shouldLogRequest = false,\n shouldLogResponse = true\n}: HttpLoggerParams<Req, Res> = {}) {\n function handleEnd(ctx: HttpResponseContext<Req, Res>) {\n const { error, logger, res, responseTime } = ctx;\n const err =\n error ||\n (res as any).err ||\n ((res as any).statusCode >= 500 &&\n new Error('failed with status code ' + (res as any).statusCode));\n\n if (err) {\n if (!condition(shouldLogError, ctx)) return;\n const errorMeta = getErrorMeta?.(ctx) ?? {\n err,\n res,\n responseTime\n };\n\n if (simple) {\n logger.error({ err }, getErrorMessage(ctx));\n logger.debug(errorMeta, 'Error details:');\n return;\n }\n logger.error(errorMeta, getErrorMessage(ctx));\n return;\n }\n if (!condition(shouldLogResponse, ctx)) return;\n const responseMeta = getResponseMeta?.(ctx) ?? {\n res,\n responseTime\n };\n if (simple) {\n logger.done(getResponseMessage(ctx));\n logger.debug(responseMeta, 'Response details:');\n return;\n }\n logger.done(responseMeta, getResponseMessage(ctx));\n }\n\n return function httpLogger(req: Req, res: Res, next?: () => void) {\n if (!condition(shouldLog, req, res)) return next?.();\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n req.id ??= getRequestId(req, res);\n\n const startTime = Date.now();\n const logger = rootLogger.fork({\n meta: {\n ...rootLogger.meta,\n ...getMeta?.(req, res),\n ...(!simple && {\n requestId: req.id,\n req\n })\n }\n });\n const baseContext = { req, res, logger, colors, responseTime: 0 };\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n req.log ??= logger;\n res[HTTP_LOG_START_TIME_SYMBOL] = startTime;\n\n const handleResponse = (error?: Error) => {\n res.removeListener('finish', handleResponse);\n res.removeListener('close', handleResponse);\n res.removeListener('error', handleResponse);\n return handleEnd({\n ...baseContext,\n error,\n responseTime: Date.now() - startTime\n });\n };\n\n res.on('error', handleResponse);\n res.on('close', handleResponse);\n res.on('finish', handleResponse);\n\n if (condition(shouldLogRequest, baseContext)) {\n if (!simple) {\n logger.info(getRequestMeta?.(baseContext) ?? {}, getRequestMessage(baseContext));\n } else {\n logger.info(getRequestMessage(baseContext));\n if (getRequestMeta) {\n logger.debug(getRequestMeta(baseContext), 'Request details:');\n }\n }\n }\n next?.();\n };\n}\n\nconst createMessageFormatter =\n (\n fn: (requestMessage: string, ctx: HttpResponseContext) => string,\n { ignoreWritableEnded = false, delimiter = (_: HttpResponseContext): string => ' ' } = {}\n ) =>\n (ctx: HttpResponseContext) => {\n const requestMessage = formatIncomingMessage(ctx.req, ctx.colors, delimiter(ctx));\n\n return ignoreWritableEnded || ctx.res.writableEnded\n ? fn(requestMessage, ctx)\n : `(aborted) ${requestMessage}`;\n };\n\nconst defaultRequestMessage = createMessageFormatter(identity, {\n ignoreWritableEnded: true\n});\nconst defaultResponseMessage = createMessageFormatter(\n (msg, ctx) => `${ctx.colors.greenBright(formatResponseTime(ctx.responseTime))} ${msg}`\n);\nconst defaultErrorMessage = createMessageFormatter(\n (msg, ctx) => `${ctx.colors.redBright(formatResponseTime(ctx.responseTime))} ${msg}`\n);\nconst simpleErrorMessage = createMessageFormatter(\n (msg, ctx) => `${ctx.colors.redBright(formatResponseTime(ctx.responseTime))} ${msg}`,\n {\n delimiter: ({ colors, res }) => colors.italic(` (${formatOutgoingMessageStatus(res)}) `)\n }\n);\n\nconst condition = <Args extends unknown[]>(\n condition: boolean | ((...args: Args) => boolean),\n ...args: Args\n) => (typeof condition === 'function' ? condition(...args) : condition);\n"],"names":["createMessageFormatter","fn","ignoreWritableEnded","delimiter","_","ctx","requestMessage","formatIncomingMessage","req","colors","res","writableEnded","defaultRequestMessage","identity","defaultResponseMessage","msg","greenBright","formatResponseTime","responseTime","defaultErrorMessage","redBright","simpleErrorMessage","italic","formatOutgoingMessageStatus","condition","args","simple","process","env","NODE_ENV","defaultColors","logger","rootLogger","createLogger","getRequestId","createRequestIdGenerator","getMeta","getErrorMeta","getErrorMessage","getRequestMeta","getRequestMessage","getResponseMeta","getResponseMessage","shouldLog","shouldLogError","shouldLogRequest","shouldLogResponse","next","id","startTime","Date","now","fork","meta","requestId","baseContext","log","HTTP_LOG_START_TIME_SYMBOL","handleResponse","error","removeListener","handleEnd","err","statusCode","Error","errorMeta","debug","responseMeta","done","on","info"],"mappings":"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"}