@pori15/logixlysia 6.0.12 → 6.0.13
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.d.ts +225 -0
- package/dist/index.js +832 -0
- package/package.json +1 -1
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { Elysia, SingletonBase } from "elysia";
|
|
2
|
+
import { Logger as PinoLogger, LoggerOptions as PinoLoggerOptions } from "pino";
|
|
3
|
+
interface ProblemDocument {
|
|
4
|
+
detail?: string;
|
|
5
|
+
instance?: string;
|
|
6
|
+
status?: number;
|
|
7
|
+
title: string;
|
|
8
|
+
type: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* RFC 9457 Problem Details Error
|
|
13
|
+
*
|
|
14
|
+
* Core members:
|
|
15
|
+
* - type: URI reference identifying the problem type (default "about:blank")
|
|
16
|
+
* - title: Short human-readable summary
|
|
17
|
+
* - status: HTTP status code
|
|
18
|
+
* - detail: Human-readable explanation for this occurrence
|
|
19
|
+
* - instance: URI reference identifying this specific occurrence
|
|
20
|
+
* - extensions: Additional properties serialized as-is
|
|
21
|
+
*/
|
|
22
|
+
declare class ProblemError extends Error {
|
|
23
|
+
readonly status: number;
|
|
24
|
+
readonly title: string;
|
|
25
|
+
readonly type: string;
|
|
26
|
+
readonly detail?: string;
|
|
27
|
+
readonly instance?: string;
|
|
28
|
+
readonly extensions?: Record<string, unknown>;
|
|
29
|
+
constructor(type: string | undefined, title: string, status: number, detail?: string, instance?: string, extensions?: Record<string, unknown>);
|
|
30
|
+
toJSON(): ProblemDocument;
|
|
31
|
+
}
|
|
32
|
+
interface ProblemConfig {
|
|
33
|
+
detail?: string;
|
|
34
|
+
extensions?: Record<string, unknown>;
|
|
35
|
+
instance?: string;
|
|
36
|
+
title?: string;
|
|
37
|
+
type?: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* 工厂函数:根据 HTTP 状态码快速创建 ProblemError
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* throw createProblem(400, { detail: 'Invalid email' })
|
|
44
|
+
* throw createProblem(409, { title: 'Duplicate', detail: 'Email already exists' })
|
|
45
|
+
*/
|
|
46
|
+
declare const createProblem: (status: number, overrides?: ProblemConfig) => ProblemError;
|
|
47
|
+
type HttpErrorFactory = (detail?: string, extensions?: Record<string, unknown>) => ProblemError;
|
|
48
|
+
interface HttpErrorConstructor {
|
|
49
|
+
BadGateway: HttpErrorFactory;
|
|
50
|
+
BadRequest: HttpErrorFactory;
|
|
51
|
+
Conflict: HttpErrorFactory;
|
|
52
|
+
Forbidden: HttpErrorFactory;
|
|
53
|
+
GatewayTimeout: HttpErrorFactory;
|
|
54
|
+
InternalServerError: HttpErrorFactory;
|
|
55
|
+
MethodNotAllowed: HttpErrorFactory;
|
|
56
|
+
NotAcceptable: HttpErrorFactory;
|
|
57
|
+
NotFound: HttpErrorFactory;
|
|
58
|
+
NotImplemented: HttpErrorFactory;
|
|
59
|
+
PaymentRequired: HttpErrorFactory;
|
|
60
|
+
ServiceUnavailable: HttpErrorFactory;
|
|
61
|
+
Unauthorized: HttpErrorFactory;
|
|
62
|
+
}
|
|
63
|
+
declare const HttpError: HttpErrorConstructor;
|
|
64
|
+
type Code = number | "PROBLEM_ERROR" | "UNKNOWN" | "VALIDATION" | "NOT_FOUND" | "PARSE" | "INTERNAL_SERVER_ERROR" | "INVALID_COOKIE_SIGNATURE" | "INVALID_FILE_TYPE";
|
|
65
|
+
/** Pino Logger 实例类型 */
|
|
66
|
+
type Pino = PinoLogger<never, boolean>;
|
|
67
|
+
/** 日志级别 */
|
|
68
|
+
type LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR";
|
|
69
|
+
/** 单次请求携带的计时和路径数据 */
|
|
70
|
+
interface StoreData {
|
|
71
|
+
/** 请求开始的纳秒时间戳(hrtime) */
|
|
72
|
+
beforeTime: bigint;
|
|
73
|
+
/** 缓存的 URL pathname,避免重复解析 */
|
|
74
|
+
pathname: string;
|
|
75
|
+
}
|
|
76
|
+
/** Elysia store 中挂载的 logixlysia 状态 */
|
|
77
|
+
interface LogixlysiaStore {
|
|
78
|
+
beforeTime?: bigint;
|
|
79
|
+
logger: Logger;
|
|
80
|
+
pathname?: string;
|
|
81
|
+
pino: Pino;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
/** 自定义日志传输接口(如 Elasticsearch、Slack 等) */
|
|
85
|
+
interface Transport {
|
|
86
|
+
/**
|
|
87
|
+
* 接收一条日志并输出到外部系统
|
|
88
|
+
*
|
|
89
|
+
* @param level - 日志级别
|
|
90
|
+
* @param message - 日志消息
|
|
91
|
+
* @param meta - 附加元数据(请求信息、耗时等)
|
|
92
|
+
*/
|
|
93
|
+
log: (level: LogLevel, message: string, meta?: Record<string, unknown>) => void | Promise<void>;
|
|
94
|
+
}
|
|
95
|
+
/** 日志文件轮转配置 */
|
|
96
|
+
interface LogRotationConfig {
|
|
97
|
+
/** 轮转后是否压缩旧文件 */
|
|
98
|
+
compress?: boolean;
|
|
99
|
+
/** 压缩算法 */
|
|
100
|
+
compression?: "gzip";
|
|
101
|
+
/** 固定间隔轮转,如 `'1d'`、`'12h'` */
|
|
102
|
+
interval?: string;
|
|
103
|
+
/** 保留的最大文件数量或时长,如 `10` 或 `'7d'` */
|
|
104
|
+
maxFiles?: number | string;
|
|
105
|
+
/** 单个日志文件最大体积,如 `'10m'`、`'5k'`,或字节数 */
|
|
106
|
+
maxSize?: string | number;
|
|
107
|
+
}
|
|
108
|
+
/** 错误码到 HTTP 响应的映射条目 */
|
|
109
|
+
interface ErrorMapping {
|
|
110
|
+
/** 错误详情描述 */
|
|
111
|
+
detail?: string;
|
|
112
|
+
/** HTTP 状态码 */
|
|
113
|
+
status: number;
|
|
114
|
+
/** 错误标题 */
|
|
115
|
+
title: string;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 自定义错误解析回调
|
|
119
|
+
*
|
|
120
|
+
* 返回 `ProblemError` 表示已处理,返回 `void` 交给下一层处理
|
|
121
|
+
*/
|
|
122
|
+
type ErrorResolver = (error: unknown, context: {
|
|
123
|
+
code: Code;
|
|
124
|
+
path: string;
|
|
125
|
+
request: Request;
|
|
126
|
+
}) => ProblemError | void;
|
|
127
|
+
/** 启动消息配置 */
|
|
128
|
+
interface StartupConfig {
|
|
129
|
+
/** 启动消息格式,默认 `"banner"` */
|
|
130
|
+
format?: "simple" | "banner";
|
|
131
|
+
/** 是否显示启动消息,默认 `true` */
|
|
132
|
+
show?: boolean;
|
|
133
|
+
}
|
|
134
|
+
/** 日志格式配置 */
|
|
135
|
+
interface FormatConfig {
|
|
136
|
+
/** 是否启用彩色输出,默认 `true`(仅 TTY) */
|
|
137
|
+
colors?: boolean;
|
|
138
|
+
/** 是否在日志中显示请求 IP,默认 `false` */
|
|
139
|
+
showIp?: boolean;
|
|
140
|
+
/** 自定义日志模板,如 `'🦊 {now} {level} {method} {pathname} {status}'` */
|
|
141
|
+
template?: string;
|
|
142
|
+
/** 时间戳格式,如 `'yyyy-mm-dd HH:MM:ss.SSS'` */
|
|
143
|
+
timestamp?: string;
|
|
144
|
+
}
|
|
145
|
+
/** 文件日志配置 */
|
|
146
|
+
interface FileConfig {
|
|
147
|
+
/** 日志文件路径(必填) */
|
|
148
|
+
path: string;
|
|
149
|
+
/** 日志轮转配置 */
|
|
150
|
+
rotation?: LogRotationConfig;
|
|
151
|
+
}
|
|
152
|
+
/** 自定义传输配置 */
|
|
153
|
+
interface TransportsConfig {
|
|
154
|
+
/** 设为 `true` 时只使用 transports,禁用控制台和文件输出 */
|
|
155
|
+
only?: boolean;
|
|
156
|
+
/** 传输目标列表 */
|
|
157
|
+
targets: Transport[];
|
|
158
|
+
}
|
|
159
|
+
/** 错误处理配置 */
|
|
160
|
+
interface ErrorConfig {
|
|
161
|
+
/** 错误码映射表(Postgres / MySQL / 自定义错误码) */
|
|
162
|
+
errorMap?: Record<string, ErrorMapping>;
|
|
163
|
+
/** 自定义错误解析回调 */
|
|
164
|
+
resolve?: ErrorResolver;
|
|
165
|
+
/** 自定义错误类型的 Base URL(RFC 9457) */
|
|
166
|
+
typeBaseUrl?: string;
|
|
167
|
+
/** 是否在控制台显示完整错误详情(detail、extensions),默认 `false` */
|
|
168
|
+
verbose?: boolean;
|
|
169
|
+
}
|
|
170
|
+
/** Logixlysia 插件配置 */
|
|
171
|
+
interface Options {
|
|
172
|
+
/** 错误处理配置 */
|
|
173
|
+
error?: ErrorConfig;
|
|
174
|
+
/** 文件日志配置,设为 `false` 禁用文件日志 */
|
|
175
|
+
file?: false | FileConfig;
|
|
176
|
+
/** 日志格式配置 */
|
|
177
|
+
format?: FormatConfig;
|
|
178
|
+
/** 日志级别过滤,接受单个级别或级别数组 */
|
|
179
|
+
logLevel?: LogLevel | LogLevel[];
|
|
180
|
+
/** Pino Logger 原生配置透传 */
|
|
181
|
+
pino?: PinoLoggerOptions;
|
|
182
|
+
/** 启动消息配置 */
|
|
183
|
+
startup?: StartupConfig;
|
|
184
|
+
/** 自定义传输(数组或带 `only` 选项的对象) */
|
|
185
|
+
transports?: Transport[] | TransportsConfig;
|
|
186
|
+
}
|
|
187
|
+
/** Logger 实例,可通过 `store.logger` 访问 */
|
|
188
|
+
interface Logger {
|
|
189
|
+
/** 记录 DEBUG 级别日志 */
|
|
190
|
+
debug: (request: Request, message: string, context?: Record<string, unknown>) => void;
|
|
191
|
+
/** 记录 ERROR 级别日志 */
|
|
192
|
+
error: (request: Request, message: string, context?: Record<string, unknown>) => void;
|
|
193
|
+
/** 处理 HTTP 错误并输出日志 */
|
|
194
|
+
handleHttpError: (request: Request, error: ProblemError, store: StoreData, options: Options) => void;
|
|
195
|
+
/** 记录 INFO 级别日志 */
|
|
196
|
+
info: (request: Request, message: string, context?: Record<string, unknown>) => void;
|
|
197
|
+
/** 记录指定级别的日志 */
|
|
198
|
+
log: (level: LogLevel, request: Request, data: Record<string, unknown>, store: StoreData) => void;
|
|
199
|
+
/** 底层 Pino Logger 实例 */
|
|
200
|
+
pino: Pino;
|
|
201
|
+
/** 记录 WARNING 级别日志 */
|
|
202
|
+
warn: (request: Request, message: string, context?: Record<string, unknown>) => void;
|
|
203
|
+
}
|
|
204
|
+
/** Logixlysia 请求上下文 */
|
|
205
|
+
interface LogixlysiaContext {
|
|
206
|
+
request: Request;
|
|
207
|
+
store: LogixlysiaStore;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* 递归提取错误码
|
|
211
|
+
* 沿 cause/error 链递归查找,支持被包装的底层错误(Postgres、MySQL、第三方库等)
|
|
212
|
+
*/
|
|
213
|
+
declare const getErrorCode: (error: unknown) => string | undefined;
|
|
214
|
+
interface NormalizeOptions {
|
|
215
|
+
errorMap?: Record<string, ErrorMapping>;
|
|
216
|
+
request?: Request;
|
|
217
|
+
resolve?: ErrorResolver;
|
|
218
|
+
typeBaseUrl?: string;
|
|
219
|
+
}
|
|
220
|
+
declare const normalizeToProblem: (error: unknown, code: Code, path: string, options?: NormalizeOptions) => ProblemError;
|
|
221
|
+
type Logixlysia = Elysia<"Logixlysia", SingletonBase & {
|
|
222
|
+
store: LogixlysiaStore;
|
|
223
|
+
}>;
|
|
224
|
+
declare const logixlysia: (options?: Options) => Logixlysia;
|
|
225
|
+
export { normalizeToProblem, logixlysia, getErrorCode, logixlysia as default, createProblem, TransportsConfig, Transport, StoreData, StartupConfig, ProblemError, ProblemDocument, ProblemConfig, Pino, Options, LogixlysiaStore, LogixlysiaContext, Logixlysia, Logger, LogLevel, HttpErrorConstructor, HttpError, FormatConfig, FileConfig, ErrorResolver, ErrorMapping, ErrorConfig, Code };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { Elysia } from "elysia";
|
|
3
|
+
|
|
4
|
+
// src/extensions/banner.ts
|
|
5
|
+
import elysiaPkg from "elysia/package.json";
|
|
6
|
+
var centerText = (text, width) => {
|
|
7
|
+
if (text.length >= width) {
|
|
8
|
+
return text.slice(0, width);
|
|
9
|
+
}
|
|
10
|
+
const left = Math.floor((width - text.length) / 2);
|
|
11
|
+
const right = width - text.length - left;
|
|
12
|
+
return `${" ".repeat(left)}${text}${" ".repeat(right)}`;
|
|
13
|
+
};
|
|
14
|
+
var renderBanner = (message) => {
|
|
15
|
+
const versionLine = `Elysia v${elysiaPkg.version}`;
|
|
16
|
+
const contentWidth = Math.max(message.length, versionLine.length);
|
|
17
|
+
const innerWidth = contentWidth + 4;
|
|
18
|
+
const top = `┌${"─".repeat(innerWidth)}┐`;
|
|
19
|
+
const bot = `└${"─".repeat(innerWidth)}┘`;
|
|
20
|
+
const empty = `│${" ".repeat(innerWidth)}│`;
|
|
21
|
+
const versionRow = `│${centerText(versionLine, innerWidth)}│`;
|
|
22
|
+
const messageRow = `│ ${message}${" ".repeat(Math.max(0, innerWidth - message.length - 4))} │`;
|
|
23
|
+
return [top, empty, versionRow, empty, messageRow, empty, bot].join(`
|
|
24
|
+
`);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// src/extensions/index.ts
|
|
28
|
+
var startServer = (server, options) => {
|
|
29
|
+
const showStartupMessage = options.startup?.show ?? true;
|
|
30
|
+
if (!showStartupMessage) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const { port, hostname, protocol } = server;
|
|
34
|
+
if (port === undefined || !hostname || !protocol) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const url = `${protocol}://${hostname}:${port}`;
|
|
38
|
+
const message = `\uD83E\uDD8A Elysia is running at ${url}`;
|
|
39
|
+
const format = options.startup?.format ?? "banner";
|
|
40
|
+
if (format === "simple") {
|
|
41
|
+
console.log(message);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
console.log(renderBanner(message));
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// src/logger/index.ts
|
|
48
|
+
import pino from "pino";
|
|
49
|
+
|
|
50
|
+
// src/logger/create-logger.ts
|
|
51
|
+
import chalk from "chalk";
|
|
52
|
+
|
|
53
|
+
// src/helpers/status.ts
|
|
54
|
+
import { StatusMap } from "elysia";
|
|
55
|
+
var DIGITS_ONLY = /^\d+$/;
|
|
56
|
+
var DELIMITERS = /[_-]+/g;
|
|
57
|
+
var CAMEL_BOUNDARY_1 = /([a-z0-9])([A-Z])/g;
|
|
58
|
+
var CAMEL_BOUNDARY_2 = /([A-Z])([A-Z][a-z])/g;
|
|
59
|
+
var APOSTROPHES = /['’]/g;
|
|
60
|
+
var NON_ALPHANUMERIC = /[^a-z0-9\s]+/g;
|
|
61
|
+
var WHITESPACE = /\s+/g;
|
|
62
|
+
var normalizeStatusName = (value) => {
|
|
63
|
+
const trimmed = value.trim();
|
|
64
|
+
if (!trimmed) {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
return trimmed.replace(DELIMITERS, " ").replace(CAMEL_BOUNDARY_1, "$1 $2").replace(CAMEL_BOUNDARY_2, "$1 $2").replace(APOSTROPHES, "").toLowerCase().replace(NON_ALPHANUMERIC, " ").replace(WHITESPACE, " ").trim();
|
|
68
|
+
};
|
|
69
|
+
var STATUS_BY_NORMALIZED_NAME = (() => {
|
|
70
|
+
const map = new Map;
|
|
71
|
+
for (const [name, code] of Object.entries(StatusMap)) {
|
|
72
|
+
map.set(normalizeStatusName(name), code);
|
|
73
|
+
}
|
|
74
|
+
return map;
|
|
75
|
+
})();
|
|
76
|
+
var getStatusCode = (value) => {
|
|
77
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
if (typeof value === "string") {
|
|
81
|
+
const trimmed = value.trim();
|
|
82
|
+
if (DIGITS_ONLY.test(trimmed)) {
|
|
83
|
+
return Number(trimmed);
|
|
84
|
+
}
|
|
85
|
+
const known = STATUS_BY_NORMALIZED_NAME.get(normalizeStatusName(trimmed));
|
|
86
|
+
return known ?? 500;
|
|
87
|
+
}
|
|
88
|
+
return 500;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/utils/format.ts
|
|
92
|
+
var pad2 = (value) => String(value).padStart(2, "0");
|
|
93
|
+
var pad3 = (value) => String(value).padStart(3, "0");
|
|
94
|
+
|
|
95
|
+
// src/logger/create-logger.ts
|
|
96
|
+
var shouldUseColors = (options) => {
|
|
97
|
+
const enabledByConfig = options.format?.colors ?? true;
|
|
98
|
+
const isTty = typeof process !== "undefined" && process.stdout?.isTTY === true;
|
|
99
|
+
return enabledByConfig && isTty;
|
|
100
|
+
};
|
|
101
|
+
var formatTimestamp = (date, pattern) => {
|
|
102
|
+
if (!pattern) {
|
|
103
|
+
return date.toISOString();
|
|
104
|
+
}
|
|
105
|
+
const yyyy = String(date.getFullYear());
|
|
106
|
+
const mm = pad2(date.getMonth() + 1);
|
|
107
|
+
const dd = pad2(date.getDate());
|
|
108
|
+
const HH = pad2(date.getHours());
|
|
109
|
+
const MM = pad2(date.getMinutes());
|
|
110
|
+
const ss = pad2(date.getSeconds());
|
|
111
|
+
const SSS = pad3(date.getMilliseconds());
|
|
112
|
+
return pattern.replaceAll("yyyy", yyyy).replaceAll("mm", mm).replaceAll("dd", dd).replaceAll("HH", HH).replaceAll("MM", MM).replaceAll("ss", ss).replaceAll("SSS", SSS);
|
|
113
|
+
};
|
|
114
|
+
var getIp = (request) => {
|
|
115
|
+
const forwarded = request.headers.get("x-forwarded-for");
|
|
116
|
+
if (forwarded) {
|
|
117
|
+
return forwarded.split(",")[0]?.trim() ?? "";
|
|
118
|
+
}
|
|
119
|
+
return request.headers.get("x-real-ip") ?? "";
|
|
120
|
+
};
|
|
121
|
+
var getColoredLevel = (level, useColors) => {
|
|
122
|
+
if (!useColors) {
|
|
123
|
+
return level;
|
|
124
|
+
}
|
|
125
|
+
if (level === "ERROR") {
|
|
126
|
+
return chalk.bgRed.black(level);
|
|
127
|
+
}
|
|
128
|
+
if (level === "WARNING") {
|
|
129
|
+
return chalk.bgYellow.black(level);
|
|
130
|
+
}
|
|
131
|
+
if (level === "DEBUG") {
|
|
132
|
+
return chalk.bgBlue.black(level);
|
|
133
|
+
}
|
|
134
|
+
return chalk.bgGreen.black(level);
|
|
135
|
+
};
|
|
136
|
+
var getColoredMethod = (method, useColors) => {
|
|
137
|
+
if (!useColors) {
|
|
138
|
+
return method;
|
|
139
|
+
}
|
|
140
|
+
const upper = method.toUpperCase();
|
|
141
|
+
if (upper === "GET") {
|
|
142
|
+
return chalk.green.bold(upper);
|
|
143
|
+
}
|
|
144
|
+
if (upper === "POST") {
|
|
145
|
+
return chalk.blue.bold(upper);
|
|
146
|
+
}
|
|
147
|
+
if (upper === "PUT") {
|
|
148
|
+
return chalk.yellow.bold(upper);
|
|
149
|
+
}
|
|
150
|
+
if (upper === "PATCH") {
|
|
151
|
+
return chalk.yellowBright.bold(upper);
|
|
152
|
+
}
|
|
153
|
+
if (upper === "DELETE") {
|
|
154
|
+
return chalk.red.bold(upper);
|
|
155
|
+
}
|
|
156
|
+
if (upper === "OPTIONS") {
|
|
157
|
+
return chalk.cyan.bold(upper);
|
|
158
|
+
}
|
|
159
|
+
if (upper === "HEAD") {
|
|
160
|
+
return chalk.greenBright.bold(upper);
|
|
161
|
+
}
|
|
162
|
+
if (upper === "TRACE") {
|
|
163
|
+
return chalk.magenta.bold(upper);
|
|
164
|
+
}
|
|
165
|
+
if (upper === "CONNECT") {
|
|
166
|
+
return chalk.cyanBright.bold(upper);
|
|
167
|
+
}
|
|
168
|
+
return chalk.white.bold(upper);
|
|
169
|
+
};
|
|
170
|
+
var getColoredStatus = (status, useColors) => {
|
|
171
|
+
if (!useColors) {
|
|
172
|
+
return status;
|
|
173
|
+
}
|
|
174
|
+
const numeric = Number.parseInt(status, 10);
|
|
175
|
+
if (!Number.isFinite(numeric)) {
|
|
176
|
+
return status;
|
|
177
|
+
}
|
|
178
|
+
if (numeric >= 500) {
|
|
179
|
+
return chalk.red(status);
|
|
180
|
+
}
|
|
181
|
+
if (numeric >= 400) {
|
|
182
|
+
return chalk.yellow(status);
|
|
183
|
+
}
|
|
184
|
+
if (numeric >= 300) {
|
|
185
|
+
return chalk.cyan(status);
|
|
186
|
+
}
|
|
187
|
+
if (numeric >= 200) {
|
|
188
|
+
return chalk.green(status);
|
|
189
|
+
}
|
|
190
|
+
return chalk.gray(status);
|
|
191
|
+
};
|
|
192
|
+
var getColoredDuration = (duration, useColors) => {
|
|
193
|
+
if (!useColors) {
|
|
194
|
+
return duration;
|
|
195
|
+
}
|
|
196
|
+
return chalk.gray(duration);
|
|
197
|
+
};
|
|
198
|
+
var getColoredTimestamp = (timestamp, useColors) => {
|
|
199
|
+
if (!useColors) {
|
|
200
|
+
return timestamp;
|
|
201
|
+
}
|
|
202
|
+
return chalk.bgHex("#FFA500").black(timestamp);
|
|
203
|
+
};
|
|
204
|
+
var getColoredPathname = (pathname, useColors) => {
|
|
205
|
+
if (!useColors) {
|
|
206
|
+
return pathname;
|
|
207
|
+
}
|
|
208
|
+
return chalk.whiteBright(pathname);
|
|
209
|
+
};
|
|
210
|
+
var getContextString = (value) => {
|
|
211
|
+
if (typeof value === "object" && value !== null) {
|
|
212
|
+
return JSON.stringify(value);
|
|
213
|
+
}
|
|
214
|
+
return "";
|
|
215
|
+
};
|
|
216
|
+
var formatLine = ({
|
|
217
|
+
level,
|
|
218
|
+
request,
|
|
219
|
+
data,
|
|
220
|
+
store,
|
|
221
|
+
options
|
|
222
|
+
}) => {
|
|
223
|
+
const fmt = options.format;
|
|
224
|
+
const useColors = shouldUseColors(options);
|
|
225
|
+
const format = fmt?.template ?? "\uD83E\uDD8A {now} {level} {duration} {method} {pathname} {status} {message} {ip} {context}";
|
|
226
|
+
const now = new Date;
|
|
227
|
+
const epoch = String(now.getTime());
|
|
228
|
+
const rawTimestamp = formatTimestamp(now, fmt?.timestamp);
|
|
229
|
+
const timestamp = getColoredTimestamp(rawTimestamp, useColors);
|
|
230
|
+
const message = typeof data.message === "string" ? data.message : "";
|
|
231
|
+
const durationMs = store.beforeTime === BigInt(0) ? 0 : Number(process.hrtime.bigint() - store.beforeTime) / 1e6;
|
|
232
|
+
const pathname = store.pathname || new URL(request.url).pathname;
|
|
233
|
+
const statusValue = data.status;
|
|
234
|
+
const statusCode = statusValue === null || statusValue === undefined ? 200 : getStatusCode(statusValue);
|
|
235
|
+
const status = String(statusCode);
|
|
236
|
+
const ip = fmt?.showIp === true ? getIp(request) : "";
|
|
237
|
+
const ctxString = getContextString(data.context);
|
|
238
|
+
const coloredLevel = getColoredLevel(level, useColors);
|
|
239
|
+
const coloredMethod = getColoredMethod(request.method, useColors);
|
|
240
|
+
const coloredPathname = getColoredPathname(pathname, useColors);
|
|
241
|
+
const coloredStatus = getColoredStatus(status, useColors);
|
|
242
|
+
const coloredDuration = getColoredDuration(`${durationMs.toFixed(2)}ms`, useColors);
|
|
243
|
+
return format.replaceAll("{now}", timestamp).replaceAll("{epoch}", epoch).replaceAll("{level}", coloredLevel).replaceAll("{duration}", coloredDuration).replaceAll("{method}", coloredMethod).replaceAll("{pathname}", coloredPathname).replaceAll("{path}", coloredPathname).replaceAll("{status}", coloredStatus).replaceAll("{message}", message).replaceAll("{ip}", ip).replaceAll("{context}", ctxString);
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
// src/output/index.ts
|
|
247
|
+
var logToTransports = (input) => {
|
|
248
|
+
const { level, request, data, store, transports } = input;
|
|
249
|
+
if (transports.length === 0) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const message = typeof data.message === "string" ? data.message : "";
|
|
253
|
+
const meta = {
|
|
254
|
+
request: {
|
|
255
|
+
method: request.method,
|
|
256
|
+
url: request.url
|
|
257
|
+
},
|
|
258
|
+
...data,
|
|
259
|
+
beforeTime: store.beforeTime
|
|
260
|
+
};
|
|
261
|
+
for (const transport of transports) {
|
|
262
|
+
try {
|
|
263
|
+
const result = transport.log(level, message, meta);
|
|
264
|
+
if (result && typeof result.catch === "function") {
|
|
265
|
+
result.catch(() => {});
|
|
266
|
+
}
|
|
267
|
+
} catch {}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/output/file.ts
|
|
272
|
+
import { appendFile } from "node:fs/promises";
|
|
273
|
+
import { dirname as dirname2 } from "node:path";
|
|
274
|
+
|
|
275
|
+
// src/output/fs.ts
|
|
276
|
+
import { promises as fs } from "node:fs";
|
|
277
|
+
var ensureDir = async (dirPath) => {
|
|
278
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// src/output/rotation-manager.ts
|
|
282
|
+
import { promises as fs3 } from "node:fs";
|
|
283
|
+
import { promisify } from "node:util";
|
|
284
|
+
import { gzip } from "node:zlib";
|
|
285
|
+
|
|
286
|
+
// src/utils/rotation.ts
|
|
287
|
+
import { promises as fs2 } from "node:fs";
|
|
288
|
+
import { basename, dirname } from "node:path";
|
|
289
|
+
var SIZE_REGEX = /^(\d+(?:\.\d+)?)(k|kb|m|mb|g|gb)$/i;
|
|
290
|
+
var INTERVAL_REGEX = /^(\d+)(h|d|w)$/i;
|
|
291
|
+
var ROTATED_REGEX = /\.(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2})(?:\.gz)?$/;
|
|
292
|
+
var parseSize = (value) => {
|
|
293
|
+
if (typeof value === "number") {
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
const trimmed = value.trim();
|
|
297
|
+
const asNumber = Number(trimmed);
|
|
298
|
+
if (Number.isFinite(asNumber)) {
|
|
299
|
+
return asNumber;
|
|
300
|
+
}
|
|
301
|
+
const match = trimmed.match(SIZE_REGEX);
|
|
302
|
+
if (!match) {
|
|
303
|
+
throw new Error(`Invalid size format: ${value}`);
|
|
304
|
+
}
|
|
305
|
+
const amount = Number(match[1]);
|
|
306
|
+
const unit = match[2].toLowerCase();
|
|
307
|
+
let base = 1024;
|
|
308
|
+
if (unit.startsWith("m")) {
|
|
309
|
+
base = 1024 * 1024;
|
|
310
|
+
} else if (unit.startsWith("g")) {
|
|
311
|
+
base = 1024 * 1024 * 1024;
|
|
312
|
+
}
|
|
313
|
+
return Math.floor(amount * base);
|
|
314
|
+
};
|
|
315
|
+
var parseInterval = (value) => {
|
|
316
|
+
const match = value.trim().match(INTERVAL_REGEX);
|
|
317
|
+
if (!match) {
|
|
318
|
+
throw new Error(`Invalid interval format: ${value}`);
|
|
319
|
+
}
|
|
320
|
+
const amount = Number(match[1]);
|
|
321
|
+
const unit = match[2].toLowerCase();
|
|
322
|
+
let ms = 60 * 60 * 1000;
|
|
323
|
+
if (unit === "d") {
|
|
324
|
+
ms = 24 * 60 * 60 * 1000;
|
|
325
|
+
} else if (unit === "w") {
|
|
326
|
+
ms = 7 * 24 * 60 * 60 * 1000;
|
|
327
|
+
}
|
|
328
|
+
return amount * ms;
|
|
329
|
+
};
|
|
330
|
+
var parseRetention = (value) => {
|
|
331
|
+
if (typeof value === "number") {
|
|
332
|
+
return { type: "count", value };
|
|
333
|
+
}
|
|
334
|
+
return { type: "time", value: parseInterval(value) };
|
|
335
|
+
};
|
|
336
|
+
var shouldRotateBySize = async (filePath, maxSizeBytes) => {
|
|
337
|
+
try {
|
|
338
|
+
const stat = await fs2.stat(filePath);
|
|
339
|
+
return stat.size > maxSizeBytes;
|
|
340
|
+
} catch {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
var getRotatedFiles = async (filePath) => {
|
|
345
|
+
const dir = dirname(filePath);
|
|
346
|
+
const base = basename(filePath);
|
|
347
|
+
let entries;
|
|
348
|
+
try {
|
|
349
|
+
entries = await fs2.readdir(dir);
|
|
350
|
+
} catch {
|
|
351
|
+
return [];
|
|
352
|
+
}
|
|
353
|
+
return entries.filter((name) => name.startsWith(`${base}.`) && ROTATED_REGEX.test(name)).map((name) => `${dir}/${name}`);
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
// src/output/rotation-manager.ts
|
|
357
|
+
var gzipAsync = promisify(gzip);
|
|
358
|
+
var getRotatedFileName = (filePath, date) => {
|
|
359
|
+
const yyyy = date.getFullYear();
|
|
360
|
+
const mm = pad2(date.getMonth() + 1);
|
|
361
|
+
const dd = pad2(date.getDate());
|
|
362
|
+
const HH = pad2(date.getHours());
|
|
363
|
+
const MM = pad2(date.getMinutes());
|
|
364
|
+
const ss = pad2(date.getSeconds());
|
|
365
|
+
return `${filePath}.${yyyy}-${mm}-${dd}-${HH}-${MM}-${ss}`;
|
|
366
|
+
};
|
|
367
|
+
var rotateFile = async (filePath) => {
|
|
368
|
+
try {
|
|
369
|
+
const stat = await fs3.stat(filePath);
|
|
370
|
+
if (stat.size === 0) {
|
|
371
|
+
return "";
|
|
372
|
+
}
|
|
373
|
+
} catch {
|
|
374
|
+
return "";
|
|
375
|
+
}
|
|
376
|
+
const rotated = getRotatedFileName(filePath, new Date);
|
|
377
|
+
await fs3.rename(filePath, rotated);
|
|
378
|
+
return rotated;
|
|
379
|
+
};
|
|
380
|
+
var compressFile = async (filePath) => {
|
|
381
|
+
const content = await fs3.readFile(filePath);
|
|
382
|
+
const compressed = await gzipAsync(content);
|
|
383
|
+
await fs3.writeFile(`${filePath}.gz`, compressed);
|
|
384
|
+
await fs3.rm(filePath, { force: true });
|
|
385
|
+
};
|
|
386
|
+
var shouldRotate = async (filePath, config) => {
|
|
387
|
+
if (config.maxSize === undefined) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
const maxSize = parseSize(config.maxSize);
|
|
391
|
+
return await shouldRotateBySize(filePath, maxSize);
|
|
392
|
+
};
|
|
393
|
+
var cleanupByCount = async (filePath, maxFiles) => {
|
|
394
|
+
const rotated = await getRotatedFiles(filePath);
|
|
395
|
+
if (rotated.length <= maxFiles) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const stats = await Promise.all(rotated.map(async (p) => ({ path: p, stat: await fs3.stat(p) })));
|
|
399
|
+
stats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs);
|
|
400
|
+
const toDelete = stats.slice(maxFiles);
|
|
401
|
+
await Promise.all(toDelete.map(({ path }) => fs3.rm(path, { force: true })));
|
|
402
|
+
};
|
|
403
|
+
var cleanupByTime = async (filePath, maxAgeMs) => {
|
|
404
|
+
const rotated = await getRotatedFiles(filePath);
|
|
405
|
+
if (rotated.length === 0) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const now = Date.now();
|
|
409
|
+
const stats = await Promise.all(rotated.map(async (p) => ({ path: p, stat: await fs3.stat(p) })));
|
|
410
|
+
const toDelete = stats.filter(({ stat }) => now - stat.mtimeMs > maxAgeMs);
|
|
411
|
+
await Promise.all(toDelete.map(({ path }) => fs3.rm(path, { force: true })));
|
|
412
|
+
};
|
|
413
|
+
var performRotation = async (filePath, config) => {
|
|
414
|
+
const rotated = await rotateFile(filePath);
|
|
415
|
+
if (!rotated) {
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const shouldCompress = config.compress === true;
|
|
419
|
+
if (shouldCompress) {
|
|
420
|
+
const algo = config.compression ?? "gzip";
|
|
421
|
+
if (algo === "gzip") {
|
|
422
|
+
await compressFile(rotated);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (config.maxFiles !== undefined) {
|
|
426
|
+
const retention = parseRetention(config.maxFiles);
|
|
427
|
+
if (retention.type === "count") {
|
|
428
|
+
await cleanupByCount(filePath, retention.value);
|
|
429
|
+
} else {
|
|
430
|
+
await cleanupByTime(filePath, retention.value);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// src/output/file.ts
|
|
436
|
+
var logToFile = async (input) => {
|
|
437
|
+
const { filePath, level, request, data, store, options } = input;
|
|
438
|
+
const message = typeof data.message === "string" ? data.message : "";
|
|
439
|
+
const durationMs = store.beforeTime === BigInt(0) ? 0 : Number(process.hrtime.bigint() - store.beforeTime) / 1e6;
|
|
440
|
+
const pathname = store.pathname || new URL(request.url).pathname;
|
|
441
|
+
const line = `${level} ${durationMs.toFixed(2)}ms ${request.method} ${pathname} ${message}
|
|
442
|
+
`;
|
|
443
|
+
await ensureDir(dirname2(filePath));
|
|
444
|
+
await appendFile(filePath, line, { encoding: "utf-8" });
|
|
445
|
+
const rotation = options.file && options.file !== false ? options.file.rotation : undefined;
|
|
446
|
+
if (!rotation) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
const should = await shouldRotate(filePath, rotation);
|
|
450
|
+
if (should) {
|
|
451
|
+
await performRotation(filePath, rotation);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// src/logger/handle-http-error.ts
|
|
456
|
+
var normalizeTransports = (transports) => {
|
|
457
|
+
if (!transports)
|
|
458
|
+
return { targets: [], only: false };
|
|
459
|
+
if (Array.isArray(transports))
|
|
460
|
+
return { targets: transports, only: false };
|
|
461
|
+
return { targets: transports.targets, only: transports.only === true };
|
|
462
|
+
};
|
|
463
|
+
var outputPipeline = (level, request, data, store, options, consoleMessage) => {
|
|
464
|
+
const { targets, only: transportsOnly } = normalizeTransports(options.transports);
|
|
465
|
+
logToTransports({ level, request, data, store, transports: targets });
|
|
466
|
+
const fileConfig = options.file;
|
|
467
|
+
const hasFile = fileConfig !== false && fileConfig !== undefined;
|
|
468
|
+
if (!transportsOnly && hasFile) {
|
|
469
|
+
logToFile({
|
|
470
|
+
filePath: fileConfig.path,
|
|
471
|
+
level,
|
|
472
|
+
request,
|
|
473
|
+
data,
|
|
474
|
+
store,
|
|
475
|
+
options
|
|
476
|
+
}).catch((e) => {
|
|
477
|
+
console.error(e);
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
if (transportsOnly)
|
|
481
|
+
return;
|
|
482
|
+
if (consoleMessage) {
|
|
483
|
+
switch (level) {
|
|
484
|
+
case "DEBUG":
|
|
485
|
+
console.debug(consoleMessage);
|
|
486
|
+
break;
|
|
487
|
+
case "INFO":
|
|
488
|
+
console.info(consoleMessage);
|
|
489
|
+
break;
|
|
490
|
+
case "WARNING":
|
|
491
|
+
console.warn(consoleMessage);
|
|
492
|
+
break;
|
|
493
|
+
case "ERROR":
|
|
494
|
+
console.error(consoleMessage);
|
|
495
|
+
break;
|
|
496
|
+
default:
|
|
497
|
+
console.log(consoleMessage);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
var handleHttpError = (request, problem, store, options) => {
|
|
502
|
+
const level = problem.status >= 500 ? "ERROR" : "WARNING";
|
|
503
|
+
const rfcData = problem.toJSON();
|
|
504
|
+
const data = {
|
|
505
|
+
status: problem.status,
|
|
506
|
+
message: problem.detail || problem.title,
|
|
507
|
+
...rfcData
|
|
508
|
+
};
|
|
509
|
+
const { only: transportsOnly } = normalizeTransports(options.transports);
|
|
510
|
+
let consoleMessage = "";
|
|
511
|
+
if (!transportsOnly) {
|
|
512
|
+
let timestamp = "";
|
|
513
|
+
if (options.format?.timestamp) {
|
|
514
|
+
timestamp = `[${new Date().toISOString()}] `;
|
|
515
|
+
}
|
|
516
|
+
const pathname = store.pathname || new URL(request.url).pathname;
|
|
517
|
+
consoleMessage = `${timestamp}${level} ${request.method} ${pathname} ${problem.status} - ${problem.title}`;
|
|
518
|
+
if (options.error?.verbose) {
|
|
519
|
+
const parts = [consoleMessage];
|
|
520
|
+
if (rfcData.detail)
|
|
521
|
+
parts.push(` Detail: ${rfcData.detail}`);
|
|
522
|
+
if (rfcData.instance)
|
|
523
|
+
parts.push(` Instance: ${rfcData.instance}`);
|
|
524
|
+
if (rfcData.type && rfcData.type !== "about:blank")
|
|
525
|
+
parts.push(` Type: ${rfcData.type}`);
|
|
526
|
+
const extensions = Object.entries(rfcData).filter(([key]) => !["type", "title", "status", "detail", "instance"].includes(key));
|
|
527
|
+
if (extensions.length > 0) {
|
|
528
|
+
parts.push(" Extensions:");
|
|
529
|
+
for (const [key, value] of extensions) {
|
|
530
|
+
parts.push(` ${key}: ${JSON.stringify(value)}`);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
consoleMessage = parts.join(`
|
|
534
|
+
`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
outputPipeline(level, request, data, store, options, consoleMessage || undefined);
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
// src/logger/index.ts
|
|
541
|
+
var shouldLog = (level, logLevel) => {
|
|
542
|
+
if (logLevel === undefined)
|
|
543
|
+
return true;
|
|
544
|
+
const levels = Array.isArray(logLevel) ? logLevel : [logLevel];
|
|
545
|
+
if (levels.length === 0)
|
|
546
|
+
return true;
|
|
547
|
+
return levels.includes(level);
|
|
548
|
+
};
|
|
549
|
+
var createLogger = (options = {}) => {
|
|
550
|
+
const pinoOptions = options.pino ?? {};
|
|
551
|
+
const isPrettyPrint = pinoOptions.transport === undefined;
|
|
552
|
+
const pinoLogger = isPrettyPrint ? pino({
|
|
553
|
+
...pinoOptions,
|
|
554
|
+
level: pinoOptions.level ?? "info",
|
|
555
|
+
messageKey: pinoOptions.messageKey,
|
|
556
|
+
errorKey: pinoOptions.errorKey,
|
|
557
|
+
transport: {
|
|
558
|
+
target: "pino-pretty",
|
|
559
|
+
options: {
|
|
560
|
+
colorize: process.stdout?.isTTY === true,
|
|
561
|
+
translateTime: options.format?.timestamp,
|
|
562
|
+
messageKey: pinoOptions.messageKey,
|
|
563
|
+
errorKey: pinoOptions.errorKey
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}) : pino({
|
|
567
|
+
...pinoOptions,
|
|
568
|
+
level: pinoOptions.level ?? "info",
|
|
569
|
+
messageKey: pinoOptions.messageKey,
|
|
570
|
+
errorKey: pinoOptions.errorKey
|
|
571
|
+
});
|
|
572
|
+
const log = (level, request, data, store) => {
|
|
573
|
+
if (!shouldLog(level, options.logLevel)) {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
const consoleMessage = formatLine({ level, request, data, store, options });
|
|
577
|
+
outputPipeline(level, request, data, store, options, consoleMessage);
|
|
578
|
+
};
|
|
579
|
+
const logWithContext = (level, request, message, context) => {
|
|
580
|
+
const store = {
|
|
581
|
+
beforeTime: process.hrtime.bigint(),
|
|
582
|
+
pathname: new URL(request.url).pathname
|
|
583
|
+
};
|
|
584
|
+
log(level, request, { message, context }, store);
|
|
585
|
+
};
|
|
586
|
+
return {
|
|
587
|
+
pino: pinoLogger,
|
|
588
|
+
log,
|
|
589
|
+
handleHttpError: (request, error, store) => {
|
|
590
|
+
handleHttpError(request, error, store, options);
|
|
591
|
+
},
|
|
592
|
+
debug: (request, message, context) => {
|
|
593
|
+
logWithContext("DEBUG", request, message, context);
|
|
594
|
+
},
|
|
595
|
+
info: (request, message, context) => {
|
|
596
|
+
logWithContext("INFO", request, message, context);
|
|
597
|
+
},
|
|
598
|
+
warn: (request, message, context) => {
|
|
599
|
+
logWithContext("WARNING", request, message, context);
|
|
600
|
+
},
|
|
601
|
+
error: (request, message, context) => {
|
|
602
|
+
logWithContext("ERROR", request, message, context);
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// src/error/errors.ts
|
|
608
|
+
class ProblemError extends Error {
|
|
609
|
+
status;
|
|
610
|
+
title;
|
|
611
|
+
type;
|
|
612
|
+
detail;
|
|
613
|
+
instance;
|
|
614
|
+
extensions;
|
|
615
|
+
constructor(type = "about:blank", title, status, detail, instance, extensions) {
|
|
616
|
+
super(detail || title);
|
|
617
|
+
Object.setPrototypeOf(this, ProblemError.prototype);
|
|
618
|
+
this.status = status;
|
|
619
|
+
this.title = title;
|
|
620
|
+
this.type = type;
|
|
621
|
+
this.detail = detail;
|
|
622
|
+
this.instance = instance;
|
|
623
|
+
this.extensions = extensions;
|
|
624
|
+
}
|
|
625
|
+
toJSON() {
|
|
626
|
+
return {
|
|
627
|
+
type: this.type,
|
|
628
|
+
title: this.title,
|
|
629
|
+
status: this.status,
|
|
630
|
+
...this.detail ? { detail: this.detail } : {},
|
|
631
|
+
...this.instance ? { instance: this.instance } : {},
|
|
632
|
+
...this.extensions
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
var DEFAULT_TITLES = {
|
|
637
|
+
400: "Bad Request",
|
|
638
|
+
401: "Unauthorized",
|
|
639
|
+
402: "Payment Required",
|
|
640
|
+
403: "Forbidden",
|
|
641
|
+
404: "Not Found",
|
|
642
|
+
405: "Method Not Allowed",
|
|
643
|
+
406: "Not Acceptable",
|
|
644
|
+
409: "Conflict",
|
|
645
|
+
500: "Internal Server Error",
|
|
646
|
+
501: "Not Implemented",
|
|
647
|
+
502: "Bad Gateway",
|
|
648
|
+
503: "Service Unavailable",
|
|
649
|
+
504: "Gateway Timeout"
|
|
650
|
+
};
|
|
651
|
+
var createProblem = (status, overrides) => {
|
|
652
|
+
const title = overrides?.title ?? DEFAULT_TITLES[status] ?? "Unknown Error";
|
|
653
|
+
return new ProblemError(overrides?.type ?? `https://httpstatuses.com/${status}`, title, status, overrides?.detail, overrides?.instance, overrides?.extensions);
|
|
654
|
+
};
|
|
655
|
+
var factory = (status) => (detail, extensions) => createProblem(status, { detail, extensions });
|
|
656
|
+
var HttpError = {
|
|
657
|
+
BadRequest: factory(400),
|
|
658
|
+
Unauthorized: factory(401),
|
|
659
|
+
PaymentRequired: factory(402),
|
|
660
|
+
Forbidden: factory(403),
|
|
661
|
+
NotFound: factory(404),
|
|
662
|
+
MethodNotAllowed: factory(405),
|
|
663
|
+
NotAcceptable: factory(406),
|
|
664
|
+
Conflict: factory(409),
|
|
665
|
+
InternalServerError: factory(500),
|
|
666
|
+
NotImplemented: factory(501),
|
|
667
|
+
BadGateway: factory(502),
|
|
668
|
+
ServiceUnavailable: factory(503),
|
|
669
|
+
GatewayTimeout: factory(504)
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
// src/utils/get-error-code.ts
|
|
673
|
+
var getErrorCode = (error) => {
|
|
674
|
+
if (typeof error !== "object" || error === null)
|
|
675
|
+
return;
|
|
676
|
+
const obj = error;
|
|
677
|
+
if ("code" in obj && typeof obj.code === "string") {
|
|
678
|
+
return obj.code;
|
|
679
|
+
}
|
|
680
|
+
if ("cause" in obj) {
|
|
681
|
+
return getErrorCode(obj.cause);
|
|
682
|
+
}
|
|
683
|
+
if ("error" in obj) {
|
|
684
|
+
return getErrorCode(obj.error);
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
// src/utils/handle-error.ts
|
|
690
|
+
var CODE_MAP = {
|
|
691
|
+
VALIDATION: { status: 400, title: "Validation Failed" },
|
|
692
|
+
NOT_FOUND: { status: 404, title: "Resource Not Found" },
|
|
693
|
+
PARSE: {
|
|
694
|
+
status: 400,
|
|
695
|
+
title: "Invalid Payload",
|
|
696
|
+
detail: "The request body could not be parsed as valid JSON."
|
|
697
|
+
},
|
|
698
|
+
INVALID_COOKIE_SIGNATURE: { status: 401, title: "Invalid Credentials" },
|
|
699
|
+
INTERNAL_SERVER_ERROR: { status: 500, title: "Internal Server Error" }
|
|
700
|
+
};
|
|
701
|
+
var normalizeToProblem = (error, code, path, options = {}) => {
|
|
702
|
+
const { typeBaseUrl = "about:blank", errorMap, resolve, request } = options;
|
|
703
|
+
const buildType = (suffix) => typeBaseUrl === "about:blank" ? typeBaseUrl : `${typeBaseUrl}/${suffix}`;
|
|
704
|
+
if (error instanceof ProblemError) {
|
|
705
|
+
if (error.instance)
|
|
706
|
+
return error;
|
|
707
|
+
return new ProblemError(error.type, error.title, error.status, error.detail, path, error.extensions);
|
|
708
|
+
}
|
|
709
|
+
if (resolve && request) {
|
|
710
|
+
const resolved = resolve(error, { code, path, request });
|
|
711
|
+
if (resolved instanceof ProblemError)
|
|
712
|
+
return resolved;
|
|
713
|
+
}
|
|
714
|
+
if (code === "VALIDATION") {
|
|
715
|
+
const validationError = error;
|
|
716
|
+
return createProblem(400, {
|
|
717
|
+
title: "Validation Failed",
|
|
718
|
+
detail: "One or more fields failed validation",
|
|
719
|
+
type: buildType("validation"),
|
|
720
|
+
instance: path,
|
|
721
|
+
extensions: {
|
|
722
|
+
errors: validationError.all?.map((err) => ({
|
|
723
|
+
field: err.path.replace(/^\//, ""),
|
|
724
|
+
message: err.summary || err.message
|
|
725
|
+
})) ?? []
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
if (errorMap) {
|
|
730
|
+
const errorCode = getErrorCode(error);
|
|
731
|
+
if (errorCode && errorCode in errorMap) {
|
|
732
|
+
const mapping = errorMap[errorCode];
|
|
733
|
+
return createProblem(mapping.status, {
|
|
734
|
+
title: mapping.title,
|
|
735
|
+
detail: mapping.detail,
|
|
736
|
+
type: buildType(errorCode),
|
|
737
|
+
instance: path
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
const builtIn = CODE_MAP[code];
|
|
742
|
+
if (builtIn) {
|
|
743
|
+
const detail2 = builtIn.detail ?? (error instanceof Error ? error.message : String(error));
|
|
744
|
+
return createProblem(builtIn.status, {
|
|
745
|
+
title: builtIn.title,
|
|
746
|
+
detail: detail2,
|
|
747
|
+
type: buildType(String(code)),
|
|
748
|
+
instance: path
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
752
|
+
const fallbackStatus = typeof error?.status === "number" ? error.status : 500;
|
|
753
|
+
const fallbackConfig = {
|
|
754
|
+
detail,
|
|
755
|
+
type: buildType("unknown"),
|
|
756
|
+
instance: path
|
|
757
|
+
};
|
|
758
|
+
if (fallbackStatus !== 500) {
|
|
759
|
+
fallbackConfig.title = undefined;
|
|
760
|
+
}
|
|
761
|
+
return createProblem(fallbackStatus, fallbackConfig);
|
|
762
|
+
};
|
|
763
|
+
|
|
764
|
+
// src/index.ts
|
|
765
|
+
var logixlysia = (options = {}) => {
|
|
766
|
+
const didCustomLog = new WeakSet;
|
|
767
|
+
const baseLogger = createLogger(options);
|
|
768
|
+
const logger = {
|
|
769
|
+
...baseLogger,
|
|
770
|
+
debug: (request, message, context) => {
|
|
771
|
+
didCustomLog.add(request);
|
|
772
|
+
baseLogger.debug(request, message, context);
|
|
773
|
+
},
|
|
774
|
+
info: (request, message, context) => {
|
|
775
|
+
didCustomLog.add(request);
|
|
776
|
+
baseLogger.info(request, message, context);
|
|
777
|
+
},
|
|
778
|
+
warn: (request, message, context) => {
|
|
779
|
+
didCustomLog.add(request);
|
|
780
|
+
baseLogger.warn(request, message, context);
|
|
781
|
+
},
|
|
782
|
+
error: (request, message, context) => {
|
|
783
|
+
didCustomLog.add(request);
|
|
784
|
+
baseLogger.error(request, message, context);
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
const app = new Elysia({
|
|
788
|
+
name: "Logixlysia"
|
|
789
|
+
});
|
|
790
|
+
const errorConfig = options.error;
|
|
791
|
+
return app.state("logger", logger).state("pino", logger.pino).state("beforeTime", BigInt(0)).state("pathname", "").onStart(({ server }) => {
|
|
792
|
+
if (server) {
|
|
793
|
+
startServer(server, options);
|
|
794
|
+
}
|
|
795
|
+
}).onRequest(({ request, store }) => {
|
|
796
|
+
store.beforeTime = process.hrtime.bigint();
|
|
797
|
+
store.pathname = new URL(request.url).pathname;
|
|
798
|
+
}).onAfterHandle(({ request, set, store }) => {
|
|
799
|
+
if (didCustomLog.has(request)) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const status = typeof set.status === "number" ? set.status : 200;
|
|
803
|
+
let level = "INFO";
|
|
804
|
+
if (status >= 500) {
|
|
805
|
+
level = "ERROR";
|
|
806
|
+
} else if (status >= 400) {
|
|
807
|
+
level = "WARNING";
|
|
808
|
+
}
|
|
809
|
+
logger.log(level, request, { status }, store);
|
|
810
|
+
}).onError(({ request, error, code, path, store, set }) => {
|
|
811
|
+
const problem = normalizeToProblem(error, code, path, {
|
|
812
|
+
typeBaseUrl: errorConfig?.typeBaseUrl,
|
|
813
|
+
errorMap: errorConfig?.errorMap,
|
|
814
|
+
resolve: errorConfig?.resolve,
|
|
815
|
+
request
|
|
816
|
+
});
|
|
817
|
+
logger.handleHttpError(request, problem, store, options);
|
|
818
|
+
set.status = problem.status;
|
|
819
|
+
set.headers["content-type"] = "application/problem+json";
|
|
820
|
+
return problem.toJSON();
|
|
821
|
+
}).as("scoped");
|
|
822
|
+
};
|
|
823
|
+
var src_default = logixlysia;
|
|
824
|
+
export {
|
|
825
|
+
normalizeToProblem,
|
|
826
|
+
logixlysia,
|
|
827
|
+
getErrorCode,
|
|
828
|
+
src_default as default,
|
|
829
|
+
createProblem,
|
|
830
|
+
ProblemError,
|
|
831
|
+
HttpError
|
|
832
|
+
};
|