samchun-gemini / tests /test_monitoring.py
JHyeok5's picture
Upload folder using huggingface_hub
02a8162 verified
"""
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 ์—”๋“œํฌ์ธํŠธ ํ…Œ์ŠคํŠธ"""
@pytest.fixture
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"])