@mhmdhammoud/meritt-utils 1.5.1 → 1.5.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/src/lib/logger.ts CHANGED
@@ -1,7 +1,7 @@
1
- import {Logger as PinoLogger, pino, stdTimeFunctions} from 'pino'
1
+ import { Logger as PinoLogger, pino, stdTimeFunctions } from 'pino'
2
2
  import * as dotenv from 'dotenv'
3
- import pinoElastic, {Options as ElasticConfig} from 'pino-elasticsearch'
4
- import {LOG_LEVEL, LogEvent} from '../types'
3
+ import { createElasticTransport } from './elastic-transport'
4
+ import { LOG_LEVEL, LogEvent, ElasticConfig } from '../types'
5
5
 
6
6
  dotenv.config()
7
7
 
@@ -10,50 +10,240 @@ dotenv.config()
10
10
  */
11
11
  let pinoLogger: PinoLogger
12
12
 
13
+ /**
14
+ * Elasticsearch transport instance - kept for cleanup
15
+ */
16
+ let esTransport: NodeJS.ReadWriteStream | 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 = [
29
+ 'ELASTICSEARCH_NODE',
30
+ 'ELASTICSEARCH_USERNAME',
31
+ 'ELASTICSEARCH_PASSWORD',
32
+ 'SERVER_NICKNAME',
33
+ ]
34
+ const missing = required.filter((key) => !process.env[key])
35
+
36
+ if (missing.length > 0) {
37
+ throw new Error(
38
+ `Missing required Elasticsearch environment variables: ${missing.join(', ')}`
39
+ )
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Safely parses an integer from an environment variable.
45
+ * @param envValue - The environment variable value to parse.
46
+ * @param defaultValue - The default value to use if parsing fails.
47
+ * @param varName - The name of the environment variable (for error messages).
48
+ * @returns The parsed integer or the default value.
49
+ * @throws Error if the value is not a valid number.
50
+ */
51
+ function parseIntEnv(
52
+ envValue: string | undefined,
53
+ defaultValue: number,
54
+ varName: string
55
+ ): number {
56
+ if (!envValue) {
57
+ return defaultValue
58
+ }
59
+
60
+ const parsed = parseInt(envValue, 10)
61
+
62
+ if (isNaN(parsed)) {
63
+ throw new Error(
64
+ `Invalid value for ${varName}: "${envValue}" is not a valid number. Using default: ${defaultValue}`
65
+ )
66
+ }
67
+
68
+ if (parsed < 0) {
69
+ throw new Error(
70
+ `Invalid value for ${varName}: "${envValue}" must be a positive number. Using default: ${defaultValue}`
71
+ )
72
+ }
73
+
74
+ return parsed
75
+ }
76
+
77
+ /**
78
+ * Registers shutdown handlers to flush logs before process exits.
79
+ */
80
+ function registerShutdownHandlers(): void {
81
+ if (shutdownHandlersRegistered) {
82
+ return
83
+ }
84
+
85
+ let isShuttingDown = false
86
+
87
+ const flushAndExit = async (signal: string, exitCode = 0) => {
88
+ // Prevent multiple shutdown attempts
89
+ if (isShuttingDown) {
90
+ return
91
+ }
92
+ isShuttingDown = true
93
+
94
+ if (esTransport) {
95
+ try {
96
+ // Flush any pending logs
97
+ await new Promise<void>((resolve) => {
98
+ const timeout = setTimeout(() => {
99
+ console.error(`[Logger] Flush timeout after 5s on ${signal}`)
100
+ resolve()
101
+ }, 5000) // 5 second timeout
102
+
103
+ // Register listener BEFORE calling end() to avoid race condition
104
+ esTransport?.once('finish', () => {
105
+ clearTimeout(timeout)
106
+ resolve()
107
+ })
108
+
109
+ // Now trigger the flush
110
+ esTransport?.end()
111
+ })
112
+ } catch (error) {
113
+ console.error(`[Logger] Error flushing logs on ${signal}:`, error)
114
+ }
115
+ }
116
+ process.exit(exitCode)
117
+ }
118
+
119
+ process.on('SIGTERM', () => {
120
+ flushAndExit('SIGTERM', 0).catch((err) => {
121
+ console.error('[Logger] Flush and exit failed:', err)
122
+ process.exit(1)
123
+ })
124
+ })
125
+
126
+ process.on('SIGINT', () => {
127
+ flushAndExit('SIGINT', 130).catch((err) => {
128
+ console.error('[Logger] Flush and exit failed:', err)
129
+ process.exit(1)
130
+ })
131
+ })
132
+
133
+ process.on('beforeExit', () => {
134
+ if (esTransport && !isShuttingDown) {
135
+ esTransport.end()
136
+ }
137
+ })
138
+
139
+ shutdownHandlersRegistered = true
140
+ }
141
+
13
142
  /**
14
143
  * Creates a Pino logger instance with specified Elasticsearch configuration.
15
144
  * @param elasticConfig - Optional Elasticsearch configuration.
16
145
  * @returns The Pino logger instance.
146
+ * @throws Error if LOG_LEVEL is invalid or required environment variables are missing.
17
147
  */
18
148
  function getLogger(elasticConfig?: ElasticConfig): PinoLogger {
19
149
  if (!pinoLogger) {
20
- if (isValidLogLevel(process.env.LOG_LEVEL)) {
21
- const transports = []
22
- if (process.env.NODE_ENV !== 'local' && process.env.NODE_ENV !== 'test') {
23
- const esConfig: ElasticConfig = {
24
- index: process.env.SERVER_NICKNAME,
25
- node: process.env.ELASTICSEARCH_NODE,
26
- auth: {
27
- username: process.env.ELASTICSEARCH_USERNAME,
28
- password: process.env.ELASTICSEARCH_PASSWORD,
29
- },
30
- flushInterval: 1000,
31
- 'flush-bytes': 1000,
32
- }
33
- if (elasticConfig) {
34
- Object.assign(esConfig, elasticConfig)
35
- }
36
-
37
- const esTransport = pinoElastic(esConfig)
38
-
39
- transports.push(esTransport)
40
- } else {
41
- transports.push(
42
- pino.destination({
43
- minLength: 128,
44
- sync: false,
45
- })
46
- )
47
- }
150
+ // Validate log level - will throw if invalid
151
+ const logLevel = process.env.LOG_LEVEL || 'info'
152
+ isValidLogLevel(logLevel)
153
+
154
+ const transports = []
155
+ if (process.env.NODE_ENV !== 'local' && process.env.NODE_ENV !== 'test') {
156
+ // Validate Elasticsearch environment variables
157
+ validateElasticsearchEnv()
158
+
159
+ // Parse flush settings from environment with validation
160
+ const flushIntervalMs = parseIntEnv(
161
+ process.env.ES_FLUSH_INTERVAL_MS,
162
+ 2000, // Default: 2 seconds
163
+ 'ES_FLUSH_INTERVAL_MS'
164
+ )
48
165
 
49
- pinoLogger = pino(
50
- {
51
- level: process.env.LOG_LEVEL,
52
- timestamp: stdTimeFunctions.isoTime.bind(stdTimeFunctions),
166
+ const flushBytes = parseIntEnv(
167
+ process.env.ES_FLUSH_BYTES,
168
+ 100 * 1024, // Default: 100KB
169
+ 'ES_FLUSH_BYTES'
170
+ )
171
+
172
+ const maxRetries = parseIntEnv(
173
+ process.env.ES_MAX_RETRIES,
174
+ 3, // Default: 3 retries
175
+ 'ES_MAX_RETRIES'
176
+ )
177
+
178
+ const requestTimeout = parseIntEnv(
179
+ process.env.ES_REQUEST_TIMEOUT_MS,
180
+ 30000, // Default: 30 seconds
181
+ 'ES_REQUEST_TIMEOUT_MS'
182
+ )
183
+
184
+ // Safe to access after validation
185
+ const esConfig: ElasticConfig = {
186
+ index: process.env.SERVER_NICKNAME,
187
+ node: process.env.ELASTICSEARCH_NODE,
188
+ auth: {
189
+ username: process.env.ELASTICSEARCH_USERNAME,
190
+ password: process.env.ELASTICSEARCH_PASSWORD,
53
191
  },
54
- ...transports
192
+ // Configurable flush settings
193
+ flushInterval: flushIntervalMs,
194
+ 'flush-bytes': flushBytes,
195
+ // Retry configuration for connection resilience
196
+ maxRetries: maxRetries,
197
+ requestTimeout: requestTimeout,
198
+ // Automatically reconnect on connection faults
199
+ sniffOnConnectionFault: true,
200
+ }
201
+ if (elasticConfig) {
202
+ Object.assign(esConfig, elasticConfig)
203
+ }
204
+
205
+ // Create transport with connection lifecycle fix (pino-elasticsearch #140)
206
+ esTransport = createElasticTransport(esConfig)
207
+
208
+ // Handle Elasticsearch connection errors
209
+ esTransport.on('error', (err: Error) => {
210
+ console.error('[Logger] Elasticsearch transport error:', err.message)
211
+ console.error(
212
+ '[Logger] Logs may not be reaching Kibana. Check Elasticsearch connection.'
213
+ )
214
+ })
215
+
216
+ // Handle insert errors (document indexing failures)
217
+ esTransport.on('insertError', (err: Error) => {
218
+ console.error('[Logger] Elasticsearch insert error:', err.message)
219
+ console.error('[Logger] Some logs failed to index to Elasticsearch.')
220
+ })
221
+
222
+ // Log successful connection (for debugging)
223
+ esTransport.on('insert', () => {
224
+ // Uncomment for debugging: console.log(`[Logger] Successfully inserted log entries`)
225
+ })
226
+
227
+ // Register shutdown handlers to flush logs on process exit
228
+ registerShutdownHandlers()
229
+
230
+ transports.push(esTransport)
231
+ } else {
232
+ transports.push(
233
+ pino.destination({
234
+ minLength: 1024,
235
+ sync: true,
236
+ })
55
237
  )
56
238
  }
239
+
240
+ pinoLogger = pino(
241
+ {
242
+ level: logLevel,
243
+ timestamp: stdTimeFunctions.isoTime.bind(stdTimeFunctions),
244
+ },
245
+ ...transports
246
+ )
57
247
  }
58
248
  return pinoLogger
59
249
  }
@@ -103,8 +293,7 @@ class Logger {
103
293
  if (process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'test') {
104
294
  detail = args
105
295
  } else {
106
- //@ts-ignore
107
- detail = JSON.stringify(...args)
296
+ detail = JSON.stringify(args)
108
297
  }
109
298
  this._logger[logLevel]({
110
299
  component: this._name,
package/src/lib/pdf.ts CHANGED
@@ -1,4 +1,4 @@
1
- import {AxiosInstance} from '../utilities'
1
+ // import { AxiosInstance } from '../utilities'
2
2
  /*
3
3
  Author : Mustafa Halabi https://github.com/mustafahalabi
4
4
  Date : 2023-06-24
@@ -16,7 +16,7 @@ class Pdf {
16
16
  * // => 'Hello-World'
17
17
  * ```
18
18
  * */
19
- getBase64Images = (url: string): string => {
19
+ getBase64Images = (_url: string): string => {
20
20
  return ''
21
21
  }
22
22
  }
@@ -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
  }
@@ -1 +1 @@
1
- export {default as AxiosInstance} from './axios'
1
+ export { default as AxiosInstance } from './axios'
package/.eslintignore DELETED
@@ -1,2 +0,0 @@
1
- *.d.ts
2
- dist
package/.eslintrc.js DELETED
@@ -1,28 +0,0 @@
1
- module.exports = {
2
- plugins: ['@typescript-eslint/eslint-plugin', 'eslint-plugin-tsdoc'],
3
- extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4
- parser: '@typescript-eslint/parser',
5
- parserOptions: {
6
- project: './tsconfig.json',
7
- tsconfigRootDir: __dirname,
8
- ecmaVersion: 2018,
9
- sourceType: 'module',
10
- },
11
- rules: {
12
- 'tsdoc/syntax': 'warn',
13
- '@typescript-eslint/ban-ts-comment': 'off',
14
- '@typescript-eslint/ban-types': 'off',
15
- '@typescript-eslint/no-namespace': 'off',
16
- '@typescript-eslint/no-unused-vars': [
17
- 'warn',
18
- {
19
- vars: 'all',
20
- args: 'after-used',
21
- ignoreRestSiblings: true,
22
- },
23
- ],
24
- '@typescript-eslint/no-explicit-any': 'off',
25
- 'no-async-promise-executor': 'off',
26
- },
27
- root: true,
28
- }