capitalsix-logger 1.0.0
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/dist/index.cjs +203 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +163 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ExpressLogger: () => ExpressLogger,
|
|
34
|
+
Logger: () => Logger,
|
|
35
|
+
loggerMiddleware: () => loggerMiddleware,
|
|
36
|
+
loggerOptions: () => loggerOptions
|
|
37
|
+
});
|
|
38
|
+
module.exports = __toCommonJS(index_exports);
|
|
39
|
+
|
|
40
|
+
// src/logger/logger.ts
|
|
41
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
42
|
+
|
|
43
|
+
// src/logger/getIpAddress.ts
|
|
44
|
+
var getIpAddress = (req) => {
|
|
45
|
+
const forwardedFor = req.headers["x-forwarded-for"];
|
|
46
|
+
const realIp = req.headers["x-real-ip"];
|
|
47
|
+
let ipAddress = req.ip;
|
|
48
|
+
if (typeof forwardedFor === "string") {
|
|
49
|
+
ipAddress = forwardedFor.split(",")[0].trim();
|
|
50
|
+
} else if (realIp) {
|
|
51
|
+
ipAddress = realIp;
|
|
52
|
+
}
|
|
53
|
+
return ipAddress;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/logger/logger.ts
|
|
57
|
+
var HeaderColor = "\x1B[38;2;128;128;128m";
|
|
58
|
+
var UserColor = "\x1B[38;2;255;255;0m";
|
|
59
|
+
var ParameterColor = "\x1B[38;2;215;0;215m";
|
|
60
|
+
var IpColor = "\x1B[38;2;0;255;0m";
|
|
61
|
+
var LevelIndicators = {
|
|
62
|
+
info: "INF",
|
|
63
|
+
warn: "WRN",
|
|
64
|
+
error: "ERR"
|
|
65
|
+
};
|
|
66
|
+
var replaceParameters = (message, useColors, data) => {
|
|
67
|
+
let dataIndex = 0;
|
|
68
|
+
const coloredMessage = message.replace(/%[sdj]/g, (match) => {
|
|
69
|
+
if (dataIndex >= data.length) {
|
|
70
|
+
return match;
|
|
71
|
+
}
|
|
72
|
+
const value = data[dataIndex++];
|
|
73
|
+
switch (match) {
|
|
74
|
+
case "%s":
|
|
75
|
+
return useColors ? `${ParameterColor}${String(value)}\x1B[0m` : `${String(value)}`;
|
|
76
|
+
case "%d":
|
|
77
|
+
return useColors ? `${ParameterColor}${Number(value).toString()}\x1B[0m` : `${Number(value).toString()}`;
|
|
78
|
+
case "%j":
|
|
79
|
+
try {
|
|
80
|
+
return useColors ? `${ParameterColor}${JSON.stringify(value)}\x1B[0m` : `${JSON.stringify(value)}`;
|
|
81
|
+
} catch {
|
|
82
|
+
return "[Circular]";
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
return match;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return coloredMessage;
|
|
89
|
+
};
|
|
90
|
+
var uncolorMessage = (message) => {
|
|
91
|
+
return message.replace(/(\x1b|\u001b)\[[0-9;]*m/g, "");
|
|
92
|
+
};
|
|
93
|
+
var formatLogMessage = (messageBuilder, logLevel, message, data) => {
|
|
94
|
+
const now = /* @__PURE__ */ new Date();
|
|
95
|
+
const nowString = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}.${now.getMilliseconds().toString().padStart(3, "0")}`;
|
|
96
|
+
const levelIndicator = LevelIndicators[logLevel];
|
|
97
|
+
const coloredParameters = replaceParameters(message, true, data);
|
|
98
|
+
const coloredMessage = messageBuilder(nowString, levelIndicator, coloredParameters);
|
|
99
|
+
return coloredMessage;
|
|
100
|
+
};
|
|
101
|
+
var loggerOptions = {};
|
|
102
|
+
var Logger = class {
|
|
103
|
+
/** Creates a logger optionally bound to an incoming request. */
|
|
104
|
+
constructor() {
|
|
105
|
+
const now = /* @__PURE__ */ new Date();
|
|
106
|
+
this.logFilename = `./log-${now.getUTCFullYear()}-${(now.getUTCMonth() + 1).toString().padStart(2, "0")}-${now.getUTCDate().toString().padStart(2, "0")}.log`;
|
|
107
|
+
}
|
|
108
|
+
/** Writes a structured message to the console and daily log file. */
|
|
109
|
+
getFormattedMessage(logLevel, message, ...data) {
|
|
110
|
+
return formatLogMessage((timeStamp, levelIndicator, message2) => `${HeaderColor}[${timeStamp} ${levelIndicator}]\x1B[0m ${message2}`, logLevel, message, data);
|
|
111
|
+
}
|
|
112
|
+
/** Writes a structured message to the console and daily log file. */
|
|
113
|
+
log(logLevel, message, ...data) {
|
|
114
|
+
const formattedMessage = this.getFormattedMessage(logLevel, message, ...data);
|
|
115
|
+
switch (logLevel) {
|
|
116
|
+
case "info":
|
|
117
|
+
console.info(formattedMessage);
|
|
118
|
+
import_fs.default.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + "\n");
|
|
119
|
+
break;
|
|
120
|
+
case "warn":
|
|
121
|
+
console.warn(formattedMessage);
|
|
122
|
+
import_fs.default.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + "\n");
|
|
123
|
+
break;
|
|
124
|
+
case "error":
|
|
125
|
+
console.error(formattedMessage);
|
|
126
|
+
import_fs.default.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + "\n");
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Writes an informational message. */
|
|
131
|
+
info(message, ...data) {
|
|
132
|
+
this.log("info", message, ...data);
|
|
133
|
+
}
|
|
134
|
+
/** Writes a warning message. */
|
|
135
|
+
warn(message, ...data) {
|
|
136
|
+
this.log("warn", message, ...data);
|
|
137
|
+
}
|
|
138
|
+
/** Writes an error message. */
|
|
139
|
+
error(message, ...data) {
|
|
140
|
+
this.log("error", message, ...data);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
var ExpressLogger = class extends Logger {
|
|
144
|
+
/** Creates a logger optionally bound to an incoming request. */
|
|
145
|
+
constructor(req, getUsername) {
|
|
146
|
+
super();
|
|
147
|
+
if (req) {
|
|
148
|
+
this.bindRequest(req);
|
|
149
|
+
}
|
|
150
|
+
this.getUsername = getUsername;
|
|
151
|
+
}
|
|
152
|
+
/** Associates request identity, user, and IP details with this logger. */
|
|
153
|
+
bindRequest(req) {
|
|
154
|
+
this.req = req;
|
|
155
|
+
this.ipAddress = ` ${getIpAddress(this.req)}`;
|
|
156
|
+
}
|
|
157
|
+
getUserName() {
|
|
158
|
+
return this.getUsername && this.req ? this.getUsername(this.req) : void 0;
|
|
159
|
+
}
|
|
160
|
+
getFormattedMessage(logLevel, message, ...data) {
|
|
161
|
+
const ipAddress = this.ipAddress ?? "";
|
|
162
|
+
let userName = this.getUserName();
|
|
163
|
+
userName = userName ? ` ${userName}` : "";
|
|
164
|
+
return formatLogMessage(
|
|
165
|
+
(timeStamp, levelIndicator, message2) => `${HeaderColor}[${timeStamp} ${levelIndicator}${IpColor}${ipAddress}${UserColor}${userName}${HeaderColor}]\x1B[0m ${message2}`,
|
|
166
|
+
logLevel,
|
|
167
|
+
message,
|
|
168
|
+
data
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/logger/loggerMiddleware.ts
|
|
174
|
+
var loggerMiddleware = (getUsername) => {
|
|
175
|
+
return (req, res, next) => {
|
|
176
|
+
const logger = new ExpressLogger(req, getUsername);
|
|
177
|
+
req.logger = logger;
|
|
178
|
+
const requestInfo = { method: req.method, url: req.originalUrl };
|
|
179
|
+
try {
|
|
180
|
+
logger.info("HTTP %s request started: %s", requestInfo.method, requestInfo.url);
|
|
181
|
+
res.on("finish", () => {
|
|
182
|
+
logger.info(
|
|
183
|
+
"HTTP %s request ended with status %d: %s",
|
|
184
|
+
requestInfo.method,
|
|
185
|
+
res.statusCode,
|
|
186
|
+
requestInfo.url
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
next();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
logger.error("HTTP %s request failed: %s\n %j", requestInfo.method, requestInfo.url, error);
|
|
192
|
+
throw error;
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
197
|
+
0 && (module.exports = {
|
|
198
|
+
ExpressLogger,
|
|
199
|
+
Logger,
|
|
200
|
+
loggerMiddleware,
|
|
201
|
+
loggerOptions
|
|
202
|
+
});
|
|
203
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/logger/logger.ts","../src/logger/getIpAddress.ts","../src/logger/loggerMiddleware.ts"],"sourcesContent":["/// <reference path=\"./types/index.d.ts\" />\n// \nexport * from './logger';","/* eslint-disable @typescript-eslint/no-unused-vars */\r\n\r\nimport express from 'express';\r\nimport { Request } from 'express';\r\nimport { ParamsDictionary, Query } from 'express-serve-static-core';\r\nimport fs from 'fs';\r\n\r\nimport { ILogger, LogLevel } from '../interfaces/ILogger';\r\nimport { getIpAddress } from './getIpAddress';\r\n\r\nconst HeaderColor = '\\u001B[38;2;128;128;128m';\r\nconst UserColor = '\\u001B[38;2;255;255;0m';\r\nconst ParameterColor = '\\u001B[38;2;215;0;215m';\r\n// Keep the short level labels stable for both console and file output.\r\nconst IpColor = '\\u001B[38;2;0;255;0m';\r\n\r\nconst LevelIndicators: Record<LogLevel, string> = {\r\n info: 'INF',\r\n warn: 'WRN',\r\n error: 'ERR',\r\n};\r\n\r\n// Replaces the supported printf-style placeholders while optionally applying colors.\r\nconst replaceParameters = (message: string, useColors: boolean, data: unknown[]): string => {\r\n let dataIndex = 0;\r\n const coloredMessage = message.replace(/%[sdj]/g, (match) => {\r\n if (dataIndex >= data.length) {\r\n return match;\r\n }\r\n const value = data[dataIndex++];\r\n switch (match) {\r\n case '%s':\r\n return useColors ? `${ParameterColor}${String(value)}\\u001B[0m` : `${String(value)}`;\r\n case '%d':\r\n return useColors ? `${ParameterColor}${Number(value).toString()}\\u001B[0m` : `${Number(value).toString()}`;\r\n case '%j':\r\n try {\r\n return useColors ? `${ParameterColor}${JSON.stringify(value)}\\u001B[0m` : `${JSON.stringify(value)}`;\r\n } catch {\r\n return '[Circular]';\r\n }\r\n default:\r\n return match;\r\n }\r\n });\r\n return coloredMessage;\r\n};\r\n\r\nconst uncolorMessage = (message: string): string => {\r\n return message.replace(/(\\x1b|\\u001b)\\[[0-9;]*m/g, '');\r\n};\r\n\r\ntype MessageBuilder = (timeStamp: string, levelIndicator: string, message: string) => string;\r\n\r\n// Builds the colored console form and the plain-text file form of a log message.\r\nconst formatLogMessage = (messageBuilder: MessageBuilder, logLevel: LogLevel, message: string, data: unknown[]): string => {\r\n const now = new Date();\r\n const nowString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}.${now.getMilliseconds().toString().padStart(3, '0')}`;\r\n const levelIndicator = LevelIndicators[logLevel];\r\n const coloredParameters = replaceParameters(message, true, data);\r\n const coloredMessage = messageBuilder(nowString, levelIndicator, coloredParameters);\r\n return coloredMessage;\r\n};\r\n\r\nexport interface ILoggerOptions {\r\n}\r\n\r\nexport const loggerOptions: ILoggerOptions = {\r\n};\r\n\r\nexport class Logger implements ILogger {\r\n private logFilename: string;\r\n\r\n /** Creates a logger optionally bound to an incoming request. */\r\n constructor() {\r\n const now = new Date();\r\n // Use one UTC log file per day so entries remain grouped across server time zones.\r\n this.logFilename = `./log-${now.getUTCFullYear()}-${(now.getUTCMonth() + 1).toString().padStart(2, '0')}-${now.getUTCDate().toString().padStart(2, '0')}.log`;\r\n }\r\n\r\n /** Writes a structured message to the console and daily log file. */\r\n getFormattedMessage(logLevel: LogLevel, message: string, ...data: unknown[]): string {\r\n return formatLogMessage((timeStamp, levelIndicator, message) =>\r\n `${HeaderColor}[${timeStamp} ${levelIndicator}]\\u001B[0m ${message}`, logLevel, message, data);\r\n }\r\n\r\n /** Writes a structured message to the console and daily log file. */\r\n log(logLevel: LogLevel, message: string, ...data: unknown[]): void {\r\n const formattedMessage = this.getFormattedMessage(logLevel, message, ...data);\r\n\r\n // Mirror the severity to the console and persist the uncolored form to disk.\r\n switch (logLevel) {\r\n case 'info':\r\n console.info(formattedMessage);\r\n fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + '\\n');\r\n break;\r\n\r\n case 'warn':\r\n console.warn(formattedMessage);\r\n fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + '\\n');\r\n break;\r\n\r\n case 'error':\r\n console.error(formattedMessage);\r\n fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + '\\n');\r\n break;\r\n }\r\n }\r\n\r\n /** Writes an informational message. */\r\n info(message: string, ...data: unknown[]): void {\r\n this.log('info', message, ...data);\r\n }\r\n\r\n /** Writes a warning message. */\r\n warn(message: string, ...data: unknown[]): void {\r\n this.log('warn', message, ...data);\r\n }\r\n\r\n /** Writes an error message. */\r\n error(message: string, ...data: unknown[]): void {\r\n this.log('error', message, ...data);\r\n }\r\n}\r\n\r\nexport type GetUserNameFunction<P = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>>\r\n = (req: Request<P, ResBody, ReqBody, ReqQuery, Locals>) => string | undefined;\r\n\r\nexport class ExpressLogger<P = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>>\r\n extends Logger {\r\n private req?: Request<P, ResBody, ReqBody, ReqQuery, Locals>;\r\n private ipAddress?: string;\r\n private getUsername?: GetUserNameFunction<P, ResBody, ReqBody, ReqQuery, Locals>;\r\n\r\n /** Creates a logger optionally bound to an incoming request. */\r\n constructor(\r\n req?: Request<P, ResBody, ReqBody, ReqQuery, Locals>,\r\n getUsername?: GetUserNameFunction<P, ResBody, ReqBody, ReqQuery, Locals>) {\r\n super();\r\n if (req) {\r\n this.bindRequest(req);\r\n }\r\n this.getUsername = getUsername;\r\n }\r\n\r\n /** Associates request identity, user, and IP details with this logger. */\r\n bindRequest(req: Request<P, ResBody, ReqBody, ReqQuery, Locals>): void {\r\n this.req = req;\r\n // Prefer proxy-provided addresses, then fall back to the direct request IP.\r\n this.ipAddress = ` ${getIpAddress(this.req as express.Request)}`;\r\n }\r\n\r\n getUserName(): string | undefined {\r\n // Placeholder for user name retrieval logic. Replace with actual implementation.\r\n return this.getUsername && this.req ? this.getUsername(this.req) : undefined;\r\n }\r\n\r\n getFormattedMessage(logLevel: LogLevel, message: string, ...data: unknown[]): string {\r\n const ipAddress = this.ipAddress ?? '';\r\n let userName = this.getUserName();\r\n\r\n userName = userName ? ` ${userName}` : '';\r\n\r\n return formatLogMessage(\r\n (timeStamp, levelIndicator, message) => `${HeaderColor}[${timeStamp} ${levelIndicator}${IpColor}${ipAddress}${UserColor}${userName}${HeaderColor}]\\x1b[0m ${message}`,\r\n logLevel,\r\n message,\r\n data);\r\n }\r\n};\r\n","import express from 'express';\n\nexport const getIpAddress = (req: express.Request): string | undefined => {\n const forwardedFor = req.headers['x-forwarded-for'];\n const realIp = req.headers['x-real-ip'] as string | undefined;\n let ipAddress = req.ip;\n if (typeof forwardedFor === 'string') {\n ipAddress = forwardedFor.split(',')[0].trim();\n } else if (realIp) {\n ipAddress = realIp;\n }\n\n return ipAddress;\n};\n","import express from 'express';\r\nimport { ExpressLogger, GetUserNameFunction } from './logger.js';\r\n\r\ntype LoggerMiddleware = (getUsername?: GetUserNameFunction) => express.RequestHandler;\r\n\r\n/** Creates a request logger and records request completion status. */\r\nexport const loggerMiddleware: LoggerMiddleware = (getUsername) => {\r\n return (req, res, next) => {\r\n // Create one logger instance so all downstream handlers share request context.\r\n const logger = new ExpressLogger(req, getUsername);\r\n\r\n req.logger = logger; // Initialize the logger for this request\r\n\r\n const requestInfo = { method: req.method, url: req.originalUrl };\r\n\r\n try {\r\n // Record the start immediately and the final status when Express finishes.\r\n logger.info('HTTP %s request started: %s', requestInfo.method, requestInfo.url);\r\n res.on('finish', () => {\r\n logger.info(\r\n 'HTTP %s request ended with status %d: %s',\r\n requestInfo.method,\r\n res.statusCode,\r\n requestInfo.url);\r\n });\r\n next();\r\n } catch (error) {\r\n logger.error('HTTP %s request failed: %s\\n %j', requestInfo.method, requestInfo.url, error);\r\n throw error;\r\n }\r\n };\r\n};\r\n"],"mappings":"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","names":["message","fs"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import express, { Request } from 'express';
|
|
2
|
+
import { ParamsDictionary, Query } from 'express-serve-static-core';
|
|
3
|
+
|
|
4
|
+
type LogLevel = 'info' | 'warn' | 'error';
|
|
5
|
+
/** Logger contract used by request-aware server components. */
|
|
6
|
+
interface ILogger {
|
|
7
|
+
/** Writes a message at the requested severity. */
|
|
8
|
+
log(logLevel: LogLevel, message: string, ...data: unknown[]): void;
|
|
9
|
+
/** Writes an informational message. */
|
|
10
|
+
info(message: string, ...data: unknown[]): void;
|
|
11
|
+
/** Writes a warning message. */
|
|
12
|
+
warn(message: string, ...data: unknown[]): void;
|
|
13
|
+
/** Writes an error message. */
|
|
14
|
+
error(message: string, ...data: unknown[]): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ILoggerOptions {
|
|
18
|
+
}
|
|
19
|
+
declare const loggerOptions: ILoggerOptions;
|
|
20
|
+
declare class Logger implements ILogger {
|
|
21
|
+
private logFilename;
|
|
22
|
+
/** Creates a logger optionally bound to an incoming request. */
|
|
23
|
+
constructor();
|
|
24
|
+
/** Writes a structured message to the console and daily log file. */
|
|
25
|
+
getFormattedMessage(logLevel: LogLevel, message: string, ...data: unknown[]): string;
|
|
26
|
+
/** Writes a structured message to the console and daily log file. */
|
|
27
|
+
log(logLevel: LogLevel, message: string, ...data: unknown[]): void;
|
|
28
|
+
/** Writes an informational message. */
|
|
29
|
+
info(message: string, ...data: unknown[]): void;
|
|
30
|
+
/** Writes a warning message. */
|
|
31
|
+
warn(message: string, ...data: unknown[]): void;
|
|
32
|
+
/** Writes an error message. */
|
|
33
|
+
error(message: string, ...data: unknown[]): void;
|
|
34
|
+
}
|
|
35
|
+
type GetUserNameFunction<P = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>> = (req: Request<P, ResBody, ReqBody, ReqQuery, Locals>) => string | undefined;
|
|
36
|
+
declare class ExpressLogger<P = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>> extends Logger {
|
|
37
|
+
private req?;
|
|
38
|
+
private ipAddress?;
|
|
39
|
+
private getUsername?;
|
|
40
|
+
/** Creates a logger optionally bound to an incoming request. */
|
|
41
|
+
constructor(req?: Request<P, ResBody, ReqBody, ReqQuery, Locals>, getUsername?: GetUserNameFunction<P, ResBody, ReqBody, ReqQuery, Locals>);
|
|
42
|
+
/** Associates request identity, user, and IP details with this logger. */
|
|
43
|
+
bindRequest(req: Request<P, ResBody, ReqBody, ReqQuery, Locals>): void;
|
|
44
|
+
getUserName(): string | undefined;
|
|
45
|
+
getFormattedMessage(logLevel: LogLevel, message: string, ...data: unknown[]): string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type LoggerMiddleware = (getUsername?: GetUserNameFunction) => express.RequestHandler;
|
|
49
|
+
/** Creates a request logger and records request completion status. */
|
|
50
|
+
declare const loggerMiddleware: LoggerMiddleware;
|
|
51
|
+
|
|
52
|
+
export { ExpressLogger, type GetUserNameFunction, type ILoggerOptions, Logger, loggerMiddleware, loggerOptions };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import express, { Request } from 'express';
|
|
2
|
+
import { ParamsDictionary, Query } from 'express-serve-static-core';
|
|
3
|
+
|
|
4
|
+
type LogLevel = 'info' | 'warn' | 'error';
|
|
5
|
+
/** Logger contract used by request-aware server components. */
|
|
6
|
+
interface ILogger {
|
|
7
|
+
/** Writes a message at the requested severity. */
|
|
8
|
+
log(logLevel: LogLevel, message: string, ...data: unknown[]): void;
|
|
9
|
+
/** Writes an informational message. */
|
|
10
|
+
info(message: string, ...data: unknown[]): void;
|
|
11
|
+
/** Writes a warning message. */
|
|
12
|
+
warn(message: string, ...data: unknown[]): void;
|
|
13
|
+
/** Writes an error message. */
|
|
14
|
+
error(message: string, ...data: unknown[]): void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface ILoggerOptions {
|
|
18
|
+
}
|
|
19
|
+
declare const loggerOptions: ILoggerOptions;
|
|
20
|
+
declare class Logger implements ILogger {
|
|
21
|
+
private logFilename;
|
|
22
|
+
/** Creates a logger optionally bound to an incoming request. */
|
|
23
|
+
constructor();
|
|
24
|
+
/** Writes a structured message to the console and daily log file. */
|
|
25
|
+
getFormattedMessage(logLevel: LogLevel, message: string, ...data: unknown[]): string;
|
|
26
|
+
/** Writes a structured message to the console and daily log file. */
|
|
27
|
+
log(logLevel: LogLevel, message: string, ...data: unknown[]): void;
|
|
28
|
+
/** Writes an informational message. */
|
|
29
|
+
info(message: string, ...data: unknown[]): void;
|
|
30
|
+
/** Writes a warning message. */
|
|
31
|
+
warn(message: string, ...data: unknown[]): void;
|
|
32
|
+
/** Writes an error message. */
|
|
33
|
+
error(message: string, ...data: unknown[]): void;
|
|
34
|
+
}
|
|
35
|
+
type GetUserNameFunction<P = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>> = (req: Request<P, ResBody, ReqBody, ReqQuery, Locals>) => string | undefined;
|
|
36
|
+
declare class ExpressLogger<P = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>> extends Logger {
|
|
37
|
+
private req?;
|
|
38
|
+
private ipAddress?;
|
|
39
|
+
private getUsername?;
|
|
40
|
+
/** Creates a logger optionally bound to an incoming request. */
|
|
41
|
+
constructor(req?: Request<P, ResBody, ReqBody, ReqQuery, Locals>, getUsername?: GetUserNameFunction<P, ResBody, ReqBody, ReqQuery, Locals>);
|
|
42
|
+
/** Associates request identity, user, and IP details with this logger. */
|
|
43
|
+
bindRequest(req: Request<P, ResBody, ReqBody, ReqQuery, Locals>): void;
|
|
44
|
+
getUserName(): string | undefined;
|
|
45
|
+
getFormattedMessage(logLevel: LogLevel, message: string, ...data: unknown[]): string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type LoggerMiddleware = (getUsername?: GetUserNameFunction) => express.RequestHandler;
|
|
49
|
+
/** Creates a request logger and records request completion status. */
|
|
50
|
+
declare const loggerMiddleware: LoggerMiddleware;
|
|
51
|
+
|
|
52
|
+
export { ExpressLogger, type GetUserNameFunction, type ILoggerOptions, Logger, loggerMiddleware, loggerOptions };
|
|
53
|
+
|
|
54
|
+
import { IExpressLogger } from '../interfaces/ILogger';
|
|
55
|
+
|
|
56
|
+
declare global {
|
|
57
|
+
namespace Express {
|
|
58
|
+
interface Request {
|
|
59
|
+
logger?: IExpressLogger;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// src/logger/logger.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
|
|
4
|
+
// src/logger/getIpAddress.ts
|
|
5
|
+
var getIpAddress = (req) => {
|
|
6
|
+
const forwardedFor = req.headers["x-forwarded-for"];
|
|
7
|
+
const realIp = req.headers["x-real-ip"];
|
|
8
|
+
let ipAddress = req.ip;
|
|
9
|
+
if (typeof forwardedFor === "string") {
|
|
10
|
+
ipAddress = forwardedFor.split(",")[0].trim();
|
|
11
|
+
} else if (realIp) {
|
|
12
|
+
ipAddress = realIp;
|
|
13
|
+
}
|
|
14
|
+
return ipAddress;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/logger/logger.ts
|
|
18
|
+
var HeaderColor = "\x1B[38;2;128;128;128m";
|
|
19
|
+
var UserColor = "\x1B[38;2;255;255;0m";
|
|
20
|
+
var ParameterColor = "\x1B[38;2;215;0;215m";
|
|
21
|
+
var IpColor = "\x1B[38;2;0;255;0m";
|
|
22
|
+
var LevelIndicators = {
|
|
23
|
+
info: "INF",
|
|
24
|
+
warn: "WRN",
|
|
25
|
+
error: "ERR"
|
|
26
|
+
};
|
|
27
|
+
var replaceParameters = (message, useColors, data) => {
|
|
28
|
+
let dataIndex = 0;
|
|
29
|
+
const coloredMessage = message.replace(/%[sdj]/g, (match) => {
|
|
30
|
+
if (dataIndex >= data.length) {
|
|
31
|
+
return match;
|
|
32
|
+
}
|
|
33
|
+
const value = data[dataIndex++];
|
|
34
|
+
switch (match) {
|
|
35
|
+
case "%s":
|
|
36
|
+
return useColors ? `${ParameterColor}${String(value)}\x1B[0m` : `${String(value)}`;
|
|
37
|
+
case "%d":
|
|
38
|
+
return useColors ? `${ParameterColor}${Number(value).toString()}\x1B[0m` : `${Number(value).toString()}`;
|
|
39
|
+
case "%j":
|
|
40
|
+
try {
|
|
41
|
+
return useColors ? `${ParameterColor}${JSON.stringify(value)}\x1B[0m` : `${JSON.stringify(value)}`;
|
|
42
|
+
} catch {
|
|
43
|
+
return "[Circular]";
|
|
44
|
+
}
|
|
45
|
+
default:
|
|
46
|
+
return match;
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
return coloredMessage;
|
|
50
|
+
};
|
|
51
|
+
var uncolorMessage = (message) => {
|
|
52
|
+
return message.replace(/(\x1b|\u001b)\[[0-9;]*m/g, "");
|
|
53
|
+
};
|
|
54
|
+
var formatLogMessage = (messageBuilder, logLevel, message, data) => {
|
|
55
|
+
const now = /* @__PURE__ */ new Date();
|
|
56
|
+
const nowString = `${now.getHours().toString().padStart(2, "0")}:${now.getMinutes().toString().padStart(2, "0")}:${now.getSeconds().toString().padStart(2, "0")}.${now.getMilliseconds().toString().padStart(3, "0")}`;
|
|
57
|
+
const levelIndicator = LevelIndicators[logLevel];
|
|
58
|
+
const coloredParameters = replaceParameters(message, true, data);
|
|
59
|
+
const coloredMessage = messageBuilder(nowString, levelIndicator, coloredParameters);
|
|
60
|
+
return coloredMessage;
|
|
61
|
+
};
|
|
62
|
+
var loggerOptions = {};
|
|
63
|
+
var Logger = class {
|
|
64
|
+
/** Creates a logger optionally bound to an incoming request. */
|
|
65
|
+
constructor() {
|
|
66
|
+
const now = /* @__PURE__ */ new Date();
|
|
67
|
+
this.logFilename = `./log-${now.getUTCFullYear()}-${(now.getUTCMonth() + 1).toString().padStart(2, "0")}-${now.getUTCDate().toString().padStart(2, "0")}.log`;
|
|
68
|
+
}
|
|
69
|
+
/** Writes a structured message to the console and daily log file. */
|
|
70
|
+
getFormattedMessage(logLevel, message, ...data) {
|
|
71
|
+
return formatLogMessage((timeStamp, levelIndicator, message2) => `${HeaderColor}[${timeStamp} ${levelIndicator}]\x1B[0m ${message2}`, logLevel, message, data);
|
|
72
|
+
}
|
|
73
|
+
/** Writes a structured message to the console and daily log file. */
|
|
74
|
+
log(logLevel, message, ...data) {
|
|
75
|
+
const formattedMessage = this.getFormattedMessage(logLevel, message, ...data);
|
|
76
|
+
switch (logLevel) {
|
|
77
|
+
case "info":
|
|
78
|
+
console.info(formattedMessage);
|
|
79
|
+
fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + "\n");
|
|
80
|
+
break;
|
|
81
|
+
case "warn":
|
|
82
|
+
console.warn(formattedMessage);
|
|
83
|
+
fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + "\n");
|
|
84
|
+
break;
|
|
85
|
+
case "error":
|
|
86
|
+
console.error(formattedMessage);
|
|
87
|
+
fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + "\n");
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Writes an informational message. */
|
|
92
|
+
info(message, ...data) {
|
|
93
|
+
this.log("info", message, ...data);
|
|
94
|
+
}
|
|
95
|
+
/** Writes a warning message. */
|
|
96
|
+
warn(message, ...data) {
|
|
97
|
+
this.log("warn", message, ...data);
|
|
98
|
+
}
|
|
99
|
+
/** Writes an error message. */
|
|
100
|
+
error(message, ...data) {
|
|
101
|
+
this.log("error", message, ...data);
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
var ExpressLogger = class extends Logger {
|
|
105
|
+
/** Creates a logger optionally bound to an incoming request. */
|
|
106
|
+
constructor(req, getUsername) {
|
|
107
|
+
super();
|
|
108
|
+
if (req) {
|
|
109
|
+
this.bindRequest(req);
|
|
110
|
+
}
|
|
111
|
+
this.getUsername = getUsername;
|
|
112
|
+
}
|
|
113
|
+
/** Associates request identity, user, and IP details with this logger. */
|
|
114
|
+
bindRequest(req) {
|
|
115
|
+
this.req = req;
|
|
116
|
+
this.ipAddress = ` ${getIpAddress(this.req)}`;
|
|
117
|
+
}
|
|
118
|
+
getUserName() {
|
|
119
|
+
return this.getUsername && this.req ? this.getUsername(this.req) : void 0;
|
|
120
|
+
}
|
|
121
|
+
getFormattedMessage(logLevel, message, ...data) {
|
|
122
|
+
const ipAddress = this.ipAddress ?? "";
|
|
123
|
+
let userName = this.getUserName();
|
|
124
|
+
userName = userName ? ` ${userName}` : "";
|
|
125
|
+
return formatLogMessage(
|
|
126
|
+
(timeStamp, levelIndicator, message2) => `${HeaderColor}[${timeStamp} ${levelIndicator}${IpColor}${ipAddress}${UserColor}${userName}${HeaderColor}]\x1B[0m ${message2}`,
|
|
127
|
+
logLevel,
|
|
128
|
+
message,
|
|
129
|
+
data
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// src/logger/loggerMiddleware.ts
|
|
135
|
+
var loggerMiddleware = (getUsername) => {
|
|
136
|
+
return (req, res, next) => {
|
|
137
|
+
const logger = new ExpressLogger(req, getUsername);
|
|
138
|
+
req.logger = logger;
|
|
139
|
+
const requestInfo = { method: req.method, url: req.originalUrl };
|
|
140
|
+
try {
|
|
141
|
+
logger.info("HTTP %s request started: %s", requestInfo.method, requestInfo.url);
|
|
142
|
+
res.on("finish", () => {
|
|
143
|
+
logger.info(
|
|
144
|
+
"HTTP %s request ended with status %d: %s",
|
|
145
|
+
requestInfo.method,
|
|
146
|
+
res.statusCode,
|
|
147
|
+
requestInfo.url
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
next();
|
|
151
|
+
} catch (error) {
|
|
152
|
+
logger.error("HTTP %s request failed: %s\n %j", requestInfo.method, requestInfo.url, error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
};
|
|
157
|
+
export {
|
|
158
|
+
ExpressLogger,
|
|
159
|
+
Logger,
|
|
160
|
+
loggerMiddleware,
|
|
161
|
+
loggerOptions
|
|
162
|
+
};
|
|
163
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/logger/logger.ts","../src/logger/getIpAddress.ts","../src/logger/loggerMiddleware.ts"],"sourcesContent":["/* eslint-disable @typescript-eslint/no-unused-vars */\r\n\r\nimport express from 'express';\r\nimport { Request } from 'express';\r\nimport { ParamsDictionary, Query } from 'express-serve-static-core';\r\nimport fs from 'fs';\r\n\r\nimport { ILogger, LogLevel } from '../interfaces/ILogger';\r\nimport { getIpAddress } from './getIpAddress';\r\n\r\nconst HeaderColor = '\\u001B[38;2;128;128;128m';\r\nconst UserColor = '\\u001B[38;2;255;255;0m';\r\nconst ParameterColor = '\\u001B[38;2;215;0;215m';\r\n// Keep the short level labels stable for both console and file output.\r\nconst IpColor = '\\u001B[38;2;0;255;0m';\r\n\r\nconst LevelIndicators: Record<LogLevel, string> = {\r\n info: 'INF',\r\n warn: 'WRN',\r\n error: 'ERR',\r\n};\r\n\r\n// Replaces the supported printf-style placeholders while optionally applying colors.\r\nconst replaceParameters = (message: string, useColors: boolean, data: unknown[]): string => {\r\n let dataIndex = 0;\r\n const coloredMessage = message.replace(/%[sdj]/g, (match) => {\r\n if (dataIndex >= data.length) {\r\n return match;\r\n }\r\n const value = data[dataIndex++];\r\n switch (match) {\r\n case '%s':\r\n return useColors ? `${ParameterColor}${String(value)}\\u001B[0m` : `${String(value)}`;\r\n case '%d':\r\n return useColors ? `${ParameterColor}${Number(value).toString()}\\u001B[0m` : `${Number(value).toString()}`;\r\n case '%j':\r\n try {\r\n return useColors ? `${ParameterColor}${JSON.stringify(value)}\\u001B[0m` : `${JSON.stringify(value)}`;\r\n } catch {\r\n return '[Circular]';\r\n }\r\n default:\r\n return match;\r\n }\r\n });\r\n return coloredMessage;\r\n};\r\n\r\nconst uncolorMessage = (message: string): string => {\r\n return message.replace(/(\\x1b|\\u001b)\\[[0-9;]*m/g, '');\r\n};\r\n\r\ntype MessageBuilder = (timeStamp: string, levelIndicator: string, message: string) => string;\r\n\r\n// Builds the colored console form and the plain-text file form of a log message.\r\nconst formatLogMessage = (messageBuilder: MessageBuilder, logLevel: LogLevel, message: string, data: unknown[]): string => {\r\n const now = new Date();\r\n const nowString = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}.${now.getMilliseconds().toString().padStart(3, '0')}`;\r\n const levelIndicator = LevelIndicators[logLevel];\r\n const coloredParameters = replaceParameters(message, true, data);\r\n const coloredMessage = messageBuilder(nowString, levelIndicator, coloredParameters);\r\n return coloredMessage;\r\n};\r\n\r\nexport interface ILoggerOptions {\r\n}\r\n\r\nexport const loggerOptions: ILoggerOptions = {\r\n};\r\n\r\nexport class Logger implements ILogger {\r\n private logFilename: string;\r\n\r\n /** Creates a logger optionally bound to an incoming request. */\r\n constructor() {\r\n const now = new Date();\r\n // Use one UTC log file per day so entries remain grouped across server time zones.\r\n this.logFilename = `./log-${now.getUTCFullYear()}-${(now.getUTCMonth() + 1).toString().padStart(2, '0')}-${now.getUTCDate().toString().padStart(2, '0')}.log`;\r\n }\r\n\r\n /** Writes a structured message to the console and daily log file. */\r\n getFormattedMessage(logLevel: LogLevel, message: string, ...data: unknown[]): string {\r\n return formatLogMessage((timeStamp, levelIndicator, message) =>\r\n `${HeaderColor}[${timeStamp} ${levelIndicator}]\\u001B[0m ${message}`, logLevel, message, data);\r\n }\r\n\r\n /** Writes a structured message to the console and daily log file. */\r\n log(logLevel: LogLevel, message: string, ...data: unknown[]): void {\r\n const formattedMessage = this.getFormattedMessage(logLevel, message, ...data);\r\n\r\n // Mirror the severity to the console and persist the uncolored form to disk.\r\n switch (logLevel) {\r\n case 'info':\r\n console.info(formattedMessage);\r\n fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + '\\n');\r\n break;\r\n\r\n case 'warn':\r\n console.warn(formattedMessage);\r\n fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + '\\n');\r\n break;\r\n\r\n case 'error':\r\n console.error(formattedMessage);\r\n fs.appendFileSync(this.logFilename, uncolorMessage(formattedMessage) + '\\n');\r\n break;\r\n }\r\n }\r\n\r\n /** Writes an informational message. */\r\n info(message: string, ...data: unknown[]): void {\r\n this.log('info', message, ...data);\r\n }\r\n\r\n /** Writes a warning message. */\r\n warn(message: string, ...data: unknown[]): void {\r\n this.log('warn', message, ...data);\r\n }\r\n\r\n /** Writes an error message. */\r\n error(message: string, ...data: unknown[]): void {\r\n this.log('error', message, ...data);\r\n }\r\n}\r\n\r\nexport type GetUserNameFunction<P = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>>\r\n = (req: Request<P, ResBody, ReqBody, ReqQuery, Locals>) => string | undefined;\r\n\r\nexport class ExpressLogger<P = ParamsDictionary, ResBody = unknown, ReqBody = unknown, ReqQuery = Query, Locals extends Record<string, unknown> = Record<string, unknown>>\r\n extends Logger {\r\n private req?: Request<P, ResBody, ReqBody, ReqQuery, Locals>;\r\n private ipAddress?: string;\r\n private getUsername?: GetUserNameFunction<P, ResBody, ReqBody, ReqQuery, Locals>;\r\n\r\n /** Creates a logger optionally bound to an incoming request. */\r\n constructor(\r\n req?: Request<P, ResBody, ReqBody, ReqQuery, Locals>,\r\n getUsername?: GetUserNameFunction<P, ResBody, ReqBody, ReqQuery, Locals>) {\r\n super();\r\n if (req) {\r\n this.bindRequest(req);\r\n }\r\n this.getUsername = getUsername;\r\n }\r\n\r\n /** Associates request identity, user, and IP details with this logger. */\r\n bindRequest(req: Request<P, ResBody, ReqBody, ReqQuery, Locals>): void {\r\n this.req = req;\r\n // Prefer proxy-provided addresses, then fall back to the direct request IP.\r\n this.ipAddress = ` ${getIpAddress(this.req as express.Request)}`;\r\n }\r\n\r\n getUserName(): string | undefined {\r\n // Placeholder for user name retrieval logic. Replace with actual implementation.\r\n return this.getUsername && this.req ? this.getUsername(this.req) : undefined;\r\n }\r\n\r\n getFormattedMessage(logLevel: LogLevel, message: string, ...data: unknown[]): string {\r\n const ipAddress = this.ipAddress ?? '';\r\n let userName = this.getUserName();\r\n\r\n userName = userName ? ` ${userName}` : '';\r\n\r\n return formatLogMessage(\r\n (timeStamp, levelIndicator, message) => `${HeaderColor}[${timeStamp} ${levelIndicator}${IpColor}${ipAddress}${UserColor}${userName}${HeaderColor}]\\x1b[0m ${message}`,\r\n logLevel,\r\n message,\r\n data);\r\n }\r\n};\r\n","import express from 'express';\n\nexport const getIpAddress = (req: express.Request): string | undefined => {\n const forwardedFor = req.headers['x-forwarded-for'];\n const realIp = req.headers['x-real-ip'] as string | undefined;\n let ipAddress = req.ip;\n if (typeof forwardedFor === 'string') {\n ipAddress = forwardedFor.split(',')[0].trim();\n } else if (realIp) {\n ipAddress = realIp;\n }\n\n return ipAddress;\n};\n","import express from 'express';\r\nimport { ExpressLogger, GetUserNameFunction } from './logger.js';\r\n\r\ntype LoggerMiddleware = (getUsername?: GetUserNameFunction) => express.RequestHandler;\r\n\r\n/** Creates a request logger and records request completion status. */\r\nexport const loggerMiddleware: LoggerMiddleware = (getUsername) => {\r\n return (req, res, next) => {\r\n // Create one logger instance so all downstream handlers share request context.\r\n const logger = new ExpressLogger(req, getUsername);\r\n\r\n req.logger = logger; // Initialize the logger for this request\r\n\r\n const requestInfo = { method: req.method, url: req.originalUrl };\r\n\r\n try {\r\n // Record the start immediately and the final status when Express finishes.\r\n logger.info('HTTP %s request started: %s', requestInfo.method, requestInfo.url);\r\n res.on('finish', () => {\r\n logger.info(\r\n 'HTTP %s request ended with status %d: %s',\r\n requestInfo.method,\r\n res.statusCode,\r\n requestInfo.url);\r\n });\r\n next();\r\n } catch (error) {\r\n logger.error('HTTP %s request failed: %s\\n %j', requestInfo.method, requestInfo.url, error);\r\n throw error;\r\n }\r\n };\r\n};\r\n"],"mappings":"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","names":["message"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "capitalsix-logger",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"typings"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"require": "./dist/index.cjs"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Mike Wijnen",
|
|
21
|
+
"email": "mike@capitalsix.nl",
|
|
22
|
+
"url": "https://capitalsix.nl"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsup",
|
|
26
|
+
"concat": "npx concat ./dist/index.d.ts ./src/types/index.d.ts -o ./dist/index.d.ts",
|
|
27
|
+
"postbuild": "npm run concat"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [],
|
|
30
|
+
"license": "ISC",
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"express": "^5.2.1"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"tsup": "^8.5.1",
|
|
36
|
+
"typescript": "^5.7.3"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/express": "^5.0.6",
|
|
40
|
+
"@types/node": "^22.10.2",
|
|
41
|
+
"concat": "^1.0.3",
|
|
42
|
+
"vite": "^8.2.2"
|
|
43
|
+
}
|
|
44
|
+
}
|