@pori15/logixlysia 0.0.1
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 +49 -0
- package/dist/index.d.ts +154 -0
- package/dist/index.js +8 -0
- package/package.json +45 -0
- package/src/Error/errors.ts +258 -0
- package/src/Error/type.ts +51 -0
- package/src/extensions/banner.ts +26 -0
- package/src/extensions/index.ts +28 -0
- package/src/helpers/status.ts +58 -0
- package/src/index.ts +141 -0
- package/src/interfaces.ts +136 -0
- package/src/logger/create-logger.ts +246 -0
- package/src/logger/handle-http-error.ts +62 -0
- package/src/logger/index.ts +125 -0
- package/src/output/file.ts +85 -0
- package/src/output/fs.ts +5 -0
- package/src/output/index.ts +58 -0
- package/src/output/rotation-manager.ts +122 -0
- package/src/utils/error.ts +15 -0
- package/src/utils/handle-error.ts +289 -0
- package/src/utils/rotation.ts +91 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
// src/libs/elysia-http-problem-json/errors.ts
|
|
2
|
+
|
|
3
|
+
export interface ProblemDocument {
|
|
4
|
+
type: string;
|
|
5
|
+
title: string;
|
|
6
|
+
status?: number;
|
|
7
|
+
detail?: string;
|
|
8
|
+
instance?: string;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* RFC 9457 Problem Details Error Base Class
|
|
14
|
+
*
|
|
15
|
+
* Core members as per RFC 9457:
|
|
16
|
+
* - type: A URI reference [RFC3986] that identifies the problem type.
|
|
17
|
+
* Defaults to "about:blank" when omitted.
|
|
18
|
+
* - title: A short, human-readable summary of the problem type.
|
|
19
|
+
* - status: The HTTP status code ([RFC7231], Section 6).
|
|
20
|
+
* - detail: A human-readable explanation specific to this occurrence of the problem.
|
|
21
|
+
* - instance: A URI reference that identifies the specific occurrence of the problem.
|
|
22
|
+
*
|
|
23
|
+
* Extension members: Additional properties can be added to provide more context.
|
|
24
|
+
* These are serialized as-is in the JSON response.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* RFC 9457 Error Base Class
|
|
28
|
+
* * 修改思路:直接使用 public readonly 属性,拒绝嵌套,拒绝 Getter。
|
|
29
|
+
*/
|
|
30
|
+
export class ProblemError extends Error {
|
|
31
|
+
// 1. 直接声明公开属性
|
|
32
|
+
public readonly status: number;
|
|
33
|
+
public readonly title: string;
|
|
34
|
+
public readonly type: string;
|
|
35
|
+
public readonly detail?: string;
|
|
36
|
+
public readonly instance?: string;
|
|
37
|
+
public readonly extensions?: Record<string, unknown>;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
type = "about:blank",
|
|
41
|
+
title: string,
|
|
42
|
+
status: number,
|
|
43
|
+
detail?: string,
|
|
44
|
+
instance?: string,
|
|
45
|
+
extensions: Record<string, unknown> = {}
|
|
46
|
+
) {
|
|
47
|
+
super(detail || title);
|
|
48
|
+
Object.setPrototypeOf(this, ProblemError.prototype);
|
|
49
|
+
|
|
50
|
+
// 2. 直接赋值给 this
|
|
51
|
+
this.status = status;
|
|
52
|
+
this.title = title;
|
|
53
|
+
this.type = type;
|
|
54
|
+
this.detail = detail;
|
|
55
|
+
this.instance = instance;
|
|
56
|
+
this.extensions = extensions;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 3. toJSON 的时候动态组装一下即可
|
|
60
|
+
toJSON(): ProblemDocument {
|
|
61
|
+
return {
|
|
62
|
+
type: this.type,
|
|
63
|
+
title: this.title,
|
|
64
|
+
status: this.status,
|
|
65
|
+
...(this.detail ? { detail: this.detail } : {}),
|
|
66
|
+
...(this.instance ? { instance: this.instance } : {}),
|
|
67
|
+
// 把扩展字段展开 (extensions)
|
|
68
|
+
...this.extensions,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// --- 40X Errors ---
|
|
74
|
+
class BadRequest extends ProblemError {
|
|
75
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
76
|
+
super(
|
|
77
|
+
"https://httpstatuses.com/400",
|
|
78
|
+
"Bad Request",
|
|
79
|
+
400,
|
|
80
|
+
detail,
|
|
81
|
+
undefined,
|
|
82
|
+
extensions
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class Unauthorized extends ProblemError {
|
|
88
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
89
|
+
super(
|
|
90
|
+
"https://httpstatuses.com/401",
|
|
91
|
+
"Unauthorized",
|
|
92
|
+
401,
|
|
93
|
+
detail,
|
|
94
|
+
undefined,
|
|
95
|
+
extensions
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class Forbidden extends ProblemError {
|
|
101
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
102
|
+
super(
|
|
103
|
+
"https://httpstatuses.com/403",
|
|
104
|
+
"Forbidden",
|
|
105
|
+
403,
|
|
106
|
+
detail,
|
|
107
|
+
undefined,
|
|
108
|
+
extensions
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
class NotFound extends ProblemError {
|
|
114
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
115
|
+
super(
|
|
116
|
+
"https://httpstatuses.com/404",
|
|
117
|
+
"Not Found",
|
|
118
|
+
404,
|
|
119
|
+
detail,
|
|
120
|
+
undefined,
|
|
121
|
+
extensions
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class Conflict extends ProblemError {
|
|
127
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
128
|
+
super(
|
|
129
|
+
"https://httpstatuses.com/409",
|
|
130
|
+
"Conflict",
|
|
131
|
+
409,
|
|
132
|
+
detail,
|
|
133
|
+
undefined,
|
|
134
|
+
extensions
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
class PaymentRequired extends ProblemError {
|
|
140
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
141
|
+
super(
|
|
142
|
+
"https://httpstatuses.com/402",
|
|
143
|
+
"Payment Required",
|
|
144
|
+
402,
|
|
145
|
+
detail,
|
|
146
|
+
undefined,
|
|
147
|
+
extensions
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
class MethodNotAllowed extends ProblemError {
|
|
153
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
154
|
+
super(
|
|
155
|
+
"https://httpstatuses.com/405",
|
|
156
|
+
"Method Not Allowed",
|
|
157
|
+
405,
|
|
158
|
+
detail,
|
|
159
|
+
undefined,
|
|
160
|
+
extensions
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
class NotAcceptable extends ProblemError {
|
|
166
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
167
|
+
super(
|
|
168
|
+
"https://httpstatuses.com/406",
|
|
169
|
+
"Not Acceptable",
|
|
170
|
+
406,
|
|
171
|
+
detail,
|
|
172
|
+
undefined,
|
|
173
|
+
extensions
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 50X Errors
|
|
179
|
+
class InternalServerError extends ProblemError {
|
|
180
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
181
|
+
super(
|
|
182
|
+
"https://httpstatuses.com/500",
|
|
183
|
+
"Internal Server Error",
|
|
184
|
+
500,
|
|
185
|
+
detail,
|
|
186
|
+
undefined,
|
|
187
|
+
extensions
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
class NotImplemented extends ProblemError {
|
|
193
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
194
|
+
super(
|
|
195
|
+
"https://httpstatuses.com/501",
|
|
196
|
+
"Not Implemented",
|
|
197
|
+
501,
|
|
198
|
+
detail,
|
|
199
|
+
undefined,
|
|
200
|
+
extensions
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
class BadGateway extends ProblemError {
|
|
206
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
207
|
+
super(
|
|
208
|
+
"https://httpstatuses.com/502",
|
|
209
|
+
"Bad Gateway",
|
|
210
|
+
502,
|
|
211
|
+
detail,
|
|
212
|
+
undefined,
|
|
213
|
+
extensions
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
class ServiceUnavailable extends ProblemError {
|
|
219
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
220
|
+
super(
|
|
221
|
+
"https://httpstatuses.com/503",
|
|
222
|
+
"Service Unavailable",
|
|
223
|
+
503,
|
|
224
|
+
detail,
|
|
225
|
+
undefined,
|
|
226
|
+
extensions
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class GatewayTimeout extends ProblemError {
|
|
232
|
+
constructor(detail?: string, extensions?: Record<string, any>) {
|
|
233
|
+
super(
|
|
234
|
+
"https://httpstatuses.com/504",
|
|
235
|
+
"Gateway Timeout",
|
|
236
|
+
504,
|
|
237
|
+
detail,
|
|
238
|
+
undefined,
|
|
239
|
+
extensions
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const HttpError = {
|
|
245
|
+
BadRequest,
|
|
246
|
+
Unauthorized,
|
|
247
|
+
PaymentRequired,
|
|
248
|
+
Forbidden,
|
|
249
|
+
NotFound,
|
|
250
|
+
MethodNotAllowed,
|
|
251
|
+
NotAcceptable,
|
|
252
|
+
Conflict,
|
|
253
|
+
InternalServerError,
|
|
254
|
+
NotImplemented,
|
|
255
|
+
BadGateway,
|
|
256
|
+
ServiceUnavailable,
|
|
257
|
+
GatewayTimeout,
|
|
258
|
+
} as const;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// src/libs/elysia-http-problem-json/types.ts
|
|
2
|
+
|
|
3
|
+
import { HttpError, ProblemError } from "./errors";
|
|
4
|
+
|
|
5
|
+
export type Code =
|
|
6
|
+
| number
|
|
7
|
+
| "PROBLEM_ERROR"
|
|
8
|
+
| "UNKNOWN"
|
|
9
|
+
| "VALIDATION"
|
|
10
|
+
| "NOT_FOUND"
|
|
11
|
+
| "PARSE"
|
|
12
|
+
| "INTERNAL_SERVER_ERROR"
|
|
13
|
+
| "INVALID_COOKIE_SIGNATURE"
|
|
14
|
+
| "INVALID_FILE_TYPE";
|
|
15
|
+
|
|
16
|
+
// 获取 HttpError 对象的所有 Key (例如 "BadRequest" | "NotFound")
|
|
17
|
+
export type HttpErrorType = keyof typeof HttpError;
|
|
18
|
+
|
|
19
|
+
export interface ErrorContext {
|
|
20
|
+
request: Request;
|
|
21
|
+
path: string;
|
|
22
|
+
code: string | number;
|
|
23
|
+
error: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface HttpProblemJsonOptions {
|
|
27
|
+
/**
|
|
28
|
+
* 自定义错误类型的 Base URL
|
|
29
|
+
* @example "https://api.mysite.com/errors"
|
|
30
|
+
*/
|
|
31
|
+
typeBaseUrl?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 🪝 Transform Hook
|
|
35
|
+
* 将未知错误转换为 HttpError。
|
|
36
|
+
* 返回 undefined/null 表示不处理(走默认逻辑)。
|
|
37
|
+
*/
|
|
38
|
+
transform?: (
|
|
39
|
+
error: unknown,
|
|
40
|
+
context: ErrorContext
|
|
41
|
+
) => ProblemError | undefined | null; // 这里直接返回 ProblemError 实例更好,或者用 HttpErrorType 也可以,看你喜好
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 📢 Listen Hook
|
|
45
|
+
* 在响应发送前触发(用于日志)。
|
|
46
|
+
*/
|
|
47
|
+
onBeforeRespond?: (
|
|
48
|
+
problem: ProblemError,
|
|
49
|
+
context: ErrorContext
|
|
50
|
+
) => void | Promise<void>;
|
|
51
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import elysiaPkg from 'elysia/package.json'
|
|
2
|
+
|
|
3
|
+
const centerText = (text: string, width: number): string => {
|
|
4
|
+
if (text.length >= width) {
|
|
5
|
+
return text.slice(0, width)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const left = Math.floor((width - text.length) / 2)
|
|
9
|
+
const right = width - text.length - left
|
|
10
|
+
return `${' '.repeat(left)}${text}${' '.repeat(right)}`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const renderBanner = (message: string): string => {
|
|
14
|
+
const versionLine = `Elysia v${elysiaPkg.version}`
|
|
15
|
+
const contentWidth = Math.max(message.length, versionLine.length)
|
|
16
|
+
const innerWidth = contentWidth + 4 // 2 spaces padding on both sides
|
|
17
|
+
|
|
18
|
+
const top = `┌${'─'.repeat(innerWidth)}┐`
|
|
19
|
+
const bot = `└${'─'.repeat(innerWidth)}┘`
|
|
20
|
+
const empty = `│${' '.repeat(innerWidth)}│`
|
|
21
|
+
|
|
22
|
+
const versionRow = `│${centerText(versionLine, innerWidth)}│`
|
|
23
|
+
const messageRow = `│ ${message}${' '.repeat(Math.max(0, innerWidth - message.length - 4))} │`
|
|
24
|
+
|
|
25
|
+
return [top, empty, versionRow, empty, messageRow, empty, bot].join('\n')
|
|
26
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Options } from '../interfaces'
|
|
2
|
+
import { renderBanner } from './banner'
|
|
3
|
+
|
|
4
|
+
export const startServer = (
|
|
5
|
+
server: { port?: number; hostname?: string; protocol?: string | null },
|
|
6
|
+
options: Options
|
|
7
|
+
): void => {
|
|
8
|
+
const showStartupMessage = options.config?.showStartupMessage ?? true
|
|
9
|
+
if (!showStartupMessage) {
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { port, hostname, protocol } = server
|
|
14
|
+
if (port === undefined || !hostname || !protocol) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const url = `${protocol}://${hostname}:${port}`
|
|
19
|
+
const message = `🦊 Elysia is running at ${url}`
|
|
20
|
+
|
|
21
|
+
const format = options.config?.startupMessageFormat ?? 'banner'
|
|
22
|
+
if (format === 'simple') {
|
|
23
|
+
console.log(message)
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(renderBanner(message))
|
|
28
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { StatusMap } from 'elysia'
|
|
2
|
+
|
|
3
|
+
const DIGITS_ONLY = /^\d+$/
|
|
4
|
+
const DELIMITERS = /[_-]+/g
|
|
5
|
+
const CAMEL_BOUNDARY_1 = /([a-z0-9])([A-Z])/g
|
|
6
|
+
const CAMEL_BOUNDARY_2 = /([A-Z])([A-Z][a-z])/g
|
|
7
|
+
const APOSTROPHES = /['’]/g
|
|
8
|
+
const NON_ALPHANUMERIC = /[^a-z0-9\s]+/g
|
|
9
|
+
const WHITESPACE = /\s+/g
|
|
10
|
+
|
|
11
|
+
const normalizeStatusName = (value: string): string => {
|
|
12
|
+
// Handles common variants:
|
|
13
|
+
// - case differences: "not found" vs "Not Found"
|
|
14
|
+
// - spacing/punctuation: "Not-Found", "not_found"
|
|
15
|
+
// - camelCase/PascalCase: "InternalServerError"
|
|
16
|
+
const trimmed = value.trim()
|
|
17
|
+
if (!trimmed) {
|
|
18
|
+
return ''
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return trimmed
|
|
22
|
+
.replace(DELIMITERS, ' ')
|
|
23
|
+
.replace(CAMEL_BOUNDARY_1, '$1 $2')
|
|
24
|
+
.replace(CAMEL_BOUNDARY_2, '$1 $2')
|
|
25
|
+
.replace(APOSTROPHES, '')
|
|
26
|
+
.toLowerCase()
|
|
27
|
+
.replace(NON_ALPHANUMERIC, ' ')
|
|
28
|
+
.replace(WHITESPACE, ' ')
|
|
29
|
+
.trim()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const STATUS_BY_NORMALIZED_NAME = (() => {
|
|
33
|
+
const map = new Map<string, number>()
|
|
34
|
+
|
|
35
|
+
for (const [name, code] of Object.entries(StatusMap)) {
|
|
36
|
+
map.set(normalizeStatusName(name), code)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return map
|
|
40
|
+
})()
|
|
41
|
+
|
|
42
|
+
export const getStatusCode = (value: unknown): number => {
|
|
43
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
44
|
+
return value
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof value === 'string') {
|
|
48
|
+
const trimmed = value.trim()
|
|
49
|
+
if (DIGITS_ONLY.test(trimmed)) {
|
|
50
|
+
return Number(trimmed)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const known = STATUS_BY_NORMALIZED_NAME.get(normalizeStatusName(trimmed))
|
|
54
|
+
return known ?? 500
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return 500
|
|
58
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Elysia, type SingletonBase } from 'elysia'
|
|
2
|
+
import { startServer } from './extensions'
|
|
3
|
+
import type { LogixlysiaStore, Options } from './interfaces'
|
|
4
|
+
import { createLogger } from './logger'
|
|
5
|
+
import { normalizeToProblem } from './utils/handle-error'
|
|
6
|
+
|
|
7
|
+
export type Logixlysia = Elysia<
|
|
8
|
+
'Logixlysia',
|
|
9
|
+
SingletonBase & { store: LogixlysiaStore }
|
|
10
|
+
>
|
|
11
|
+
|
|
12
|
+
export const logixlysia = (options: Options = {}): Logixlysia => {
|
|
13
|
+
const didCustomLog = new WeakSet<Request>()
|
|
14
|
+
const baseLogger = createLogger(options)
|
|
15
|
+
const logger = {
|
|
16
|
+
...baseLogger,
|
|
17
|
+
debug: (
|
|
18
|
+
request: Request,
|
|
19
|
+
message: string,
|
|
20
|
+
context?: Record<string, unknown>
|
|
21
|
+
) => {
|
|
22
|
+
didCustomLog.add(request)
|
|
23
|
+
baseLogger.debug(request, message, context)
|
|
24
|
+
},
|
|
25
|
+
info: (
|
|
26
|
+
request: Request,
|
|
27
|
+
message: string,
|
|
28
|
+
context?: Record<string, unknown>
|
|
29
|
+
) => {
|
|
30
|
+
didCustomLog.add(request)
|
|
31
|
+
baseLogger.info(request, message, context)
|
|
32
|
+
},
|
|
33
|
+
warn: (
|
|
34
|
+
request: Request,
|
|
35
|
+
message: string,
|
|
36
|
+
context?: Record<string, unknown>
|
|
37
|
+
) => {
|
|
38
|
+
didCustomLog.add(request)
|
|
39
|
+
baseLogger.warn(request, message, context)
|
|
40
|
+
},
|
|
41
|
+
error: (
|
|
42
|
+
request: Request,
|
|
43
|
+
message: string,
|
|
44
|
+
context?: Record<string, unknown>
|
|
45
|
+
) => {
|
|
46
|
+
didCustomLog.add(request)
|
|
47
|
+
baseLogger.error(request, message, context)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const app = new Elysia({
|
|
52
|
+
name: 'Logixlysia',
|
|
53
|
+
detail: {
|
|
54
|
+
description:
|
|
55
|
+
'Logixlysia is a plugin for Elysia that provides a logger and pino logger.',
|
|
56
|
+
tags: ['logging', 'pino']
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
app
|
|
62
|
+
.state('logger', logger)
|
|
63
|
+
.state('pino', logger.pino)
|
|
64
|
+
.state('beforeTime', BigInt(0))
|
|
65
|
+
.onStart(({ server }) => {
|
|
66
|
+
if (server) {
|
|
67
|
+
startServer(server, options)
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
.onRequest(({ store }) => {
|
|
71
|
+
store.beforeTime = process.hrtime.bigint()
|
|
72
|
+
})
|
|
73
|
+
.onAfterHandle(({ request, set, store }) => {
|
|
74
|
+
if (didCustomLog.has(request)) {
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const status = typeof set.status === 'number' ? set.status : 200
|
|
79
|
+
let level: 'INFO' | 'WARNING' | 'ERROR' = 'INFO'
|
|
80
|
+
if (status >= 500) {
|
|
81
|
+
level = 'ERROR'
|
|
82
|
+
} else if (status >= 400) {
|
|
83
|
+
level = 'WARNING'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.log(level, request, { status }, store)
|
|
87
|
+
})
|
|
88
|
+
.onError(({ request, error,code, path,store,set }) => {
|
|
89
|
+
// logger.handleHttpError(request, error, store)
|
|
90
|
+
|
|
91
|
+
// ==========================================
|
|
92
|
+
// Phase 1: Transform (转换)
|
|
93
|
+
// ==========================================
|
|
94
|
+
let result = options.transform ? options.transform(error, { request, code }) : error
|
|
95
|
+
|
|
96
|
+
// ==========================================
|
|
97
|
+
// Phase 2: Normalization (规范化)
|
|
98
|
+
// ==========================================
|
|
99
|
+
// 统一转为 ProblemError 实例
|
|
100
|
+
const problem = normalizeToProblem(result, code, path, options.config?.error?.problemJson?.typeBaseUrl)
|
|
101
|
+
|
|
102
|
+
// ==========================================
|
|
103
|
+
// Phase 3: Logging (日志)
|
|
104
|
+
// ==========================================
|
|
105
|
+
// 调用上面改造后的函数,它现在只负责记录,不负责逻辑判断
|
|
106
|
+
logger.handleHttpError(request, problem, store, options)
|
|
107
|
+
|
|
108
|
+
// ==========================================
|
|
109
|
+
// Phase 4: Response (响应)
|
|
110
|
+
// ==========================================
|
|
111
|
+
// 统一设置 Header 和 Status
|
|
112
|
+
set.status = problem.status
|
|
113
|
+
set.headers['content-type'] = 'application/problem+json'
|
|
114
|
+
|
|
115
|
+
// 返回符合 RFC 标准的 JSON
|
|
116
|
+
return problem.toJSON()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
})
|
|
120
|
+
// Ensure plugin lifecycle hooks (onRequest/onAfterHandle/onError) apply to the parent app.
|
|
121
|
+
.as('scoped') as unknown as Logixlysia
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export type {
|
|
126
|
+
Logger,
|
|
127
|
+
LogixlysiaContext,
|
|
128
|
+
LogixlysiaStore,
|
|
129
|
+
LogLevel,
|
|
130
|
+
Options,
|
|
131
|
+
Pino,
|
|
132
|
+
StoreData,
|
|
133
|
+
Transport,
|
|
134
|
+
} from './interfaces'
|
|
135
|
+
|
|
136
|
+
export { HttpError } from './interfaces'
|
|
137
|
+
export { toProblemJson, formatProblemJsonLog } from './utils/handle-error'
|
|
138
|
+
export type { ProblemJson } from './utils/handle-error'
|
|
139
|
+
|
|
140
|
+
export default logixlysia
|
|
141
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Logger as PinoLogger,
|
|
3
|
+
LoggerOptions as PinoLoggerOptions
|
|
4
|
+
} from 'pino'
|
|
5
|
+
import { ProblemError } from './Error/errors'
|
|
6
|
+
import { Code } from './Error/type'
|
|
7
|
+
|
|
8
|
+
export type Pino = PinoLogger<never, boolean>
|
|
9
|
+
export type Request = Request
|
|
10
|
+
|
|
11
|
+
export type LogLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'
|
|
12
|
+
|
|
13
|
+
export interface StoreData {
|
|
14
|
+
beforeTime: bigint
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LogixlysiaStore {
|
|
18
|
+
logger: Logger
|
|
19
|
+
pino: Pino
|
|
20
|
+
beforeTime?: bigint
|
|
21
|
+
[key: string]: unknown
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface Transport {
|
|
25
|
+
log: (
|
|
26
|
+
level: LogLevel,
|
|
27
|
+
message: string,
|
|
28
|
+
meta?: Record<string, unknown>
|
|
29
|
+
) => void | Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface LogRotationConfig {
|
|
33
|
+
/**
|
|
34
|
+
* Max log file size before rotation, e.g. '10m', '5k', or a byte count.
|
|
35
|
+
*/
|
|
36
|
+
maxSize?: string | number
|
|
37
|
+
/**
|
|
38
|
+
* Keep at most N files or keep files for a duration like '7d'.
|
|
39
|
+
*/
|
|
40
|
+
maxFiles?: number | string
|
|
41
|
+
/**
|
|
42
|
+
* Rotate at a fixed interval, e.g. '1d', '12h'.
|
|
43
|
+
*/
|
|
44
|
+
interval?: string
|
|
45
|
+
compress?: boolean
|
|
46
|
+
compression?: 'gzip'
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
export interface Options {
|
|
54
|
+
config?: {
|
|
55
|
+
showStartupMessage?: boolean
|
|
56
|
+
startupMessageFormat?: 'simple' | 'banner'
|
|
57
|
+
useColors?: boolean
|
|
58
|
+
ip?: boolean
|
|
59
|
+
timestamp?: {
|
|
60
|
+
translateTime?: string
|
|
61
|
+
}
|
|
62
|
+
customLogFormat?: string
|
|
63
|
+
|
|
64
|
+
// Outputs
|
|
65
|
+
transports?: Transport[]
|
|
66
|
+
useTransportsOnly?: boolean
|
|
67
|
+
disableInternalLogger?: boolean
|
|
68
|
+
disableFileLogging?: boolean
|
|
69
|
+
logFilePath?: string
|
|
70
|
+
logRotation?: LogRotationConfig
|
|
71
|
+
|
|
72
|
+
// Pino
|
|
73
|
+
pino?: (PinoLoggerOptions & { prettyPrint?: boolean }) | undefined
|
|
74
|
+
|
|
75
|
+
error?:{
|
|
76
|
+
problemJson?:{
|
|
77
|
+
typeBaseUrl?: string
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
transform?: (error: unknown, context: { request: Request; code: Code }) => ProblemError | unknown
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class HttpError extends Error {
|
|
89
|
+
readonly status: number
|
|
90
|
+
|
|
91
|
+
constructor(status: number, message: string) {
|
|
92
|
+
super(message)
|
|
93
|
+
this.status = status
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Logger {
|
|
98
|
+
pino: Pino
|
|
99
|
+
log: (
|
|
100
|
+
level: LogLevel,
|
|
101
|
+
request: Request,
|
|
102
|
+
data: Record<string, unknown>,
|
|
103
|
+
store: StoreData
|
|
104
|
+
) => void
|
|
105
|
+
handleHttpError: (
|
|
106
|
+
request: Request,
|
|
107
|
+
error: ProblemError,
|
|
108
|
+
store: StoreData,
|
|
109
|
+
options: Options
|
|
110
|
+
) => void
|
|
111
|
+
debug: (
|
|
112
|
+
request: Request,
|
|
113
|
+
message: string,
|
|
114
|
+
context?: Record<string, unknown>
|
|
115
|
+
) => void
|
|
116
|
+
info: (
|
|
117
|
+
request: Request,
|
|
118
|
+
message: string,
|
|
119
|
+
context?: Record<string, unknown>
|
|
120
|
+
) => void
|
|
121
|
+
warn: (
|
|
122
|
+
request: Request,
|
|
123
|
+
message: string,
|
|
124
|
+
context?: Record<string, unknown>
|
|
125
|
+
) => void
|
|
126
|
+
error: (
|
|
127
|
+
request: Request,
|
|
128
|
+
message: string,
|
|
129
|
+
context?: Record<string, unknown>
|
|
130
|
+
) => void
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface LogixlysiaContext {
|
|
134
|
+
request: Request
|
|
135
|
+
store: LogixlysiaStore
|
|
136
|
+
}
|