main
py 369 lines 12.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Tests for Uvicorn logging configuration and access log filtering."""
4
5 import logging
6
7 from vllm.logging_utils.uvicorn_logging import (
8 UvicornAccessLogFilter,
9 create_uvicorn_log_config,
10 )
11
12
13 class TestUvicornAccessLogFilter:
14 """Test cases for UvicornAccessLogFilter."""
15
16 def test_filter_allows_all_when_no_excluded_paths(self):
17 """Filter should allow all logs when no paths are excluded."""
18 filter = UvicornAccessLogFilter(excluded_paths=[])
19
20 record = logging.LogRecord(
21 name="uvicorn.access",
22 level=logging.INFO,
23 pathname="",
24 lineno=0,
25 msg='%s - "%s %s HTTP/%s" %d',
26 args=("127.0.0.1:12345", "GET", "/v1/completions", "1.1", 200),
27 exc_info=None,
28 )
29
30 assert filter.filter(record) is True
31
32 def test_filter_allows_all_when_excluded_paths_is_none(self):
33 """Filter should allow all logs when excluded_paths is None."""
34 filter = UvicornAccessLogFilter(excluded_paths=None)
35
36 record = logging.LogRecord(
37 name="uvicorn.access",
38 level=logging.INFO,
39 pathname="",
40 lineno=0,
41 msg='%s - "%s %s HTTP/%s" %d',
42 args=("127.0.0.1:12345", "GET", "/health", "1.1", 200),
43 exc_info=None,
44 )
45
46 assert filter.filter(record) is True
47
48 def test_filter_excludes_health_endpoint(self):
49 """Filter should exclude /health endpoint when configured."""
50 filter = UvicornAccessLogFilter(excluded_paths=["/health"])
51
52 record = logging.LogRecord(
53 name="uvicorn.access",
54 level=logging.INFO,
55 pathname="",
56 lineno=0,
57 msg='%s - "%s %s HTTP/%s" %d',
58 args=("127.0.0.1:12345", "GET", "/health", "1.1", 200),
59 exc_info=None,
60 )
61
62 assert filter.filter(record) is False
63
64 def test_filter_excludes_metrics_endpoint(self):
65 """Filter should exclude /metrics endpoint when configured."""
66 filter = UvicornAccessLogFilter(excluded_paths=["/metrics"])
67
68 record = logging.LogRecord(
69 name="uvicorn.access",
70 level=logging.INFO,
71 pathname="",
72 lineno=0,
73 msg='%s - "%s %s HTTP/%s" %d',
74 args=("127.0.0.1:12345", "GET", "/metrics", "1.1", 200),
75 exc_info=None,
76 )
77
78 assert filter.filter(record) is False
79
80 def test_filter_allows_non_excluded_endpoints(self):
81 """Filter should allow endpoints not in the excluded list."""
82 filter = UvicornAccessLogFilter(excluded_paths=["/health", "/metrics"])
83
84 record = logging.LogRecord(
85 name="uvicorn.access",
86 level=logging.INFO,
87 pathname="",
88 lineno=0,
89 msg='%s - "%s %s HTTP/%s" %d',
90 args=("127.0.0.1:12345", "POST", "/v1/completions", "1.1", 200),
91 exc_info=None,
92 )
93
94 assert filter.filter(record) is True
95
96 def test_filter_excludes_multiple_endpoints(self):
97 """Filter should exclude multiple configured endpoints."""
98 filter = UvicornAccessLogFilter(excluded_paths=["/health", "/metrics", "/ping"])
99
100 # Test /health
101 record_health = logging.LogRecord(
102 name="uvicorn.access",
103 level=logging.INFO,
104 pathname="",
105 lineno=0,
106 msg='%s - "%s %s HTTP/%s" %d',
107 args=("127.0.0.1:12345", "GET", "/health", "1.1", 200),
108 exc_info=None,
109 )
110 assert filter.filter(record_health) is False
111
112 # Test /metrics
113 record_metrics = logging.LogRecord(
114 name="uvicorn.access",
115 level=logging.INFO,
116 pathname="",
117 lineno=0,
118 msg='%s - "%s %s HTTP/%s" %d',
119 args=("127.0.0.1:12345", "GET", "/metrics", "1.1", 200),
120 exc_info=None,
121 )
122 assert filter.filter(record_metrics) is False
123
124 # Test /ping
125 record_ping = logging.LogRecord(
126 name="uvicorn.access",
127 level=logging.INFO,
128 pathname="",
129 lineno=0,
130 msg='%s - "%s %s HTTP/%s" %d',
131 args=("127.0.0.1:12345", "GET", "/ping", "1.1", 200),
132 exc_info=None,
133 )
134 assert filter.filter(record_ping) is False
135
136 def test_filter_with_query_parameters(self):
137 """Filter should exclude endpoints even with query parameters."""
138 filter = UvicornAccessLogFilter(excluded_paths=["/health"])
139
140 record = logging.LogRecord(
141 name="uvicorn.access",
142 level=logging.INFO,
143 pathname="",
144 lineno=0,
145 msg='%s - "%s %s HTTP/%s" %d',
146 args=("127.0.0.1:12345", "GET", "/health?verbose=true", "1.1", 200),
147 exc_info=None,
148 )
149
150 assert filter.filter(record) is False
151
152 def test_filter_different_http_methods(self):
153 """Filter should exclude endpoints regardless of HTTP method."""
154 filter = UvicornAccessLogFilter(excluded_paths=["/ping"])
155
156 # Test GET
157 record_get = logging.LogRecord(
158 name="uvicorn.access",
159 level=logging.INFO,
160 pathname="",
161 lineno=0,
162 msg='%s - "%s %s HTTP/%s" %d',
163 args=("127.0.0.1:12345", "GET", "/ping", "1.1", 200),
164 exc_info=None,
165 )
166 assert filter.filter(record_get) is False
167
168 # Test POST
169 record_post = logging.LogRecord(
170 name="uvicorn.access",
171 level=logging.INFO,
172 pathname="",
173 lineno=0,
174 msg='%s - "%s %s HTTP/%s" %d',
175 args=("127.0.0.1:12345", "POST", "/ping", "1.1", 200),
176 exc_info=None,
177 )
178 assert filter.filter(record_post) is False
179
180 def test_filter_with_different_status_codes(self):
181 """Filter should exclude endpoints regardless of status code."""
182 filter = UvicornAccessLogFilter(excluded_paths=["/health"])
183
184 for status_code in [200, 500, 503]:
185 record = logging.LogRecord(
186 name="uvicorn.access",
187 level=logging.INFO,
188 pathname="",
189 lineno=0,
190 msg='%s - "%s %s HTTP/%s" %d',
191 args=("127.0.0.1:12345", "GET", "/health", "1.1", status_code),
192 exc_info=None,
193 )
194 assert filter.filter(record) is False
195
196
197 class TestCreateUvicornLogConfig:
198 """Test cases for create_uvicorn_log_config function."""
199
200 def test_creates_valid_config_structure(self):
201 """Config should have required logging configuration keys."""
202 config = create_uvicorn_log_config(excluded_paths=["/health"])
203
204 assert "version" in config
205 assert config["version"] == 1
206 assert "disable_existing_loggers" in config
207 assert "formatters" in config
208 assert "handlers" in config
209 assert "loggers" in config
210 assert "filters" in config
211
212 def test_config_includes_access_log_filter(self):
213 """Config should include the access log filter."""
214 config = create_uvicorn_log_config(excluded_paths=["/health", "/metrics"])
215
216 assert "access_log_filter" in config["filters"]
217 filter_config = config["filters"]["access_log_filter"]
218 assert filter_config["()"] == UvicornAccessLogFilter
219 assert filter_config["excluded_paths"] == ["/health", "/metrics"]
220
221 def test_config_applies_filter_to_access_handler(self):
222 """Config should apply the filter to the access handler."""
223 config = create_uvicorn_log_config(excluded_paths=["/health"])
224
225 assert "access" in config["handlers"]
226 assert "filters" in config["handlers"]["access"]
227 assert "access_log_filter" in config["handlers"]["access"]["filters"]
228
229 def test_config_with_custom_log_level(self):
230 """Config should respect custom log level."""
231 config = create_uvicorn_log_config(
232 excluded_paths=["/health"], log_level="debug"
233 )
234
235 assert config["loggers"]["uvicorn"]["level"] == "DEBUG"
236 assert config["loggers"]["uvicorn.access"]["level"] == "DEBUG"
237 assert config["loggers"]["uvicorn.error"]["level"] == "DEBUG"
238
239 def test_config_with_empty_excluded_paths(self):
240 """Config should work with empty excluded paths."""
241 config = create_uvicorn_log_config(excluded_paths=[])
242
243 assert config["filters"]["access_log_filter"]["excluded_paths"] == []
244
245 def test_config_with_none_excluded_paths(self):
246 """Config should work with None excluded paths."""
247 config = create_uvicorn_log_config(excluded_paths=None)
248
249 assert config["filters"]["access_log_filter"]["excluded_paths"] == []
250
251
252 class TestIntegration:
253 """Integration tests for the access log filter."""
254
255 def test_filter_with_real_logger(self):
256 """Test filter works with a real Python logger simulating uvicorn."""
257 # Create a logger with our filter (simulating uvicorn.access)
258 logger = logging.getLogger("uvicorn.access")
259 logger.setLevel(logging.INFO)
260
261 # Clear any existing handlers
262 logger.handlers = []
263
264 # Create a custom handler that tracks messages
265 logged_messages: list[str] = []
266
267 class TrackingHandler(logging.Handler):
268 def emit(self, record):
269 logged_messages.append(record.getMessage())
270
271 handler = TrackingHandler()
272 handler.setLevel(logging.INFO)
273 filter = UvicornAccessLogFilter(excluded_paths=["/health", "/metrics"])
274 handler.addFilter(filter)
275 logger.addHandler(handler)
276
277 # Log using uvicorn's format with args tuple
278 # Format: '%s - "%s %s HTTP/%s" %d'
279 logger.info(
280 '%s - "%s %s HTTP/%s" %d',
281 "127.0.0.1:12345",
282 "GET",
283 "/health",
284 "1.1",
285 200,
286 )
287 logger.info(
288 '%s - "%s %s HTTP/%s" %d',
289 "127.0.0.1:12345",
290 "GET",
291 "/v1/completions",
292 "1.1",
293 200,
294 )
295 logger.info(
296 '%s - "%s %s HTTP/%s" %d',
297 "127.0.0.1:12345",
298 "GET",
299 "/metrics",
300 "1.1",
301 200,
302 )
303 logger.info(
304 '%s - "%s %s HTTP/%s" %d',
305 "127.0.0.1:12345",
306 "POST",
307 "/v1/chat/completions",
308 "1.1",
309 200,
310 )
311
312 # Verify only non-excluded endpoints were logged
313 assert len(logged_messages) == 2
314 assert "/v1/completions" in logged_messages[0]
315 assert "/v1/chat/completions" in logged_messages[1]
316
317 def test_filter_allows_non_uvicorn_access_logs(self):
318 """Test filter allows logs from non-uvicorn.access loggers."""
319 filter = UvicornAccessLogFilter(excluded_paths=["/health"])
320
321 # Log record from a different logger name
322 record = logging.LogRecord(
323 name="uvicorn.error",
324 level=logging.INFO,
325 pathname="",
326 lineno=0,
327 msg="Some error message about /health",
328 args=(),
329 exc_info=None,
330 )
331
332 # Should allow because it's not from uvicorn.access
333 assert filter.filter(record) is True
334
335 def test_filter_handles_malformed_args(self):
336 """Test filter handles log records with unexpected args format."""
337 filter = UvicornAccessLogFilter(excluded_paths=["/health"])
338
339 # Log record with insufficient args
340 record = logging.LogRecord(
341 name="uvicorn.access",
342 level=logging.INFO,
343 pathname="",
344 lineno=0,
345 msg="Some message",
346 args=("only", "two"),
347 exc_info=None,
348 )
349
350 # Should allow because args doesn't have expected format
351 assert filter.filter(record) is True
352
353 def test_filter_handles_non_tuple_args(self):
354 """Test filter handles log records with non-tuple args."""
355 filter = UvicornAccessLogFilter(excluded_paths=["/health"])
356
357 # Log record with None args
358 record = logging.LogRecord(
359 name="uvicorn.access",
360 level=logging.INFO,
361 pathname="",
362 lineno=0,
363 msg="Some message without args",
364 args=None,
365 exc_info=None,
366 )
367
368 # Should allow because args is None
369 assert filter.filter(record) is True