@mhmdhammoud/meritt-utils 1.6.2 → 1.6.3
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/ReleaseNotes.md +17 -1
- package/dist/__tests__/elastic-transport.test.js +13 -0
- package/dist/__tests__/logger.test.js +21 -0
- package/dist/lib/elastic-transport.js +3 -1
- package/dist/lib/logger.js +4 -3
- package/dist/types/logger.d.ts +2 -1
- package/package.json +9 -3
- package/.github/workflows/npm-publish.yml +0 -84
- package/.github/workflows/push.yml +0 -83
- package/.husky/pre-commit +0 -62
- package/.prettierrc +0 -8
- package/.prettierrc.json +0 -8
- package/ISSUES.md +0 -0
- package/eslint.config.mjs +0 -64
- package/example.env +0 -6
- package/jest.config.js +0 -13
- package/src/__tests__/colorful.test.ts +0 -77
- package/src/__tests__/elastic-transport.test.ts +0 -94
- package/src/__tests__/formatter.test.ts +0 -96
- package/src/__tests__/logger.test.ts +0 -122
- package/src/env.d.ts +0 -9
- package/src/index.ts +0 -1
- package/src/lib/colorful.ts +0 -167
- package/src/lib/cypto.ts +0 -296
- package/src/lib/elastic-transport.ts +0 -356
- package/src/lib/formatter.ts +0 -125
- package/src/lib/imagefull.ts +0 -41
- package/src/lib/index.ts +0 -12
- package/src/lib/logger.ts +0 -600
- package/src/lib/pdf.ts +0 -25
- package/src/lib/trace-store.ts +0 -46
- package/src/types/index.ts +0 -1
- package/src/types/logger.ts +0 -66
- package/src/utilities/axios.ts +0 -4
- package/src/utilities/index.ts +0 -1
- package/tsconfig.json +0 -16
package/src/lib/logger.ts
DELETED
|
@@ -1,600 +0,0 @@
|
|
|
1
|
-
import { Logger as PinoLogger, pino, stdTimeFunctions } from 'pino'
|
|
2
|
-
import * as dotenv from 'dotenv'
|
|
3
|
-
import { hostname } from 'os'
|
|
4
|
-
import {
|
|
5
|
-
createElasticTransport,
|
|
6
|
-
ElasticTransportStream,
|
|
7
|
-
} from './elastic-transport'
|
|
8
|
-
import { getTraceContext } from './trace-store'
|
|
9
|
-
import { LOG_LEVEL, LogEvent, ElasticConfig } from '../types'
|
|
10
|
-
|
|
11
|
-
dotenv.config()
|
|
12
|
-
|
|
13
|
-
/** Convert camelCase to snake_case for Kibana/ECS-friendly field names */
|
|
14
|
-
const toSnakeCase = (str: string): string =>
|
|
15
|
-
str.replace(/[A-Z]/g, (c) => `_${c.toLowerCase()}`)
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Elasticsearch reserves metadata fields (e.g. `_id`) and rejects documents
|
|
19
|
-
* containing them in payload body. Remap them to safe application fields.
|
|
20
|
-
*/
|
|
21
|
-
const RESERVED_ES_FIELD_ALIASES: Record<string, string> = {
|
|
22
|
-
_id: 'mongo_id',
|
|
23
|
-
_index: 'es_index',
|
|
24
|
-
_type: 'es_type',
|
|
25
|
-
_score: 'es_score',
|
|
26
|
-
_source: 'es_source',
|
|
27
|
-
_routing: 'es_routing',
|
|
28
|
-
_seq_no: 'es_seq_no',
|
|
29
|
-
_primary_term: 'es_primary_term',
|
|
30
|
-
_version: 'es_version',
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Convert arbitrary field names into ES-safe flattened keys */
|
|
34
|
-
const toSafeElasticFieldName = (key: string): string => {
|
|
35
|
-
const normalized = toSnakeCase(key)
|
|
36
|
-
const mapped = RESERVED_ES_FIELD_ALIASES[normalized]
|
|
37
|
-
if (mapped) {
|
|
38
|
-
return mapped
|
|
39
|
-
}
|
|
40
|
-
// Any leading underscore can conflict with ES internals, remap defensively.
|
|
41
|
-
return normalized.startsWith('_') ? `meta${normalized}` : normalized
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const isObjectIdLike = (v: unknown): v is { toHexString: () => string } =>
|
|
45
|
-
v !== null &&
|
|
46
|
-
typeof v === 'object' &&
|
|
47
|
-
'toHexString' in v &&
|
|
48
|
-
typeof (v as { toHexString?: unknown }).toHexString === 'function'
|
|
49
|
-
|
|
50
|
-
/** Check if value is a plain object (not Error, Date, Array, null) */
|
|
51
|
-
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
52
|
-
v !== null &&
|
|
53
|
-
typeof v === 'object' &&
|
|
54
|
-
!Array.isArray(v) &&
|
|
55
|
-
!(v instanceof Error) &&
|
|
56
|
-
!(v instanceof Date)
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Recursively sanitize log values for Elasticsearch safety.
|
|
60
|
-
* - Remaps reserved key names (e.g. `_id` -> `mongo_id`)
|
|
61
|
-
* - Converts Error to a stable serializable shape
|
|
62
|
-
* - Converts ObjectId-like objects to hex strings
|
|
63
|
-
* - Prevents circular structure failures
|
|
64
|
-
*/
|
|
65
|
-
const sanitizeForElastic = (
|
|
66
|
-
value: unknown,
|
|
67
|
-
seen: WeakSet<object> = new WeakSet<object>()
|
|
68
|
-
): unknown => {
|
|
69
|
-
if (
|
|
70
|
-
value === null ||
|
|
71
|
-
typeof value === 'string' ||
|
|
72
|
-
typeof value === 'number' ||
|
|
73
|
-
typeof value === 'boolean'
|
|
74
|
-
) {
|
|
75
|
-
return value
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (value instanceof Date) {
|
|
79
|
-
return value.toISOString()
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
if (value instanceof Error) {
|
|
83
|
-
return { message: value.message, type: value.constructor.name }
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (isObjectIdLike(value)) {
|
|
87
|
-
return value.toHexString()
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (Array.isArray(value)) {
|
|
91
|
-
return value.map((item) => sanitizeForElastic(item, seen))
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (isPlainObject(value)) {
|
|
95
|
-
if (seen.has(value)) {
|
|
96
|
-
return '[Circular]'
|
|
97
|
-
}
|
|
98
|
-
seen.add(value)
|
|
99
|
-
|
|
100
|
-
const sanitizedObject: Record<string, unknown> = {}
|
|
101
|
-
for (const [k, v] of Object.entries(value)) {
|
|
102
|
-
sanitizedObject[toSafeElasticFieldName(k)] = sanitizeForElastic(v, seen)
|
|
103
|
-
}
|
|
104
|
-
return sanitizedObject
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Fallback for class instances and non-plain objects.
|
|
108
|
-
return String(value)
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Captures the call site from the stack when log event is missing.
|
|
113
|
-
* Returns file:line (e.g. product.service.ts:2266) for Kibana/Elasticsearch filtering.
|
|
114
|
-
*/
|
|
115
|
-
const getCallSiteForMissingLog = (): string | undefined => {
|
|
116
|
-
try {
|
|
117
|
-
const stack = new Error().stack ?? ''
|
|
118
|
-
const lines = stack.split('\n')
|
|
119
|
-
// First frame outside Logger / node_modules / meritt-utils
|
|
120
|
-
const appFrame = lines.find(
|
|
121
|
-
(line) =>
|
|
122
|
-
!line.includes('node_modules') &&
|
|
123
|
-
!line.includes('meritt-utils') &&
|
|
124
|
-
!line.includes('Logger.')
|
|
125
|
-
)
|
|
126
|
-
if (!appFrame) return undefined
|
|
127
|
-
// Extract file:line e.g. "product.service.ts:2266"
|
|
128
|
-
const match = appFrame.match(/([^/\\]+\.(?:ts|js|tsx|jsx)):(\d+)/)
|
|
129
|
-
if (match) {
|
|
130
|
-
return `${match[1]}:${match[2]}`
|
|
131
|
-
}
|
|
132
|
-
return appFrame.trim().slice(0, 100)
|
|
133
|
-
} catch {
|
|
134
|
-
return undefined
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Pino logger backend - singleton
|
|
140
|
-
*/
|
|
141
|
-
let pinoLogger: PinoLogger
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Elasticsearch transport instance - kept for cleanup
|
|
145
|
-
*/
|
|
146
|
-
let esTransport: ElasticTransportStream | null = null
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Flag to track if shutdown handlers are registered
|
|
150
|
-
*/
|
|
151
|
-
let shutdownHandlersRegistered = false
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Validates required Elasticsearch environment variables.
|
|
155
|
-
* @throws Error if required environment variables are missing.
|
|
156
|
-
*/
|
|
157
|
-
function validateElasticsearchEnv(): void {
|
|
158
|
-
const required = [
|
|
159
|
-
'ELASTICSEARCH_NODE',
|
|
160
|
-
'ELASTICSEARCH_USERNAME',
|
|
161
|
-
'ELASTICSEARCH_PASSWORD',
|
|
162
|
-
'SERVER_NICKNAME',
|
|
163
|
-
]
|
|
164
|
-
const missing = required.filter((key) => !process.env[key])
|
|
165
|
-
|
|
166
|
-
if (missing.length > 0) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
`Missing required Elasticsearch environment variables: ${missing.join(', ')}`
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Safely parses an integer from an environment variable.
|
|
175
|
-
* @param envValue - The environment variable value to parse.
|
|
176
|
-
* @param defaultValue - The default value to use if parsing fails.
|
|
177
|
-
* @param varName - The name of the environment variable (for error messages).
|
|
178
|
-
* @returns The parsed integer or the default value.
|
|
179
|
-
* @throws Error if the value is not a valid number.
|
|
180
|
-
*/
|
|
181
|
-
function parseIntEnv(
|
|
182
|
-
envValue: string | undefined,
|
|
183
|
-
defaultValue: number,
|
|
184
|
-
varName: string
|
|
185
|
-
): number {
|
|
186
|
-
if (!envValue) {
|
|
187
|
-
return defaultValue
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
const parsed = parseInt(envValue, 10)
|
|
191
|
-
|
|
192
|
-
if (isNaN(parsed)) {
|
|
193
|
-
throw new Error(
|
|
194
|
-
`Invalid value for ${varName}: "${envValue}" is not a valid number. Using default: ${defaultValue}`
|
|
195
|
-
)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
if (parsed < 0) {
|
|
199
|
-
throw new Error(
|
|
200
|
-
`Invalid value for ${varName}: "${envValue}" must be a positive number. Using default: ${defaultValue}`
|
|
201
|
-
)
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return parsed
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Registers shutdown handlers to flush logs before process exits.
|
|
209
|
-
*/
|
|
210
|
-
function registerShutdownHandlers(): void {
|
|
211
|
-
if (shutdownHandlersRegistered) {
|
|
212
|
-
return
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
let isShuttingDown = false
|
|
216
|
-
|
|
217
|
-
const flushAndExit = async (signal: string, exitCode = 0) => {
|
|
218
|
-
// Prevent multiple shutdown attempts
|
|
219
|
-
if (isShuttingDown) {
|
|
220
|
-
return
|
|
221
|
-
}
|
|
222
|
-
isShuttingDown = true
|
|
223
|
-
|
|
224
|
-
if (esTransport) {
|
|
225
|
-
try {
|
|
226
|
-
// Flush any pending logs
|
|
227
|
-
await new Promise<void>((resolve) => {
|
|
228
|
-
const timeout = setTimeout(() => {
|
|
229
|
-
console.error(`[Logger] Flush timeout after 5s on ${signal}`)
|
|
230
|
-
resolve()
|
|
231
|
-
}, 5000) // 5 second timeout
|
|
232
|
-
|
|
233
|
-
// Register listener BEFORE calling end() to avoid race condition
|
|
234
|
-
esTransport?.once('finish', () => {
|
|
235
|
-
clearTimeout(timeout)
|
|
236
|
-
resolve()
|
|
237
|
-
})
|
|
238
|
-
|
|
239
|
-
esTransport
|
|
240
|
-
?.flush()
|
|
241
|
-
.catch((error) => {
|
|
242
|
-
console.error(`[Logger] Error flushing logs on ${signal}:`, error)
|
|
243
|
-
})
|
|
244
|
-
.finally(() => {
|
|
245
|
-
// Now trigger stream shutdown
|
|
246
|
-
esTransport?.end()
|
|
247
|
-
})
|
|
248
|
-
})
|
|
249
|
-
} catch (error) {
|
|
250
|
-
console.error(`[Logger] Error flushing logs on ${signal}:`, error)
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
process.exit(exitCode)
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
process.on('SIGTERM', () => {
|
|
257
|
-
flushAndExit('SIGTERM', 0).catch((err) => {
|
|
258
|
-
console.error('[Logger] Flush and exit failed:', err)
|
|
259
|
-
process.exit(1)
|
|
260
|
-
})
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
process.on('SIGINT', () => {
|
|
264
|
-
flushAndExit('SIGINT', 130).catch((err) => {
|
|
265
|
-
console.error('[Logger] Flush and exit failed:', err)
|
|
266
|
-
process.exit(1)
|
|
267
|
-
})
|
|
268
|
-
})
|
|
269
|
-
|
|
270
|
-
process.on('beforeExit', () => {
|
|
271
|
-
if (esTransport && !isShuttingDown) {
|
|
272
|
-
esTransport.end()
|
|
273
|
-
}
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
shutdownHandlersRegistered = true
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Creates a Pino logger instance with specified Elasticsearch configuration.
|
|
281
|
-
* @param elasticConfig - Optional Elasticsearch configuration.
|
|
282
|
-
* @returns The Pino logger instance.
|
|
283
|
-
* @throws Error if LOG_LEVEL is invalid or required environment variables are missing.
|
|
284
|
-
*/
|
|
285
|
-
function getLogger(elasticConfig?: ElasticConfig): PinoLogger {
|
|
286
|
-
if (!pinoLogger) {
|
|
287
|
-
// Validate log level - will throw if invalid
|
|
288
|
-
const logLevel = process.env.LOG_LEVEL || 'info'
|
|
289
|
-
isValidLogLevel(logLevel)
|
|
290
|
-
|
|
291
|
-
const transports = []
|
|
292
|
-
if (process.env.NODE_ENV !== 'local' && process.env.NODE_ENV !== 'test') {
|
|
293
|
-
// Validate Elasticsearch environment variables
|
|
294
|
-
validateElasticsearchEnv()
|
|
295
|
-
|
|
296
|
-
// Parse flush settings from environment with validation
|
|
297
|
-
const flushIntervalMs = parseIntEnv(
|
|
298
|
-
process.env.ES_FLUSH_INTERVAL_MS,
|
|
299
|
-
2000, // Default: 2 seconds
|
|
300
|
-
'ES_FLUSH_INTERVAL_MS'
|
|
301
|
-
)
|
|
302
|
-
|
|
303
|
-
const flushBytes = parseIntEnv(
|
|
304
|
-
process.env.ES_FLUSH_BYTES,
|
|
305
|
-
100 * 1024, // Default: 100KB
|
|
306
|
-
'ES_FLUSH_BYTES'
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
const maxRetries = parseIntEnv(
|
|
310
|
-
process.env.ES_MAX_RETRIES,
|
|
311
|
-
3, // Default: 3 retries
|
|
312
|
-
'ES_MAX_RETRIES'
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
const requestTimeout = parseIntEnv(
|
|
316
|
-
process.env.ES_REQUEST_TIMEOUT_MS,
|
|
317
|
-
30000, // Default: 30 seconds
|
|
318
|
-
'ES_REQUEST_TIMEOUT_MS'
|
|
319
|
-
)
|
|
320
|
-
|
|
321
|
-
// Safe to access after validation
|
|
322
|
-
const esConfig: ElasticConfig = {
|
|
323
|
-
index: process.env.SERVER_NICKNAME,
|
|
324
|
-
node: process.env.ELASTICSEARCH_NODE,
|
|
325
|
-
auth: {
|
|
326
|
-
username: process.env.ELASTICSEARCH_USERNAME,
|
|
327
|
-
password: process.env.ELASTICSEARCH_PASSWORD,
|
|
328
|
-
},
|
|
329
|
-
// Configurable flush settings
|
|
330
|
-
flushInterval: flushIntervalMs,
|
|
331
|
-
'flush-bytes': flushBytes,
|
|
332
|
-
// Retry configuration for connection resilience
|
|
333
|
-
maxRetries: maxRetries,
|
|
334
|
-
requestTimeout: requestTimeout,
|
|
335
|
-
// Automatically reconnect on connection faults
|
|
336
|
-
sniffOnConnectionFault: true,
|
|
337
|
-
}
|
|
338
|
-
if (elasticConfig) {
|
|
339
|
-
Object.assign(esConfig, elasticConfig)
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
// Create transport with connection lifecycle fix (pino-elasticsearch #140)
|
|
343
|
-
esTransport = createElasticTransport(esConfig)
|
|
344
|
-
|
|
345
|
-
// Handle Elasticsearch connection errors
|
|
346
|
-
esTransport.on('bulkError', (err: Error) => {
|
|
347
|
-
console.error('[Logger] Elasticsearch transport error:', err.message)
|
|
348
|
-
console.error(
|
|
349
|
-
'[Logger] Logs may not be reaching Kibana. Check Elasticsearch connection.'
|
|
350
|
-
)
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
// Handle insert errors (document indexing failures)
|
|
354
|
-
esTransport.on('insertError', (err: Error & { document?: unknown }) => {
|
|
355
|
-
console.error('[Logger] Elasticsearch insert error:', err.message)
|
|
356
|
-
console.error('[Logger] Some logs failed to index to Elasticsearch.')
|
|
357
|
-
if (err.document) {
|
|
358
|
-
const docStr = JSON.stringify(err.document)
|
|
359
|
-
const preview =
|
|
360
|
-
docStr.length > 500
|
|
361
|
-
? `${docStr.substring(0, 500)}... (truncated)`
|
|
362
|
-
: docStr
|
|
363
|
-
console.error('[Logger] Dropped document preview:', preview)
|
|
364
|
-
}
|
|
365
|
-
})
|
|
366
|
-
|
|
367
|
-
// Log successful connection (for debugging)
|
|
368
|
-
esTransport.on('insert', () => {
|
|
369
|
-
// Uncomment for debugging: console.log(`[Logger] Successfully inserted log entries`)
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
// Register shutdown handlers to flush logs on process exit
|
|
373
|
-
registerShutdownHandlers()
|
|
374
|
-
|
|
375
|
-
transports.push(esTransport)
|
|
376
|
-
} else {
|
|
377
|
-
transports.push(
|
|
378
|
-
pino.destination({
|
|
379
|
-
minLength: 1024,
|
|
380
|
-
sync: true,
|
|
381
|
-
})
|
|
382
|
-
)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
pinoLogger = pino(
|
|
386
|
-
{
|
|
387
|
-
level: logLevel,
|
|
388
|
-
timestamp: stdTimeFunctions.isoTime.bind(stdTimeFunctions),
|
|
389
|
-
},
|
|
390
|
-
...transports
|
|
391
|
-
)
|
|
392
|
-
}
|
|
393
|
-
return pinoLogger
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
/**
|
|
397
|
-
* Checks if a given log level is valid.
|
|
398
|
-
* @param level - The log level to check.
|
|
399
|
-
* @returns Whether the log level is valid.
|
|
400
|
-
*/
|
|
401
|
-
export function isValidLogLevel(level: string): level is LOG_LEVEL {
|
|
402
|
-
if (!['error', 'warn', 'info', 'debug', 'trace'].includes(level)) {
|
|
403
|
-
throw new Error(
|
|
404
|
-
`Invalid log level "${level}": only error, warn, info, debug, trace are valid.`
|
|
405
|
-
)
|
|
406
|
-
}
|
|
407
|
-
return true
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
/**
|
|
411
|
-
* Logger Wrapper.
|
|
412
|
-
* Wraps a Pino logger instance and provides logging methods.
|
|
413
|
-
*/
|
|
414
|
-
class Logger {
|
|
415
|
-
private readonly _name: string
|
|
416
|
-
private readonly _logger: PinoLogger
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Creates a new Logger instance with default configuration.
|
|
420
|
-
* @param name - The name of the logger.
|
|
421
|
-
*/
|
|
422
|
-
constructor(name: string)
|
|
423
|
-
|
|
424
|
-
/**
|
|
425
|
-
* Creates a new Logger instance with custom Elasticsearch configuration.
|
|
426
|
-
* @param name - The name of the logger.
|
|
427
|
-
* @param elasticConfig - Optional Elasticsearch configuration.
|
|
428
|
-
*/
|
|
429
|
-
constructor(name: string, elasticConfig?: ElasticConfig)
|
|
430
|
-
|
|
431
|
-
constructor(name: string, elasticConfig?: ElasticConfig) {
|
|
432
|
-
this._name = name
|
|
433
|
-
this._logger = getLogger(elasticConfig)
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
/**
|
|
437
|
-
* Build ECS-aligned and structured log payload.
|
|
438
|
-
* - ECS: log.level, log.logger, event.code, service.name, service.environment, message
|
|
439
|
-
* - Structured: Single plain object flattened as top-level snake_case fields (Kibana filterable)
|
|
440
|
-
* - Trace: trace.id when running inside runWithTrace
|
|
441
|
-
*/
|
|
442
|
-
private buildPayload(
|
|
443
|
-
logLevel: LOG_LEVEL,
|
|
444
|
-
logEvent: LogEvent,
|
|
445
|
-
args: unknown[]
|
|
446
|
-
) {
|
|
447
|
-
const isLocal =
|
|
448
|
-
process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'test'
|
|
449
|
-
|
|
450
|
-
// Defensive: missing Logs constant (undefined) crashes on logEvent.code
|
|
451
|
-
const useFallback =
|
|
452
|
-
!logEvent || typeof logEvent !== 'object' || !('code' in logEvent)
|
|
453
|
-
const event = useFallback
|
|
454
|
-
? { code: 'UNKNOWN', msg: 'Missing or invalid log event constant' }
|
|
455
|
-
: logEvent
|
|
456
|
-
|
|
457
|
-
// ECS-aligned fields for Kibana (flat names to avoid mapping conflicts with existing indices)
|
|
458
|
-
const ecs: Record<string, unknown> = {
|
|
459
|
-
log_level: logLevel,
|
|
460
|
-
log_logger: this._name,
|
|
461
|
-
event_code: event.code,
|
|
462
|
-
message: event.msg,
|
|
463
|
-
service_name: process.env.SERVER_NICKNAME ?? 'unknown',
|
|
464
|
-
service_environment: process.env.NODE_ENV ?? 'development',
|
|
465
|
-
host_name: hostname(),
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Trace context for request-scoped correlation
|
|
469
|
-
const trace = getTraceContext()
|
|
470
|
-
if (trace) {
|
|
471
|
-
ecs.trace_id = trace.traceId
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Structured context: put in single 'context' field to avoid ES mapping conflicts.
|
|
475
|
-
// Flattening to top-level caused document_parsing_exception (object vs scalar type mismatches).
|
|
476
|
-
// Nesting in context keeps structure consistent and avoids per-field mapping conflicts.
|
|
477
|
-
let context: Record<string, unknown> | undefined
|
|
478
|
-
let detail: unknown
|
|
479
|
-
if (
|
|
480
|
-
args.length === 1 &&
|
|
481
|
-
isPlainObject(args[0]) &&
|
|
482
|
-
Object.keys(args[0]).length > 0
|
|
483
|
-
) {
|
|
484
|
-
context = sanitizeForElastic(args[0]) as Record<string, unknown>
|
|
485
|
-
} else {
|
|
486
|
-
detail = isLocal ? args : JSON.stringify(sanitizeForElastic(args))
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
// Legacy fields for backward compatibility (component, code, msg)
|
|
490
|
-
const base: Record<string, unknown> = {
|
|
491
|
-
...ecs,
|
|
492
|
-
component: this._name,
|
|
493
|
-
code: event.code,
|
|
494
|
-
msg: event.msg,
|
|
495
|
-
}
|
|
496
|
-
if (context !== undefined) {
|
|
497
|
-
base.context = context
|
|
498
|
-
}
|
|
499
|
-
if (detail !== undefined) {
|
|
500
|
-
base.detail = detail
|
|
501
|
-
}
|
|
502
|
-
// When fallback used: add call site for Kibana/Elasticsearch querying
|
|
503
|
-
if (useFallback) {
|
|
504
|
-
const callSite = getCallSiteForMissingLog()
|
|
505
|
-
if (callSite) {
|
|
506
|
-
base.missing_log_call_site = callSite
|
|
507
|
-
}
|
|
508
|
-
}
|
|
509
|
-
return base
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
private log(logLevel: LOG_LEVEL, logEvent: LogEvent, ...args: unknown[]) {
|
|
513
|
-
const payload = this.buildPayload(logLevel, logEvent, args)
|
|
514
|
-
this._logger[logLevel](payload)
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
/**
|
|
518
|
-
* Logs an error message.
|
|
519
|
-
* @param logEvent - The event to log.
|
|
520
|
-
* @param args - Additional arguments to include in the log.
|
|
521
|
-
*/
|
|
522
|
-
error(logEvent: LogEvent, ...args: unknown[]) {
|
|
523
|
-
this.log('error', logEvent, ...args)
|
|
524
|
-
}
|
|
525
|
-
|
|
526
|
-
/**
|
|
527
|
-
* Logs a warning message.
|
|
528
|
-
* @param logEvent - The event to log.
|
|
529
|
-
* @param args - Additional arguments to include in the log.
|
|
530
|
-
*/
|
|
531
|
-
warn(logEvent: LogEvent, ...args: unknown[]) {
|
|
532
|
-
this.log('warn', logEvent, ...args)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Logs an informational message.
|
|
537
|
-
* @param logEvent - The event to log.
|
|
538
|
-
* @param args - Additional arguments to include in the log.
|
|
539
|
-
*/
|
|
540
|
-
info(logEvent: LogEvent, ...args: unknown[]) {
|
|
541
|
-
this.log('info', logEvent, ...args)
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
/**
|
|
545
|
-
* Logs a debug message.
|
|
546
|
-
* @param logEvent - The event to log.
|
|
547
|
-
* @param args - Additional arguments to include in the log.
|
|
548
|
-
*/
|
|
549
|
-
debug(logEvent: LogEvent, ...args: unknown[]) {
|
|
550
|
-
this.log('debug', logEvent, ...args)
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Logs a trace message.
|
|
555
|
-
* @param logEvent - The event to log.
|
|
556
|
-
* @param args - Additional arguments to include in the log.
|
|
557
|
-
*/
|
|
558
|
-
trace(logEvent: LogEvent, ...args: unknown[]) {
|
|
559
|
-
this.log('trace', logEvent, ...args)
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/**
|
|
563
|
-
* Runs an async operation and logs its duration.
|
|
564
|
-
* Adds event.duration (ms) for Kibana performance dashboards and alerts.
|
|
565
|
-
*
|
|
566
|
-
* @param logEvent - The event to log on completion
|
|
567
|
-
* @param fn - Async function to execute
|
|
568
|
-
* @param context - Optional context object (flattened as top-level fields)
|
|
569
|
-
* @returns Result of fn
|
|
570
|
-
*/
|
|
571
|
-
async withDuration<T>(
|
|
572
|
-
logEvent: LogEvent,
|
|
573
|
-
fn: () => Promise<T>,
|
|
574
|
-
context?: Record<string, unknown>
|
|
575
|
-
): Promise<T> {
|
|
576
|
-
const start = Date.now()
|
|
577
|
-
try {
|
|
578
|
-
const result = await fn()
|
|
579
|
-
const durationMs = Date.now() - start
|
|
580
|
-
const payload = this.buildPayload('info', logEvent, [
|
|
581
|
-
{ ...context, duration_ms: durationMs, success: true },
|
|
582
|
-
])
|
|
583
|
-
this._logger.info(payload)
|
|
584
|
-
return result
|
|
585
|
-
} catch (error) {
|
|
586
|
-
const durationMs = Date.now() - start
|
|
587
|
-
const errObj =
|
|
588
|
-
error instanceof Error
|
|
589
|
-
? { error_message: error.message, error_type: error.constructor.name }
|
|
590
|
-
: {}
|
|
591
|
-
const payload = this.buildPayload('error', logEvent, [
|
|
592
|
-
{ ...context, ...errObj, duration_ms: durationMs, success: false },
|
|
593
|
-
])
|
|
594
|
-
this._logger.error(payload)
|
|
595
|
-
throw error
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export default Logger
|
package/src/lib/pdf.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
// import { AxiosInstance } from '../utilities'
|
|
2
|
-
/*
|
|
3
|
-
Author : Mustafa Halabi https://github.com/mustafahalabi
|
|
4
|
-
Date : 2023-06-24
|
|
5
|
-
Description : Image to PDF converter
|
|
6
|
-
Version : 1.0
|
|
7
|
-
*/
|
|
8
|
-
class Pdf {
|
|
9
|
-
/**
|
|
10
|
-
* @remarks Convert image links into base64 format
|
|
11
|
-
* @param url - The image url
|
|
12
|
-
* @returns string
|
|
13
|
-
* @example
|
|
14
|
-
* ```typescript
|
|
15
|
-
* pdf.getBase64Images('https://fastly.picsum.photos/id/15/200/300.jpg?hmac=lozQletmrLG9PGBV1hTM1PnmvHxKEU0lAZWu8F2oL30')
|
|
16
|
-
* // => 'Hello-World'
|
|
17
|
-
* ```
|
|
18
|
-
* */
|
|
19
|
-
getBase64Images = (_url: string): string => {
|
|
20
|
-
return ''
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const pdf = new Pdf()
|
|
25
|
-
export default pdf
|
package/src/lib/trace-store.ts
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'async_hooks'
|
|
2
|
-
import { randomUUID } from 'crypto'
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Trace context for request-scoped correlation in Kibana.
|
|
6
|
-
* Enables filtering all logs from a single request/job by trace.id.
|
|
7
|
-
*/
|
|
8
|
-
export interface TraceContext {
|
|
9
|
-
/** Unique ID for the entire request/job flow */
|
|
10
|
-
traceId: string
|
|
11
|
-
/** Optional span ID for sub-operations */
|
|
12
|
-
spanId?: string
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const traceStorage = new AsyncLocalStorage<TraceContext>()
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Run an async function with a new trace context.
|
|
19
|
-
* All Logger calls within the callback will automatically include trace.id.
|
|
20
|
-
*
|
|
21
|
-
* @param fn - Async function to run within the trace context
|
|
22
|
-
* @param traceId - Optional trace ID (defaults to random UUID)
|
|
23
|
-
* @returns Result of fn
|
|
24
|
-
*/
|
|
25
|
-
export const runWithTrace = async <T>(
|
|
26
|
-
fn: () => Promise<T>,
|
|
27
|
-
traceId?: string
|
|
28
|
-
): Promise<T> => {
|
|
29
|
-
const id = traceId ?? randomUUID()
|
|
30
|
-
return traceStorage.run({ traceId: id }, () => fn())
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Run a sync function with a new trace context.
|
|
35
|
-
*/
|
|
36
|
-
export const runWithTraceSync = <T>(fn: () => T, traceId?: string): T => {
|
|
37
|
-
const id = traceId ?? randomUUID()
|
|
38
|
-
return traceStorage.run({ traceId: id }, () => fn())
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Get the current trace context (if running inside runWithTrace).
|
|
43
|
-
*/
|
|
44
|
-
export const getTraceContext = (): TraceContext | undefined => {
|
|
45
|
-
return traceStorage.getStore()
|
|
46
|
-
}
|
package/src/types/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from './logger'
|