log-collector-async 1.1.0__py3-none-any.whl

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,10 @@
1
+ log_collector/__init__.py,sha256=mMKUApIhihjC2dUpFqDIETlsv6fyTHSW3Wi-xmr9y6Q,235
2
+ log_collector/async_client.py,sha256=5C7GMpleOWyLc66MMfGtORHIiOSB-cb7lW8mTsCRyw0,23029
3
+ tests/__init__.py,sha256=nIjtuR9Yjo1I5YSO71TTufw2_pFXvJn0rKNEorpYaJE,23
4
+ tests/test_async_client.py,sha256=j-dtf9ayL3kaVctYA9IOHsyFzj57a-xRS1i_Az8MfAg,10155
5
+ tests/test_integration.py,sha256=5dF5GHZBtiyDWZjQBDSFzZ_ZvfiESBb0sohGMx_F8iw,12318
6
+ tests/test_performance.py,sha256=zXMNz4QjDgXgU1i_5T-n_BeDb3NI6dafKva1-DfLUBA,4852
7
+ log_collector_async-1.1.0.dist-info/METADATA,sha256=IoGHUgWjsOlb1XCQSnpMteZpFvd4vx1D1a_N-0Wy1SI,22325
8
+ log_collector_async-1.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ log_collector_async-1.1.0.dist-info/top_level.txt,sha256=4fOfJ7egtA7Xk4uHLPZt1hEfWk7w2i3DdkXWskkcPag,20
10
+ log_collector_async-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ log_collector
2
+ tests
tests/__init__.py ADDED
@@ -0,0 +1 @@
1
+ # Python tests package
@@ -0,0 +1,292 @@
1
+ """
2
+ 단위 테스트: AsyncLogClient 기본 동작 검증
3
+ 로그 서버 없이도 실행 가능한 테스트
4
+ """
5
+ import pytest
6
+ import time
7
+ from log_collector import AsyncLogClient
8
+
9
+
10
+ def test_client_initialization():
11
+ """클라이언트 초기화 테스트"""
12
+ client = AsyncLogClient("http://localhost:8000")
13
+ assert client is not None
14
+ assert client.server_url == "http://localhost:8000"
15
+
16
+
17
+ def test_log_queueing():
18
+ """로그 큐잉 테스트 (블로킹 없음)"""
19
+ client = AsyncLogClient("http://localhost:8000", batch_size=1000)
20
+
21
+ start = time.time()
22
+ client.log("INFO", "test message", test_id="queue_test")
23
+ elapsed = time.time() - start
24
+
25
+ # 앱 블로킹 < 0.001초 확인
26
+ assert elapsed < 0.001, f"Blocking time {elapsed}s exceeded 0.001s"
27
+
28
+
29
+ def test_batch_size_option():
30
+ """배치 크기 옵션 테스트"""
31
+ client = AsyncLogClient("http://localhost:8000", batch_size=500)
32
+ assert client.batch_size == 500
33
+
34
+
35
+ def test_flush_interval_option():
36
+ """Flush 간격 옵션 테스트"""
37
+ client = AsyncLogClient("http://localhost:8000", flush_interval=2.0)
38
+ assert client.flush_interval == 2.0
39
+
40
+
41
+ def test_multiple_log_levels():
42
+ """다양한 로그 레벨 테스트"""
43
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
44
+
45
+ # 다양한 레벨 로그 생성
46
+ client.log("INFO", "info message")
47
+ client.log("WARN", "warning message")
48
+ client.log("ERROR", "error message")
49
+ client.log("DEBUG", "debug message")
50
+ client.log("FATAL", "fatal message")
51
+
52
+ # 에러 없이 실행되어야 함
53
+ assert True
54
+
55
+
56
+ def test_metadata_parameters():
57
+ """메타데이터 전달 테스트"""
58
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
59
+
60
+ # 임의의 메타데이터 전달
61
+ client.log("INFO", "test", user_id=123, action="login", success=True)
62
+
63
+ # 에러 없이 실행되어야 함
64
+ assert True
65
+
66
+
67
+ def test_service_name_option():
68
+ """서비스 이름 옵션 테스트"""
69
+ client = AsyncLogClient("http://localhost:8000", service="test-service")
70
+ # AsyncLogClient가 service 파라미터를 지원한다면 검증
71
+ # 현재 구현에서는 로그 호출 시 service를 전달
72
+ client.log("INFO", "test", service="custom-service")
73
+ assert True
74
+
75
+
76
+ def test_auto_caller_enabled():
77
+ """호출 위치 자동 추적 테스트 (auto_caller=True)"""
78
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
79
+
80
+ # 로그 호출
81
+ client.info("Test auto caller")
82
+
83
+ # 큐에서 로그 확인
84
+ assert len(client.queue) > 0
85
+ log_entry = client.queue[-1]
86
+
87
+ # function_name, file_path가 자동으로 포함되었는지 확인
88
+ assert "function_name" in log_entry
89
+ assert "file_path" in log_entry
90
+
91
+ # 함수명이 현재 테스트 함수명과 일치하는지 확인
92
+ assert log_entry["function_name"] == "test_auto_caller_enabled"
93
+
94
+ # 파일 경로에 test_async_client.py가 포함되는지 확인
95
+ assert "test_async_client.py" in log_entry["file_path"]
96
+
97
+
98
+ def test_auto_caller_disabled():
99
+ """호출 위치 자동 추적 비활성화 테스트 (auto_caller=False)"""
100
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
101
+
102
+ # auto_caller=False로 로그 호출
103
+ client.log("INFO", "Test without auto caller", auto_caller=False)
104
+
105
+ # 큐에서 로그 확인
106
+ assert len(client.queue) > 0
107
+ log_entry = client.queue[-1]
108
+
109
+ # function_name이 자동으로 추가되지 않았는지 확인
110
+ # (수동으로 전달하지 않았으므로 없어야 함)
111
+ assert "function_name" not in log_entry or log_entry["function_name"] is None
112
+
113
+
114
+ def test_auto_caller_manual_override():
115
+ """호출 위치 수동 재정의 테스트"""
116
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
117
+
118
+ # 수동으로 function_name 전달 (자동 추적보다 우선)
119
+ client.info("Test manual override", function_name="custom_function", file_path="/custom/path.py")
120
+
121
+ # 큐에서 로그 확인
122
+ assert len(client.queue) > 0
123
+ log_entry = client.queue[-1]
124
+
125
+ # 수동으로 전달한 값이 사용되었는지 확인
126
+ assert log_entry["function_name"] == "custom_function"
127
+ assert log_entry["file_path"] == "/custom/path.py"
128
+
129
+
130
+ def test_convenience_methods_auto_caller():
131
+ """편의 메서드(info, debug 등)에서 호출 위치 자동 추적 테스트"""
132
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
133
+
134
+ # 여러 편의 메서드 테스트
135
+ client.trace("Trace message")
136
+ client.debug("Debug message")
137
+ client.info("Info message")
138
+ client.warn("Warn message")
139
+ client.error("Error message")
140
+ client.fatal("Fatal message")
141
+
142
+ # 모든 로그가 큐에 추가되었는지 확인
143
+ assert len(client.queue) == 6
144
+
145
+ # 각 로그에 function_name이 포함되었는지 확인
146
+ for log_entry in client.queue:
147
+ assert "function_name" in log_entry
148
+ # 모든 로그가 이 테스트 함수에서 호출되었으므로 함수명 일치
149
+ assert log_entry["function_name"] == "test_convenience_methods_auto_caller"
150
+
151
+
152
+ def test_nested_function_auto_caller():
153
+ """중첩 함수에서 호출 위치 자동 추적 테스트"""
154
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
155
+
156
+ def inner_function():
157
+ client.info("Message from inner function")
158
+
159
+ # 내부 함수 호출
160
+ inner_function()
161
+
162
+ # 큐에서 로그 확인
163
+ assert len(client.queue) > 0
164
+ log_entry = client.queue[-1]
165
+
166
+ # function_name이 inner_function인지 확인
167
+ assert log_entry["function_name"] == "inner_function"
168
+
169
+
170
+ # Feature 3: 사용자 컨텍스트 관리 테스트
171
+ def test_user_context_enabled():
172
+ """사용자 컨텍스트 자동 포함 테스트"""
173
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
174
+
175
+ # 컨텍스트 없이 로그
176
+ client.info("Without context")
177
+ log_without_context = client.queue[-1]
178
+ assert "user_id" not in log_without_context
179
+ assert "trace_id" not in log_without_context
180
+
181
+ # 컨텍스트 있이 로그
182
+ with AsyncLogClient.user_context(user_id="user_123", trace_id="trace_xyz"):
183
+ client.info("With context")
184
+ log_with_context = client.queue[-1]
185
+
186
+ # user_id, trace_id가 자동으로 포함되었는지 확인
187
+ assert log_with_context["user_id"] == "user_123"
188
+ assert log_with_context["trace_id"] == "trace_xyz"
189
+ assert log_with_context["message"] == "With context"
190
+
191
+
192
+ def test_user_context_clear():
193
+ """사용자 컨텍스트 초기화 테스트"""
194
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
195
+
196
+ # with 블록 내에서 컨텍스트 포함
197
+ with AsyncLogClient.user_context(user_id="user_456"):
198
+ client.info("Inside context")
199
+ assert client.queue[-1]["user_id"] == "user_456"
200
+
201
+ # with 블록 벗어나면 컨텍스트 자동 초기화
202
+ client.info("Outside context")
203
+ log_outside = client.queue[-1]
204
+ assert "user_id" not in log_outside
205
+
206
+
207
+ def test_user_context_set_clear():
208
+ """set/clear 방식 사용자 컨텍스트 테스트"""
209
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
210
+
211
+ # 컨텍스트 설정
212
+ AsyncLogClient.set_user_context(user_id="user_789", session_id="sess_abc")
213
+
214
+ client.info("After set")
215
+ log_after_set = client.queue[-1]
216
+ assert log_after_set["user_id"] == "user_789"
217
+ assert log_after_set["session_id"] == "sess_abc"
218
+
219
+ # 컨텍스트 초기화
220
+ AsyncLogClient.clear_user_context()
221
+
222
+ client.info("After clear")
223
+ log_after_clear = client.queue[-1]
224
+ assert "user_id" not in log_after_clear
225
+ assert "session_id" not in log_after_clear
226
+
227
+
228
+ def test_user_context_nested():
229
+ """중첩 사용자 컨텍스트 테스트"""
230
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
231
+
232
+ # 외부 컨텍스트: tenant_id
233
+ with AsyncLogClient.user_context(tenant_id="tenant_1"):
234
+ client.info("Outer context")
235
+ outer_log = client.queue[-1]
236
+ assert outer_log["tenant_id"] == "tenant_1"
237
+ assert "user_id" not in outer_log
238
+
239
+ # 내부 컨텍스트: user_id 추가
240
+ with AsyncLogClient.user_context(user_id="user_999"):
241
+ client.info("Inner context")
242
+ inner_log = client.queue[-1]
243
+ # tenant_id와 user_id 둘 다 포함되어야 함
244
+ assert inner_log["tenant_id"] == "tenant_1"
245
+ assert inner_log["user_id"] == "user_999"
246
+
247
+ # 내부 컨텍스트 벗어나면 user_id 없음
248
+ client.info("Back to outer")
249
+ back_to_outer = client.queue[-1]
250
+ assert back_to_outer["tenant_id"] == "tenant_1"
251
+ assert "user_id" not in back_to_outer
252
+
253
+
254
+ def test_user_context_with_http_context():
255
+ """HTTP 컨텍스트와 사용자 컨텍스트 함께 사용 테스트"""
256
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
257
+
258
+ # HTTP 컨텍스트 설정
259
+ AsyncLogClient.set_request_context(path="/api/users", method="GET", ip="127.0.0.1")
260
+
261
+ # 사용자 컨텍스트 설정
262
+ with AsyncLogClient.user_context(user_id="user_combo", trace_id="trace_combo"):
263
+ client.info("Combined contexts")
264
+ log_entry = client.queue[-1]
265
+
266
+ # HTTP 컨텍스트 필드
267
+ assert log_entry["path"] == "/api/users"
268
+ assert log_entry["method"] == "GET"
269
+ assert log_entry["ip"] == "127.0.0.1"
270
+
271
+ # 사용자 컨텍스트 필드
272
+ assert log_entry["user_id"] == "user_combo"
273
+ assert log_entry["trace_id"] == "trace_combo"
274
+
275
+ # 기본 필드
276
+ assert log_entry["message"] == "Combined contexts"
277
+
278
+ # 정리
279
+ AsyncLogClient.clear_request_context()
280
+
281
+
282
+ def test_user_context_manual_override():
283
+ """사용자 컨텍스트보다 수동 값이 우선하는지 테스트"""
284
+ client = AsyncLogClient("http://localhost:8000", batch_size=100)
285
+
286
+ with AsyncLogClient.user_context(user_id="auto_user"):
287
+ # 수동으로 user_id 전달
288
+ client.info("Manual override", user_id="manual_user")
289
+ log_entry = client.queue[-1]
290
+
291
+ # 수동 값이 우선해야 함
292
+ assert log_entry["user_id"] == "manual_user"
@@ -0,0 +1,372 @@
1
+ """
2
+ 통합 테스트: 로그 서버와의 E2E 검증
3
+ 실행 전 요구사항:
4
+ 1. PostgreSQL 실행 (localhost:5432)
5
+ 2. 로그 서버 실행 (localhost:8000)
6
+ 3. 스키마 생성 완료
7
+ """
8
+ import pytest
9
+ import time
10
+ import requests
11
+ from log_collector import AsyncLogClient
12
+
13
+
14
+ @pytest.fixture
15
+ def log_server_url():
16
+ """로그 서버 URL (서버 실행 필요)"""
17
+ return "http://localhost:8000"
18
+
19
+
20
+ @pytest.fixture
21
+ def check_server_running(log_server_url):
22
+ """로그 서버 실행 여부 확인"""
23
+ try:
24
+ response = requests.get(log_server_url, timeout=1)
25
+ if response.status_code != 200:
26
+ pytest.skip("로그 서버가 실행되지 않았습니다")
27
+ except requests.exceptions.RequestException:
28
+ pytest.skip("로그 서버가 실행되지 않았습니다")
29
+
30
+
31
+ def test_end_to_end_logging(log_server_url, check_server_running):
32
+ """E2E: 로그 전송 → 서버 수신 → DB 저장"""
33
+ client = AsyncLogClient(log_server_url, batch_size=5, flush_interval=1.0)
34
+
35
+ # 5개 로그 전송
36
+ for i in range(5):
37
+ client.log("INFO", f"Integration test {i}", test_id="e2e_test", iteration=i)
38
+
39
+ # Flush 대기
40
+ time.sleep(2)
41
+
42
+ # 서버 통계 확인
43
+ response = requests.get(f"{log_server_url}/stats")
44
+ assert response.status_code == 200
45
+
46
+ stats = response.json()
47
+ assert stats['total_logs'] >= 5, f"Expected >= 5 logs, got {stats['total_logs']}"
48
+
49
+
50
+ def test_batch_sending(log_server_url, check_server_running):
51
+ """배치 전송 테스트: 10개 로그 → 자동 배치 전송"""
52
+ client = AsyncLogClient(log_server_url, batch_size=10)
53
+
54
+ # 10개 로그 → 자동 배치 전송
55
+ for i in range(10):
56
+ client.log("INFO", f"Batch test {i}", test_id="batch_test", iteration=i)
57
+
58
+ # 배치 전송 대기
59
+ time.sleep(1.5)
60
+
61
+ # 서버 통계 확인
62
+ response = requests.get(f"{log_server_url}/stats")
63
+ assert response.status_code == 200
64
+
65
+ stats = response.json()
66
+ assert stats['total_logs'] >= 10
67
+
68
+
69
+ def test_flush_interval(log_server_url, check_server_running):
70
+ """Flush 간격 테스트: 1초 후 자동 전송"""
71
+ client = AsyncLogClient(log_server_url, batch_size=1000, flush_interval=1.0)
72
+
73
+ # 5개만 보내기 (배치 크기 미달)
74
+ for i in range(5):
75
+ client.log("INFO", f"Flush interval test {i}", test_id="flush_test")
76
+
77
+ # 1초 대기 → flush_interval에 의해 자동 전송되어야 함
78
+ time.sleep(1.5)
79
+
80
+ # 서버 통계 확인
81
+ response = requests.get(f"{log_server_url}/stats")
82
+ assert response.status_code == 200
83
+
84
+
85
+ def test_manual_flush(log_server_url, check_server_running):
86
+ """수동 flush 테스트"""
87
+ client = AsyncLogClient(log_server_url, batch_size=1000, flush_interval=60)
88
+
89
+ # 3개 로그 (배치 크기 미달, flush_interval도 길음)
90
+ for i in range(3):
91
+ client.log("INFO", f"Manual flush test {i}", test_id="manual_flush_test")
92
+
93
+ # 수동 flush 호출
94
+ client.flush()
95
+
96
+ # 약간의 대기
97
+ time.sleep(0.5)
98
+
99
+ # 서버 통계 확인
100
+ response = requests.get(f"{log_server_url}/stats")
101
+ assert response.status_code == 200
102
+
103
+
104
+ def test_multiple_services(log_server_url, check_server_running):
105
+ """여러 서비스의 로그 동시 전송"""
106
+ services = ["auth-service", "api-service", "db-service", "cache-service"]
107
+
108
+ client = AsyncLogClient(log_server_url, batch_size=20)
109
+
110
+ # 각 서비스에서 5개씩 로그 전송
111
+ for service in services:
112
+ for i in range(5):
113
+ client.log(
114
+ "INFO",
115
+ f"Log from {service} #{i}",
116
+ service=service,
117
+ test_id="multi_service_test"
118
+ )
119
+
120
+ # 배치 전송 대기
121
+ time.sleep(2)
122
+
123
+ # 서버 통계 확인
124
+ response = requests.get(f"{log_server_url}/stats")
125
+ assert response.status_code == 200
126
+ stats = response.json()
127
+ assert stats['total_logs'] >= 20
128
+
129
+
130
+ def test_error_logging(log_server_url, check_server_running):
131
+ """에러 로그 전송 테스트"""
132
+ client = AsyncLogClient(log_server_url, batch_size=5)
133
+
134
+ # 에러 로그 전송
135
+ for i in range(5):
136
+ client.log(
137
+ "ERROR",
138
+ f"Database connection failed: attempt {i}",
139
+ error_code="DB_CONN_ERR",
140
+ service="db-service",
141
+ test_id="error_test"
142
+ )
143
+
144
+ time.sleep(1.5)
145
+
146
+ # 서버 통계 확인
147
+ response = requests.get(f"{log_server_url}/stats")
148
+ assert response.status_code == 200
149
+
150
+ stats = response.json()
151
+ # 레벨별 분포에 ERROR가 있어야 함
152
+ assert 'level_distribution' in stats
153
+ assert any(item['level'] == 'ERROR' for item in stats['level_distribution'])
154
+
155
+
156
+ def test_auto_caller_integration(log_server_url, check_server_running):
157
+ """호출 위치 자동 추적 통합 테스트 (E2E)"""
158
+ client = AsyncLogClient(log_server_url, batch_size=5, flush_interval=1.0)
159
+
160
+ # auto_caller 활성화된 상태로 로그 전송
161
+ client.info("Auto caller test - line 1", test_id="auto_caller_integration")
162
+ client.debug("Auto caller test - line 2", test_id="auto_caller_integration")
163
+ client.warn("Auto caller test - line 3", test_id="auto_caller_integration")
164
+
165
+ # 편의 메서드를 통한 로그도 테스트
166
+ def helper_function():
167
+ client.info("Message from helper function", test_id="auto_caller_integration")
168
+
169
+ helper_function()
170
+
171
+ # Flush 대기
172
+ time.sleep(2)
173
+
174
+ # 서버 통계 확인 (에러 없이 전송되었는지)
175
+ response = requests.get(f"{log_server_url}/stats")
176
+ assert response.status_code == 200
177
+
178
+ stats = response.json()
179
+ assert stats['total_logs'] >= 4
180
+
181
+ # Note: function_name, file_path가 실제로 DB에 저장되었는지는
182
+ # PostgreSQL 직접 쿼리로 확인 필요:
183
+ # SELECT function_name, file_path, message
184
+ # FROM logs
185
+ # WHERE metadata->>'test_id' = 'auto_caller_integration';
186
+
187
+
188
+ def test_auto_caller_disabled_integration(log_server_url, check_server_running):
189
+ """호출 위치 자동 추적 비활성화 통합 테스트"""
190
+ client = AsyncLogClient(log_server_url, batch_size=5, flush_interval=1.0)
191
+
192
+ # auto_caller=False로 로그 전송
193
+ client.log(
194
+ "INFO",
195
+ "Auto caller disabled test",
196
+ auto_caller=False,
197
+ test_id="auto_caller_disabled"
198
+ )
199
+
200
+ # Flush 대기
201
+ time.sleep(2)
202
+
203
+ # 서버 통계 확인 (에러 없이 전송되었는지)
204
+ response = requests.get(f"{log_server_url}/stats")
205
+ assert response.status_code == 200
206
+
207
+
208
+ def test_timer_with_auto_caller(log_server_url, check_server_running):
209
+ """타이머 기능과 auto_caller 통합 테스트"""
210
+ client = AsyncLogClient(log_server_url, batch_size=5, flush_interval=1.0)
211
+
212
+ # 타이머 사용 시에도 auto_caller가 동작해야 함
213
+ timer = client.start_timer()
214
+ time.sleep(0.1) # 작업 시뮬레이션
215
+ client.end_timer(timer, "INFO", "Timer test completed", test_id="timer_auto_caller")
216
+
217
+ # 컨텍스트 매니저 타이머
218
+ with client.timer("Context manager timer test"):
219
+ time.sleep(0.05)
220
+
221
+ # Flush 대기
222
+ time.sleep(2)
223
+
224
+ # 서버 통계 확인
225
+ response = requests.get(f"{log_server_url}/stats")
226
+ assert response.status_code == 200
227
+
228
+
229
+ def test_error_with_trace_integration(log_server_url, check_server_running):
230
+ """error_with_trace와 auto_caller 통합 테스트"""
231
+ client = AsyncLogClient(log_server_url, batch_size=5, flush_interval=1.0)
232
+
233
+ # 예외 발생 시뮬레이션
234
+ try:
235
+ raise ValueError("Test exception for integration test")
236
+ except Exception as e:
237
+ client.error_with_trace(
238
+ "Integration test exception",
239
+ exception=e,
240
+ test_id="error_trace_integration"
241
+ )
242
+
243
+ # Flush 대기
244
+ time.sleep(2)
245
+
246
+ # 서버 통계 확인
247
+ response = requests.get(f"{log_server_url}/stats")
248
+ assert response.status_code == 200
249
+
250
+ # Note: stack_trace, error_type, function_name, file_path가 DB에 저장되었는지는
251
+ # PostgreSQL 직접 쿼리로 확인:
252
+ # SELECT stack_trace, error_type, function_name, file_path
253
+ # FROM logs
254
+ # WHERE metadata->>'test_id' = 'error_trace_integration';
255
+
256
+
257
+ # Feature 3: 사용자 컨텍스트 관리 통합 테스트
258
+ def test_user_context_integration(log_server_url, check_server_running):
259
+ """사용자 컨텍스트 자동 포함 통합 테스트 (E2E)"""
260
+ client = AsyncLogClient(log_server_url, batch_size=10, flush_interval=1.0)
261
+
262
+ # 컨텍스트 없이 로그
263
+ client.info("Without user context", test_id="user_context_integration")
264
+
265
+ # 컨텍스트 있이 로그
266
+ with AsyncLogClient.user_context(
267
+ user_id="integration_user_123",
268
+ trace_id="integration_trace_xyz"
269
+ ):
270
+ client.info("With user context 1", test_id="user_context_integration")
271
+ client.info("With user context 2", test_id="user_context_integration")
272
+ client.warn("User context warning", test_id="user_context_integration")
273
+
274
+ # 컨텍스트 밖에서 다시 로그
275
+ client.info("After user context", test_id="user_context_integration")
276
+
277
+ # Flush 대기
278
+ time.sleep(2)
279
+
280
+ # 서버 통계 확인 (에러 없이 전송되었는지)
281
+ response = requests.get(f"{log_server_url}/stats")
282
+ assert response.status_code == 200
283
+
284
+ stats = response.json()
285
+ assert stats['total_logs'] >= 5
286
+
287
+ # Note: user_id, trace_id가 실제로 DB에 저장되었는지는
288
+ # PostgreSQL 직접 쿼리로 확인 필요:
289
+ # SELECT
290
+ # message,
291
+ # metadata->>'user_id' as user_id,
292
+ # metadata->>'trace_id' as trace_id
293
+ # FROM logs
294
+ # WHERE metadata->>'test_id' = 'user_context_integration'
295
+ # ORDER BY created_at;
296
+
297
+
298
+ def test_user_context_with_http_context_integration(log_server_url, check_server_running):
299
+ """HTTP 컨텍스트와 사용자 컨텍스트 함께 사용 통합 테스트"""
300
+ client = AsyncLogClient(log_server_url, batch_size=10, flush_interval=1.0)
301
+
302
+ # HTTP 컨텍스트 설정
303
+ AsyncLogClient.set_request_context(
304
+ path="/api/test",
305
+ method="POST",
306
+ ip="192.168.1.100"
307
+ )
308
+
309
+ # 사용자 컨텍스트와 함께 사용
310
+ with AsyncLogClient.user_context(
311
+ user_id="combined_user_456",
312
+ trace_id="combined_trace_abc",
313
+ session_id="combined_session_xyz"
314
+ ):
315
+ client.info("Combined contexts test 1", test_id="combined_contexts_integration")
316
+ client.debug("Combined contexts test 2", test_id="combined_contexts_integration")
317
+
318
+ # 정리
319
+ AsyncLogClient.clear_request_context()
320
+
321
+ # Flush 대기
322
+ time.sleep(2)
323
+
324
+ # 서버 통계 확인
325
+ response = requests.get(f"{log_server_url}/stats")
326
+ assert response.status_code == 200
327
+
328
+ stats = response.json()
329
+ assert stats['total_logs'] >= 2
330
+
331
+ # Note: HTTP와 User 컨텍스트가 모두 DB에 저장되었는지 확인:
332
+ # SELECT
333
+ # message,
334
+ # metadata->>'path' as path,
335
+ # metadata->>'method' as method,
336
+ # metadata->>'ip' as ip,
337
+ # metadata->>'user_id' as user_id,
338
+ # metadata->>'trace_id' as trace_id,
339
+ # metadata->>'session_id' as session_id
340
+ # FROM logs
341
+ # WHERE metadata->>'test_id' = 'combined_contexts_integration';
342
+
343
+
344
+ def test_user_context_nested_integration(log_server_url, check_server_running):
345
+ """중첩 사용자 컨텍스트 통합 테스트"""
346
+ client = AsyncLogClient(log_server_url, batch_size=10, flush_interval=1.0)
347
+
348
+ # 외부 컨텍스트
349
+ with AsyncLogClient.user_context(tenant_id="tenant_integration"):
350
+ client.info("Outer context", test_id="nested_user_context_integration")
351
+
352
+ # 내부 컨텍스트
353
+ with AsyncLogClient.user_context(user_id="nested_user_789"):
354
+ client.info("Inner context (both)", test_id="nested_user_context_integration")
355
+
356
+ client.info("Back to outer", test_id="nested_user_context_integration")
357
+
358
+ # Flush 대기
359
+ time.sleep(2)
360
+
361
+ # 서버 통계 확인
362
+ response = requests.get(f"{log_server_url}/stats")
363
+ assert response.status_code == 200
364
+
365
+ # Note: 중첩 컨텍스트가 올바르게 저장되었는지 확인:
366
+ # SELECT
367
+ # message,
368
+ # metadata->>'tenant_id' as tenant_id,
369
+ # metadata->>'user_id' as user_id
370
+ # FROM logs
371
+ # WHERE metadata->>'test_id' = 'nested_user_context_integration'
372
+ # ORDER BY created_at;