@mandujs/core 0.12.1 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +304 -304
- package/README.md +653 -653
- package/package.json +8 -8
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +94 -96
- package/src/config/validate.ts +213 -215
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/classifier.ts +2 -2
- package/src/error/domains.ts +265 -265
- package/src/error/formatter.ts +32 -32
- package/src/error/result.ts +46 -46
- package/src/error/stack-analyzer.ts +5 -0
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +569 -569
- package/src/filling/deps.ts +238 -238
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +80 -79
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/healing.ts +2 -0
- package/src/guard/index.ts +2 -0
- package/src/guard/negotiation.ts +430 -4
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/cqrs.test.ts +175 -0
- package/src/guard/presets/cqrs.ts +107 -0
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -288
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -352
- package/src/guard/types.ts +348 -347
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +1 -0
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/build.ts +1 -1
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-scanner.ts +497 -497
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +24 -24
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +367 -367
- package/src/runtime/streaming-ssr.ts +1245 -1245
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/rules.ts +5 -5
package/src/runtime/logger.ts
CHANGED
|
@@ -1,677 +1,677 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Runtime Logger 📝
|
|
3
|
-
* Trace 기반 요청/응답 로깅 레이어
|
|
4
|
-
*
|
|
5
|
-
* 역할 분리:
|
|
6
|
-
* - Trace = 수집 (원본 이벤트, duration 측정)
|
|
7
|
-
* - Logger = 출력 (포맷/필터/레드액션/샘플링)
|
|
8
|
-
*
|
|
9
|
-
* 기본값은 안전:
|
|
10
|
-
* - includeHeaders: false
|
|
11
|
-
* - includeBody: false
|
|
12
|
-
* - redact: 민감 정보 자동 마스킹
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* ```typescript
|
|
16
|
-
* import { logger } from "@mandujs/core";
|
|
17
|
-
*
|
|
18
|
-
* // 기본 사용
|
|
19
|
-
* app.use(logger());
|
|
20
|
-
* // → GET /api/users
|
|
21
|
-
* // ← GET /api/users 200 23ms
|
|
22
|
-
*
|
|
23
|
-
* // 개발 모드
|
|
24
|
-
* app.use(logger({
|
|
25
|
-
* level: "debug",
|
|
26
|
-
* includeHeaders: true,
|
|
27
|
-
* }));
|
|
28
|
-
*
|
|
29
|
-
* // 프로덕션 (JSON 형식)
|
|
30
|
-
* app.use(logger({
|
|
31
|
-
* format: "json",
|
|
32
|
-
* slowThresholdMs: 500,
|
|
33
|
-
* }));
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
|
|
37
|
-
import type { ManduContext } from "../filling/context";
|
|
38
|
-
import {
|
|
39
|
-
enableTrace,
|
|
40
|
-
getTrace,
|
|
41
|
-
buildTraceReport,
|
|
42
|
-
type TraceReport,
|
|
43
|
-
} from "./trace";
|
|
44
|
-
|
|
45
|
-
// ============================================
|
|
46
|
-
// Types
|
|
47
|
-
// ============================================
|
|
48
|
-
|
|
49
|
-
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
50
|
-
export type LogFormat = "pretty" | "json";
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Logger 옵션
|
|
54
|
-
*/
|
|
55
|
-
export interface LoggerOptions {
|
|
56
|
-
/**
|
|
57
|
-
* 로그 포맷
|
|
58
|
-
* - pretty: 개발용 컬러 출력
|
|
59
|
-
* - json: 운영용 구조화 로그
|
|
60
|
-
* @default "pretty"
|
|
61
|
-
*/
|
|
62
|
-
format?: LogFormat;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 로그 레벨
|
|
66
|
-
* - debug: 모든 요청 상세 출력
|
|
67
|
-
* - info: 기본 요청/응답 (기본값)
|
|
68
|
-
* - warn: 느린 요청 + 에러
|
|
69
|
-
* - error: 에러만
|
|
70
|
-
* @default "info"
|
|
71
|
-
*/
|
|
72
|
-
level?: LogLevel;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* 헤더 포함 여부
|
|
76
|
-
* ⚠️ 기본 OFF - 민감 정보 노출 위험
|
|
77
|
-
* @default false
|
|
78
|
-
*/
|
|
79
|
-
includeHeaders?: boolean;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 바디 포함 여부
|
|
83
|
-
* ⚠️ 기본 OFF - 민감 정보 노출 + 스트림 문제
|
|
84
|
-
* @default false
|
|
85
|
-
*/
|
|
86
|
-
includeBody?: boolean;
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* 바디 최대 바이트 (includeBody=true 시)
|
|
90
|
-
* @default 1024
|
|
91
|
-
*/
|
|
92
|
-
maxBodyBytes?: number;
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* 레드액션 대상 헤더/필드명 (기본값 내장)
|
|
96
|
-
* 추가할 필드만 지정하면 기본값과 병합됨
|
|
97
|
-
*/
|
|
98
|
-
redact?: string[];
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Request ID 생성 방식
|
|
102
|
-
* - "auto": crypto.randomUUID() 또는 타임스탬프 기반
|
|
103
|
-
* - 함수: 커스텀 생성
|
|
104
|
-
* @default "auto"
|
|
105
|
-
*/
|
|
106
|
-
requestId?: "auto" | ((ctx: ManduContext) => string);
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* 샘플링 비율 (0-1)
|
|
110
|
-
* 운영 환경에서 로그 양 조절
|
|
111
|
-
* @default 1 (100%)
|
|
112
|
-
*/
|
|
113
|
-
sampleRate?: number;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 느린 요청 임계값 (ms)
|
|
117
|
-
* 이 값 초과 시 warn 레벨로 상세 출력
|
|
118
|
-
* @default 1000
|
|
119
|
-
*/
|
|
120
|
-
slowThresholdMs?: number;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Trace 리포트 포함 여부 (느린 요청 시)
|
|
124
|
-
* @default true
|
|
125
|
-
*/
|
|
126
|
-
includeTraceOnSlow?: boolean;
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* 커스텀 로그 싱크 (외부 시스템 연동용)
|
|
130
|
-
* 지정 시 console 출력 대신 이 함수 호출
|
|
131
|
-
*/
|
|
132
|
-
sink?: (entry: LogEntry) => void;
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* 로깅 제외 경로 패턴
|
|
136
|
-
* @example ["/health", "/metrics", /^\/static\//]
|
|
137
|
-
*/
|
|
138
|
-
skip?: (string | RegExp)[];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* 로그 엔트리 (JSON 출력 및 sink용)
|
|
143
|
-
*/
|
|
144
|
-
export interface LogEntry {
|
|
145
|
-
timestamp: string;
|
|
146
|
-
requestId: string;
|
|
147
|
-
method: string;
|
|
148
|
-
path: string;
|
|
149
|
-
status?: number;
|
|
150
|
-
duration?: number;
|
|
151
|
-
level: LogLevel;
|
|
152
|
-
error?: {
|
|
153
|
-
message: string;
|
|
154
|
-
stack?: string;
|
|
155
|
-
};
|
|
156
|
-
headers?: Record<string, string>;
|
|
157
|
-
body?: unknown;
|
|
158
|
-
trace?: TraceReport;
|
|
159
|
-
slow?: boolean;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ============================================
|
|
163
|
-
// Constants
|
|
164
|
-
// ============================================
|
|
165
|
-
|
|
166
|
-
/** 기본 레드액션 대상 (대소문자 무시) */
|
|
167
|
-
const DEFAULT_REDACT_PATTERNS = [
|
|
168
|
-
"authorization",
|
|
169
|
-
"cookie",
|
|
170
|
-
"set-cookie",
|
|
171
|
-
"x-api-key",
|
|
172
|
-
"api-key",
|
|
173
|
-
"apikey",
|
|
174
|
-
"api_key",
|
|
175
|
-
"password",
|
|
176
|
-
"passwd",
|
|
177
|
-
"secret",
|
|
178
|
-
"token",
|
|
179
|
-
"bearer",
|
|
180
|
-
"credential",
|
|
181
|
-
"credentials",
|
|
182
|
-
"private",
|
|
183
|
-
"session",
|
|
184
|
-
"jwt",
|
|
185
|
-
];
|
|
186
|
-
|
|
187
|
-
/** Context 저장 키 */
|
|
188
|
-
const LOGGER_START_KEY = "__mandu_logger_start";
|
|
189
|
-
const LOGGER_REQUEST_ID_KEY = "__mandu_logger_request_id";
|
|
190
|
-
const LOGGER_ERROR_KEY = "__mandu_logger_error";
|
|
191
|
-
const LOGGER_RESPONSE_KEY = "__mandu_logger_response";
|
|
192
|
-
|
|
193
|
-
/** 로그 레벨 우선순위 */
|
|
194
|
-
const LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
195
|
-
debug: 0,
|
|
196
|
-
info: 1,
|
|
197
|
-
warn: 2,
|
|
198
|
-
error: 3,
|
|
199
|
-
};
|
|
200
|
-
|
|
201
|
-
/** ANSI 컬러 코드 */
|
|
202
|
-
const COLORS = {
|
|
203
|
-
reset: "\x1b[0m",
|
|
204
|
-
dim: "\x1b[2m",
|
|
205
|
-
cyan: "\x1b[36m",
|
|
206
|
-
green: "\x1b[32m",
|
|
207
|
-
yellow: "\x1b[33m",
|
|
208
|
-
red: "\x1b[31m",
|
|
209
|
-
magenta: "\x1b[35m",
|
|
210
|
-
};
|
|
211
|
-
|
|
212
|
-
// ============================================
|
|
213
|
-
// Utilities
|
|
214
|
-
// ============================================
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Request ID 생성
|
|
218
|
-
*/
|
|
219
|
-
function generateRequestId(): string {
|
|
220
|
-
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
221
|
-
return crypto.randomUUID().slice(0, 8);
|
|
222
|
-
}
|
|
223
|
-
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* 현재 시간 (고해상도)
|
|
228
|
-
*/
|
|
229
|
-
function now(): number {
|
|
230
|
-
if (typeof performance !== "undefined" && performance.now) {
|
|
231
|
-
return performance.now();
|
|
232
|
-
}
|
|
233
|
-
return Date.now();
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* URL에서 pathname 추출
|
|
238
|
-
*/
|
|
239
|
-
function getPathname(url: string): string {
|
|
240
|
-
try {
|
|
241
|
-
return new URL(url).pathname;
|
|
242
|
-
} catch {
|
|
243
|
-
return url;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/**
|
|
248
|
-
* 헤더 레드액션 처리
|
|
249
|
-
*/
|
|
250
|
-
function redactHeaders(
|
|
251
|
-
headers: Headers,
|
|
252
|
-
patterns: string[]
|
|
253
|
-
): Record<string, string> {
|
|
254
|
-
const result: Record<string, string> = {};
|
|
255
|
-
const lowerPatterns = patterns.map((p) => p.toLowerCase());
|
|
256
|
-
|
|
257
|
-
headers.forEach((value, key) => {
|
|
258
|
-
const lowerKey = key.toLowerCase();
|
|
259
|
-
const shouldRedact = lowerPatterns.some(
|
|
260
|
-
(pattern) => lowerKey.includes(pattern) || pattern.includes(lowerKey)
|
|
261
|
-
);
|
|
262
|
-
result[key] = shouldRedact ? "[REDACTED]" : value;
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
return result;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* 객체 내 민감 필드 레드액션
|
|
270
|
-
*/
|
|
271
|
-
function redactObject(
|
|
272
|
-
obj: unknown,
|
|
273
|
-
patterns: string[],
|
|
274
|
-
maxBytes: number
|
|
275
|
-
): unknown {
|
|
276
|
-
if (obj === null || obj === undefined) return obj;
|
|
277
|
-
|
|
278
|
-
// 문자열이면 길이 제한만
|
|
279
|
-
if (typeof obj === "string") {
|
|
280
|
-
if (obj.length > maxBytes) {
|
|
281
|
-
return obj.slice(0, maxBytes) + `... [truncated ${obj.length - maxBytes} bytes]`;
|
|
282
|
-
}
|
|
283
|
-
return obj;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
// 배열
|
|
287
|
-
if (Array.isArray(obj)) {
|
|
288
|
-
const str = JSON.stringify(obj);
|
|
289
|
-
if (str.length > maxBytes) {
|
|
290
|
-
return `[Array length=${obj.length}, truncated]`;
|
|
291
|
-
}
|
|
292
|
-
return obj.map((item) => redactObject(item, patterns, maxBytes));
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// 객체
|
|
296
|
-
if (typeof obj === "object") {
|
|
297
|
-
const lowerPatterns = patterns.map((p) => p.toLowerCase());
|
|
298
|
-
const result: Record<string, unknown> = {};
|
|
299
|
-
|
|
300
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
301
|
-
const lowerKey = key.toLowerCase();
|
|
302
|
-
const shouldRedact = lowerPatterns.some(
|
|
303
|
-
(pattern) => lowerKey.includes(pattern) || pattern.includes(lowerKey)
|
|
304
|
-
);
|
|
305
|
-
|
|
306
|
-
if (shouldRedact) {
|
|
307
|
-
result[key] = "[REDACTED]";
|
|
308
|
-
} else if (typeof value === "object" && value !== null) {
|
|
309
|
-
result[key] = redactObject(value, patterns, maxBytes);
|
|
310
|
-
} else {
|
|
311
|
-
result[key] = value;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return result;
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
return obj;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
/**
|
|
322
|
-
* 경로가 skip 패턴에 매칭되는지 확인
|
|
323
|
-
*/
|
|
324
|
-
function shouldSkip(path: string, patterns: (string | RegExp)[]): boolean {
|
|
325
|
-
return patterns.some((pattern) => {
|
|
326
|
-
if (typeof pattern === "string") {
|
|
327
|
-
return path === pattern || path.startsWith(pattern + "/");
|
|
328
|
-
}
|
|
329
|
-
return pattern.test(path);
|
|
330
|
-
});
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* 로그 레벨 필터링
|
|
335
|
-
*/
|
|
336
|
-
function shouldLog(entryLevel: LogLevel, configLevel: LogLevel): boolean {
|
|
337
|
-
return LEVEL_PRIORITY[entryLevel] >= LEVEL_PRIORITY[configLevel];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
/**
|
|
341
|
-
* HTTP 상태 코드에 따른 색상
|
|
342
|
-
*/
|
|
343
|
-
function getStatusColor(status: number): string {
|
|
344
|
-
if (status >= 500) return COLORS.red;
|
|
345
|
-
if (status >= 400) return COLORS.yellow;
|
|
346
|
-
if (status >= 300) return COLORS.cyan;
|
|
347
|
-
return COLORS.green;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// ============================================
|
|
351
|
-
// Pretty Formatter
|
|
352
|
-
// ============================================
|
|
353
|
-
|
|
354
|
-
function formatPretty(entry: LogEntry): string {
|
|
355
|
-
const { method, path, status, duration, requestId, error, slow, headers, trace } = entry;
|
|
356
|
-
|
|
357
|
-
const lines: string[] = [];
|
|
358
|
-
|
|
359
|
-
// 요청 라인
|
|
360
|
-
if (status === undefined) {
|
|
361
|
-
// 요청 시작
|
|
362
|
-
lines.push(
|
|
363
|
-
`${COLORS.dim}[${requestId}]${COLORS.reset} ${COLORS.cyan}→${COLORS.reset} ${method} ${path}`
|
|
364
|
-
);
|
|
365
|
-
} else {
|
|
366
|
-
// 응답
|
|
367
|
-
const statusColor = getStatusColor(status);
|
|
368
|
-
const durationStr = duration !== undefined ? ` ${duration.toFixed(0)}ms` : "";
|
|
369
|
-
const slowIndicator = slow ? ` ${COLORS.yellow}[SLOW]${COLORS.reset}` : "";
|
|
370
|
-
|
|
371
|
-
lines.push(
|
|
372
|
-
`${COLORS.dim}[${requestId}]${COLORS.reset} ${COLORS.magenta}←${COLORS.reset} ${method} ${path} ${statusColor}${status}${COLORS.reset}${durationStr}${slowIndicator}`
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// 에러
|
|
377
|
-
if (error) {
|
|
378
|
-
lines.push(` ${COLORS.red}Error: ${error.message}${COLORS.reset}`);
|
|
379
|
-
if (error.stack) {
|
|
380
|
-
const stackLines = error.stack.split("\n").slice(1, 4);
|
|
381
|
-
stackLines.forEach((line) => {
|
|
382
|
-
lines.push(` ${COLORS.dim}${line.trim()}${COLORS.reset}`);
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// 헤더 (debug 모드)
|
|
388
|
-
if (headers && Object.keys(headers).length > 0) {
|
|
389
|
-
lines.push(` ${COLORS.dim}Headers:${COLORS.reset}`);
|
|
390
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
391
|
-
lines.push(` ${COLORS.dim}${key}:${COLORS.reset} ${value}`);
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Trace 리포트 (느린 요청)
|
|
396
|
-
if (trace && trace.entries.length > 0) {
|
|
397
|
-
lines.push(` ${COLORS.dim}Trace:${COLORS.reset}`);
|
|
398
|
-
for (const traceEntry of trace.entries) {
|
|
399
|
-
const name = traceEntry.name ? ` (${traceEntry.name})` : "";
|
|
400
|
-
lines.push(
|
|
401
|
-
` ${COLORS.dim}${traceEntry.event}${name}: ${traceEntry.duration.toFixed(1)}ms${COLORS.reset}`
|
|
402
|
-
);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return lines.join("\n");
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
// ============================================
|
|
410
|
-
// JSON Formatter
|
|
411
|
-
// ============================================
|
|
412
|
-
|
|
413
|
-
function formatJson(entry: LogEntry): string {
|
|
414
|
-
return JSON.stringify(entry);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
// ============================================
|
|
418
|
-
// Logger Middleware Factory
|
|
419
|
-
// ============================================
|
|
420
|
-
|
|
421
|
-
/**
|
|
422
|
-
* Logger 미들웨어 생성
|
|
423
|
-
*
|
|
424
|
-
* @example
|
|
425
|
-
* ```typescript
|
|
426
|
-
* // 기본 사용
|
|
427
|
-
* app.use(logger());
|
|
428
|
-
*
|
|
429
|
-
* // 개발 모드
|
|
430
|
-
* app.use(logger({
|
|
431
|
-
* level: "debug",
|
|
432
|
-
* includeHeaders: true,
|
|
433
|
-
* }));
|
|
434
|
-
*
|
|
435
|
-
* // 프로덕션
|
|
436
|
-
* app.use(logger({
|
|
437
|
-
* format: "json",
|
|
438
|
-
* sampleRate: 0.1, // 10% 샘플링
|
|
439
|
-
* slowThresholdMs: 500,
|
|
440
|
-
* }));
|
|
441
|
-
* ```
|
|
442
|
-
*/
|
|
443
|
-
export function logger(options: LoggerOptions = {}) {
|
|
444
|
-
const config = {
|
|
445
|
-
format: options.format ?? "pretty",
|
|
446
|
-
level: options.level ?? "info",
|
|
447
|
-
includeHeaders: options.includeHeaders ?? false,
|
|
448
|
-
includeBody: options.includeBody ?? false,
|
|
449
|
-
maxBodyBytes: options.maxBodyBytes ?? 1024,
|
|
450
|
-
redact: [...DEFAULT_REDACT_PATTERNS, ...(options.redact ?? [])],
|
|
451
|
-
requestId: options.requestId ?? "auto",
|
|
452
|
-
sampleRate: options.sampleRate ?? 1,
|
|
453
|
-
slowThresholdMs: options.slowThresholdMs ?? 1000,
|
|
454
|
-
includeTraceOnSlow: options.includeTraceOnSlow ?? true,
|
|
455
|
-
sink: options.sink,
|
|
456
|
-
skip: options.skip ?? [],
|
|
457
|
-
};
|
|
458
|
-
|
|
459
|
-
const formatter = config.format === "json" ? formatJson : formatPretty;
|
|
460
|
-
|
|
461
|
-
/**
|
|
462
|
-
* 로그 출력 함수
|
|
463
|
-
*/
|
|
464
|
-
function log(entry: LogEntry): void {
|
|
465
|
-
if (!shouldLog(entry.level, config.level)) return;
|
|
466
|
-
|
|
467
|
-
if (config.sink) {
|
|
468
|
-
config.sink(entry);
|
|
469
|
-
} else {
|
|
470
|
-
const output = formatter(entry);
|
|
471
|
-
switch (entry.level) {
|
|
472
|
-
case "error":
|
|
473
|
-
console.error(output);
|
|
474
|
-
break;
|
|
475
|
-
case "warn":
|
|
476
|
-
console.warn(output);
|
|
477
|
-
break;
|
|
478
|
-
default:
|
|
479
|
-
console.log(output);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
return {
|
|
485
|
-
/**
|
|
486
|
-
* onRequest 훅 - 요청 시작 기록
|
|
487
|
-
*/
|
|
488
|
-
onRequest(ctx: ManduContext): void {
|
|
489
|
-
const path = getPathname(ctx.url);
|
|
490
|
-
|
|
491
|
-
// Skip 체크
|
|
492
|
-
if (shouldSkip(path, config.skip)) return;
|
|
493
|
-
|
|
494
|
-
// 샘플링 체크
|
|
495
|
-
if (config.sampleRate < 1 && Math.random() > config.sampleRate) return;
|
|
496
|
-
|
|
497
|
-
// Trace 활성화
|
|
498
|
-
enableTrace(ctx);
|
|
499
|
-
|
|
500
|
-
// 시작 시간 저장
|
|
501
|
-
ctx.set(LOGGER_START_KEY, now());
|
|
502
|
-
|
|
503
|
-
// Request ID 생성/저장
|
|
504
|
-
const requestId =
|
|
505
|
-
config.requestId === "auto"
|
|
506
|
-
? generateRequestId()
|
|
507
|
-
: config.requestId(ctx);
|
|
508
|
-
ctx.set(LOGGER_REQUEST_ID_KEY, requestId);
|
|
509
|
-
|
|
510
|
-
// debug 레벨이면 요청 시작도 로깅
|
|
511
|
-
if (config.level === "debug") {
|
|
512
|
-
const entry: LogEntry = {
|
|
513
|
-
timestamp: new Date().toISOString(),
|
|
514
|
-
requestId,
|
|
515
|
-
method: ctx.method,
|
|
516
|
-
path,
|
|
517
|
-
level: "debug",
|
|
518
|
-
};
|
|
519
|
-
|
|
520
|
-
if (config.includeHeaders) {
|
|
521
|
-
entry.headers = redactHeaders(ctx.headers, config.redact);
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
log(entry);
|
|
525
|
-
}
|
|
526
|
-
},
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* onError 훅 - 에러 캡처
|
|
530
|
-
*/
|
|
531
|
-
onError(ctx: ManduContext, error: Error): void {
|
|
532
|
-
ctx.set(LOGGER_ERROR_KEY, error);
|
|
533
|
-
},
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* afterHandle 훅 - 응답 캡처 (바디 로깅용)
|
|
537
|
-
*/
|
|
538
|
-
afterHandle(ctx: ManduContext, response: Response): Response {
|
|
539
|
-
ctx.set(LOGGER_RESPONSE_KEY, response);
|
|
540
|
-
return response;
|
|
541
|
-
},
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* afterResponse 훅 - 최종 로그 출력
|
|
545
|
-
*/
|
|
546
|
-
async afterResponse(ctx: ManduContext): Promise<void> {
|
|
547
|
-
const startTime = ctx.get<number>(LOGGER_START_KEY);
|
|
548
|
-
const requestId = ctx.get<string>(LOGGER_REQUEST_ID_KEY);
|
|
549
|
-
|
|
550
|
-
// 시작 기록이 없으면 skip된 요청
|
|
551
|
-
if (startTime === undefined || requestId === undefined) return;
|
|
552
|
-
|
|
553
|
-
const path = getPathname(ctx.url);
|
|
554
|
-
const duration = now() - startTime;
|
|
555
|
-
const error = ctx.get<Error>(LOGGER_ERROR_KEY);
|
|
556
|
-
const response = ctx.get<Response>(LOGGER_RESPONSE_KEY);
|
|
557
|
-
const status = response?.status ?? (error ? 500 : 200);
|
|
558
|
-
const isSlow = duration > config.slowThresholdMs;
|
|
559
|
-
|
|
560
|
-
// 로그 레벨 결정
|
|
561
|
-
let level: LogLevel = "info";
|
|
562
|
-
if (error) {
|
|
563
|
-
level = "error";
|
|
564
|
-
} else if (isSlow) {
|
|
565
|
-
level = "warn";
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// 로그 엔트리 생성
|
|
569
|
-
const entry: LogEntry = {
|
|
570
|
-
timestamp: new Date().toISOString(),
|
|
571
|
-
requestId,
|
|
572
|
-
method: ctx.method,
|
|
573
|
-
path,
|
|
574
|
-
status,
|
|
575
|
-
duration,
|
|
576
|
-
level,
|
|
577
|
-
slow: isSlow,
|
|
578
|
-
};
|
|
579
|
-
|
|
580
|
-
// 에러 정보
|
|
581
|
-
if (error) {
|
|
582
|
-
entry.error = {
|
|
583
|
-
message: error.message,
|
|
584
|
-
stack: error.stack,
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// 헤더 (debug 또는 느린 요청)
|
|
589
|
-
if (config.includeHeaders || (isSlow && config.level === "debug")) {
|
|
590
|
-
entry.headers = redactHeaders(ctx.headers, config.redact);
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
// 바디 (명시적 활성화 + debug 레벨만)
|
|
594
|
-
if (config.includeBody && config.level === "debug" && response) {
|
|
595
|
-
try {
|
|
596
|
-
const cloned = response.clone();
|
|
597
|
-
const contentType = cloned.headers.get("content-type") || "";
|
|
598
|
-
if (contentType.includes("application/json")) {
|
|
599
|
-
const body = await cloned.json();
|
|
600
|
-
entry.body = redactObject(body, config.redact, config.maxBodyBytes);
|
|
601
|
-
}
|
|
602
|
-
} catch {
|
|
603
|
-
// 바디 파싱 실패 시 무시
|
|
604
|
-
}
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Trace 리포트 (느린 요청)
|
|
608
|
-
if (isSlow && config.includeTraceOnSlow) {
|
|
609
|
-
const collector = getTrace(ctx);
|
|
610
|
-
if (collector) {
|
|
611
|
-
entry.trace = buildTraceReport(collector);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
log(entry);
|
|
616
|
-
},
|
|
617
|
-
};
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
/**
|
|
621
|
-
* Logger 훅들을 LifecycleStore에 등록하는 헬퍼
|
|
622
|
-
*
|
|
623
|
-
* @example
|
|
624
|
-
* ```typescript
|
|
625
|
-
* import { createLifecycleStore } from "./lifecycle";
|
|
626
|
-
* import { logger, applyLogger } from "./logger";
|
|
627
|
-
*
|
|
628
|
-
* const lifecycle = createLifecycleStore();
|
|
629
|
-
* applyLogger(lifecycle, logger({ level: "debug" }));
|
|
630
|
-
* ```
|
|
631
|
-
*/
|
|
632
|
-
export function applyLogger(
|
|
633
|
-
lifecycle: {
|
|
634
|
-
onRequest: Array<{ fn: (ctx: ManduContext) => void | Promise<void>; scope: string }>;
|
|
635
|
-
onError: Array<{ fn: (ctx: ManduContext, error: Error) => void | Promise<void>; scope: string }>;
|
|
636
|
-
afterHandle: Array<{ fn: (ctx: ManduContext, response: Response) => Response | Promise<Response>; scope: string }>;
|
|
637
|
-
afterResponse: Array<{ fn: (ctx: ManduContext) => void | Promise<void>; scope: string }>;
|
|
638
|
-
},
|
|
639
|
-
loggerInstance: ReturnType<typeof logger>
|
|
640
|
-
): void {
|
|
641
|
-
lifecycle.onRequest.push({ fn: loggerInstance.onRequest, scope: "global" });
|
|
642
|
-
lifecycle.onError.push({ fn: loggerInstance.onError as any, scope: "global" });
|
|
643
|
-
lifecycle.afterHandle.push({ fn: loggerInstance.afterHandle, scope: "global" });
|
|
644
|
-
lifecycle.afterResponse.push({ fn: loggerInstance.afterResponse, scope: "global" });
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
// ============================================
|
|
648
|
-
// Convenience Presets
|
|
649
|
-
// ============================================
|
|
650
|
-
|
|
651
|
-
/**
|
|
652
|
-
* 개발용 로거 프리셋
|
|
653
|
-
*/
|
|
654
|
-
export function devLogger(options: Partial<LoggerOptions> = {}) {
|
|
655
|
-
return logger({
|
|
656
|
-
format: "pretty",
|
|
657
|
-
level: "debug",
|
|
658
|
-
includeHeaders: true,
|
|
659
|
-
slowThresholdMs: 500,
|
|
660
|
-
...options,
|
|
661
|
-
});
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* 프로덕션용 로거 프리셋
|
|
666
|
-
*/
|
|
667
|
-
export function prodLogger(options: Partial<LoggerOptions> = {}) {
|
|
668
|
-
return logger({
|
|
669
|
-
format: "json",
|
|
670
|
-
level: "info",
|
|
671
|
-
includeHeaders: false,
|
|
672
|
-
includeBody: false,
|
|
673
|
-
sampleRate: 1,
|
|
674
|
-
slowThresholdMs: 1000,
|
|
675
|
-
...options,
|
|
676
|
-
});
|
|
677
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Runtime Logger 📝
|
|
3
|
+
* Trace 기반 요청/응답 로깅 레이어
|
|
4
|
+
*
|
|
5
|
+
* 역할 분리:
|
|
6
|
+
* - Trace = 수집 (원본 이벤트, duration 측정)
|
|
7
|
+
* - Logger = 출력 (포맷/필터/레드액션/샘플링)
|
|
8
|
+
*
|
|
9
|
+
* 기본값은 안전:
|
|
10
|
+
* - includeHeaders: false
|
|
11
|
+
* - includeBody: false
|
|
12
|
+
* - redact: 민감 정보 자동 마스킹
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* import { logger } from "@mandujs/core";
|
|
17
|
+
*
|
|
18
|
+
* // 기본 사용
|
|
19
|
+
* app.use(logger());
|
|
20
|
+
* // → GET /api/users
|
|
21
|
+
* // ← GET /api/users 200 23ms
|
|
22
|
+
*
|
|
23
|
+
* // 개발 모드
|
|
24
|
+
* app.use(logger({
|
|
25
|
+
* level: "debug",
|
|
26
|
+
* includeHeaders: true,
|
|
27
|
+
* }));
|
|
28
|
+
*
|
|
29
|
+
* // 프로덕션 (JSON 형식)
|
|
30
|
+
* app.use(logger({
|
|
31
|
+
* format: "json",
|
|
32
|
+
* slowThresholdMs: 500,
|
|
33
|
+
* }));
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import type { ManduContext } from "../filling/context";
|
|
38
|
+
import {
|
|
39
|
+
enableTrace,
|
|
40
|
+
getTrace,
|
|
41
|
+
buildTraceReport,
|
|
42
|
+
type TraceReport,
|
|
43
|
+
} from "./trace";
|
|
44
|
+
|
|
45
|
+
// ============================================
|
|
46
|
+
// Types
|
|
47
|
+
// ============================================
|
|
48
|
+
|
|
49
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
50
|
+
export type LogFormat = "pretty" | "json";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Logger 옵션
|
|
54
|
+
*/
|
|
55
|
+
export interface LoggerOptions {
|
|
56
|
+
/**
|
|
57
|
+
* 로그 포맷
|
|
58
|
+
* - pretty: 개발용 컬러 출력
|
|
59
|
+
* - json: 운영용 구조화 로그
|
|
60
|
+
* @default "pretty"
|
|
61
|
+
*/
|
|
62
|
+
format?: LogFormat;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 로그 레벨
|
|
66
|
+
* - debug: 모든 요청 상세 출력
|
|
67
|
+
* - info: 기본 요청/응답 (기본값)
|
|
68
|
+
* - warn: 느린 요청 + 에러
|
|
69
|
+
* - error: 에러만
|
|
70
|
+
* @default "info"
|
|
71
|
+
*/
|
|
72
|
+
level?: LogLevel;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 헤더 포함 여부
|
|
76
|
+
* ⚠️ 기본 OFF - 민감 정보 노출 위험
|
|
77
|
+
* @default false
|
|
78
|
+
*/
|
|
79
|
+
includeHeaders?: boolean;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 바디 포함 여부
|
|
83
|
+
* ⚠️ 기본 OFF - 민감 정보 노출 + 스트림 문제
|
|
84
|
+
* @default false
|
|
85
|
+
*/
|
|
86
|
+
includeBody?: boolean;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 바디 최대 바이트 (includeBody=true 시)
|
|
90
|
+
* @default 1024
|
|
91
|
+
*/
|
|
92
|
+
maxBodyBytes?: number;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 레드액션 대상 헤더/필드명 (기본값 내장)
|
|
96
|
+
* 추가할 필드만 지정하면 기본값과 병합됨
|
|
97
|
+
*/
|
|
98
|
+
redact?: string[];
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Request ID 생성 방식
|
|
102
|
+
* - "auto": crypto.randomUUID() 또는 타임스탬프 기반
|
|
103
|
+
* - 함수: 커스텀 생성
|
|
104
|
+
* @default "auto"
|
|
105
|
+
*/
|
|
106
|
+
requestId?: "auto" | ((ctx: ManduContext) => string);
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 샘플링 비율 (0-1)
|
|
110
|
+
* 운영 환경에서 로그 양 조절
|
|
111
|
+
* @default 1 (100%)
|
|
112
|
+
*/
|
|
113
|
+
sampleRate?: number;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 느린 요청 임계값 (ms)
|
|
117
|
+
* 이 값 초과 시 warn 레벨로 상세 출력
|
|
118
|
+
* @default 1000
|
|
119
|
+
*/
|
|
120
|
+
slowThresholdMs?: number;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Trace 리포트 포함 여부 (느린 요청 시)
|
|
124
|
+
* @default true
|
|
125
|
+
*/
|
|
126
|
+
includeTraceOnSlow?: boolean;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 커스텀 로그 싱크 (외부 시스템 연동용)
|
|
130
|
+
* 지정 시 console 출력 대신 이 함수 호출
|
|
131
|
+
*/
|
|
132
|
+
sink?: (entry: LogEntry) => void;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 로깅 제외 경로 패턴
|
|
136
|
+
* @example ["/health", "/metrics", /^\/static\//]
|
|
137
|
+
*/
|
|
138
|
+
skip?: (string | RegExp)[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 로그 엔트리 (JSON 출력 및 sink용)
|
|
143
|
+
*/
|
|
144
|
+
export interface LogEntry {
|
|
145
|
+
timestamp: string;
|
|
146
|
+
requestId: string;
|
|
147
|
+
method: string;
|
|
148
|
+
path: string;
|
|
149
|
+
status?: number;
|
|
150
|
+
duration?: number;
|
|
151
|
+
level: LogLevel;
|
|
152
|
+
error?: {
|
|
153
|
+
message: string;
|
|
154
|
+
stack?: string;
|
|
155
|
+
};
|
|
156
|
+
headers?: Record<string, string>;
|
|
157
|
+
body?: unknown;
|
|
158
|
+
trace?: TraceReport;
|
|
159
|
+
slow?: boolean;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================
|
|
163
|
+
// Constants
|
|
164
|
+
// ============================================
|
|
165
|
+
|
|
166
|
+
/** 기본 레드액션 대상 (대소문자 무시) */
|
|
167
|
+
const DEFAULT_REDACT_PATTERNS = [
|
|
168
|
+
"authorization",
|
|
169
|
+
"cookie",
|
|
170
|
+
"set-cookie",
|
|
171
|
+
"x-api-key",
|
|
172
|
+
"api-key",
|
|
173
|
+
"apikey",
|
|
174
|
+
"api_key",
|
|
175
|
+
"password",
|
|
176
|
+
"passwd",
|
|
177
|
+
"secret",
|
|
178
|
+
"token",
|
|
179
|
+
"bearer",
|
|
180
|
+
"credential",
|
|
181
|
+
"credentials",
|
|
182
|
+
"private",
|
|
183
|
+
"session",
|
|
184
|
+
"jwt",
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
/** Context 저장 키 */
|
|
188
|
+
const LOGGER_START_KEY = "__mandu_logger_start";
|
|
189
|
+
const LOGGER_REQUEST_ID_KEY = "__mandu_logger_request_id";
|
|
190
|
+
const LOGGER_ERROR_KEY = "__mandu_logger_error";
|
|
191
|
+
const LOGGER_RESPONSE_KEY = "__mandu_logger_response";
|
|
192
|
+
|
|
193
|
+
/** 로그 레벨 우선순위 */
|
|
194
|
+
const LEVEL_PRIORITY: Record<LogLevel, number> = {
|
|
195
|
+
debug: 0,
|
|
196
|
+
info: 1,
|
|
197
|
+
warn: 2,
|
|
198
|
+
error: 3,
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
/** ANSI 컬러 코드 */
|
|
202
|
+
const COLORS = {
|
|
203
|
+
reset: "\x1b[0m",
|
|
204
|
+
dim: "\x1b[2m",
|
|
205
|
+
cyan: "\x1b[36m",
|
|
206
|
+
green: "\x1b[32m",
|
|
207
|
+
yellow: "\x1b[33m",
|
|
208
|
+
red: "\x1b[31m",
|
|
209
|
+
magenta: "\x1b[35m",
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// ============================================
|
|
213
|
+
// Utilities
|
|
214
|
+
// ============================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Request ID 생성
|
|
218
|
+
*/
|
|
219
|
+
function generateRequestId(): string {
|
|
220
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
221
|
+
return crypto.randomUUID().slice(0, 8);
|
|
222
|
+
}
|
|
223
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 현재 시간 (고해상도)
|
|
228
|
+
*/
|
|
229
|
+
function now(): number {
|
|
230
|
+
if (typeof performance !== "undefined" && performance.now) {
|
|
231
|
+
return performance.now();
|
|
232
|
+
}
|
|
233
|
+
return Date.now();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* URL에서 pathname 추출
|
|
238
|
+
*/
|
|
239
|
+
function getPathname(url: string): string {
|
|
240
|
+
try {
|
|
241
|
+
return new URL(url).pathname;
|
|
242
|
+
} catch {
|
|
243
|
+
return url;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 헤더 레드액션 처리
|
|
249
|
+
*/
|
|
250
|
+
function redactHeaders(
|
|
251
|
+
headers: Headers,
|
|
252
|
+
patterns: string[]
|
|
253
|
+
): Record<string, string> {
|
|
254
|
+
const result: Record<string, string> = {};
|
|
255
|
+
const lowerPatterns = patterns.map((p) => p.toLowerCase());
|
|
256
|
+
|
|
257
|
+
headers.forEach((value, key) => {
|
|
258
|
+
const lowerKey = key.toLowerCase();
|
|
259
|
+
const shouldRedact = lowerPatterns.some(
|
|
260
|
+
(pattern) => lowerKey.includes(pattern) || pattern.includes(lowerKey)
|
|
261
|
+
);
|
|
262
|
+
result[key] = shouldRedact ? "[REDACTED]" : value;
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return result;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 객체 내 민감 필드 레드액션
|
|
270
|
+
*/
|
|
271
|
+
function redactObject(
|
|
272
|
+
obj: unknown,
|
|
273
|
+
patterns: string[],
|
|
274
|
+
maxBytes: number
|
|
275
|
+
): unknown {
|
|
276
|
+
if (obj === null || obj === undefined) return obj;
|
|
277
|
+
|
|
278
|
+
// 문자열이면 길이 제한만
|
|
279
|
+
if (typeof obj === "string") {
|
|
280
|
+
if (obj.length > maxBytes) {
|
|
281
|
+
return obj.slice(0, maxBytes) + `... [truncated ${obj.length - maxBytes} bytes]`;
|
|
282
|
+
}
|
|
283
|
+
return obj;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 배열
|
|
287
|
+
if (Array.isArray(obj)) {
|
|
288
|
+
const str = JSON.stringify(obj);
|
|
289
|
+
if (str.length > maxBytes) {
|
|
290
|
+
return `[Array length=${obj.length}, truncated]`;
|
|
291
|
+
}
|
|
292
|
+
return obj.map((item) => redactObject(item, patterns, maxBytes));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 객체
|
|
296
|
+
if (typeof obj === "object") {
|
|
297
|
+
const lowerPatterns = patterns.map((p) => p.toLowerCase());
|
|
298
|
+
const result: Record<string, unknown> = {};
|
|
299
|
+
|
|
300
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
301
|
+
const lowerKey = key.toLowerCase();
|
|
302
|
+
const shouldRedact = lowerPatterns.some(
|
|
303
|
+
(pattern) => lowerKey.includes(pattern) || pattern.includes(lowerKey)
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
if (shouldRedact) {
|
|
307
|
+
result[key] = "[REDACTED]";
|
|
308
|
+
} else if (typeof value === "object" && value !== null) {
|
|
309
|
+
result[key] = redactObject(value, patterns, maxBytes);
|
|
310
|
+
} else {
|
|
311
|
+
result[key] = value;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return result;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return obj;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* 경로가 skip 패턴에 매칭되는지 확인
|
|
323
|
+
*/
|
|
324
|
+
function shouldSkip(path: string, patterns: (string | RegExp)[]): boolean {
|
|
325
|
+
return patterns.some((pattern) => {
|
|
326
|
+
if (typeof pattern === "string") {
|
|
327
|
+
return path === pattern || path.startsWith(pattern + "/");
|
|
328
|
+
}
|
|
329
|
+
return pattern.test(path);
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 로그 레벨 필터링
|
|
335
|
+
*/
|
|
336
|
+
function shouldLog(entryLevel: LogLevel, configLevel: LogLevel): boolean {
|
|
337
|
+
return LEVEL_PRIORITY[entryLevel] >= LEVEL_PRIORITY[configLevel];
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* HTTP 상태 코드에 따른 색상
|
|
342
|
+
*/
|
|
343
|
+
function getStatusColor(status: number): string {
|
|
344
|
+
if (status >= 500) return COLORS.red;
|
|
345
|
+
if (status >= 400) return COLORS.yellow;
|
|
346
|
+
if (status >= 300) return COLORS.cyan;
|
|
347
|
+
return COLORS.green;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// ============================================
|
|
351
|
+
// Pretty Formatter
|
|
352
|
+
// ============================================
|
|
353
|
+
|
|
354
|
+
function formatPretty(entry: LogEntry): string {
|
|
355
|
+
const { method, path, status, duration, requestId, error, slow, headers, trace } = entry;
|
|
356
|
+
|
|
357
|
+
const lines: string[] = [];
|
|
358
|
+
|
|
359
|
+
// 요청 라인
|
|
360
|
+
if (status === undefined) {
|
|
361
|
+
// 요청 시작
|
|
362
|
+
lines.push(
|
|
363
|
+
`${COLORS.dim}[${requestId}]${COLORS.reset} ${COLORS.cyan}→${COLORS.reset} ${method} ${path}`
|
|
364
|
+
);
|
|
365
|
+
} else {
|
|
366
|
+
// 응답
|
|
367
|
+
const statusColor = getStatusColor(status);
|
|
368
|
+
const durationStr = duration !== undefined ? ` ${duration.toFixed(0)}ms` : "";
|
|
369
|
+
const slowIndicator = slow ? ` ${COLORS.yellow}[SLOW]${COLORS.reset}` : "";
|
|
370
|
+
|
|
371
|
+
lines.push(
|
|
372
|
+
`${COLORS.dim}[${requestId}]${COLORS.reset} ${COLORS.magenta}←${COLORS.reset} ${method} ${path} ${statusColor}${status}${COLORS.reset}${durationStr}${slowIndicator}`
|
|
373
|
+
);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 에러
|
|
377
|
+
if (error) {
|
|
378
|
+
lines.push(` ${COLORS.red}Error: ${error.message}${COLORS.reset}`);
|
|
379
|
+
if (error.stack) {
|
|
380
|
+
const stackLines = error.stack.split("\n").slice(1, 4);
|
|
381
|
+
stackLines.forEach((line) => {
|
|
382
|
+
lines.push(` ${COLORS.dim}${line.trim()}${COLORS.reset}`);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 헤더 (debug 모드)
|
|
388
|
+
if (headers && Object.keys(headers).length > 0) {
|
|
389
|
+
lines.push(` ${COLORS.dim}Headers:${COLORS.reset}`);
|
|
390
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
391
|
+
lines.push(` ${COLORS.dim}${key}:${COLORS.reset} ${value}`);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Trace 리포트 (느린 요청)
|
|
396
|
+
if (trace && trace.entries.length > 0) {
|
|
397
|
+
lines.push(` ${COLORS.dim}Trace:${COLORS.reset}`);
|
|
398
|
+
for (const traceEntry of trace.entries) {
|
|
399
|
+
const name = traceEntry.name ? ` (${traceEntry.name})` : "";
|
|
400
|
+
lines.push(
|
|
401
|
+
` ${COLORS.dim}${traceEntry.event}${name}: ${traceEntry.duration.toFixed(1)}ms${COLORS.reset}`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return lines.join("\n");
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// ============================================
|
|
410
|
+
// JSON Formatter
|
|
411
|
+
// ============================================
|
|
412
|
+
|
|
413
|
+
function formatJson(entry: LogEntry): string {
|
|
414
|
+
return JSON.stringify(entry);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// ============================================
|
|
418
|
+
// Logger Middleware Factory
|
|
419
|
+
// ============================================
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Logger 미들웨어 생성
|
|
423
|
+
*
|
|
424
|
+
* @example
|
|
425
|
+
* ```typescript
|
|
426
|
+
* // 기본 사용
|
|
427
|
+
* app.use(logger());
|
|
428
|
+
*
|
|
429
|
+
* // 개발 모드
|
|
430
|
+
* app.use(logger({
|
|
431
|
+
* level: "debug",
|
|
432
|
+
* includeHeaders: true,
|
|
433
|
+
* }));
|
|
434
|
+
*
|
|
435
|
+
* // 프로덕션
|
|
436
|
+
* app.use(logger({
|
|
437
|
+
* format: "json",
|
|
438
|
+
* sampleRate: 0.1, // 10% 샘플링
|
|
439
|
+
* slowThresholdMs: 500,
|
|
440
|
+
* }));
|
|
441
|
+
* ```
|
|
442
|
+
*/
|
|
443
|
+
export function logger(options: LoggerOptions = {}) {
|
|
444
|
+
const config = {
|
|
445
|
+
format: options.format ?? "pretty",
|
|
446
|
+
level: options.level ?? "info",
|
|
447
|
+
includeHeaders: options.includeHeaders ?? false,
|
|
448
|
+
includeBody: options.includeBody ?? false,
|
|
449
|
+
maxBodyBytes: options.maxBodyBytes ?? 1024,
|
|
450
|
+
redact: [...DEFAULT_REDACT_PATTERNS, ...(options.redact ?? [])],
|
|
451
|
+
requestId: options.requestId ?? "auto",
|
|
452
|
+
sampleRate: options.sampleRate ?? 1,
|
|
453
|
+
slowThresholdMs: options.slowThresholdMs ?? 1000,
|
|
454
|
+
includeTraceOnSlow: options.includeTraceOnSlow ?? true,
|
|
455
|
+
sink: options.sink,
|
|
456
|
+
skip: options.skip ?? [],
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
const formatter = config.format === "json" ? formatJson : formatPretty;
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 로그 출력 함수
|
|
463
|
+
*/
|
|
464
|
+
function log(entry: LogEntry): void {
|
|
465
|
+
if (!shouldLog(entry.level, config.level)) return;
|
|
466
|
+
|
|
467
|
+
if (config.sink) {
|
|
468
|
+
config.sink(entry);
|
|
469
|
+
} else {
|
|
470
|
+
const output = formatter(entry);
|
|
471
|
+
switch (entry.level) {
|
|
472
|
+
case "error":
|
|
473
|
+
console.error(output);
|
|
474
|
+
break;
|
|
475
|
+
case "warn":
|
|
476
|
+
console.warn(output);
|
|
477
|
+
break;
|
|
478
|
+
default:
|
|
479
|
+
console.log(output);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return {
|
|
485
|
+
/**
|
|
486
|
+
* onRequest 훅 - 요청 시작 기록
|
|
487
|
+
*/
|
|
488
|
+
onRequest(ctx: ManduContext): void {
|
|
489
|
+
const path = getPathname(ctx.url);
|
|
490
|
+
|
|
491
|
+
// Skip 체크
|
|
492
|
+
if (shouldSkip(path, config.skip)) return;
|
|
493
|
+
|
|
494
|
+
// 샘플링 체크
|
|
495
|
+
if (config.sampleRate < 1 && Math.random() > config.sampleRate) return;
|
|
496
|
+
|
|
497
|
+
// Trace 활성화
|
|
498
|
+
enableTrace(ctx);
|
|
499
|
+
|
|
500
|
+
// 시작 시간 저장
|
|
501
|
+
ctx.set(LOGGER_START_KEY, now());
|
|
502
|
+
|
|
503
|
+
// Request ID 생성/저장
|
|
504
|
+
const requestId =
|
|
505
|
+
config.requestId === "auto"
|
|
506
|
+
? generateRequestId()
|
|
507
|
+
: config.requestId(ctx);
|
|
508
|
+
ctx.set(LOGGER_REQUEST_ID_KEY, requestId);
|
|
509
|
+
|
|
510
|
+
// debug 레벨이면 요청 시작도 로깅
|
|
511
|
+
if (config.level === "debug") {
|
|
512
|
+
const entry: LogEntry = {
|
|
513
|
+
timestamp: new Date().toISOString(),
|
|
514
|
+
requestId,
|
|
515
|
+
method: ctx.method,
|
|
516
|
+
path,
|
|
517
|
+
level: "debug",
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
if (config.includeHeaders) {
|
|
521
|
+
entry.headers = redactHeaders(ctx.headers, config.redact);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
log(entry);
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* onError 훅 - 에러 캡처
|
|
530
|
+
*/
|
|
531
|
+
onError(ctx: ManduContext, error: Error): void {
|
|
532
|
+
ctx.set(LOGGER_ERROR_KEY, error);
|
|
533
|
+
},
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* afterHandle 훅 - 응답 캡처 (바디 로깅용)
|
|
537
|
+
*/
|
|
538
|
+
afterHandle(ctx: ManduContext, response: Response): Response {
|
|
539
|
+
ctx.set(LOGGER_RESPONSE_KEY, response);
|
|
540
|
+
return response;
|
|
541
|
+
},
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* afterResponse 훅 - 최종 로그 출력
|
|
545
|
+
*/
|
|
546
|
+
async afterResponse(ctx: ManduContext): Promise<void> {
|
|
547
|
+
const startTime = ctx.get<number>(LOGGER_START_KEY);
|
|
548
|
+
const requestId = ctx.get<string>(LOGGER_REQUEST_ID_KEY);
|
|
549
|
+
|
|
550
|
+
// 시작 기록이 없으면 skip된 요청
|
|
551
|
+
if (startTime === undefined || requestId === undefined) return;
|
|
552
|
+
|
|
553
|
+
const path = getPathname(ctx.url);
|
|
554
|
+
const duration = now() - startTime;
|
|
555
|
+
const error = ctx.get<Error>(LOGGER_ERROR_KEY);
|
|
556
|
+
const response = ctx.get<Response>(LOGGER_RESPONSE_KEY);
|
|
557
|
+
const status = response?.status ?? (error ? 500 : 200);
|
|
558
|
+
const isSlow = duration > config.slowThresholdMs;
|
|
559
|
+
|
|
560
|
+
// 로그 레벨 결정
|
|
561
|
+
let level: LogLevel = "info";
|
|
562
|
+
if (error) {
|
|
563
|
+
level = "error";
|
|
564
|
+
} else if (isSlow) {
|
|
565
|
+
level = "warn";
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// 로그 엔트리 생성
|
|
569
|
+
const entry: LogEntry = {
|
|
570
|
+
timestamp: new Date().toISOString(),
|
|
571
|
+
requestId,
|
|
572
|
+
method: ctx.method,
|
|
573
|
+
path,
|
|
574
|
+
status,
|
|
575
|
+
duration,
|
|
576
|
+
level,
|
|
577
|
+
slow: isSlow,
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
// 에러 정보
|
|
581
|
+
if (error) {
|
|
582
|
+
entry.error = {
|
|
583
|
+
message: error.message,
|
|
584
|
+
stack: error.stack,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 헤더 (debug 또는 느린 요청)
|
|
589
|
+
if (config.includeHeaders || (isSlow && config.level === "debug")) {
|
|
590
|
+
entry.headers = redactHeaders(ctx.headers, config.redact);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 바디 (명시적 활성화 + debug 레벨만)
|
|
594
|
+
if (config.includeBody && config.level === "debug" && response) {
|
|
595
|
+
try {
|
|
596
|
+
const cloned = response.clone();
|
|
597
|
+
const contentType = cloned.headers.get("content-type") || "";
|
|
598
|
+
if (contentType.includes("application/json")) {
|
|
599
|
+
const body = await cloned.json();
|
|
600
|
+
entry.body = redactObject(body, config.redact, config.maxBodyBytes);
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
// 바디 파싱 실패 시 무시
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Trace 리포트 (느린 요청)
|
|
608
|
+
if (isSlow && config.includeTraceOnSlow) {
|
|
609
|
+
const collector = getTrace(ctx);
|
|
610
|
+
if (collector) {
|
|
611
|
+
entry.trace = buildTraceReport(collector);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
log(entry);
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Logger 훅들을 LifecycleStore에 등록하는 헬퍼
|
|
622
|
+
*
|
|
623
|
+
* @example
|
|
624
|
+
* ```typescript
|
|
625
|
+
* import { createLifecycleStore } from "./lifecycle";
|
|
626
|
+
* import { logger, applyLogger } from "./logger";
|
|
627
|
+
*
|
|
628
|
+
* const lifecycle = createLifecycleStore();
|
|
629
|
+
* applyLogger(lifecycle, logger({ level: "debug" }));
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
export function applyLogger(
|
|
633
|
+
lifecycle: {
|
|
634
|
+
onRequest: Array<{ fn: (ctx: ManduContext) => void | Promise<void>; scope: string }>;
|
|
635
|
+
onError: Array<{ fn: (ctx: ManduContext, error: Error) => void | Promise<void>; scope: string }>;
|
|
636
|
+
afterHandle: Array<{ fn: (ctx: ManduContext, response: Response) => Response | Promise<Response>; scope: string }>;
|
|
637
|
+
afterResponse: Array<{ fn: (ctx: ManduContext) => void | Promise<void>; scope: string }>;
|
|
638
|
+
},
|
|
639
|
+
loggerInstance: ReturnType<typeof logger>
|
|
640
|
+
): void {
|
|
641
|
+
lifecycle.onRequest.push({ fn: loggerInstance.onRequest, scope: "global" });
|
|
642
|
+
lifecycle.onError.push({ fn: loggerInstance.onError as any, scope: "global" });
|
|
643
|
+
lifecycle.afterHandle.push({ fn: loggerInstance.afterHandle, scope: "global" });
|
|
644
|
+
lifecycle.afterResponse.push({ fn: loggerInstance.afterResponse, scope: "global" });
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ============================================
|
|
648
|
+
// Convenience Presets
|
|
649
|
+
// ============================================
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* 개발용 로거 프리셋
|
|
653
|
+
*/
|
|
654
|
+
export function devLogger(options: Partial<LoggerOptions> = {}) {
|
|
655
|
+
return logger({
|
|
656
|
+
format: "pretty",
|
|
657
|
+
level: "debug",
|
|
658
|
+
includeHeaders: true,
|
|
659
|
+
slowThresholdMs: 500,
|
|
660
|
+
...options,
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* 프로덕션용 로거 프리셋
|
|
666
|
+
*/
|
|
667
|
+
export function prodLogger(options: Partial<LoggerOptions> = {}) {
|
|
668
|
+
return logger({
|
|
669
|
+
format: "json",
|
|
670
|
+
level: "info",
|
|
671
|
+
includeHeaders: false,
|
|
672
|
+
includeBody: false,
|
|
673
|
+
sampleRate: 1,
|
|
674
|
+
slowThresholdMs: 1000,
|
|
675
|
+
...options,
|
|
676
|
+
});
|
|
677
|
+
}
|