logixlysia 5.2.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +68 -85
- package/dist/index.js +5 -12
- package/package.json +27 -73
- 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 +106 -0
- package/src/interfaces.ts +117 -0
- package/src/logger/create-logger.ts +246 -0
- package/src/logger/handle-http-error.ts +51 -0
- package/src/logger/index.ts +126 -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/rotation.ts +91 -0
- package/LICENSE +0 -21
- package/README.md +0 -51
- package/dist/index.cjs +0 -13
- package/dist/index.d.cts +0 -88
|
@@ -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,106 @@
|
|
|
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
|
+
|
|
6
|
+
export type Logixlysia = Elysia<
|
|
7
|
+
'Logixlysia',
|
|
8
|
+
SingletonBase & { store: LogixlysiaStore }
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
export const logixlysia = (options: Options = {}): Logixlysia => {
|
|
12
|
+
const didCustomLog = new WeakSet<Request>()
|
|
13
|
+
const baseLogger = createLogger(options)
|
|
14
|
+
const logger = {
|
|
15
|
+
...baseLogger,
|
|
16
|
+
debug: (
|
|
17
|
+
request: Request,
|
|
18
|
+
message: string,
|
|
19
|
+
context?: Record<string, unknown>
|
|
20
|
+
) => {
|
|
21
|
+
didCustomLog.add(request)
|
|
22
|
+
baseLogger.debug(request, message, context)
|
|
23
|
+
},
|
|
24
|
+
info: (
|
|
25
|
+
request: Request,
|
|
26
|
+
message: string,
|
|
27
|
+
context?: Record<string, unknown>
|
|
28
|
+
) => {
|
|
29
|
+
didCustomLog.add(request)
|
|
30
|
+
baseLogger.info(request, message, context)
|
|
31
|
+
},
|
|
32
|
+
warn: (
|
|
33
|
+
request: Request,
|
|
34
|
+
message: string,
|
|
35
|
+
context?: Record<string, unknown>
|
|
36
|
+
) => {
|
|
37
|
+
didCustomLog.add(request)
|
|
38
|
+
baseLogger.warn(request, message, context)
|
|
39
|
+
},
|
|
40
|
+
error: (
|
|
41
|
+
request: Request,
|
|
42
|
+
message: string,
|
|
43
|
+
context?: Record<string, unknown>
|
|
44
|
+
) => {
|
|
45
|
+
didCustomLog.add(request)
|
|
46
|
+
baseLogger.error(request, message, context)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const app = new Elysia({
|
|
51
|
+
name: 'Logixlysia',
|
|
52
|
+
detail: {
|
|
53
|
+
description:
|
|
54
|
+
'Logixlysia is a plugin for Elysia that provides a logger and pino logger.',
|
|
55
|
+
tags: ['logging', 'pino']
|
|
56
|
+
}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
app
|
|
61
|
+
.state('logger', logger)
|
|
62
|
+
.state('pino', logger.pino)
|
|
63
|
+
.state('beforeTime', BigInt(0))
|
|
64
|
+
.onStart(({ server }) => {
|
|
65
|
+
if (server) {
|
|
66
|
+
startServer(server, options)
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.onRequest(({ store }) => {
|
|
70
|
+
store.beforeTime = process.hrtime.bigint()
|
|
71
|
+
})
|
|
72
|
+
.onAfterHandle(({ request, set, store }) => {
|
|
73
|
+
if (didCustomLog.has(request)) {
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const status = typeof set.status === 'number' ? set.status : 200
|
|
78
|
+
let level: 'INFO' | 'WARNING' | 'ERROR' = 'INFO'
|
|
79
|
+
if (status >= 500) {
|
|
80
|
+
level = 'ERROR'
|
|
81
|
+
} else if (status >= 400) {
|
|
82
|
+
level = 'WARNING'
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
logger.log(level, request, { status }, store)
|
|
86
|
+
})
|
|
87
|
+
.onError(({ request, error, store }) => {
|
|
88
|
+
logger.handleHttpError(request, error, store)
|
|
89
|
+
})
|
|
90
|
+
// Ensure plugin lifecycle hooks (onRequest/onAfterHandle/onError) apply to the parent app.
|
|
91
|
+
.as('scoped') as unknown as Logixlysia
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export type {
|
|
96
|
+
Logger,
|
|
97
|
+
LogixlysiaContext,
|
|
98
|
+
LogixlysiaStore,
|
|
99
|
+
LogLevel,
|
|
100
|
+
Options,
|
|
101
|
+
Pino,
|
|
102
|
+
StoreData,
|
|
103
|
+
Transport
|
|
104
|
+
} from './interfaces'
|
|
105
|
+
|
|
106
|
+
export default logixlysia
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Logger as PinoLogger,
|
|
3
|
+
LoggerOptions as PinoLoggerOptions
|
|
4
|
+
} from 'pino'
|
|
5
|
+
|
|
6
|
+
export type Pino = PinoLogger<never, boolean>
|
|
7
|
+
|
|
8
|
+
export type LogLevel = 'DEBUG' | 'INFO' | 'WARNING' | 'ERROR'
|
|
9
|
+
|
|
10
|
+
export type StoreData = {
|
|
11
|
+
beforeTime: bigint
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type LogixlysiaStore = {
|
|
15
|
+
logger: Logger
|
|
16
|
+
pino: Pino
|
|
17
|
+
beforeTime?: bigint
|
|
18
|
+
[key: string]: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type Transport = {
|
|
22
|
+
log: (
|
|
23
|
+
level: LogLevel,
|
|
24
|
+
message: string,
|
|
25
|
+
meta?: Record<string, unknown>
|
|
26
|
+
) => void | Promise<void>
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type LogRotationConfig = {
|
|
30
|
+
/**
|
|
31
|
+
* Max log file size before rotation, e.g. '10m', '5k', or a byte count.
|
|
32
|
+
*/
|
|
33
|
+
maxSize?: string | number
|
|
34
|
+
/**
|
|
35
|
+
* Keep at most N files or keep files for a duration like '7d'.
|
|
36
|
+
*/
|
|
37
|
+
maxFiles?: number | string
|
|
38
|
+
/**
|
|
39
|
+
* Rotate at a fixed interval, e.g. '1d', '12h'.
|
|
40
|
+
*/
|
|
41
|
+
interval?: string
|
|
42
|
+
compress?: boolean
|
|
43
|
+
compression?: 'gzip'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Options = {
|
|
47
|
+
config?: {
|
|
48
|
+
showStartupMessage?: boolean
|
|
49
|
+
startupMessageFormat?: 'simple' | 'banner'
|
|
50
|
+
useColors?: boolean
|
|
51
|
+
ip?: boolean
|
|
52
|
+
timestamp?: {
|
|
53
|
+
translateTime?: string
|
|
54
|
+
}
|
|
55
|
+
customLogFormat?: string
|
|
56
|
+
|
|
57
|
+
// Outputs
|
|
58
|
+
transports?: Transport[]
|
|
59
|
+
useTransportsOnly?: boolean
|
|
60
|
+
disableInternalLogger?: boolean
|
|
61
|
+
disableFileLogging?: boolean
|
|
62
|
+
logFilePath?: string
|
|
63
|
+
logRotation?: LogRotationConfig
|
|
64
|
+
|
|
65
|
+
// Pino
|
|
66
|
+
pino?: (PinoLoggerOptions & { prettyPrint?: boolean }) | undefined
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export class HttpError extends Error {
|
|
71
|
+
readonly status: number
|
|
72
|
+
|
|
73
|
+
constructor(status: number, message: string) {
|
|
74
|
+
super(message)
|
|
75
|
+
this.status = status
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export type Logger = {
|
|
80
|
+
pino: Pino
|
|
81
|
+
log: (
|
|
82
|
+
level: LogLevel,
|
|
83
|
+
request: RequestInfo,
|
|
84
|
+
data: Record<string, unknown>,
|
|
85
|
+
store: StoreData
|
|
86
|
+
) => void
|
|
87
|
+
handleHttpError: (
|
|
88
|
+
request: RequestInfo,
|
|
89
|
+
error: unknown,
|
|
90
|
+
store: StoreData
|
|
91
|
+
) => void
|
|
92
|
+
debug: (
|
|
93
|
+
request: RequestInfo,
|
|
94
|
+
message: string,
|
|
95
|
+
context?: Record<string, unknown>
|
|
96
|
+
) => void
|
|
97
|
+
info: (
|
|
98
|
+
request: RequestInfo,
|
|
99
|
+
message: string,
|
|
100
|
+
context?: Record<string, unknown>
|
|
101
|
+
) => void
|
|
102
|
+
warn: (
|
|
103
|
+
request: RequestInfo,
|
|
104
|
+
message: string,
|
|
105
|
+
context?: Record<string, unknown>
|
|
106
|
+
) => void
|
|
107
|
+
error: (
|
|
108
|
+
request: RequestInfo,
|
|
109
|
+
message: string,
|
|
110
|
+
context?: Record<string, unknown>
|
|
111
|
+
) => void
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export type LogixlysiaContext = {
|
|
115
|
+
request: Request
|
|
116
|
+
store: LogixlysiaStore
|
|
117
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
import { getStatusCode } from '../helpers/status'
|
|
3
|
+
import type {
|
|
4
|
+
LogLevel,
|
|
5
|
+
Options,
|
|
6
|
+
Pino,
|
|
7
|
+
RequestInfo,
|
|
8
|
+
StoreData
|
|
9
|
+
} from '../interfaces'
|
|
10
|
+
|
|
11
|
+
const pad2 = (value: number): string => String(value).padStart(2, '0')
|
|
12
|
+
const pad3 = (value: number): string => String(value).padStart(3, '0')
|
|
13
|
+
|
|
14
|
+
const shouldUseColors = (options: Options): boolean => {
|
|
15
|
+
const config = options.config
|
|
16
|
+
const enabledByConfig = config?.useColors ?? true
|
|
17
|
+
|
|
18
|
+
// Avoid ANSI sequences in non-interactive output (pipes, CI logs, files).
|
|
19
|
+
const isTty = typeof process !== 'undefined' && process.stdout?.isTTY === true
|
|
20
|
+
return enabledByConfig && isTty
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const formatTimestamp = (date: Date, pattern?: string): string => {
|
|
24
|
+
if (!pattern) {
|
|
25
|
+
return date.toISOString()
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const yyyy = String(date.getFullYear())
|
|
29
|
+
const mm = pad2(date.getMonth() + 1)
|
|
30
|
+
const dd = pad2(date.getDate())
|
|
31
|
+
const HH = pad2(date.getHours())
|
|
32
|
+
const MM = pad2(date.getMinutes())
|
|
33
|
+
const ss = pad2(date.getSeconds())
|
|
34
|
+
const SSS = pad3(date.getMilliseconds())
|
|
35
|
+
|
|
36
|
+
return pattern
|
|
37
|
+
.replaceAll('yyyy', yyyy)
|
|
38
|
+
.replaceAll('mm', mm)
|
|
39
|
+
.replaceAll('dd', dd)
|
|
40
|
+
.replaceAll('HH', HH)
|
|
41
|
+
.replaceAll('MM', MM)
|
|
42
|
+
.replaceAll('ss', ss)
|
|
43
|
+
.replaceAll('SSS', SSS)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const getIp = (request: RequestInfo): string => {
|
|
47
|
+
const forwarded = request.headers.get('x-forwarded-for')
|
|
48
|
+
if (forwarded) {
|
|
49
|
+
return forwarded.split(',')[0]?.trim() ?? ''
|
|
50
|
+
}
|
|
51
|
+
return request.headers.get('x-real-ip') ?? ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const getColoredLevel = (level: LogLevel, useColors: boolean): string => {
|
|
55
|
+
if (!useColors) {
|
|
56
|
+
return level
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (level === 'ERROR') {
|
|
60
|
+
return chalk.bgRed.black(level)
|
|
61
|
+
}
|
|
62
|
+
if (level === 'WARNING') {
|
|
63
|
+
return chalk.bgYellow.black(level)
|
|
64
|
+
}
|
|
65
|
+
if (level === 'DEBUG') {
|
|
66
|
+
return chalk.bgBlue.black(level)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return chalk.bgGreen.black(level)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const getColoredMethod = (method: string, useColors: boolean): string => {
|
|
73
|
+
if (!useColors) {
|
|
74
|
+
return method
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const upper = method.toUpperCase()
|
|
78
|
+
if (upper === 'GET') {
|
|
79
|
+
return chalk.green.bold(upper)
|
|
80
|
+
}
|
|
81
|
+
if (upper === 'POST') {
|
|
82
|
+
return chalk.blue.bold(upper)
|
|
83
|
+
}
|
|
84
|
+
if (upper === 'PUT') {
|
|
85
|
+
return chalk.yellow.bold(upper)
|
|
86
|
+
}
|
|
87
|
+
if (upper === 'PATCH') {
|
|
88
|
+
return chalk.yellowBright.bold(upper)
|
|
89
|
+
}
|
|
90
|
+
if (upper === 'DELETE') {
|
|
91
|
+
return chalk.red.bold(upper)
|
|
92
|
+
}
|
|
93
|
+
if (upper === 'OPTIONS') {
|
|
94
|
+
return chalk.cyan.bold(upper)
|
|
95
|
+
}
|
|
96
|
+
if (upper === 'HEAD') {
|
|
97
|
+
return chalk.greenBright.bold(upper)
|
|
98
|
+
}
|
|
99
|
+
if (upper === 'TRACE') {
|
|
100
|
+
return chalk.magenta.bold(upper)
|
|
101
|
+
}
|
|
102
|
+
if (upper === 'CONNECT') {
|
|
103
|
+
return chalk.cyanBright.bold(upper)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return chalk.white.bold(upper)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const getColoredStatus = (status: string, useColors: boolean): string => {
|
|
110
|
+
if (!useColors) {
|
|
111
|
+
return status
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const numeric = Number.parseInt(status, 10)
|
|
115
|
+
if (!Number.isFinite(numeric)) {
|
|
116
|
+
return status
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (numeric >= 500) {
|
|
120
|
+
return chalk.red(status)
|
|
121
|
+
}
|
|
122
|
+
if (numeric >= 400) {
|
|
123
|
+
return chalk.yellow(status)
|
|
124
|
+
}
|
|
125
|
+
if (numeric >= 300) {
|
|
126
|
+
return chalk.cyan(status)
|
|
127
|
+
}
|
|
128
|
+
if (numeric >= 200) {
|
|
129
|
+
return chalk.green(status)
|
|
130
|
+
}
|
|
131
|
+
return chalk.gray(status)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const getColoredDuration = (duration: string, useColors: boolean): string => {
|
|
135
|
+
if (!useColors) {
|
|
136
|
+
return duration
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return chalk.gray(duration)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const getColoredTimestamp = (timestamp: string, useColors: boolean): string => {
|
|
143
|
+
if (!useColors) {
|
|
144
|
+
return timestamp
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return chalk.bgHex('#FFA500').black(timestamp)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const getColoredPathname = (pathname: string, useColors: boolean): string => {
|
|
151
|
+
if (!useColors) {
|
|
152
|
+
return pathname
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return chalk.whiteBright(pathname)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const getContextString = (value: unknown): string => {
|
|
159
|
+
if (typeof value === 'object' && value !== null) {
|
|
160
|
+
return JSON.stringify(value)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return ''
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export const formatLine = ({
|
|
167
|
+
level,
|
|
168
|
+
request,
|
|
169
|
+
data,
|
|
170
|
+
store,
|
|
171
|
+
options
|
|
172
|
+
}: {
|
|
173
|
+
level: LogLevel
|
|
174
|
+
request: RequestInfo
|
|
175
|
+
data: Record<string, unknown>
|
|
176
|
+
store: StoreData
|
|
177
|
+
options: Options
|
|
178
|
+
}): string => {
|
|
179
|
+
const config = options.config
|
|
180
|
+
const useColors = shouldUseColors(options)
|
|
181
|
+
const format =
|
|
182
|
+
config?.customLogFormat ??
|
|
183
|
+
'🦊 {now} {level} {duration} {method} {pathname} {status} {message} {ip} {context}'
|
|
184
|
+
|
|
185
|
+
const now = new Date()
|
|
186
|
+
const epoch = String(now.getTime())
|
|
187
|
+
const rawTimestamp = formatTimestamp(now, config?.timestamp?.translateTime)
|
|
188
|
+
const timestamp = getColoredTimestamp(rawTimestamp, useColors)
|
|
189
|
+
|
|
190
|
+
const message = typeof data.message === 'string' ? data.message : ''
|
|
191
|
+
const durationMs =
|
|
192
|
+
store.beforeTime === BigInt(0)
|
|
193
|
+
? 0
|
|
194
|
+
: Number(process.hrtime.bigint() - store.beforeTime) / 1_000_000
|
|
195
|
+
|
|
196
|
+
const pathname = new URL(request.url).pathname
|
|
197
|
+
const statusValue = data.status
|
|
198
|
+
const statusCode =
|
|
199
|
+
statusValue === null || statusValue === undefined
|
|
200
|
+
? 200
|
|
201
|
+
: getStatusCode(statusValue)
|
|
202
|
+
const status = String(statusCode)
|
|
203
|
+
const ip = config?.ip === true ? getIp(request) : ''
|
|
204
|
+
const ctxString = getContextString(data.context)
|
|
205
|
+
const coloredLevel = getColoredLevel(level, useColors)
|
|
206
|
+
const coloredMethod = getColoredMethod(request.method, useColors)
|
|
207
|
+
const coloredPathname = getColoredPathname(pathname, useColors)
|
|
208
|
+
const coloredStatus = getColoredStatus(status, useColors)
|
|
209
|
+
const coloredDuration = getColoredDuration(
|
|
210
|
+
`${durationMs.toFixed(2)}ms`,
|
|
211
|
+
useColors
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return format
|
|
215
|
+
.replaceAll('{now}', timestamp)
|
|
216
|
+
.replaceAll('{epoch}', epoch)
|
|
217
|
+
.replaceAll('{level}', coloredLevel)
|
|
218
|
+
.replaceAll('{duration}', coloredDuration)
|
|
219
|
+
.replaceAll('{method}', coloredMethod)
|
|
220
|
+
.replaceAll('{pathname}', coloredPathname)
|
|
221
|
+
.replaceAll('{path}', coloredPathname)
|
|
222
|
+
.replaceAll('{status}', coloredStatus)
|
|
223
|
+
.replaceAll('{message}', message)
|
|
224
|
+
.replaceAll('{ip}', ip)
|
|
225
|
+
.replaceAll('{context}', ctxString)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export const logWithPino = (
|
|
229
|
+
logger: Pino,
|
|
230
|
+
level: LogLevel,
|
|
231
|
+
data: Record<string, unknown>
|
|
232
|
+
): void => {
|
|
233
|
+
if (level === 'ERROR') {
|
|
234
|
+
logger.error(data)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
if (level === 'WARNING') {
|
|
238
|
+
logger.warn(data)
|
|
239
|
+
return
|
|
240
|
+
}
|
|
241
|
+
if (level === 'DEBUG') {
|
|
242
|
+
logger.debug(data)
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
logger.info(data)
|
|
246
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { LogLevel, Options, RequestInfo, StoreData } from '../interfaces'
|
|
2
|
+
import { logToTransports } from '../output'
|
|
3
|
+
import { logToFile } from '../output/file'
|
|
4
|
+
import { parseError } from '../utils/error'
|
|
5
|
+
|
|
6
|
+
const isErrorWithStatus = (
|
|
7
|
+
value: unknown
|
|
8
|
+
): value is { status: number; message?: string } =>
|
|
9
|
+
typeof value === 'object' &&
|
|
10
|
+
value !== null &&
|
|
11
|
+
'status' in value &&
|
|
12
|
+
typeof (value as { status?: unknown }).status === 'number'
|
|
13
|
+
|
|
14
|
+
export const handleHttpError = (
|
|
15
|
+
request: RequestInfo,
|
|
16
|
+
error: unknown,
|
|
17
|
+
store: StoreData,
|
|
18
|
+
options: Options
|
|
19
|
+
): void => {
|
|
20
|
+
const config = options.config
|
|
21
|
+
const useTransportsOnly = config?.useTransportsOnly === true
|
|
22
|
+
const disableInternalLogger = config?.disableInternalLogger === true
|
|
23
|
+
const disableFileLogging = config?.disableFileLogging === true
|
|
24
|
+
|
|
25
|
+
const status = isErrorWithStatus(error) ? error.status : 500
|
|
26
|
+
const message = parseError(error)
|
|
27
|
+
|
|
28
|
+
const level: LogLevel = 'ERROR'
|
|
29
|
+
const data: Record<string, unknown> = { status, message, error }
|
|
30
|
+
|
|
31
|
+
logToTransports({ level, request, data, store, options })
|
|
32
|
+
|
|
33
|
+
if (!(useTransportsOnly || disableFileLogging)) {
|
|
34
|
+
const filePath = config?.logFilePath
|
|
35
|
+
if (filePath) {
|
|
36
|
+
logToFile({ filePath, level, request, data, store, options }).catch(
|
|
37
|
+
() => {
|
|
38
|
+
// Ignore errors
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (useTransportsOnly || disableInternalLogger) {
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
console.error(
|
|
49
|
+
`${level} ${request.method} ${new URL(request.url).pathname} ${message}`
|
|
50
|
+
)
|
|
51
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import pino from 'pino'
|
|
2
|
+
import type {
|
|
3
|
+
Logger,
|
|
4
|
+
LogLevel,
|
|
5
|
+
Options,
|
|
6
|
+
Pino,
|
|
7
|
+
RequestInfo,
|
|
8
|
+
StoreData
|
|
9
|
+
} from '../interfaces'
|
|
10
|
+
import { logToTransports } from '../output'
|
|
11
|
+
import { logToFile } from '../output/file'
|
|
12
|
+
import { formatLine } from './create-logger'
|
|
13
|
+
import { handleHttpError } from './handle-http-error'
|
|
14
|
+
|
|
15
|
+
export const createLogger = (options: Options = {}): Logger => {
|
|
16
|
+
const config = options.config
|
|
17
|
+
|
|
18
|
+
const pinoConfig = config?.pino
|
|
19
|
+
const { prettyPrint, ...pinoOptions } = pinoConfig ?? {}
|
|
20
|
+
|
|
21
|
+
const shouldPrettyPrint =
|
|
22
|
+
prettyPrint === true && pinoOptions.transport === undefined
|
|
23
|
+
|
|
24
|
+
const transport = shouldPrettyPrint
|
|
25
|
+
? pino.transport({
|
|
26
|
+
target: 'pino-pretty',
|
|
27
|
+
options: {
|
|
28
|
+
colorize: process.stdout?.isTTY === true,
|
|
29
|
+
translateTime: config?.timestamp?.translateTime,
|
|
30
|
+
messageKey: pinoOptions.messageKey,
|
|
31
|
+
errorKey: pinoOptions.errorKey
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
: pinoOptions.transport
|
|
35
|
+
|
|
36
|
+
const pinoLogger: Pino = pino({
|
|
37
|
+
...pinoOptions,
|
|
38
|
+
level: pinoOptions.level ?? 'info',
|
|
39
|
+
messageKey: pinoOptions.messageKey,
|
|
40
|
+
errorKey: pinoOptions.errorKey,
|
|
41
|
+
transport
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const log = (
|
|
45
|
+
level: LogLevel,
|
|
46
|
+
request: RequestInfo,
|
|
47
|
+
data: Record<string, unknown>,
|
|
48
|
+
store: StoreData
|
|
49
|
+
): void => {
|
|
50
|
+
logToTransports({ level, request, data, store, options })
|
|
51
|
+
|
|
52
|
+
const useTransportsOnly = config?.useTransportsOnly === true
|
|
53
|
+
const disableInternalLogger = config?.disableInternalLogger === true
|
|
54
|
+
const disableFileLogging = config?.disableFileLogging === true
|
|
55
|
+
|
|
56
|
+
if (!(useTransportsOnly || disableFileLogging)) {
|
|
57
|
+
const filePath = config?.logFilePath
|
|
58
|
+
if (filePath) {
|
|
59
|
+
logToFile({ filePath, level, request, data, store, options }).catch(
|
|
60
|
+
() => {
|
|
61
|
+
// Ignore errors
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (useTransportsOnly || disableInternalLogger) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const message = formatLine({ level, request, data, store, options })
|
|
72
|
+
|
|
73
|
+
switch (level) {
|
|
74
|
+
case 'DEBUG': {
|
|
75
|
+
console.debug(message)
|
|
76
|
+
break
|
|
77
|
+
}
|
|
78
|
+
case 'INFO': {
|
|
79
|
+
console.info(message)
|
|
80
|
+
break
|
|
81
|
+
}
|
|
82
|
+
case 'WARNING': {
|
|
83
|
+
console.warn(message)
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
case 'ERROR': {
|
|
87
|
+
console.error(message)
|
|
88
|
+
break
|
|
89
|
+
}
|
|
90
|
+
default: {
|
|
91
|
+
console.log(message)
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const logWithContext = (
|
|
98
|
+
level: LogLevel,
|
|
99
|
+
request: RequestInfo,
|
|
100
|
+
message: string,
|
|
101
|
+
context?: Record<string, unknown>
|
|
102
|
+
): void => {
|
|
103
|
+
const store: StoreData = { beforeTime: process.hrtime.bigint() }
|
|
104
|
+
log(level, request, { message, context }, store)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
pino: pinoLogger,
|
|
109
|
+
log,
|
|
110
|
+
handleHttpError: (request, error, store) => {
|
|
111
|
+
handleHttpError(request, error, store, options)
|
|
112
|
+
},
|
|
113
|
+
debug: (request, message, context) => {
|
|
114
|
+
logWithContext('DEBUG', request, message, context)
|
|
115
|
+
},
|
|
116
|
+
info: (request, message, context) => {
|
|
117
|
+
logWithContext('INFO', request, message, context)
|
|
118
|
+
},
|
|
119
|
+
warn: (request, message, context) => {
|
|
120
|
+
logWithContext('WARNING', request, message, context)
|
|
121
|
+
},
|
|
122
|
+
error: (request, message, context) => {
|
|
123
|
+
logWithContext('ERROR', request, message, context)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|