log-collector-async 1.1.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.
@@ -0,0 +1,418 @@
1
+ /**
2
+ * 단위 테스트: createLogClient 기본 동작 검증
3
+ * 로그 서버 없이도 실행 가능한 테스트
4
+ */
5
+ import { createLogClient } from '../src/index.js';
6
+ import { WorkerThreadsLogClient } from '../src/node-client.js';
7
+
8
+ describe('createLogClient', () => {
9
+ test('should create a client instance', () => {
10
+ const logger = createLogClient('http://localhost:8000');
11
+ expect(logger).toBeDefined();
12
+ expect(logger.info).toBeInstanceOf(Function);
13
+ });
14
+
15
+ test('should accept options', () => {
16
+ const logger = createLogClient('http://localhost:8000', {
17
+ batchSize: 500,
18
+ flushInterval: 2000
19
+ });
20
+ expect(logger).toBeDefined();
21
+ });
22
+
23
+ test('should have all log level methods', () => {
24
+ const logger = createLogClient('http://localhost:8000');
25
+
26
+ expect(logger.info).toBeInstanceOf(Function);
27
+ expect(logger.warn).toBeInstanceOf(Function);
28
+ expect(logger.error).toBeInstanceOf(Function);
29
+ expect(logger.debug).toBeInstanceOf(Function);
30
+ expect(logger.fatal).toBeInstanceOf(Function);
31
+ });
32
+
33
+ test('should accept custom batch size', () => {
34
+ const customBatchSize = 250;
35
+ const logger = createLogClient('http://localhost:8000', {
36
+ batchSize: customBatchSize
37
+ });
38
+
39
+ expect(logger).toBeDefined();
40
+ // Note: 실제 batch size 확인은 내부 구현에 의존
41
+ });
42
+
43
+ test('should accept custom flush interval', () => {
44
+ const customInterval = 3000;
45
+ const logger = createLogClient('http://localhost:8000', {
46
+ flushInterval: customInterval
47
+ });
48
+
49
+ expect(logger).toBeDefined();
50
+ });
51
+
52
+ test('should handle log calls without errors', () => {
53
+ const logger = createLogClient('http://localhost:8000');
54
+
55
+ // 로그 호출이 에러 없이 실행되어야 함
56
+ expect(() => {
57
+ logger.info('test message');
58
+ logger.warn('warning message');
59
+ logger.error('error message');
60
+ }).not.toThrow();
61
+ });
62
+
63
+ test('should handle log calls with metadata', () => {
64
+ const logger = createLogClient('http://localhost:8000');
65
+
66
+ // 메타데이터와 함께 로그 호출
67
+ expect(() => {
68
+ logger.info('test with metadata', {
69
+ user_id: 123,
70
+ action: 'login',
71
+ success: true
72
+ });
73
+ }).not.toThrow();
74
+ });
75
+
76
+ test('should handle multiple consecutive log calls', () => {
77
+ const logger = createLogClient('http://localhost:8000', {
78
+ batchSize: 100
79
+ });
80
+
81
+ // 연속 로그 호출이 블로킹 없이 실행되어야 함
82
+ const start = Date.now();
83
+
84
+ for (let i = 0; i < 10; i++) {
85
+ logger.info(`test log ${i}`);
86
+ }
87
+
88
+ const elapsed = Date.now() - start;
89
+
90
+ // 10개 로그가 10ms 이내에 큐잉되어야 함 (비동기 처리)
91
+ expect(elapsed).toBeLessThan(100);
92
+ });
93
+
94
+ test('should create different clients independently', () => {
95
+ const logger1 = createLogClient('http://localhost:8000', { batchSize: 100 });
96
+ const logger2 = createLogClient('http://localhost:9000', { batchSize: 200 });
97
+
98
+ expect(logger1).toBeDefined();
99
+ expect(logger2).toBeDefined();
100
+ expect(logger1).not.toBe(logger2);
101
+ });
102
+ });
103
+
104
+ describe('Log Level Methods', () => {
105
+ let logger;
106
+
107
+ beforeEach(() => {
108
+ logger = createLogClient('http://localhost:8000');
109
+ });
110
+
111
+ test('info method should work', () => {
112
+ expect(() => logger.info('info message')).not.toThrow();
113
+ });
114
+
115
+ test('warn method should work', () => {
116
+ expect(() => logger.warn('warning message')).not.toThrow();
117
+ });
118
+
119
+ test('error method should work', () => {
120
+ expect(() => logger.error('error message')).not.toThrow();
121
+ });
122
+
123
+ test('debug method should work', () => {
124
+ expect(() => logger.debug('debug message')).not.toThrow();
125
+ });
126
+
127
+ test('fatal method should work', () => {
128
+ expect(() => logger.fatal('fatal message')).not.toThrow();
129
+ });
130
+ });
131
+
132
+ describe('Performance', () => {
133
+ test('log calls should have minimal overhead', () => {
134
+ const logger = createLogClient('http://localhost:8000', { batchSize: 1000 });
135
+
136
+ const iterations = 1000;
137
+ const start = Date.now();
138
+
139
+ for (let i = 0; i < iterations; i++) {
140
+ logger.info(`perf test ${i}`);
141
+ }
142
+
143
+ const elapsed = Date.now() - start;
144
+ const msPerCall = elapsed / iterations;
145
+
146
+ console.log(`Performance: ${msPerCall.toFixed(3)}ms per call`);
147
+
148
+ // 1000개 로그가 1초 이내에 큐잉되어야 함
149
+ expect(elapsed).toBeLessThan(1000);
150
+ });
151
+ });
152
+
153
+ describe('Auto Caller Feature', () => {
154
+ let logger;
155
+
156
+ beforeEach(() => {
157
+ logger = createLogClient('http://localhost:8000', { batchSize: 100 });
158
+ });
159
+
160
+ test('should not throw error with auto caller enabled (default)', () => {
161
+ // autoCaller가 기본으로 활성화되어 있어야 함
162
+ expect(() => {
163
+ logger.info('Test auto caller');
164
+ }).not.toThrow();
165
+ });
166
+
167
+ test('should handle autoCaller disabled', () => {
168
+ // autoCaller를 명시적으로 비활성화
169
+ expect(() => {
170
+ logger.log('INFO', 'Test without auto caller', { autoCaller: false });
171
+ }).not.toThrow();
172
+ });
173
+
174
+ test('should handle manual function_name override', () => {
175
+ // 수동으로 function_name을 전달해도 에러 없어야 함
176
+ expect(() => {
177
+ logger.info('Test manual override', {
178
+ function_name: 'custom_function',
179
+ file_path: '/custom/path.js'
180
+ });
181
+ }).not.toThrow();
182
+ });
183
+
184
+ test('all convenience methods should work with auto caller', () => {
185
+ // 모든 편의 메서드가 auto caller와 함께 동작해야 함
186
+ expect(() => {
187
+ logger.trace('Trace message');
188
+ logger.debug('Debug message');
189
+ logger.info('Info message');
190
+ logger.warn('Warn message');
191
+ logger.error('Error message');
192
+ logger.fatal('Fatal message');
193
+ }).not.toThrow();
194
+ });
195
+
196
+ test('should handle nested function calls', () => {
197
+ function outerFunction() {
198
+ function innerFunction() {
199
+ logger.info('Message from inner function');
200
+ }
201
+ innerFunction();
202
+ }
203
+
204
+ // 중첩 함수에서도 에러 없이 동작해야 함
205
+ expect(() => {
206
+ outerFunction();
207
+ }).not.toThrow();
208
+ });
209
+
210
+ test('should handle async functions', async () => {
211
+ async function asyncFunction() {
212
+ logger.info('Message from async function');
213
+ }
214
+
215
+ // async 함수에서도 에러 없이 동작해야 함
216
+ await expect(asyncFunction()).resolves.not.toThrow();
217
+ });
218
+
219
+ test('should handle arrow functions', () => {
220
+ const arrowFunction = () => {
221
+ logger.info('Message from arrow function');
222
+ };
223
+
224
+ // 화살표 함수에서도 에러 없이 동작해야 함
225
+ expect(() => {
226
+ arrowFunction();
227
+ }).not.toThrow();
228
+ });
229
+
230
+ test('performance with auto caller should still be fast', () => {
231
+ // auto caller가 활성화되어도 성능이 크게 떨어지지 않아야 함
232
+ const iterations = 1000;
233
+ const start = Date.now();
234
+
235
+ for (let i = 0; i < iterations; i++) {
236
+ logger.info(`perf test ${i}`);
237
+ }
238
+
239
+ const elapsed = Date.now() - start;
240
+ const msPerCall = elapsed / iterations;
241
+
242
+ console.log(`Performance with auto caller: ${msPerCall.toFixed(3)}ms per call`);
243
+
244
+ // 1000개 로그가 여전히 1초 이내에 처리되어야 함
245
+ expect(elapsed).toBeLessThan(1000);
246
+ });
247
+ });
248
+
249
+ // Feature 3: 사용자 컨텍스트 관리 테스트
250
+ describe('User Context Management', () => {
251
+ let logger;
252
+
253
+ beforeEach(() => {
254
+ logger = new WorkerThreadsLogClient('http://localhost:8000', {
255
+ service: 'test-service',
256
+ batchSize: 100
257
+ });
258
+ });
259
+
260
+ afterEach(() => {
261
+ if (logger && logger.worker) {
262
+ logger.worker.terminate();
263
+ }
264
+ // 컨텍스트 초기화
265
+ WorkerThreadsLogClient.clearUserContext();
266
+ });
267
+
268
+ test('should auto-include user context', (done) => {
269
+ WorkerThreadsLogClient.runWithUserContext({
270
+ user_id: 'test_user_123',
271
+ trace_id: 'test_trace_xyz'
272
+ }, () => {
273
+ logger.info('Test with user context');
274
+
275
+ // worker로 전송된 메시지 확인
276
+ setTimeout(() => {
277
+ // runWithUserContext가 제대로 작동하는지 확인
278
+ expect(WorkerThreadsLogClient.getUserContext()).toBeDefined();
279
+ done();
280
+ }, 50);
281
+ });
282
+ });
283
+
284
+ test('should clear user context after block', (done) => {
285
+ // 블록 내부
286
+ WorkerThreadsLogClient.runWithUserContext({
287
+ user_id: 'temp_user'
288
+ }, () => {
289
+ expect(WorkerThreadsLogClient.getUserContext()).toBeDefined();
290
+ });
291
+
292
+ // 블록 외부 - 컨텍스트가 초기화되어야 함
293
+ setTimeout(() => {
294
+ expect(WorkerThreadsLogClient.getUserContext()).toBeUndefined();
295
+ done();
296
+ }, 50);
297
+ });
298
+
299
+ test('should handle nested user context', (done) => {
300
+ // 외부 컨텍스트
301
+ WorkerThreadsLogClient.runWithUserContext({
302
+ tenant_id: 'tenant_1'
303
+ }, () => {
304
+ logger.info('Outer context');
305
+
306
+ // 내부 컨텍스트
307
+ WorkerThreadsLogClient.runWithUserContext({
308
+ user_id: 'nested_user'
309
+ }, () => {
310
+ logger.info('Inner context (both)');
311
+
312
+ const ctx = WorkerThreadsLogClient.getUserContext();
313
+ expect(ctx).toBeDefined();
314
+ expect(ctx.user_id).toBe('nested_user');
315
+ });
316
+
317
+ logger.info('Back to outer');
318
+ });
319
+
320
+ setTimeout(done, 50);
321
+ });
322
+
323
+ test('should work with async functions', async () => {
324
+ await WorkerThreadsLogClient.runWithUserContext({
325
+ user_id: 'async_user',
326
+ trace_id: 'async_trace'
327
+ }, async () => {
328
+ logger.info('Start async operation');
329
+
330
+ await new Promise(resolve => setTimeout(resolve, 10));
331
+
332
+ logger.info('End async operation');
333
+
334
+ const ctx = WorkerThreadsLogClient.getUserContext();
335
+ expect(ctx).toBeDefined();
336
+ expect(ctx.user_id).toBe('async_user');
337
+ });
338
+ });
339
+
340
+ test('should combine with HTTP context', (done) => {
341
+ // HTTP 컨텍스트 설정
342
+ WorkerThreadsLogClient.runWithContext({
343
+ path: '/api/test',
344
+ method: 'POST'
345
+ }, () => {
346
+ // 사용자 컨텍스트 추가
347
+ WorkerThreadsLogClient.runWithUserContext({
348
+ user_id: 'combined_user'
349
+ }, () => {
350
+ logger.info('Combined contexts');
351
+
352
+ // 둘 다 존재해야 함
353
+ expect(WorkerThreadsLogClient.getRequestContext()).toBeDefined();
354
+ expect(WorkerThreadsLogClient.getUserContext()).toBeDefined();
355
+ });
356
+ });
357
+
358
+ setTimeout(done, 50);
359
+ });
360
+
361
+ test('setUserContext and clearUserContext should work', () => {
362
+ // 설정
363
+ WorkerThreadsLogClient.setUserContext({
364
+ user_id: 'set_user',
365
+ session_id: 'set_session'
366
+ });
367
+
368
+ logger.info('After setUserContext');
369
+
370
+ const ctx = WorkerThreadsLogClient.getUserContext();
371
+ expect(ctx).toBeDefined();
372
+ expect(ctx.user_id).toBe('set_user');
373
+ expect(ctx.session_id).toBe('set_session');
374
+
375
+ // 초기화
376
+ WorkerThreadsLogClient.clearUserContext();
377
+
378
+ logger.info('After clearUserContext');
379
+
380
+ expect(WorkerThreadsLogClient.getUserContext()).toBeUndefined();
381
+ });
382
+
383
+ test('manual values should override context', (done) => {
384
+ WorkerThreadsLogClient.runWithUserContext({
385
+ user_id: 'context_user'
386
+ }, () => {
387
+ // user_id를 수동으로 전달
388
+ logger.info('Manual override', { user_id: 'manual_user' });
389
+
390
+ // 컨텍스트는 여전히 존재
391
+ const ctx = WorkerThreadsLogClient.getUserContext();
392
+ expect(ctx).toBeDefined();
393
+ expect(ctx.user_id).toBe('context_user');
394
+ });
395
+
396
+ setTimeout(done, 50);
397
+ });
398
+
399
+ test('should handle multiple sequential contexts', (done) => {
400
+ // 첫 번째 컨텍스트
401
+ WorkerThreadsLogClient.runWithUserContext({
402
+ user_id: 'user_1'
403
+ }, () => {
404
+ logger.info('First context');
405
+ expect(WorkerThreadsLogClient.getUserContext().user_id).toBe('user_1');
406
+ });
407
+
408
+ // 두 번째 컨텍스트 (첫 번째와 독립적)
409
+ WorkerThreadsLogClient.runWithUserContext({
410
+ user_id: 'user_2'
411
+ }, () => {
412
+ logger.info('Second context');
413
+ expect(WorkerThreadsLogClient.getUserContext().user_id).toBe('user_2');
414
+ });
415
+
416
+ setTimeout(done, 50);
417
+ });
418
+ });
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Express HTTP 컨텍스트 자동 수집 예제
3
+ *
4
+ * 실행 방법:
5
+ * npm install express
6
+ * node example_express.js
7
+ *
8
+ * 테스트:
9
+ * curl http://localhost:3000/api/users/123
10
+ * curl -X POST http://localhost:3000/api/users -H "Content-Type: application/json" -d '{"name":"John"}'
11
+ */
12
+ const express = require('express');
13
+ const { WorkerThreadsLogClient } = require('./src/node-client');
14
+
15
+ const app = express();
16
+ app.use(express.json());
17
+
18
+ // 로그 클라이언트 초기화
19
+ const logger = new WorkerThreadsLogClient('http://localhost:8000', {
20
+ service: 'express-example',
21
+ environment: 'development'
22
+ });
23
+
24
+ // HTTP 컨텍스트 미들웨어
25
+ app.use((req, res, next) => {
26
+ WorkerThreadsLogClient.runWithContext({
27
+ path: req.path,
28
+ method: req.method,
29
+ ip: req.ip
30
+ }, () => {
31
+ const startTime = Date.now();
32
+
33
+ // 요청 시작 로그
34
+ logger.info(`Request started: ${req.method} ${req.path}`);
35
+
36
+ // 응답 후킹
37
+ res.on('finish', () => {
38
+ const durationMs = Date.now() - startTime;
39
+ logger.info(`Request completed: ${res.statusCode}`, {
40
+ status_code: res.statusCode,
41
+ duration_ms: durationMs
42
+ });
43
+ });
44
+
45
+ next();
46
+ });
47
+ });
48
+
49
+ // 라우트
50
+ app.get('/api/users/:userId', (req, res) => {
51
+ const { userId } = req.params;
52
+
53
+ logger.info('Fetching user from database', { user_id: userId });
54
+ // 자동으로 포함됨: path="/api/users/123", method="GET", ip="::1"
55
+
56
+ // 가짜 데이터
57
+ const user = { id: userId, name: `User ${userId}` };
58
+
59
+ logger.info('User fetched successfully', { user_id: userId });
60
+ res.json({ user });
61
+ });
62
+
63
+ app.post('/api/users', (req, res) => {
64
+ const { name } = req.body;
65
+
66
+ logger.info('Creating new user', { username: name });
67
+ // 자동으로 포함됨: path="/api/users", method="POST", ip="::1"
68
+
69
+ // 가짜 생성
70
+ const newUser = { id: 999, name };
71
+
72
+ logger.info('User created successfully', { user_id: newUser.id });
73
+ res.status(201).json({ user: newUser });
74
+ });
75
+
76
+ app.get('/api/error', (req, res) => {
77
+ logger.warn('About to trigger an error');
78
+
79
+ try {
80
+ throw new Error('This is a test error');
81
+ } catch (err) {
82
+ logger.errorWithTrace('Error occurred', err);
83
+ // HTTP 컨텍스트도 에러 로그에 포함됨
84
+ res.status(500).json({ error: err.message });
85
+ }
86
+ });
87
+
88
+ // 서버 시작
89
+ const PORT = 3000;
90
+ app.listen(PORT, () => {
91
+ logger.info('Server started', { port: PORT });
92
+ console.log(`Starting Express server on port ${PORT}...`);
93
+ console.log('Test with:');
94
+ console.log(` curl http://localhost:${PORT}/api/users/123`);
95
+ console.log(` curl -X POST http://localhost:${PORT}/api/users -H "Content-Type: application/json" -d '{"name":"John"}'`);
96
+ console.log(` curl http://localhost:${PORT}/api/error`);
97
+ console.log('\nCheck logs in PostgreSQL:');
98
+ console.log(' SELECT path, method, ip, function_name, message FROM logs ORDER BY created_at DESC LIMIT 10;');
99
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * 글로벌 에러 핸들러 사용 예제
3
+ *
4
+ * enableGlobalErrorHandler 옵션을 활성화하면
5
+ * 모든 uncaught errors와 unhandled rejections가 자동으로 로깅됩니다.
6
+ */
7
+
8
+ import { createLogClient } from 'log-collector-async';
9
+
10
+ // ============================================================================
11
+ // 방법 1: 생성자 옵션으로 활성화
12
+ // ============================================================================
13
+ const logger = createLogClient('http://localhost:8000', {
14
+ service: 'my-app',
15
+ environment: 'production',
16
+ enableGlobalErrorHandler: true // 글로벌 에러 핸들러 활성화
17
+ });
18
+
19
+ console.log('✅ 글로벌 에러 핸들러가 활성화되었습니다.');
20
+ console.log('이제 모든 에러가 자동으로 로깅됩니다!\n');
21
+
22
+ // ============================================================================
23
+ // 테스트 1: Uncaught Exception (동기 에러)
24
+ // ============================================================================
25
+ setTimeout(() => {
26
+ console.log('\n📌 테스트 1: Uncaught Exception');
27
+ // 이 에러는 자동으로 로깅됩니다 (try-catch 없이도!)
28
+ throw new Error('This is an uncaught error!');
29
+ }, 1000);
30
+
31
+ // ============================================================================
32
+ // 테스트 2: Unhandled Promise Rejection (비동기 에러)
33
+ // ============================================================================
34
+ setTimeout(() => {
35
+ console.log('\n📌 테스트 2: Unhandled Promise Rejection');
36
+ // 이 Promise rejection도 자동으로 로깅됩니다
37
+ Promise.reject(new Error('This is an unhandled rejection!'));
38
+ }, 2000);
39
+
40
+ // ============================================================================
41
+ // 테스트 3: 일반 로깅도 여전히 작동
42
+ // ============================================================================
43
+ logger.info('Application started', { version: '1.0.0' });
44
+
45
+ setTimeout(() => {
46
+ logger.warn('This is a warning', { custom_field: 'test' });
47
+ }, 500);
48
+
49
+ // ============================================================================
50
+ // 방법 2: 환경 변수로 활성화
51
+ // ============================================================================
52
+ // .env 파일에 추가:
53
+ // ENABLE_GLOBAL_ERROR_HANDLER=true
54
+ //
55
+ // 그러면 명시적으로 옵션을 전달하지 않아도 자동으로 활성화됩니다:
56
+ // const logger = createLogClient('http://localhost:8000', {
57
+ // service: 'my-app'
58
+ // });
59
+
60
+ // ============================================================================
61
+ // 주의사항
62
+ // ============================================================================
63
+ // 1. enableGlobalErrorHandler는 기본값이 false입니다
64
+ // 2. 프로덕션 환경에서는 신중하게 사용하세요
65
+ // 3. 기존 에러 핸들러와 충돌할 수 있으니 테스트 필요
66
+ // 4. close() 호출 시 자동으로 핸들러가 해제됩니다
67
+
68
+ // Graceful shutdown
69
+ process.on('SIGINT', async () => {
70
+ console.log('\nShutting down...');
71
+ logger.flush();
72
+
73
+ setTimeout(async () => {
74
+ await logger.close(); // 글로벌 핸들러도 자동으로 해제됨
75
+ process.exit(0);
76
+ }, 1000);
77
+ });