@mhmdhammoud/meritt-utils 1.5.0 → 1.5.2
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 +55 -0
- package/dist/lib/logger.d.ts +1 -2
- package/dist/lib/logger.js +163 -30
- package/dist/types/logger.d.ts +17 -0
- package/package.json +1 -1
- package/src/lib/logger.ts +216 -37
- package/src/types/logger.ts +17 -0
package/ReleaseNotes.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Changes
|
|
2
2
|
|
|
3
|
+
## Version 1.5.3
|
|
4
|
+
|
|
5
|
+
### Logger Improvements - Critical Bug Fixes & Production Reliability
|
|
6
|
+
|
|
7
|
+
#### Critical Fixes
|
|
8
|
+
|
|
9
|
+
- **Fixed broken JSON.stringify**: Corrected production log serialization from `JSON.stringify(...args)` to `JSON.stringify(args)` - previously only stringified first argument
|
|
10
|
+
- **Fixed silent failures**: Logger now throws clear errors when `LOG_LEVEL` is invalid instead of silently failing with undefined logger
|
|
11
|
+
- **Fixed return type mismatch**: `getLogger()` now properly returns `PinoLogger` type with explicit error handling
|
|
12
|
+
- **Fixed buffer configuration**: Corrected inconsistent buffer settings to match tests (`minLength: 1024, sync: true`)
|
|
13
|
+
- **Fixed race condition**: Event listener now registered before calling `.end()` to prevent missed flush events on shutdown
|
|
14
|
+
|
|
15
|
+
#### Environment Variable Validation
|
|
16
|
+
|
|
17
|
+
- **Added Elasticsearch validation**: Validates required env vars (`ELASTICSEARCH_NODE`, `ELASTICSEARCH_USERNAME`, `ELASTICSEARCH_PASSWORD`, `SERVER_NICKNAME`) with clear error messages
|
|
18
|
+
- **Added integer parsing validation**: All numeric env vars now validated to prevent `NaN` values from breaking configuration
|
|
19
|
+
- **Validates positive numbers**: Ensures flush intervals, buffer sizes, retries, and timeouts are positive integers
|
|
20
|
+
|
|
21
|
+
#### Configurable Settings (New Environment Variables)
|
|
22
|
+
|
|
23
|
+
- `ES_FLUSH_INTERVAL_MS` - How often to send logs (default: 2000ms = 2 seconds)
|
|
24
|
+
- `ES_FLUSH_BYTES` - Buffer size before forcing flush (default: 102400 = 100KB)
|
|
25
|
+
- `ES_MAX_RETRIES` - Number of retry attempts on failure (default: 3)
|
|
26
|
+
- `ES_REQUEST_TIMEOUT_MS` - Request timeout before retry (default: 30000ms = 30 seconds)
|
|
27
|
+
|
|
28
|
+
#### Reliability Improvements
|
|
29
|
+
|
|
30
|
+
- **Error monitoring**: Elasticsearch transport errors now logged to console instead of silent failure
|
|
31
|
+
- `error` event: Connection errors with clear messaging
|
|
32
|
+
- `insertError` event: Document indexing failures
|
|
33
|
+
- **Graceful shutdown**: Properly flushes buffered logs before process exit
|
|
34
|
+
- Handles `SIGTERM` and `SIGINT` signals
|
|
35
|
+
- 5-second timeout prevents hanging forever
|
|
36
|
+
- Proper async/await handling to ensure flush completes
|
|
37
|
+
- **Auto-reconnection**: `sniffOnConnectionFault: true` enables automatic reconnection when Elasticsearch nodes fail
|
|
38
|
+
- **Retry logic**: Configurable retry attempts with timeout for failed requests
|
|
39
|
+
|
|
40
|
+
#### Type Safety
|
|
41
|
+
|
|
42
|
+
- **Removed `any` types**: All configurations now use proper TypeScript interfaces
|
|
43
|
+
- **Extended ElasticConfig interface**: Added proper types for `maxRetries`, `requestTimeout`, `sniffOnConnectionFault`, and buffer settings
|
|
44
|
+
- **Full compile-time validation**: Type system now validates all configuration options
|
|
45
|
+
|
|
46
|
+
#### Performance
|
|
47
|
+
|
|
48
|
+
- **Optimized flush settings**: Default 2-second interval with 100KB buffer balances real-time logs with reliability
|
|
49
|
+
- **Reduced network overhead**: Larger buffer prevents excessive network calls during high-traffic periods
|
|
50
|
+
|
|
51
|
+
#### Migration Notes
|
|
52
|
+
|
|
53
|
+
- All changes are backward compatible
|
|
54
|
+
- Default values ensure existing deployments work without changes
|
|
55
|
+
- Optional environment variables allow fine-tuning per environment
|
|
56
|
+
- No breaking changes to the Logger API
|
|
57
|
+
|
|
3
58
|
## Version 1.4.1
|
|
4
59
|
|
|
5
60
|
- Json stringify the log message if its being pushed to Kibana
|
package/dist/lib/logger.d.ts
CHANGED
package/dist/lib/logger.js
CHANGED
|
@@ -35,43 +35,177 @@ dotenv.config();
|
|
|
35
35
|
* Pino logger backend - singleton
|
|
36
36
|
*/
|
|
37
37
|
let pinoLogger;
|
|
38
|
+
/**
|
|
39
|
+
* Elasticsearch transport instance - kept for cleanup
|
|
40
|
+
*/
|
|
41
|
+
let esTransport = null;
|
|
42
|
+
/**
|
|
43
|
+
* Flag to track if shutdown handlers are registered
|
|
44
|
+
*/
|
|
45
|
+
let shutdownHandlersRegistered = false;
|
|
46
|
+
/**
|
|
47
|
+
* Validates required Elasticsearch environment variables.
|
|
48
|
+
* @throws Error if required environment variables are missing.
|
|
49
|
+
*/
|
|
50
|
+
function validateElasticsearchEnv() {
|
|
51
|
+
const required = ['ELASTICSEARCH_NODE', 'ELASTICSEARCH_USERNAME', 'ELASTICSEARCH_PASSWORD', 'SERVER_NICKNAME'];
|
|
52
|
+
const missing = required.filter(key => !process.env[key]);
|
|
53
|
+
if (missing.length > 0) {
|
|
54
|
+
throw new Error(`Missing required Elasticsearch environment variables: ${missing.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Safely parses an integer from an environment variable.
|
|
59
|
+
* @param envValue - The environment variable value to parse.
|
|
60
|
+
* @param defaultValue - The default value to use if parsing fails.
|
|
61
|
+
* @param varName - The name of the environment variable (for error messages).
|
|
62
|
+
* @returns The parsed integer or the default value.
|
|
63
|
+
* @throws Error if the value is not a valid number.
|
|
64
|
+
*/
|
|
65
|
+
function parseIntEnv(envValue, defaultValue, varName) {
|
|
66
|
+
if (!envValue) {
|
|
67
|
+
return defaultValue;
|
|
68
|
+
}
|
|
69
|
+
const parsed = parseInt(envValue, 10);
|
|
70
|
+
if (isNaN(parsed)) {
|
|
71
|
+
throw new Error(`Invalid value for ${varName}: "${envValue}" is not a valid number. Using default: ${defaultValue}`);
|
|
72
|
+
}
|
|
73
|
+
if (parsed < 0) {
|
|
74
|
+
throw new Error(`Invalid value for ${varName}: "${envValue}" must be a positive number. Using default: ${defaultValue}`);
|
|
75
|
+
}
|
|
76
|
+
return parsed;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Registers shutdown handlers to flush logs before process exits.
|
|
80
|
+
*/
|
|
81
|
+
function registerShutdownHandlers() {
|
|
82
|
+
if (shutdownHandlersRegistered) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
let isShuttingDown = false;
|
|
86
|
+
const flushAndExit = async (signal, exitCode = 0) => {
|
|
87
|
+
// Prevent multiple shutdown attempts
|
|
88
|
+
if (isShuttingDown) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
isShuttingDown = true;
|
|
92
|
+
if (esTransport) {
|
|
93
|
+
try {
|
|
94
|
+
// Flush any pending logs
|
|
95
|
+
await new Promise((resolve) => {
|
|
96
|
+
const timeout = setTimeout(() => {
|
|
97
|
+
console.error(`[Logger] Flush timeout after 5s on ${signal}`);
|
|
98
|
+
resolve();
|
|
99
|
+
}, 5000); // 5 second timeout
|
|
100
|
+
// Register listener BEFORE calling end() to avoid race condition
|
|
101
|
+
esTransport === null || esTransport === void 0 ? void 0 : esTransport.once('finish', () => {
|
|
102
|
+
clearTimeout(timeout);
|
|
103
|
+
resolve();
|
|
104
|
+
});
|
|
105
|
+
// Now trigger the flush
|
|
106
|
+
esTransport === null || esTransport === void 0 ? void 0 : esTransport.end();
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
console.error(`[Logger] Error flushing logs on ${signal}:`, error);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
process.exit(exitCode);
|
|
114
|
+
};
|
|
115
|
+
process.on('SIGTERM', () => {
|
|
116
|
+
flushAndExit('SIGTERM', 0).catch((err) => {
|
|
117
|
+
console.error('[Logger] Flush and exit failed:', err);
|
|
118
|
+
process.exit(1);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
process.on('SIGINT', () => {
|
|
122
|
+
flushAndExit('SIGINT', 130).catch((err) => {
|
|
123
|
+
console.error('[Logger] Flush and exit failed:', err);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
process.on('beforeExit', () => {
|
|
128
|
+
if (esTransport && !isShuttingDown) {
|
|
129
|
+
esTransport.end();
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
shutdownHandlersRegistered = true;
|
|
133
|
+
}
|
|
38
134
|
/**
|
|
39
135
|
* Creates a Pino logger instance with specified Elasticsearch configuration.
|
|
40
136
|
* @param elasticConfig - Optional Elasticsearch configuration.
|
|
41
137
|
* @returns The Pino logger instance.
|
|
138
|
+
* @throws Error if LOG_LEVEL is invalid or required environment variables are missing.
|
|
42
139
|
*/
|
|
43
140
|
function getLogger(elasticConfig) {
|
|
44
141
|
if (!pinoLogger) {
|
|
45
|
-
if
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
142
|
+
// Validate log level - will throw if invalid
|
|
143
|
+
const logLevel = process.env.LOG_LEVEL || 'info';
|
|
144
|
+
isValidLogLevel(logLevel);
|
|
145
|
+
const transports = [];
|
|
146
|
+
if (process.env.NODE_ENV !== 'local' && process.env.NODE_ENV !== 'test') {
|
|
147
|
+
// Validate Elasticsearch environment variables
|
|
148
|
+
validateElasticsearchEnv();
|
|
149
|
+
// Parse flush settings from environment with validation
|
|
150
|
+
const flushIntervalMs = parseIntEnv(process.env.ES_FLUSH_INTERVAL_MS, 2000, // Default: 2 seconds
|
|
151
|
+
'ES_FLUSH_INTERVAL_MS');
|
|
152
|
+
const flushBytes = parseIntEnv(process.env.ES_FLUSH_BYTES, 100 * 1024, // Default: 100KB
|
|
153
|
+
'ES_FLUSH_BYTES');
|
|
154
|
+
const maxRetries = parseIntEnv(process.env.ES_MAX_RETRIES, 3, // Default: 3 retries
|
|
155
|
+
'ES_MAX_RETRIES');
|
|
156
|
+
const requestTimeout = parseIntEnv(process.env.ES_REQUEST_TIMEOUT_MS, 30000, // Default: 30 seconds
|
|
157
|
+
'ES_REQUEST_TIMEOUT_MS');
|
|
158
|
+
// Safe to access after validation
|
|
159
|
+
const esConfig = {
|
|
160
|
+
index: process.env.SERVER_NICKNAME,
|
|
161
|
+
node: process.env.ELASTICSEARCH_NODE,
|
|
162
|
+
auth: {
|
|
163
|
+
username: process.env.ELASTICSEARCH_USERNAME,
|
|
164
|
+
password: process.env.ELASTICSEARCH_PASSWORD,
|
|
165
|
+
},
|
|
166
|
+
// Configurable flush settings
|
|
167
|
+
flushInterval: flushIntervalMs,
|
|
168
|
+
'flush-bytes': flushBytes,
|
|
169
|
+
// Retry configuration for connection resilience
|
|
170
|
+
maxRetries: maxRetries,
|
|
171
|
+
requestTimeout: requestTimeout,
|
|
172
|
+
// Automatically reconnect on connection faults
|
|
173
|
+
sniffOnConnectionFault: true,
|
|
174
|
+
};
|
|
175
|
+
if (elasticConfig) {
|
|
176
|
+
Object.assign(esConfig, elasticConfig);
|
|
63
177
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
178
|
+
// Create transport and store reference for cleanup
|
|
179
|
+
// Cast to PinoElasticOptions since our ElasticConfig includes ClientOptions properties
|
|
180
|
+
esTransport = (0, pino_elasticsearch_1.default)(esConfig);
|
|
181
|
+
// Handle Elasticsearch connection errors
|
|
182
|
+
esTransport.on('error', (err) => {
|
|
183
|
+
console.error('[Logger] Elasticsearch transport error:', err.message);
|
|
184
|
+
console.error('[Logger] Logs may not be reaching Kibana. Check Elasticsearch connection.');
|
|
185
|
+
});
|
|
186
|
+
// Handle insert errors (document indexing failures)
|
|
187
|
+
esTransport.on('insertError', (err) => {
|
|
188
|
+
console.error('[Logger] Elasticsearch insert error:', err.message);
|
|
189
|
+
console.error('[Logger] Some logs failed to index to Elasticsearch.');
|
|
190
|
+
});
|
|
191
|
+
// Log successful connection (for debugging)
|
|
192
|
+
esTransport.on('insert', () => {
|
|
193
|
+
// Uncomment for debugging: console.log(`[Logger] Successfully inserted log entries`)
|
|
194
|
+
});
|
|
195
|
+
// Register shutdown handlers to flush logs on process exit
|
|
196
|
+
registerShutdownHandlers();
|
|
197
|
+
transports.push(esTransport);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
transports.push(pino_1.pino.destination({
|
|
201
|
+
minLength: 1024,
|
|
202
|
+
sync: true,
|
|
203
|
+
}));
|
|
74
204
|
}
|
|
205
|
+
pinoLogger = (0, pino_1.pino)({
|
|
206
|
+
level: logLevel,
|
|
207
|
+
timestamp: pino_1.stdTimeFunctions.isoTime.bind(pino_1.stdTimeFunctions),
|
|
208
|
+
}, ...transports);
|
|
75
209
|
}
|
|
76
210
|
return pinoLogger;
|
|
77
211
|
}
|
|
@@ -102,8 +236,7 @@ class Logger {
|
|
|
102
236
|
detail = args;
|
|
103
237
|
}
|
|
104
238
|
else {
|
|
105
|
-
|
|
106
|
-
detail = JSON.stringify(...args);
|
|
239
|
+
detail = JSON.stringify(args);
|
|
107
240
|
}
|
|
108
241
|
this._logger[logLevel]({
|
|
109
242
|
component: this._name,
|
package/dist/types/logger.d.ts
CHANGED
|
@@ -44,4 +44,21 @@ export interface ElasticConfig {
|
|
|
44
44
|
* The interval (in milliseconds) at which logs are flushed to Elasticsearch.
|
|
45
45
|
*/
|
|
46
46
|
flushInterval?: number;
|
|
47
|
+
/**
|
|
48
|
+
* The number of bytes to buffer before flushing to Elasticsearch.
|
|
49
|
+
*/
|
|
50
|
+
'flush-bytes'?: number;
|
|
51
|
+
/**
|
|
52
|
+
* Maximum number of retries for failed Elasticsearch requests.
|
|
53
|
+
*/
|
|
54
|
+
maxRetries?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Request timeout in milliseconds before considering a request failed.
|
|
57
|
+
*/
|
|
58
|
+
requestTimeout?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Whether to sniff for additional Elasticsearch nodes on connection fault.
|
|
61
|
+
* Enables automatic reconnection when a node fails.
|
|
62
|
+
*/
|
|
63
|
+
sniffOnConnectionFault?: boolean;
|
|
47
64
|
}
|
package/package.json
CHANGED
package/src/lib/logger.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {Logger as PinoLogger, pino, stdTimeFunctions} from 'pino'
|
|
2
2
|
import * as dotenv from 'dotenv'
|
|
3
|
-
import pinoElastic, {Options as
|
|
4
|
-
import {LOG_LEVEL, LogEvent} from '../types'
|
|
3
|
+
import pinoElastic, {Options as PinoElasticOptions} from 'pino-elasticsearch'
|
|
4
|
+
import {LOG_LEVEL, LogEvent, ElasticConfig} from '../types'
|
|
5
5
|
|
|
6
6
|
dotenv.config()
|
|
7
7
|
|
|
@@ -10,50 +10,230 @@ dotenv.config()
|
|
|
10
10
|
*/
|
|
11
11
|
let pinoLogger: PinoLogger
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Elasticsearch transport instance - kept for cleanup
|
|
15
|
+
*/
|
|
16
|
+
let esTransport: ReturnType<typeof pinoElastic> | null = null
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Flag to track if shutdown handlers are registered
|
|
20
|
+
*/
|
|
21
|
+
let shutdownHandlersRegistered = false
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates required Elasticsearch environment variables.
|
|
25
|
+
* @throws Error if required environment variables are missing.
|
|
26
|
+
*/
|
|
27
|
+
function validateElasticsearchEnv(): void {
|
|
28
|
+
const required = ['ELASTICSEARCH_NODE', 'ELASTICSEARCH_USERNAME', 'ELASTICSEARCH_PASSWORD', 'SERVER_NICKNAME']
|
|
29
|
+
const missing = required.filter(key => !process.env[key])
|
|
30
|
+
|
|
31
|
+
if (missing.length > 0) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Missing required Elasticsearch environment variables: ${missing.join(', ')}`
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Safely parses an integer from an environment variable.
|
|
40
|
+
* @param envValue - The environment variable value to parse.
|
|
41
|
+
* @param defaultValue - The default value to use if parsing fails.
|
|
42
|
+
* @param varName - The name of the environment variable (for error messages).
|
|
43
|
+
* @returns The parsed integer or the default value.
|
|
44
|
+
* @throws Error if the value is not a valid number.
|
|
45
|
+
*/
|
|
46
|
+
function parseIntEnv(envValue: string | undefined, defaultValue: number, varName: string): number {
|
|
47
|
+
if (!envValue) {
|
|
48
|
+
return defaultValue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const parsed = parseInt(envValue, 10)
|
|
52
|
+
|
|
53
|
+
if (isNaN(parsed)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`Invalid value for ${varName}: "${envValue}" is not a valid number. Using default: ${defaultValue}`
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (parsed < 0) {
|
|
60
|
+
throw new Error(
|
|
61
|
+
`Invalid value for ${varName}: "${envValue}" must be a positive number. Using default: ${defaultValue}`
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return parsed
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Registers shutdown handlers to flush logs before process exits.
|
|
70
|
+
*/
|
|
71
|
+
function registerShutdownHandlers(): void {
|
|
72
|
+
if (shutdownHandlersRegistered) {
|
|
73
|
+
return
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let isShuttingDown = false
|
|
77
|
+
|
|
78
|
+
const flushAndExit = async (signal: string, exitCode = 0) => {
|
|
79
|
+
// Prevent multiple shutdown attempts
|
|
80
|
+
if (isShuttingDown) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
isShuttingDown = true
|
|
84
|
+
|
|
85
|
+
if (esTransport) {
|
|
86
|
+
try {
|
|
87
|
+
// Flush any pending logs
|
|
88
|
+
await new Promise<void>((resolve) => {
|
|
89
|
+
const timeout = setTimeout(() => {
|
|
90
|
+
console.error(`[Logger] Flush timeout after 5s on ${signal}`)
|
|
91
|
+
resolve()
|
|
92
|
+
}, 5000) // 5 second timeout
|
|
93
|
+
|
|
94
|
+
// Register listener BEFORE calling end() to avoid race condition
|
|
95
|
+
esTransport?.once('finish', () => {
|
|
96
|
+
clearTimeout(timeout)
|
|
97
|
+
resolve()
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// Now trigger the flush
|
|
101
|
+
esTransport?.end()
|
|
102
|
+
})
|
|
103
|
+
} catch (error) {
|
|
104
|
+
console.error(`[Logger] Error flushing logs on ${signal}:`, error)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
process.exit(exitCode)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
process.on('SIGTERM', () => {
|
|
111
|
+
flushAndExit('SIGTERM', 0).catch((err) => {
|
|
112
|
+
console.error('[Logger] Flush and exit failed:', err)
|
|
113
|
+
process.exit(1)
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
process.on('SIGINT', () => {
|
|
118
|
+
flushAndExit('SIGINT', 130).catch((err) => {
|
|
119
|
+
console.error('[Logger] Flush and exit failed:', err)
|
|
120
|
+
process.exit(1)
|
|
121
|
+
})
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
process.on('beforeExit', () => {
|
|
125
|
+
if (esTransport && !isShuttingDown) {
|
|
126
|
+
esTransport.end()
|
|
127
|
+
}
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
shutdownHandlersRegistered = true
|
|
131
|
+
}
|
|
132
|
+
|
|
13
133
|
/**
|
|
14
134
|
* Creates a Pino logger instance with specified Elasticsearch configuration.
|
|
15
135
|
* @param elasticConfig - Optional Elasticsearch configuration.
|
|
16
136
|
* @returns The Pino logger instance.
|
|
137
|
+
* @throws Error if LOG_LEVEL is invalid or required environment variables are missing.
|
|
17
138
|
*/
|
|
18
139
|
function getLogger(elasticConfig?: ElasticConfig): PinoLogger {
|
|
19
140
|
if (!pinoLogger) {
|
|
20
|
-
if
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
}
|
|
141
|
+
// Validate log level - will throw if invalid
|
|
142
|
+
const logLevel = process.env.LOG_LEVEL || 'info'
|
|
143
|
+
isValidLogLevel(logLevel)
|
|
144
|
+
|
|
145
|
+
const transports = []
|
|
146
|
+
if (process.env.NODE_ENV !== 'local' && process.env.NODE_ENV !== 'test') {
|
|
147
|
+
// Validate Elasticsearch environment variables
|
|
148
|
+
validateElasticsearchEnv()
|
|
149
|
+
|
|
150
|
+
// Parse flush settings from environment with validation
|
|
151
|
+
const flushIntervalMs = parseIntEnv(
|
|
152
|
+
process.env.ES_FLUSH_INTERVAL_MS,
|
|
153
|
+
2000, // Default: 2 seconds
|
|
154
|
+
'ES_FLUSH_INTERVAL_MS'
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
const flushBytes = parseIntEnv(
|
|
158
|
+
process.env.ES_FLUSH_BYTES,
|
|
159
|
+
100 * 1024, // Default: 100KB
|
|
160
|
+
'ES_FLUSH_BYTES'
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
const maxRetries = parseIntEnv(
|
|
164
|
+
process.env.ES_MAX_RETRIES,
|
|
165
|
+
3, // Default: 3 retries
|
|
166
|
+
'ES_MAX_RETRIES'
|
|
167
|
+
)
|
|
48
168
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
169
|
+
const requestTimeout = parseIntEnv(
|
|
170
|
+
process.env.ES_REQUEST_TIMEOUT_MS,
|
|
171
|
+
30000, // Default: 30 seconds
|
|
172
|
+
'ES_REQUEST_TIMEOUT_MS'
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
// Safe to access after validation
|
|
176
|
+
const esConfig: ElasticConfig = {
|
|
177
|
+
index: process.env.SERVER_NICKNAME,
|
|
178
|
+
node: process.env.ELASTICSEARCH_NODE,
|
|
179
|
+
auth: {
|
|
180
|
+
username: process.env.ELASTICSEARCH_USERNAME as string,
|
|
181
|
+
password: process.env.ELASTICSEARCH_PASSWORD as string,
|
|
53
182
|
},
|
|
54
|
-
|
|
183
|
+
// Configurable flush settings
|
|
184
|
+
flushInterval: flushIntervalMs,
|
|
185
|
+
'flush-bytes': flushBytes,
|
|
186
|
+
// Retry configuration for connection resilience
|
|
187
|
+
maxRetries: maxRetries,
|
|
188
|
+
requestTimeout: requestTimeout,
|
|
189
|
+
// Automatically reconnect on connection faults
|
|
190
|
+
sniffOnConnectionFault: true,
|
|
191
|
+
}
|
|
192
|
+
if (elasticConfig) {
|
|
193
|
+
Object.assign(esConfig, elasticConfig)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Create transport and store reference for cleanup
|
|
197
|
+
// Cast to PinoElasticOptions since our ElasticConfig includes ClientOptions properties
|
|
198
|
+
esTransport = pinoElastic(esConfig as PinoElasticOptions)
|
|
199
|
+
|
|
200
|
+
// Handle Elasticsearch connection errors
|
|
201
|
+
esTransport.on('error', (err) => {
|
|
202
|
+
console.error('[Logger] Elasticsearch transport error:', err.message)
|
|
203
|
+
console.error('[Logger] Logs may not be reaching Kibana. Check Elasticsearch connection.')
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// Handle insert errors (document indexing failures)
|
|
207
|
+
esTransport.on('insertError', (err) => {
|
|
208
|
+
console.error('[Logger] Elasticsearch insert error:', err.message)
|
|
209
|
+
console.error('[Logger] Some logs failed to index to Elasticsearch.')
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Log successful connection (for debugging)
|
|
213
|
+
esTransport.on('insert', () => {
|
|
214
|
+
// Uncomment for debugging: console.log(`[Logger] Successfully inserted log entries`)
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
// Register shutdown handlers to flush logs on process exit
|
|
218
|
+
registerShutdownHandlers()
|
|
219
|
+
|
|
220
|
+
transports.push(esTransport)
|
|
221
|
+
} else {
|
|
222
|
+
transports.push(
|
|
223
|
+
pino.destination({
|
|
224
|
+
minLength: 1024,
|
|
225
|
+
sync: true,
|
|
226
|
+
})
|
|
55
227
|
)
|
|
56
228
|
}
|
|
229
|
+
|
|
230
|
+
pinoLogger = pino(
|
|
231
|
+
{
|
|
232
|
+
level: logLevel,
|
|
233
|
+
timestamp: stdTimeFunctions.isoTime.bind(stdTimeFunctions),
|
|
234
|
+
},
|
|
235
|
+
...transports
|
|
236
|
+
)
|
|
57
237
|
}
|
|
58
238
|
return pinoLogger
|
|
59
239
|
}
|
|
@@ -103,8 +283,7 @@ class Logger {
|
|
|
103
283
|
if (process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'test') {
|
|
104
284
|
detail = args
|
|
105
285
|
} else {
|
|
106
|
-
|
|
107
|
-
detail = JSON.stringify(...args)
|
|
286
|
+
detail = JSON.stringify(args)
|
|
108
287
|
}
|
|
109
288
|
this._logger[logLevel]({
|
|
110
289
|
component: this._name,
|
package/src/types/logger.ts
CHANGED
|
@@ -46,4 +46,21 @@ export interface ElasticConfig {
|
|
|
46
46
|
* The interval (in milliseconds) at which logs are flushed to Elasticsearch.
|
|
47
47
|
*/
|
|
48
48
|
flushInterval?: number
|
|
49
|
+
/**
|
|
50
|
+
* The number of bytes to buffer before flushing to Elasticsearch.
|
|
51
|
+
*/
|
|
52
|
+
'flush-bytes'?: number
|
|
53
|
+
/**
|
|
54
|
+
* Maximum number of retries for failed Elasticsearch requests.
|
|
55
|
+
*/
|
|
56
|
+
maxRetries?: number
|
|
57
|
+
/**
|
|
58
|
+
* Request timeout in milliseconds before considering a request failed.
|
|
59
|
+
*/
|
|
60
|
+
requestTimeout?: number
|
|
61
|
+
/**
|
|
62
|
+
* Whether to sniff for additional Elasticsearch nodes on connection fault.
|
|
63
|
+
* Enables automatic reconnection when a node fails.
|
|
64
|
+
*/
|
|
65
|
+
sniffOnConnectionFault?: boolean
|
|
49
66
|
}
|