Spaces:
Sleeping
Sleeping
| """ | |
| Monitoring Feature Tests | |
| ๋ชจ๋ํฐ๋ง ๊ธฐ๋ฅ ํ ์คํธ | |
| @changelog | |
| - v1.0.0 (2026-01-25): ์ด๊ธฐ ๊ตฌํ | |
| - ๋ฉํธ๋ฆญ ์๋ํฌ์ธํธ ํ ์คํธ | |
| - ์์ฒญ ์ถ์ ํ ์คํธ | |
| - ์๋ฌ ์ถ์ ํ ์คํธ | |
| """ | |
| import pytest | |
| import time | |
| import asyncio | |
| from unittest.mock import MagicMock, patch | |
| from fastapi import FastAPI, HTTPException | |
| from fastapi.testclient import TestClient | |
| from starlette.middleware.base import BaseHTTPMiddleware | |
| # ============================================================================== | |
| # MetricsCollector Tests | |
| # ============================================================================== | |
| class TestMetricsCollector: | |
| """๋ฉํธ๋ฆญ ์์ง๊ธฐ ํ ์คํธ""" | |
| def test_singleton_pattern(self): | |
| """์ฑ๊ธํค ํจํด ๊ฒ์ฆ""" | |
| from routers.health import MetricsCollector | |
| collector1 = MetricsCollector() | |
| collector2 = MetricsCollector() | |
| assert collector1 is collector2 | |
| def test_record_request(self): | |
| """์์ฒญ ๋ฉํธ๋ฆญ ๊ธฐ๋ก ํ ์คํธ""" | |
| from routers.health import MetricsCollector | |
| collector = MetricsCollector() | |
| initial_count = collector.total_requests | |
| collector.record_request( | |
| method="GET", | |
| path="/test", | |
| status_code=200, | |
| duration_ms=50.0, | |
| request_id="test-123" | |
| ) | |
| assert collector.total_requests == initial_count + 1 | |
| assert 200 in collector.status_code_counts | |
| def test_record_error(self): | |
| """์๋ฌ ๊ธฐ๋ก ํ ์คํธ""" | |
| from routers.health import MetricsCollector | |
| collector = MetricsCollector() | |
| collector.record_error( | |
| request_id="test-error-123", | |
| endpoint="/test", | |
| method="POST", | |
| status_code=500, | |
| error_type="ValueError", | |
| error_message="Test error", | |
| stack_trace="at test.py:10" | |
| ) | |
| recent_errors = collector.get_recent_errors(limit=10) | |
| assert len(recent_errors) > 0 | |
| # ๊ฐ์ฅ ์ต๊ทผ ์๋ฌ ํ์ธ | |
| latest_error = recent_errors[0] | |
| assert latest_error["request_id"] == "test-error-123" | |
| assert latest_error["error_type"] == "ValueError" | |
| def test_error_rate_calculation(self): | |
| """์๋ฌ์จ ๊ณ์ฐ ํ ์คํธ""" | |
| from routers.health import MetricsCollector | |
| collector = MetricsCollector() | |
| # ํ ์คํธ์ฉ ์์ฒญ ๊ธฐ๋ก (์ฑ๊ณต 8๊ฐ, ์๋ฌ 2๊ฐ) | |
| for i in range(8): | |
| collector.record_request( | |
| method="GET", | |
| path=f"/test/{i}", | |
| status_code=200, | |
| duration_ms=10.0 | |
| ) | |
| for i in range(2): | |
| collector.record_request( | |
| method="GET", | |
| path=f"/error/{i}", | |
| status_code=500, | |
| duration_ms=10.0 | |
| ) | |
| # ์๋ฌ์จ ํ์ธ (์ ์ฒด ๊ธฐ์ค์ด๋ฏ๋ก ์ด๊ธฐ๊ฐ์ ๋ฐ๋ผ ๋ค๋ฆ) | |
| summary = collector.get_metrics_summary() | |
| assert "error_rate_percent" in summary | |
| assert summary["total_errors"] >= 2 | |
| def test_prometheus_metrics_format(self): | |
| """Prometheus ๋ฉํธ๋ฆญ ํฌ๋งท ํ ์คํธ""" | |
| from routers.health import MetricsCollector | |
| collector = MetricsCollector() | |
| prometheus_output = collector.get_prometheus_metrics() | |
| # Prometheus ํฌ๋งท ๊ฒ์ฆ | |
| assert "# HELP" in prometheus_output | |
| assert "# TYPE" in prometheus_output | |
| assert "aewol_replay_requests_total" in prometheus_output | |
| assert "aewol_replay_errors_total" in prometheus_output | |
| assert "aewol_replay_response_time_ms" in prometheus_output | |
| def test_active_connections_tracking(self): | |
| """ํ์ฑ ์ฐ๊ฒฐ ์ ์ถ์ ํ ์คํธ""" | |
| from routers.health import MetricsCollector | |
| collector = MetricsCollector() | |
| initial = collector.active_connections | |
| collector.increment_active_connections() | |
| assert collector.active_connections == initial + 1 | |
| collector.decrement_active_connections() | |
| assert collector.active_connections == initial | |
| # ์์๊ฐ ๋์ง ์๋์ง ํ์ธ | |
| for _ in range(10): | |
| collector.decrement_active_connections() | |
| assert collector.active_connections >= 0 | |
| # ============================================================================== | |
| # Request Tracking Tests | |
| # ============================================================================== | |
| class TestRequestTracking: | |
| """์์ฒญ ์ถ์ ํ ์คํธ""" | |
| def test_get_request_id(self): | |
| """์์ฒญ ID ์กฐํ ํ ์คํธ""" | |
| from middleware.request_tracking import get_request_id, request_id_var | |
| # ์ด๊ธฐ๊ฐ์ None | |
| assert get_request_id() is None | |
| # ์ค์ ํ ์กฐํ | |
| request_id_var.set("test-request-id") | |
| assert get_request_id() == "test-request-id" | |
| # ์ ๋ฆฌ | |
| request_id_var.set(None) | |
| def test_get_request_context(self): | |
| """์์ฒญ ์ปจํ ์คํธ ์กฐํ ํ ์คํธ""" | |
| from middleware.request_tracking import get_request_context, request_context_var | |
| # ์ด๊ธฐ๊ฐ์ ๋น ๋์ ๋๋ฆฌ | |
| assert get_request_context() == {} | |
| # ์ค์ ํ ์กฐํ | |
| test_context = {"method": "GET", "path": "/test"} | |
| request_context_var.set(test_context) | |
| assert get_request_context() == test_context | |
| # ์ ๋ฆฌ | |
| request_context_var.set({}) | |
| # ============================================================================== | |
| # JSONFormatter Tests | |
| # ============================================================================== | |
| class TestJSONFormatter: | |
| """JSON ํฌ๋งคํฐ ํ ์คํธ""" | |
| def test_basic_format(self): | |
| """๊ธฐ๋ณธ ํฌ๋งท ํ ์คํธ""" | |
| import json | |
| import logging | |
| from utils.logging_config import JSONFormatter | |
| formatter = JSONFormatter(include_request_id=False) | |
| record = logging.LogRecord( | |
| name="test", | |
| level=logging.INFO, | |
| pathname="test.py", | |
| lineno=10, | |
| msg="Test message", | |
| args=(), | |
| exc_info=None | |
| ) | |
| output = formatter.format(record) | |
| parsed = json.loads(output) | |
| assert parsed["level"] == "INFO" | |
| assert parsed["message"] == "Test message" | |
| assert parsed["logger"] == "test" | |
| assert "timestamp" in parsed | |
| def test_exception_format(self): | |
| """์์ธ ํฌ๋งท ํ ์คํธ""" | |
| import json | |
| import logging | |
| import sys | |
| from utils.logging_config import JSONFormatter | |
| formatter = JSONFormatter() | |
| try: | |
| raise ValueError("Test error") | |
| except ValueError: | |
| exc_info = sys.exc_info() | |
| record = logging.LogRecord( | |
| name="test", | |
| level=logging.ERROR, | |
| pathname="test.py", | |
| lineno=10, | |
| msg="Error occurred", | |
| args=(), | |
| exc_info=exc_info | |
| ) | |
| output = formatter.format(record) | |
| parsed = json.loads(output) | |
| assert "exception" in parsed | |
| assert parsed["exception"]["type"] == "ValueError" | |
| assert "Test error" in parsed["exception"]["message"] | |
| assert "traceback" in parsed["exception"] | |
| # ============================================================================== | |
| # Error Tracking Tests | |
| # ============================================================================== | |
| class TestErrorTracking: | |
| """์๋ฌ ์ถ์ ํ ์คํธ""" | |
| def test_log_error_with_context(self): | |
| """์ปจํ ์คํธ ํฌํจ ์๋ฌ ๋ก๊น ํ ์คํธ""" | |
| import logging | |
| from utils.logging_config import log_error_with_context | |
| logger = logging.getLogger("test_error") | |
| try: | |
| raise ValueError("Test error for logging") | |
| except ValueError as e: | |
| # ๋ก๊น ์ด ์๋ฌ ์์ด ์ํ๋๋์ง ํ์ธ | |
| log_error_with_context( | |
| logger, | |
| e, | |
| endpoint="/api/test", | |
| method="POST", | |
| user_id="user-123", | |
| params={"key": "value", "password": "secret"} | |
| ) | |
| def test_error_tracker_context_manager(self): | |
| """ErrorTracker ์ปจํ ์คํธ ๋งค๋์ ํ ์คํธ""" | |
| import logging | |
| from utils.logging_config import ErrorTracker | |
| logger = logging.getLogger("test_tracker") | |
| # ์๋ฌ ๋ฐ์ ์ ๋ก๊น ํ ์ฌ๋ฐ์ | |
| with pytest.raises(ValueError): | |
| with ErrorTracker(logger, endpoint="/test", reraise=True): | |
| raise ValueError("Test error") | |
| # ์๋ฌ ์ต์ | |
| with ErrorTracker(logger, endpoint="/test", reraise=False): | |
| raise ValueError("Suppressed error") | |
| # ์ฌ๊ธฐ ๋๋ฌํ๋ฉด ์๋ฌ๊ฐ ์ต์ ๋ ๊ฒ | |
| # ============================================================================== | |
| # Integration Tests (API Endpoints) | |
| # ============================================================================== | |
| class TestMetricsEndpoints: | |
| """๋ฉํธ๋ฆญ API ์๋ํฌ์ธํธ ํ ์คํธ""" | |
| def client(self): | |
| """ํ ์คํธ ํด๋ผ์ด์ธํธ ์์ฑ""" | |
| from server import app | |
| return TestClient(app) | |
| def test_health_metrics_endpoint(self, client): | |
| """GET /health/metrics ํ ์คํธ""" | |
| response = client.get("/health/metrics") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert "uptime_seconds" in data | |
| assert "total_requests" in data | |
| assert "total_errors" in data | |
| assert "error_rate_percent" in data | |
| assert "avg_response_time_ms" in data | |
| assert "active_connections" in data | |
| assert "status_code_distribution" in data | |
| assert "top_endpoints" in data | |
| def test_prometheus_metrics_endpoint(self, client): | |
| """GET /health/metrics/prometheus ํ ์คํธ""" | |
| response = client.get("/health/metrics/prometheus") | |
| assert response.status_code == 200 | |
| assert response.headers["content-type"].startswith("text/plain") | |
| content = response.text | |
| assert "aewol_replay_requests_total" in content | |
| assert "# HELP" in content | |
| assert "# TYPE" in content | |
| def test_health_errors_endpoint(self, client): | |
| """GET /health/errors ํ ์คํธ""" | |
| response = client.get("/health/errors") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert "count" in data | |
| assert "errors" in data | |
| assert isinstance(data["errors"], list) | |
| def test_health_errors_limit(self, client): | |
| """GET /health/errors?limit=10 ํ ์คํธ""" | |
| response = client.get("/health/errors?limit=10") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["count"] <= 10 | |
| def test_request_id_header(self, client): | |
| """X-Request-ID ํค๋ ํ ์คํธ""" | |
| # ์์ฒญ ID ์์ด ์์ฒญ | |
| response = client.get("/health") | |
| assert response.status_code == 200 | |
| assert "X-Request-ID" in response.headers | |
| # ์์ฒญ ID ์ง์ ํ์ฌ ์์ฒญ | |
| custom_id = "custom-test-id-12345" | |
| response = client.get("/health", headers={"X-Request-ID": custom_id}) | |
| assert response.status_code == 200 | |
| assert response.headers["X-Request-ID"] == custom_id | |
| def test_response_time_header(self, client): | |
| """X-Response-Time ํค๋ ํ ์คํธ""" | |
| response = client.get("/health") | |
| assert response.status_code == 200 | |
| assert "X-Response-Time" in response.headers | |
| assert "ms" in response.headers["X-Response-Time"] | |
| # ============================================================================== | |
| # Run Tests | |
| # ============================================================================== | |
| if __name__ == "__main__": | |
| pytest.main([__file__, "-v"]) | |