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.
- package/.env.example +58 -0
- package/README.md +866 -0
- package/__tests__/client.test.js +418 -0
- package/example_express.js +99 -0
- package/example_global_error_handler.js +77 -0
- package/example_user_context.js +305 -0
- package/jest.config.js +13 -0
- package/package.json +60 -0
- package/src/browser-client.js +382 -0
- package/src/browser-worker.js +118 -0
- package/src/index.js +29 -0
- package/src/node-client.js +561 -0
- package/src/node-worker.js +127 -0
- package/test-manual.js +29 -0
- package/test_local.js +45 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 브라우저 로그 클라이언트 (Web Worker 사용)
|
|
3
|
+
*
|
|
4
|
+
* 특징:
|
|
5
|
+
* - 메인 스레드 렉 0% (완전 격리)
|
|
6
|
+
* - 앱 블로킹 < 0.01ms
|
|
7
|
+
* - Graceful shutdown (beforeunload)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class WebWorkerLogClient {
|
|
11
|
+
/**
|
|
12
|
+
* @param {string} serverUrl - 로그 서버 URL (기본: 환경 변수 VUE_APP_LOG_SERVER_URL 등)
|
|
13
|
+
* @param {Object} options - 옵션
|
|
14
|
+
* @param {string} options.service - 서비스 이름 (기본: 환경 변수에서 읽기)
|
|
15
|
+
* @param {string} options.environment - 환경 (기본: 환경 변수에서 읽기 또는 'development')
|
|
16
|
+
* @param {string} options.serviceVersion - 서비스 버전 (기본: 환경 변수에서 읽기)
|
|
17
|
+
* @param {string} options.logType - 로그 타입 (기본: 환경 변수에서 읽기 또는 'FRONTEND')
|
|
18
|
+
* @param {number} options.batchSize - 배치 크기 (기본: 1000)
|
|
19
|
+
* @param {number} options.flushInterval - Flush 간격 (ms, 기본: 1000)
|
|
20
|
+
* @param {boolean} options.enableCompression - 압축 활성화 (기본: true)
|
|
21
|
+
* @param {boolean} options.enableGlobalErrorHandler - 글로벌 에러 핸들러 활성화 (기본: false)
|
|
22
|
+
*
|
|
23
|
+
* 환경 변수 우선순위: 명시적 파라미터 > 빌드 시점 환경 변수 > 기본값
|
|
24
|
+
*
|
|
25
|
+
* 빌드 시점 환경 변수 예시 (webpack/vite):
|
|
26
|
+
* - React: REACT_APP_LOG_SERVER_URL
|
|
27
|
+
* - Vue: VUE_APP_LOG_SERVER_URL
|
|
28
|
+
* - Vite: VITE_LOG_SERVER_URL
|
|
29
|
+
*
|
|
30
|
+
* .env 파일 예시:
|
|
31
|
+
* VITE_LOG_SERVER_URL=http://localhost:8000
|
|
32
|
+
* VITE_SERVICE_NAME=web-app
|
|
33
|
+
* VITE_ENVIRONMENT=production
|
|
34
|
+
* VITE_SERVICE_VERSION=v2.1.0
|
|
35
|
+
* VITE_LOG_TYPE=FRONTEND
|
|
36
|
+
* VITE_ENABLE_GLOBAL_ERROR_HANDLER=true
|
|
37
|
+
*/
|
|
38
|
+
constructor(serverUrl = null, options = {}) {
|
|
39
|
+
// 빌드 시점 환경 변수에서 자동 로드
|
|
40
|
+
// 다양한 프레임워크 지원 (React, Vue, Vite)
|
|
41
|
+
const getEnv = (name) => {
|
|
42
|
+
return (
|
|
43
|
+
typeof process !== 'undefined' && process.env?.[`REACT_APP_${name}`] ||
|
|
44
|
+
typeof process !== 'undefined' && process.env?.[`VUE_APP_${name}`] ||
|
|
45
|
+
typeof process !== 'undefined' && process.env?.[`VITE_${name}`] ||
|
|
46
|
+
typeof import.meta !== 'undefined' && import.meta.env?.[`VITE_${name}`] ||
|
|
47
|
+
null
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
this.serverUrl = (serverUrl || getEnv('LOG_SERVER_URL') || 'http://localhost:8000').replace(/\/$/, '');
|
|
52
|
+
this.service = options.service || getEnv('SERVICE_NAME') || null;
|
|
53
|
+
this.environment = options.environment || getEnv('ENVIRONMENT') || 'development';
|
|
54
|
+
this.serviceVersion = options.serviceVersion || getEnv('SERVICE_VERSION') || 'v0.0.0-dev';
|
|
55
|
+
this.logType = options.logType || getEnv('LOG_TYPE') || 'FRONTEND';
|
|
56
|
+
|
|
57
|
+
this.options = {
|
|
58
|
+
batchSize: options.batchSize || 1000,
|
|
59
|
+
flushInterval: options.flushInterval || 1000,
|
|
60
|
+
enableCompression: options.enableCompression !== false,
|
|
61
|
+
enableGlobalErrorHandler: options.enableGlobalErrorHandler || false
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Web Worker 생성
|
|
65
|
+
this._createWorker();
|
|
66
|
+
|
|
67
|
+
// Graceful shutdown 설정
|
|
68
|
+
this._setupGracefulShutdown();
|
|
69
|
+
|
|
70
|
+
// 글로벌 에러 핸들러 설정 (옵션)
|
|
71
|
+
if (this.options.enableGlobalErrorHandler) {
|
|
72
|
+
this._setupGlobalErrorHandler();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
_createWorker() {
|
|
77
|
+
try {
|
|
78
|
+
// Worker 스크립트 URL 생성
|
|
79
|
+
this.worker = new Worker(
|
|
80
|
+
new URL('./browser-worker.js', import.meta.url),
|
|
81
|
+
{ type: 'module' }
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Worker 초기화
|
|
85
|
+
this.worker.postMessage({
|
|
86
|
+
type: 'init',
|
|
87
|
+
serverUrl: this.serverUrl,
|
|
88
|
+
...this.options
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Worker 에러 핸들링
|
|
92
|
+
this.worker.onerror = (error) => {
|
|
93
|
+
console.error('[Log Client] Worker error:', error);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('[Log Client] Failed to create worker:', error);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
_setupGracefulShutdown() {
|
|
103
|
+
// 브라우저 종료 시 큐 비우기
|
|
104
|
+
window.addEventListener('beforeunload', () => {
|
|
105
|
+
this.flush();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 페이지 숨김 시 (모바일, 탭 전환)
|
|
109
|
+
document.addEventListener('visibilitychange', () => {
|
|
110
|
+
if (document.hidden) {
|
|
111
|
+
this.flush();
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 글로벌 에러 핸들러 설정
|
|
118
|
+
* 모든 uncaught errors와 unhandled promise rejections를 자동으로 로깅
|
|
119
|
+
*/
|
|
120
|
+
_setupGlobalErrorHandler() {
|
|
121
|
+
// 동기 에러 핸들러
|
|
122
|
+
window.addEventListener('error', (event) => {
|
|
123
|
+
this.errorWithTrace('Uncaught error', event.error || new Error(event.message), {
|
|
124
|
+
source: event.filename,
|
|
125
|
+
line: event.lineno,
|
|
126
|
+
column: event.colno,
|
|
127
|
+
error_type: 'UncaughtError'
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// 비동기 에러 핸들러 (Promise rejection)
|
|
132
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
133
|
+
const error = event.reason instanceof Error ? event.reason : new Error(String(event.reason));
|
|
134
|
+
this.errorWithTrace('Unhandled promise rejection', error, {
|
|
135
|
+
error_type: 'UnhandledRejection',
|
|
136
|
+
reason: String(event.reason)
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 로그 추가 (비블로킹, ~0.01ms)
|
|
143
|
+
* @param {string} level - 로그 레벨
|
|
144
|
+
* @param {string} message - 로그 메시지
|
|
145
|
+
* @param {Object} metadata - 추가 메타데이터
|
|
146
|
+
* @param {boolean} metadata.autoCaller - 호출 위치 자동 추적 활성화 (기본: true)
|
|
147
|
+
*/
|
|
148
|
+
log(level, message, metadata = {}) {
|
|
149
|
+
if (!this.worker) {
|
|
150
|
+
console.warn('[Log Client] Worker not initialized');
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 공통 필드 자동 추가
|
|
155
|
+
const logEntry = {
|
|
156
|
+
level,
|
|
157
|
+
message,
|
|
158
|
+
created_at: Date.now() / 1000, // Unix timestamp (초 단위)
|
|
159
|
+
...metadata
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
// 호출 위치 자동 추적 (function_name, file_path)
|
|
163
|
+
if (metadata.autoCaller !== false && !metadata.function_name) {
|
|
164
|
+
try {
|
|
165
|
+
const stack = new Error().stack;
|
|
166
|
+
const stackLines = stack.split('\n');
|
|
167
|
+
const callerLine = stackLines[2];
|
|
168
|
+
|
|
169
|
+
if (callerLine) {
|
|
170
|
+
// 브라우저 스타일: " at functionName (http://example.com/file.js:123:45)"
|
|
171
|
+
let match = callerLine.match(/at\s+([^\s]+)\s+\(([^:]+):\d+:\d+\)/);
|
|
172
|
+
if (match) {
|
|
173
|
+
logEntry.function_name = logEntry.function_name || match[1];
|
|
174
|
+
logEntry.file_path = logEntry.file_path || match[2];
|
|
175
|
+
} else {
|
|
176
|
+
// 단순 형식: " at http://example.com/file.js:123:45"
|
|
177
|
+
match = callerLine.match(/at\s+([^:]+):\d+:\d+/);
|
|
178
|
+
if (match) {
|
|
179
|
+
logEntry.file_path = logEntry.file_path || match[1];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
} catch (err) {
|
|
184
|
+
// 스택 추출 실패 시 무시
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this.service) logEntry.service = logEntry.service || this.service;
|
|
189
|
+
if (this.environment) logEntry.environment = logEntry.environment || this.environment;
|
|
190
|
+
if (this.serviceVersion) logEntry.service_version = logEntry.service_version || this.serviceVersion;
|
|
191
|
+
if (this.logType) logEntry.log_type = logEntry.log_type || this.logType;
|
|
192
|
+
|
|
193
|
+
// Worker로 메시지만 전달 (즉시 리턴!)
|
|
194
|
+
this.worker.postMessage({
|
|
195
|
+
type: 'log',
|
|
196
|
+
data: logEntry
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 타이머 시작
|
|
202
|
+
* @returns {number} 시작 시간 (ms)
|
|
203
|
+
*/
|
|
204
|
+
startTimer() {
|
|
205
|
+
return Date.now();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 타이머 종료 및 로그 전송 (duration_ms 자동 계산)
|
|
210
|
+
* @param {number} startTime - startTimer()의 반환값
|
|
211
|
+
* @param {string} level - 로그 레벨
|
|
212
|
+
* @param {string} message - 로그 메시지
|
|
213
|
+
* @param {Object} metadata - 추가 메타데이터
|
|
214
|
+
*/
|
|
215
|
+
endTimer(startTime, level, message, metadata = {}) {
|
|
216
|
+
const durationMs = Date.now() - startTime;
|
|
217
|
+
this.log(level, message, { ...metadata, duration_ms: durationMs });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* 함수 실행 시간 측정 래퍼
|
|
222
|
+
* @param {Function} fn - 측정할 함수
|
|
223
|
+
* @param {string} message - 로그 메시지 (기본: 함수명)
|
|
224
|
+
* @param {string} level - 로그 레벨 (기본: INFO)
|
|
225
|
+
* @returns {*} 함수 실행 결과
|
|
226
|
+
*/
|
|
227
|
+
measure(fn, message = null, level = 'INFO') {
|
|
228
|
+
const startTime = this.startTimer();
|
|
229
|
+
const functionName = fn.name || 'anonymous';
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
const result = fn();
|
|
233
|
+
|
|
234
|
+
// Promise 처리
|
|
235
|
+
if (result && typeof result.then === 'function') {
|
|
236
|
+
return result
|
|
237
|
+
.then(res => {
|
|
238
|
+
const durationMs = Date.now() - startTime;
|
|
239
|
+
this.log(level, message || `${functionName} completed`, {
|
|
240
|
+
duration_ms: durationMs,
|
|
241
|
+
function_name: functionName
|
|
242
|
+
});
|
|
243
|
+
return res;
|
|
244
|
+
})
|
|
245
|
+
.catch(err => {
|
|
246
|
+
const durationMs = Date.now() - startTime;
|
|
247
|
+
this.errorWithTrace(
|
|
248
|
+
message || `${functionName} failed`,
|
|
249
|
+
err,
|
|
250
|
+
{ duration_ms: durationMs, function_name: functionName }
|
|
251
|
+
);
|
|
252
|
+
throw err;
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// 동기 함수 처리
|
|
257
|
+
const durationMs = Date.now() - startTime;
|
|
258
|
+
this.log(level, message || `${functionName} completed`, {
|
|
259
|
+
duration_ms: durationMs,
|
|
260
|
+
function_name: functionName
|
|
261
|
+
});
|
|
262
|
+
return result;
|
|
263
|
+
|
|
264
|
+
} catch (err) {
|
|
265
|
+
const durationMs = Date.now() - startTime;
|
|
266
|
+
this.errorWithTrace(
|
|
267
|
+
message || `${functionName} failed`,
|
|
268
|
+
err,
|
|
269
|
+
{ duration_ms: durationMs, function_name: functionName }
|
|
270
|
+
);
|
|
271
|
+
throw err;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 에러 로그 + stack_trace 자동 추출
|
|
277
|
+
* @param {string} message - 에러 메시지
|
|
278
|
+
* @param {Error} error - Error 객체
|
|
279
|
+
* @param {Object} metadata - 추가 메타데이터
|
|
280
|
+
*/
|
|
281
|
+
errorWithTrace(message, error = null, metadata = {}) {
|
|
282
|
+
let stackTrace = null;
|
|
283
|
+
let errorType = null;
|
|
284
|
+
let functionName = null;
|
|
285
|
+
let filePath = null;
|
|
286
|
+
|
|
287
|
+
if (error && error.stack) {
|
|
288
|
+
stackTrace = error.stack;
|
|
289
|
+
errorType = error.name || 'Error';
|
|
290
|
+
|
|
291
|
+
// Stack trace 파싱
|
|
292
|
+
const stackLines = stackTrace.split('\n');
|
|
293
|
+
for (const line of stackLines) {
|
|
294
|
+
// 예: "at functionName (http://example.com/file.js:123:45)"
|
|
295
|
+
const match = line.match(/at\s+([^\s]+)\s+\(([^:]+):(\d+):(\d+)\)/);
|
|
296
|
+
if (match) {
|
|
297
|
+
functionName = match[1];
|
|
298
|
+
filePath = match[2];
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
// 예: "at http://example.com/file.js:123:45"
|
|
302
|
+
const simpleMatch = line.match(/at\s+([^:]+):(\d+):(\d+)/);
|
|
303
|
+
if (simpleMatch) {
|
|
304
|
+
filePath = simpleMatch[1];
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
// 현재 stack trace 캡처
|
|
310
|
+
const err = new Error();
|
|
311
|
+
stackTrace = err.stack;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
this.log('ERROR', message, {
|
|
315
|
+
...metadata,
|
|
316
|
+
stack_trace: stackTrace,
|
|
317
|
+
error_type: errorType,
|
|
318
|
+
function_name: functionName,
|
|
319
|
+
file_path: filePath
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* 수동 flush - 큐에 있는 모든 로그 즉시 전송
|
|
325
|
+
*/
|
|
326
|
+
flush() {
|
|
327
|
+
if (this.worker) {
|
|
328
|
+
this.worker.postMessage({ type: 'flush' });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* 클라이언트 종료
|
|
334
|
+
*/
|
|
335
|
+
close() {
|
|
336
|
+
if (this.worker) {
|
|
337
|
+
this.flush();
|
|
338
|
+
this.worker.terminate();
|
|
339
|
+
this.worker = null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// 편의 메서드
|
|
344
|
+
trace(message, metadata = {}) { this._logWithCallerAdjustment('TRACE', message, metadata); }
|
|
345
|
+
debug(message, metadata = {}) { this._logWithCallerAdjustment('DEBUG', message, metadata); }
|
|
346
|
+
info(message, metadata = {}) { this._logWithCallerAdjustment('INFO', message, metadata); }
|
|
347
|
+
warn(message, metadata = {}) { this._logWithCallerAdjustment('WARN', message, metadata); }
|
|
348
|
+
error(message, metadata = {}) { this._logWithCallerAdjustment('ERROR', message, metadata); }
|
|
349
|
+
fatal(message, metadata = {}) { this._logWithCallerAdjustment('FATAL', message, metadata); }
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 편의 메서드를 위한 로그 호출 (호출자 스택 조정)
|
|
353
|
+
* 편의 메서드를 통해 호출되므로 한 단계 위의 스택을 추적
|
|
354
|
+
*/
|
|
355
|
+
_logWithCallerAdjustment(level, message, metadata = {}) {
|
|
356
|
+
if (metadata.autoCaller !== false && !metadata.function_name) {
|
|
357
|
+
try {
|
|
358
|
+
const stack = new Error().stack;
|
|
359
|
+
const stackLines = stack.split('\n');
|
|
360
|
+
const callerLine = stackLines[3];
|
|
361
|
+
|
|
362
|
+
if (callerLine) {
|
|
363
|
+
let match = callerLine.match(/at\s+([^\s]+)\s+\(([^:]+):\d+:\d+\)/);
|
|
364
|
+
if (match) {
|
|
365
|
+
metadata.function_name = metadata.function_name || match[1];
|
|
366
|
+
metadata.file_path = metadata.file_path || match[2];
|
|
367
|
+
} else {
|
|
368
|
+
match = callerLine.match(/at\s+([^:]+):\d+:\d+/);
|
|
369
|
+
if (match) {
|
|
370
|
+
metadata.file_path = metadata.file_path || match[1];
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
} catch (err) {
|
|
375
|
+
// 스택 추출 실패 시 무시
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// autoCaller를 false로 설정해서 log()에서 중복 추출 방지
|
|
380
|
+
this.log(level, message, { ...metadata, autoCaller: false });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 브라우저 Web Worker 스크립트
|
|
3
|
+
*
|
|
4
|
+
* 백그라운드에서 실행되어 메인 스레드에 영향 없이 로그 전송
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
let queue = [];
|
|
8
|
+
let serverUrl = '';
|
|
9
|
+
let batchSize = 1000;
|
|
10
|
+
let flushInterval = 1000;
|
|
11
|
+
let enableCompression = true;
|
|
12
|
+
let maxQueueSize = 10000;
|
|
13
|
+
let flushTimer = null;
|
|
14
|
+
|
|
15
|
+
// 메인 스레드로부터 메시지 수신
|
|
16
|
+
self.onmessage = (event) => {
|
|
17
|
+
const { type, data } = event.data;
|
|
18
|
+
|
|
19
|
+
switch (type) {
|
|
20
|
+
case 'init':
|
|
21
|
+
// 초기화
|
|
22
|
+
serverUrl = event.data.serverUrl;
|
|
23
|
+
batchSize = event.data.batchSize || 1000;
|
|
24
|
+
flushInterval = event.data.flushInterval || 1000;
|
|
25
|
+
enableCompression = event.data.enableCompression !== false;
|
|
26
|
+
startFlushLoop();
|
|
27
|
+
break;
|
|
28
|
+
|
|
29
|
+
case 'log':
|
|
30
|
+
// 로그 추가
|
|
31
|
+
queue.push(data);
|
|
32
|
+
|
|
33
|
+
// 큐 크기 제한
|
|
34
|
+
if (queue.length > maxQueueSize) {
|
|
35
|
+
queue.shift(); // 오래된 로그 제거
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 배치 크기 도달 시 즉시 전송
|
|
39
|
+
if (queue.length >= batchSize) {
|
|
40
|
+
sendBatch();
|
|
41
|
+
}
|
|
42
|
+
break;
|
|
43
|
+
|
|
44
|
+
case 'flush':
|
|
45
|
+
// 강제 flush
|
|
46
|
+
if (queue.length > 0) {
|
|
47
|
+
sendBatch();
|
|
48
|
+
}
|
|
49
|
+
break;
|
|
50
|
+
|
|
51
|
+
default:
|
|
52
|
+
console.warn('[Worker] Unknown message type:', type);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 주기적 flush 루프 시작
|
|
58
|
+
*/
|
|
59
|
+
function startFlushLoop() {
|
|
60
|
+
if (flushTimer) {
|
|
61
|
+
clearInterval(flushTimer);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
flushTimer = setInterval(() => {
|
|
65
|
+
if (queue.length > 0 && queue.length < batchSize) {
|
|
66
|
+
// 배치 크기에 도달하지 않았지만 시간이 지나면 전송
|
|
67
|
+
sendBatch();
|
|
68
|
+
}
|
|
69
|
+
}, flushInterval);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 배치 전송
|
|
74
|
+
*/
|
|
75
|
+
async function sendBatch() {
|
|
76
|
+
if (queue.length === 0) return;
|
|
77
|
+
|
|
78
|
+
// 큐에서 배치 추출
|
|
79
|
+
const batch = queue.splice(0, Math.min(batchSize, queue.length));
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
// JSON 직렬화
|
|
83
|
+
let payload = JSON.stringify({ logs: batch });
|
|
84
|
+
let headers = { 'Content-Type': 'application/json' };
|
|
85
|
+
|
|
86
|
+
// 압축 (100건 이상)
|
|
87
|
+
if (enableCompression && batch.length >= 100) {
|
|
88
|
+
// 브라우저에서는 CompressionStream API 사용 (Chrome 80+)
|
|
89
|
+
if (typeof CompressionStream !== 'undefined') {
|
|
90
|
+
const stream = new Response(payload).body
|
|
91
|
+
.pipeThrough(new CompressionStream('gzip'));
|
|
92
|
+
payload = await new Response(stream).blob();
|
|
93
|
+
headers['Content-Encoding'] = 'gzip';
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// HTTP POST
|
|
98
|
+
const response = await fetch(`${serverUrl}/logs`, {
|
|
99
|
+
method: 'POST',
|
|
100
|
+
headers: headers,
|
|
101
|
+
body: payload
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (!response.ok) {
|
|
105
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('[Worker] Log send failed:', error);
|
|
110
|
+
// 실패한 로그는 큐 맨 앞에 다시 추가 (재시도)
|
|
111
|
+
queue.unshift(...batch);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Worker 에러 핸들링
|
|
116
|
+
self.onerror = (error) => {
|
|
117
|
+
console.error('[Worker] Error:', error);
|
|
118
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 로그 수집 클라이언트 - 진입점
|
|
3
|
+
*
|
|
4
|
+
* 환경에 따라 최적 구현 자동 선택:
|
|
5
|
+
* - 브라우저: Web Worker (렉 0%)
|
|
6
|
+
* - Node.js: Worker Threads
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { WorkerThreadsLogClient } from './node-client.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 로그 클라이언트 생성
|
|
13
|
+
* @param {string} serverUrl - 로그 서버 URL
|
|
14
|
+
* @param {Object} options - 옵션
|
|
15
|
+
* @returns {Object} 로그 클라이언트 인스턴스
|
|
16
|
+
*/
|
|
17
|
+
export function createLogClient(serverUrl, options = {}) {
|
|
18
|
+
// 브라우저 환경은 번들러를 통해 처리됨
|
|
19
|
+
// Node.js 환경
|
|
20
|
+
if (typeof process !== 'undefined' && process.versions && process.versions.node) {
|
|
21
|
+
return new WorkerThreadsLogClient(serverUrl, options);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
throw new Error('Unsupported environment. Requires Node.js 12+ or browser with bundler');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// 기본 내보내기
|
|
29
|
+
export default { createLogClient };
|