@pori15/logixlysia 6.0.8 → 6.0.9
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/README.md +79 -49
- package/package.json +1 -1
- package/src/extensions/index.ts +2 -2
- package/src/index.ts +6 -1
- package/src/interfaces.ts +101 -87
- package/src/logger/create-logger.ts +5 -6
- package/src/logger/handle-http-error.ts +37 -19
- package/src/logger/index.ts +16 -25
- package/src/output/file.ts +2 -1
- package/src/output/index.ts +3 -4
package/README.md
CHANGED
|
@@ -1,49 +1,79 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
<h1><code>🦊</code> Logixlysia</h1>
|
|
3
|
-
<strong>Logixlysia is a logging library for ElysiaJS</strong>
|
|
4
|
-
<img src="https://github.com/PunGrumpy/logixlysia/blob/main/apps/docs/app/opengraph-image.png?raw=true" alt="Logixlysia" width="100%" height="auto" />
|
|
5
|
-
</div>
|
|
6
|
-
|
|
7
|
-
## `📩` Installation
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
bun add logixlysia
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
## `📝` Usage
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
.use(
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1><code>🦊</code> Logixlysia</h1>
|
|
3
|
+
<strong>Logixlysia is a logging library for ElysiaJS</strong>
|
|
4
|
+
<img src="https://github.com/PunGrumpy/logixlysia/blob/main/apps/docs/app/opengraph-image.png?raw=true" alt="Logixlysia" width="100%" height="auto" />
|
|
5
|
+
</div>
|
|
6
|
+
|
|
7
|
+
## `📩` Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add logixlysia
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## `📝` Usage
|
|
14
|
+
|
|
15
|
+
### Basic
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Elysia } from 'elysia'
|
|
19
|
+
import logixlysia from 'logixlysia'
|
|
20
|
+
|
|
21
|
+
const app = new Elysia()
|
|
22
|
+
.use(logixlysia())
|
|
23
|
+
.get('/', () => 'Hello World')
|
|
24
|
+
.listen(3000)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### With Configuration
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
app.use(logixlysia({
|
|
31
|
+
startup: {
|
|
32
|
+
show: true,
|
|
33
|
+
format: 'simple',
|
|
34
|
+
},
|
|
35
|
+
format: {
|
|
36
|
+
timestamp: 'yyyy-mm-dd HH:MM:ss.SSS',
|
|
37
|
+
showIp: true,
|
|
38
|
+
template: '🦊 {now} {level} {duration} {method} {pathname} {status} {message} {ip}',
|
|
39
|
+
},
|
|
40
|
+
file: {
|
|
41
|
+
path: './logs/example.log',
|
|
42
|
+
rotation: {
|
|
43
|
+
maxSize: '10m',
|
|
44
|
+
maxFiles: 7,
|
|
45
|
+
compress: true,
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
logLevel: ['ERROR', 'WARNING'],
|
|
49
|
+
}))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## `⚙️` Options
|
|
53
|
+
|
|
54
|
+
| Option | Type | Default | Description |
|
|
55
|
+
|--------|------|---------|-------------|
|
|
56
|
+
| `startup.show` | `boolean` | `true` | Show startup message |
|
|
57
|
+
| `startup.format` | `"simple" \| "banner"` | `"banner"` | Startup message style |
|
|
58
|
+
| `format.colors` | `boolean` | `true` (TTY) | Enable colored output |
|
|
59
|
+
| `format.timestamp` | `string` | — | Timestamp format pattern (e.g. `'yyyy-mm-dd HH:MM:ss.SSS'`) |
|
|
60
|
+
| `format.template` | `string` | — | Custom log format template |
|
|
61
|
+
| `format.showIp` | `boolean` | `false` | Show IP address in logs |
|
|
62
|
+
| `logLevel` | `LogLevel \| LogLevel[]` | — | Filter logs by level(s) |
|
|
63
|
+
| `file` | `false \| { path, rotation? }` | — | File logging config (`false` to disable) |
|
|
64
|
+
| `file.path` | `string` | — | Log file path (required when file logging enabled) |
|
|
65
|
+
| `file.rotation` | `LogRotationConfig` | — | Log file rotation settings |
|
|
66
|
+
| `transports` | `Transport[] \| { targets, only? }` | — | Custom transports |
|
|
67
|
+
| `pino` | `PinoLoggerOptions` | — | Pino logger options |
|
|
68
|
+
| `error.typeBaseUrl` | `string` | — | Base URL for error types (RFC 9457) |
|
|
69
|
+
| `error.errorMap` | `Record<string, ErrorMapping>` | — | Error code to HTTP status mapping |
|
|
70
|
+
| `error.resolve` | `ErrorResolver` | — | Custom error resolver function |
|
|
71
|
+
| `error.verbose` | `boolean` | `false` | Show full error details in console |
|
|
72
|
+
|
|
73
|
+
## `📚` Documentation
|
|
74
|
+
|
|
75
|
+
Check out the [website](https://logixlysia.vercel.app) for more detailed documentation and examples.
|
|
76
|
+
|
|
77
|
+
## `📄` License
|
|
78
|
+
|
|
79
|
+
Licensed under the [MIT License](LICENSE).
|
package/package.json
CHANGED
package/src/extensions/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export const startServer = (
|
|
|
5
5
|
server: { port?: number; hostname?: string; protocol?: string | null },
|
|
6
6
|
options: Options
|
|
7
7
|
): void => {
|
|
8
|
-
const showStartupMessage = options.
|
|
8
|
+
const showStartupMessage = options.startup?.show ?? true;
|
|
9
9
|
if (!showStartupMessage) {
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
@@ -18,7 +18,7 @@ export const startServer = (
|
|
|
18
18
|
const url = `${protocol}://${hostname}:${port}`;
|
|
19
19
|
const message = `🦊 Elysia is running at ${url}`;
|
|
20
20
|
|
|
21
|
-
const format = options.
|
|
21
|
+
const format = options.startup?.format ?? "banner";
|
|
22
22
|
if (format === "simple") {
|
|
23
23
|
console.log(message);
|
|
24
24
|
return;
|
package/src/index.ts
CHANGED
|
@@ -52,7 +52,7 @@ export const logixlysia = (options: Options = {}): Logixlysia => {
|
|
|
52
52
|
name: "Logixlysia",
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
const errorConfig = options.
|
|
55
|
+
const errorConfig = options.error;
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
58
|
app
|
|
@@ -131,16 +131,21 @@ export { normalizeToProblem } from "./utils/handle-error";
|
|
|
131
131
|
// ==========================================
|
|
132
132
|
|
|
133
133
|
export type {
|
|
134
|
+
ErrorConfig,
|
|
134
135
|
ErrorMapping,
|
|
135
136
|
ErrorResolver,
|
|
137
|
+
FileConfig,
|
|
138
|
+
FormatConfig,
|
|
136
139
|
Logger,
|
|
137
140
|
LogixlysiaContext,
|
|
138
141
|
LogixlysiaStore,
|
|
139
142
|
LogLevel,
|
|
140
143
|
Options,
|
|
141
144
|
Pino,
|
|
145
|
+
StartupConfig,
|
|
142
146
|
StoreData,
|
|
143
147
|
Transport,
|
|
148
|
+
TransportsConfig,
|
|
144
149
|
} from "./interfaces";
|
|
145
150
|
|
|
146
151
|
export default logixlysia;
|
package/src/interfaces.ts
CHANGED
|
@@ -5,15 +5,21 @@ import type {
|
|
|
5
5
|
import type { ProblemError } from "./error/errors";
|
|
6
6
|
import type { Code } from "./error/type";
|
|
7
7
|
|
|
8
|
+
/** Pino Logger 实例类型 */
|
|
8
9
|
export type Pino = PinoLogger<never, boolean>;
|
|
9
10
|
|
|
11
|
+
/** 日志级别 */
|
|
10
12
|
export type LogLevel = "DEBUG" | "INFO" | "WARNING" | "ERROR";
|
|
11
13
|
|
|
14
|
+
/** 单次请求携带的计时和路径数据 */
|
|
12
15
|
export interface StoreData {
|
|
16
|
+
/** 请求开始的纳秒时间戳(hrtime) */
|
|
13
17
|
beforeTime: bigint;
|
|
18
|
+
/** 缓存的 URL pathname,避免重复解析 */
|
|
14
19
|
pathname: string;
|
|
15
20
|
}
|
|
16
21
|
|
|
22
|
+
/** Elysia store 中挂载的 logixlysia 状态 */
|
|
17
23
|
export interface LogixlysiaStore {
|
|
18
24
|
beforeTime?: bigint;
|
|
19
25
|
logger: Logger;
|
|
@@ -22,7 +28,15 @@ export interface LogixlysiaStore {
|
|
|
22
28
|
[key: string]: unknown;
|
|
23
29
|
}
|
|
24
30
|
|
|
31
|
+
/** 自定义日志传输接口(如 Elasticsearch、Slack 等) */
|
|
25
32
|
export interface Transport {
|
|
33
|
+
/**
|
|
34
|
+
* 接收一条日志并输出到外部系统
|
|
35
|
+
*
|
|
36
|
+
* @param level - 日志级别
|
|
37
|
+
* @param message - 日志消息
|
|
38
|
+
* @param meta - 附加元数据(请求信息、耗时等)
|
|
39
|
+
*/
|
|
26
40
|
log: (
|
|
27
41
|
level: LogLevel,
|
|
28
42
|
message: string,
|
|
@@ -30,41 +44,39 @@ export interface Transport {
|
|
|
30
44
|
) => void | Promise<void>;
|
|
31
45
|
}
|
|
32
46
|
|
|
47
|
+
/** 日志文件轮转配置 */
|
|
33
48
|
export interface LogRotationConfig {
|
|
49
|
+
/** 轮转后是否压缩旧文件 */
|
|
34
50
|
compress?: boolean;
|
|
51
|
+
/** 压缩算法 */
|
|
35
52
|
compression?: "gzip";
|
|
36
|
-
/**
|
|
37
|
-
* Rotate at a fixed interval, e.g. '1d', '12h'.
|
|
38
|
-
*/
|
|
53
|
+
/** 固定间隔轮转,如 `'1d'`、`'12h'` */
|
|
39
54
|
interval?: string;
|
|
40
|
-
/**
|
|
41
|
-
* Keep at most N files or keep files for a duration like '7d'.
|
|
42
|
-
*/
|
|
55
|
+
/** 保留的最大文件数量或时长,如 `10` 或 `'7d'` */
|
|
43
56
|
maxFiles?: number | string;
|
|
44
|
-
/**
|
|
45
|
-
* Max log file size before rotation, e.g. '10m', '5k', or a byte count.
|
|
46
|
-
*/
|
|
57
|
+
/** 单个日志文件最大体积,如 `'10m'`、`'5k'`,或字节数 */
|
|
47
58
|
maxSize?: string | number;
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
export interface LogFilter {
|
|
51
|
-
/**
|
|
52
|
-
* Array of log levels to allow. If specified, only logs with these levels will be processed.
|
|
53
|
-
* If not specified, all log levels will be allowed.
|
|
54
|
-
*/
|
|
55
|
-
level?: LogLevel[];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
61
|
// ==========================================
|
|
59
62
|
// Error Mapping
|
|
60
63
|
// ==========================================
|
|
61
64
|
|
|
65
|
+
/** 错误码到 HTTP 响应的映射条目 */
|
|
62
66
|
export interface ErrorMapping {
|
|
67
|
+
/** 错误详情描述 */
|
|
63
68
|
detail?: string;
|
|
69
|
+
/** HTTP 状态码 */
|
|
64
70
|
status: number;
|
|
71
|
+
/** 错误标题 */
|
|
65
72
|
title: string;
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
/**
|
|
76
|
+
* 自定义错误解析回调
|
|
77
|
+
*
|
|
78
|
+
* 返回 `ProblemError` 表示已处理,返回 `void` 交给下一层处理
|
|
79
|
+
*/
|
|
68
80
|
export type ErrorResolver = (
|
|
69
81
|
error: unknown,
|
|
70
82
|
context: { code: Code; path: string; request: Request }
|
|
@@ -74,112 +86,113 @@ export type ErrorResolver = (
|
|
|
74
86
|
// Options
|
|
75
87
|
// ==========================================
|
|
76
88
|
|
|
89
|
+
/** 启动消息配置 */
|
|
90
|
+
export interface StartupConfig {
|
|
91
|
+
/** 启动消息格式,默认 `"banner"` */
|
|
92
|
+
format?: "simple" | "banner";
|
|
93
|
+
/** 是否显示启动消息,默认 `true` */
|
|
94
|
+
show?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** 日志格式配置 */
|
|
98
|
+
export interface FormatConfig {
|
|
99
|
+
/** 是否启用彩色输出,默认 `true`(仅 TTY) */
|
|
100
|
+
colors?: boolean;
|
|
101
|
+
/** 是否在日志中显示请求 IP,默认 `false` */
|
|
102
|
+
showIp?: boolean;
|
|
103
|
+
/** 自定义日志模板,如 `'🦊 {now} {level} {method} {pathname} {status}'` */
|
|
104
|
+
template?: string;
|
|
105
|
+
/** 时间戳格式,如 `'yyyy-mm-dd HH:MM:ss.SSS'` */
|
|
106
|
+
timestamp?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 文件日志配置 */
|
|
110
|
+
export interface FileConfig {
|
|
111
|
+
/** 日志文件路径(必填) */
|
|
112
|
+
path: string;
|
|
113
|
+
/** 日志轮转配置 */
|
|
114
|
+
rotation?: LogRotationConfig;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** 自定义传输配置 */
|
|
118
|
+
export interface TransportsConfig {
|
|
119
|
+
/** 设为 `true` 时只使用 transports,禁用控制台和文件输出 */
|
|
120
|
+
only?: boolean;
|
|
121
|
+
/** 传输目标列表 */
|
|
122
|
+
targets: Transport[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** 错误处理配置 */
|
|
126
|
+
export interface ErrorConfig {
|
|
127
|
+
/** 错误码映射表(Postgres / MySQL / 自定义错误码) */
|
|
128
|
+
errorMap?: Record<string, ErrorMapping>;
|
|
129
|
+
/** 自定义错误解析回调 */
|
|
130
|
+
resolve?: ErrorResolver;
|
|
131
|
+
/** 自定义错误类型的 Base URL(RFC 9457) */
|
|
132
|
+
typeBaseUrl?: string;
|
|
133
|
+
/** 是否在控制台显示完整错误详情(detail、extensions),默认 `false` */
|
|
134
|
+
verbose?: boolean;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Logixlysia 插件配置 */
|
|
77
138
|
export interface Options {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
transports?: Transport[];
|
|
93
|
-
useTransportsOnly?: boolean;
|
|
94
|
-
disableInternalLogger?: boolean;
|
|
95
|
-
disableFileLogging?: boolean;
|
|
96
|
-
logFilePath?: string;
|
|
97
|
-
logRotation?: LogRotationConfig;
|
|
98
|
-
|
|
99
|
-
// Pino
|
|
100
|
-
pino?: (PinoLoggerOptions & { prettyPrint?: boolean }) | undefined;
|
|
101
|
-
|
|
102
|
-
// Error handling
|
|
103
|
-
error?: {
|
|
104
|
-
/**
|
|
105
|
-
* 自定义错误类型的 Base URL
|
|
106
|
-
* @example "https://api.mysite.com/errors"
|
|
107
|
-
*/
|
|
108
|
-
typeBaseUrl?: string;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* 错误码映射表(Postgres / MySQL / 自定义错误码)
|
|
112
|
-
* 键为错误码字符串,值为 ProblemError 字段
|
|
113
|
-
*
|
|
114
|
-
* @example
|
|
115
|
-
* ```ts
|
|
116
|
-
* errorMap: {
|
|
117
|
-
* '23505': { status: 409, title: 'Resource already exists', detail: 'Unique constraint violation' },
|
|
118
|
-
* '23503': { status: 400, title: 'Foreign key constraint failed' },
|
|
119
|
-
* 'ER_DUP_ENTRY': { status: 409, title: 'Duplicate entry' },
|
|
120
|
-
* }
|
|
121
|
-
* ```
|
|
122
|
-
*/
|
|
123
|
-
errorMap?: Record<string, ErrorMapping>;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 用户自定义错误解析回调
|
|
127
|
-
* 返回 ProblemError 表示已处理,返回 void 表示交给下一层
|
|
128
|
-
*
|
|
129
|
-
* @example
|
|
130
|
-
* ```ts
|
|
131
|
-
* resolve(error, { code, path }) {
|
|
132
|
-
* if (isStripeError(error)) {
|
|
133
|
-
* return createProblem(402, { detail: error.message })
|
|
134
|
-
* }
|
|
135
|
-
* // return void → 交给下一层
|
|
136
|
-
* }
|
|
137
|
-
* ```
|
|
138
|
-
*/
|
|
139
|
-
resolve?: ErrorResolver;
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 是否在控制台显示完整的错误详情(包括 detail 和 extensions)
|
|
143
|
-
* @default false
|
|
144
|
-
*/
|
|
145
|
-
verboseErrorLogging?: boolean;
|
|
146
|
-
};
|
|
147
|
-
};
|
|
139
|
+
/** 错误处理配置 */
|
|
140
|
+
error?: ErrorConfig;
|
|
141
|
+
/** 文件日志配置,设为 `false` 禁用文件日志 */
|
|
142
|
+
file?: false | FileConfig;
|
|
143
|
+
/** 日志格式配置 */
|
|
144
|
+
format?: FormatConfig;
|
|
145
|
+
/** 日志级别过滤,接受单个级别或级别数组 */
|
|
146
|
+
logLevel?: LogLevel | LogLevel[];
|
|
147
|
+
/** Pino Logger 原生配置透传 */
|
|
148
|
+
pino?: PinoLoggerOptions;
|
|
149
|
+
/** 启动消息配置 */
|
|
150
|
+
startup?: StartupConfig;
|
|
151
|
+
/** 自定义传输(数组或带 `only` 选项的对象) */
|
|
152
|
+
transports?: Transport[] | TransportsConfig;
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
// ==========================================
|
|
151
156
|
// Logger
|
|
152
157
|
// ==========================================
|
|
153
158
|
|
|
159
|
+
/** Logger 实例,可通过 `store.logger` 访问 */
|
|
154
160
|
export interface Logger {
|
|
161
|
+
/** 记录 DEBUG 级别日志 */
|
|
155
162
|
debug: (
|
|
156
163
|
request: Request,
|
|
157
164
|
message: string,
|
|
158
165
|
context?: Record<string, unknown>
|
|
159
166
|
) => void;
|
|
167
|
+
/** 记录 ERROR 级别日志 */
|
|
160
168
|
error: (
|
|
161
169
|
request: Request,
|
|
162
170
|
message: string,
|
|
163
171
|
context?: Record<string, unknown>
|
|
164
172
|
) => void;
|
|
173
|
+
/** 处理 HTTP 错误并输出日志 */
|
|
165
174
|
handleHttpError: (
|
|
166
175
|
request: Request,
|
|
167
176
|
error: ProblemError,
|
|
168
177
|
store: StoreData,
|
|
169
178
|
options: Options
|
|
170
179
|
) => void;
|
|
180
|
+
/** 记录 INFO 级别日志 */
|
|
171
181
|
info: (
|
|
172
182
|
request: Request,
|
|
173
183
|
message: string,
|
|
174
184
|
context?: Record<string, unknown>
|
|
175
185
|
) => void;
|
|
186
|
+
/** 记录指定级别的日志 */
|
|
176
187
|
log: (
|
|
177
188
|
level: LogLevel,
|
|
178
189
|
request: Request,
|
|
179
190
|
data: Record<string, unknown>,
|
|
180
191
|
store: StoreData
|
|
181
192
|
) => void;
|
|
193
|
+
/** 底层 Pino Logger 实例 */
|
|
182
194
|
pino: Pino;
|
|
195
|
+
/** 记录 WARNING 级别日志 */
|
|
183
196
|
warn: (
|
|
184
197
|
request: Request,
|
|
185
198
|
message: string,
|
|
@@ -187,6 +200,7 @@ export interface Logger {
|
|
|
187
200
|
) => void;
|
|
188
201
|
}
|
|
189
202
|
|
|
203
|
+
/** Logixlysia 请求上下文 */
|
|
190
204
|
export interface LogixlysiaContext {
|
|
191
205
|
request: Request;
|
|
192
206
|
store: LogixlysiaStore;
|
|
@@ -4,8 +4,7 @@ import type { LogLevel, Options, Pino, StoreData } from "../interfaces";
|
|
|
4
4
|
import { pad2, pad3 } from "../utils/format";
|
|
5
5
|
|
|
6
6
|
const shouldUseColors = (options: Options): boolean => {
|
|
7
|
-
const
|
|
8
|
-
const enabledByConfig = config?.useColors ?? true;
|
|
7
|
+
const enabledByConfig = options.format?.colors ?? true;
|
|
9
8
|
|
|
10
9
|
// Avoid ANSI sequences in non-interactive output (pipes, CI logs, files).
|
|
11
10
|
const isTty =
|
|
@@ -169,15 +168,15 @@ export const formatLine = ({
|
|
|
169
168
|
store: StoreData;
|
|
170
169
|
options: Options;
|
|
171
170
|
}): string => {
|
|
172
|
-
const
|
|
171
|
+
const fmt = options.format;
|
|
173
172
|
const useColors = shouldUseColors(options);
|
|
174
173
|
const format =
|
|
175
|
-
|
|
174
|
+
fmt?.template ??
|
|
176
175
|
"🦊 {now} {level} {duration} {method} {pathname} {status} {message} {ip} {context}";
|
|
177
176
|
|
|
178
177
|
const now = new Date();
|
|
179
178
|
const epoch = String(now.getTime());
|
|
180
|
-
const rawTimestamp = formatTimestamp(now,
|
|
179
|
+
const rawTimestamp = formatTimestamp(now, fmt?.timestamp);
|
|
181
180
|
const timestamp = getColoredTimestamp(rawTimestamp, useColors);
|
|
182
181
|
|
|
183
182
|
const message = typeof data.message === "string" ? data.message : "";
|
|
@@ -193,7 +192,7 @@ export const formatLine = ({
|
|
|
193
192
|
? 200
|
|
194
193
|
: getStatusCode(statusValue);
|
|
195
194
|
const status = String(statusCode);
|
|
196
|
-
const ip =
|
|
195
|
+
const ip = fmt?.showIp === true ? getIp(request) : "";
|
|
197
196
|
const ctxString = getContextString(data.context);
|
|
198
197
|
const coloredLevel = getColoredLevel(level, useColors);
|
|
199
198
|
const coloredMethod = getColoredMethod(request.method, useColors);
|
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
import type { ProblemError } from "../error/errors";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
LogLevel,
|
|
4
|
+
Options,
|
|
5
|
+
StoreData,
|
|
6
|
+
Transport,
|
|
7
|
+
TransportsConfig,
|
|
8
|
+
} from "../interfaces";
|
|
3
9
|
import { logToTransports } from "../output";
|
|
4
10
|
import { logToFile } from "../output/file";
|
|
5
11
|
|
|
12
|
+
const normalizeTransports = (
|
|
13
|
+
transports?: Transport[] | TransportsConfig
|
|
14
|
+
): { targets: Transport[]; only: boolean } => {
|
|
15
|
+
if (!transports) return { targets: [], only: false };
|
|
16
|
+
if (Array.isArray(transports)) return { targets: transports, only: false };
|
|
17
|
+
return { targets: transports.targets, only: transports.only === true };
|
|
18
|
+
};
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* 统一输出管道:transports → file → console
|
|
8
22
|
* handleHttpError 和 log 共用同一管道,不再重复判断配置
|
|
@@ -15,27 +29,31 @@ const outputPipeline = (
|
|
|
15
29
|
options: Options,
|
|
16
30
|
consoleMessage?: string
|
|
17
31
|
): void => {
|
|
18
|
-
const
|
|
32
|
+
const { targets, only: transportsOnly } = normalizeTransports(
|
|
33
|
+
options.transports
|
|
34
|
+
);
|
|
19
35
|
|
|
20
36
|
// 1. Transports
|
|
21
|
-
logToTransports({ level, request, data, store,
|
|
37
|
+
logToTransports({ level, request, data, store, transports: targets });
|
|
22
38
|
|
|
23
39
|
// 2. File
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
-
if (!
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
40
|
+
const fileConfig = options.file;
|
|
41
|
+
const hasFile = fileConfig !== false && fileConfig !== undefined;
|
|
42
|
+
if (!transportsOnly && hasFile) {
|
|
43
|
+
logToFile({
|
|
44
|
+
filePath: fileConfig.path,
|
|
45
|
+
level,
|
|
46
|
+
request,
|
|
47
|
+
data,
|
|
48
|
+
store,
|
|
49
|
+
options,
|
|
50
|
+
}).catch((e) => {
|
|
51
|
+
console.error(e);
|
|
52
|
+
});
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
// 3. Console
|
|
38
|
-
if (
|
|
56
|
+
if (transportsOnly) return;
|
|
39
57
|
|
|
40
58
|
if (consoleMessage) {
|
|
41
59
|
switch (level) {
|
|
@@ -63,7 +81,6 @@ export const handleHttpError = (
|
|
|
63
81
|
store: StoreData,
|
|
64
82
|
options: Options
|
|
65
83
|
): void => {
|
|
66
|
-
const config = options.config;
|
|
67
84
|
const level: LogLevel = problem.status >= 500 ? "ERROR" : "WARNING";
|
|
68
85
|
const rfcData = problem.toJSON();
|
|
69
86
|
const data = {
|
|
@@ -73,17 +90,18 @@ export const handleHttpError = (
|
|
|
73
90
|
};
|
|
74
91
|
|
|
75
92
|
// 构建 console 消息
|
|
93
|
+
const { only: transportsOnly } = normalizeTransports(options.transports);
|
|
76
94
|
let consoleMessage = "";
|
|
77
|
-
if (!
|
|
95
|
+
if (!transportsOnly) {
|
|
78
96
|
let timestamp = "";
|
|
79
|
-
if (
|
|
97
|
+
if (options.format?.timestamp) {
|
|
80
98
|
timestamp = `[${new Date().toISOString()}] `;
|
|
81
99
|
}
|
|
82
100
|
const pathname = store.pathname || new URL(request.url).pathname;
|
|
83
101
|
consoleMessage = `${timestamp}${level} ${request.method} ${pathname} ${problem.status} - ${problem.title}`;
|
|
84
102
|
|
|
85
103
|
// 详细错误日志
|
|
86
|
-
if (
|
|
104
|
+
if (options.error?.verbose) {
|
|
87
105
|
const parts = [consoleMessage];
|
|
88
106
|
if (rfcData.detail) parts.push(` Detail: ${rfcData.detail}`);
|
|
89
107
|
if (rfcData.instance) parts.push(` Instance: ${rfcData.instance}`);
|
package/src/logger/index.ts
CHANGED
|
@@ -1,25 +1,23 @@
|
|
|
1
1
|
import pino from "pino";
|
|
2
|
-
import type {
|
|
3
|
-
LogFilter,
|
|
4
|
-
Logger,
|
|
5
|
-
LogLevel,
|
|
6
|
-
Options,
|
|
7
|
-
Pino,
|
|
8
|
-
StoreData,
|
|
9
|
-
} from "../interfaces";
|
|
2
|
+
import type { Logger, LogLevel, Options, Pino, StoreData } from "../interfaces";
|
|
10
3
|
import { formatLine } from "./create-logger";
|
|
11
4
|
import { handleHttpError, outputPipeline } from "./handle-http-error";
|
|
12
5
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
6
|
+
const shouldLog = (
|
|
7
|
+
level: LogLevel,
|
|
8
|
+
logLevel?: LogLevel | LogLevel[]
|
|
9
|
+
): boolean => {
|
|
10
|
+
if (logLevel === undefined) return true;
|
|
11
|
+
const levels = Array.isArray(logLevel) ? logLevel : [logLevel];
|
|
12
|
+
if (levels.length === 0) return true;
|
|
13
|
+
return levels.includes(level);
|
|
14
|
+
};
|
|
18
15
|
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
export const createLogger = (options: Options = {}): Logger => {
|
|
17
|
+
const pinoOptions = options.pino ?? {};
|
|
18
|
+
const isPrettyPrint = pinoOptions.transport === undefined;
|
|
21
19
|
|
|
22
|
-
const pinoLogger: Pino =
|
|
20
|
+
const pinoLogger: Pino = isPrettyPrint
|
|
23
21
|
? pino({
|
|
24
22
|
...pinoOptions,
|
|
25
23
|
level: pinoOptions.level ?? "info",
|
|
@@ -29,7 +27,7 @@ export const createLogger = (options: Options = {}): Logger => {
|
|
|
29
27
|
target: "pino-pretty",
|
|
30
28
|
options: {
|
|
31
29
|
colorize: process.stdout?.isTTY === true,
|
|
32
|
-
translateTime:
|
|
30
|
+
translateTime: options.format?.timestamp,
|
|
33
31
|
messageKey: pinoOptions.messageKey,
|
|
34
32
|
errorKey: pinoOptions.errorKey,
|
|
35
33
|
},
|
|
@@ -42,20 +40,13 @@ export const createLogger = (options: Options = {}): Logger => {
|
|
|
42
40
|
errorKey: pinoOptions.errorKey,
|
|
43
41
|
});
|
|
44
42
|
|
|
45
|
-
const shouldLog = (level: LogLevel, logFilter?: LogFilter): boolean => {
|
|
46
|
-
if (!logFilter?.level || logFilter.level.length === 0) {
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
return logFilter.level.includes(level);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
43
|
const log = (
|
|
53
44
|
level: LogLevel,
|
|
54
45
|
request: Request,
|
|
55
46
|
data: Record<string, unknown>,
|
|
56
47
|
store: StoreData
|
|
57
48
|
): void => {
|
|
58
|
-
if (!shouldLog(level,
|
|
49
|
+
if (!shouldLog(level, options.logLevel)) {
|
|
59
50
|
return;
|
|
60
51
|
}
|
|
61
52
|
|
package/src/output/file.ts
CHANGED
|
@@ -27,7 +27,8 @@ export const logToFile = async (input: LogToFileInput): Promise<void> => {
|
|
|
27
27
|
await ensureDir(dirname(filePath));
|
|
28
28
|
await appendFile(filePath, line, { encoding: "utf-8" });
|
|
29
29
|
|
|
30
|
-
const rotation =
|
|
30
|
+
const rotation =
|
|
31
|
+
options.file && options.file !== false ? options.file.rotation : undefined;
|
|
31
32
|
if (!rotation) {
|
|
32
33
|
return;
|
|
33
34
|
}
|
package/src/output/index.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
import type { LogLevel,
|
|
1
|
+
import type { LogLevel, StoreData, Transport } from "../interfaces";
|
|
2
2
|
|
|
3
3
|
interface LogToTransportsInput {
|
|
4
4
|
data: Record<string, unknown>;
|
|
5
5
|
level: LogLevel;
|
|
6
|
-
options: Options;
|
|
7
6
|
request: Request;
|
|
8
7
|
store: StoreData;
|
|
8
|
+
transports: Transport[];
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export const logToTransports = (input: LogToTransportsInput): void => {
|
|
12
|
-
const { level, request, data, store,
|
|
13
|
-
const transports = options.config?.transports ?? [];
|
|
12
|
+
const { level, request, data, store, transports } = input;
|
|
14
13
|
if (transports.length === 0) {
|
|
15
14
|
return;
|
|
16
15
|
}
|