Implement frontend and backend observability alignment
Align frontend and backend error observability with correlation IDs and structured telemetry for distributed tracing across systems. Backend changes: - Add CorrelationIdMiddleware to generate/extract correlation IDs - Include correlation_id in all ErrorResponse objects - Store correlation ID in structlog contextvars for automatic inclusion in logs - Add correlation ID to response headers (X-Correlation-ID) Frontend changes: - API client automatically generates session-scoped UUID4 and includes X-Correlation-ID header in all requests - Extract correlation ID from API error responses - Update error handlers to use telemetry with correlation IDs - Add telemetry logging to ErrorBoundary, PageErrorBoundary, SectionErrorBoundary - Implement redaction utilities for privacy-safe logging of sensitive data Documentation: - Add observability guidelines to Web-Development.md * Correlation ID usage patterns * Privacy & security best practices * Telemetry event structure * Redaction utilities for sensitive data - Add distributed tracing architecture section to Architecture.md * Correlation ID flow across frontend/backend * Example troubleshooting scenario * Implementation details for future enhancements Testing: - Add comprehensive tests for correlation middleware - Update error boundary tests to verify telemetry integration - Verify TypeScript and ESLint pass with no warnings Fixes: Issue #40 - Frontend and backend observability are not aligned Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
110
backend/tests/test_correlation_middleware.py
Normal file
110
backend/tests/test_correlation_middleware.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""Unit tests for correlation ID middleware and distributed tracing."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from app.config import Settings
|
||||
from app.main import create_app
|
||||
from app.middleware.correlation import CORRELATION_ID_CONTEXT_KEY
|
||||
|
||||
|
||||
def test_correlation_middleware_generates_uuid_when_header_absent() -> None:
|
||||
"""Correlation middleware generates a UUID4 when X-Correlation-ID header is missing."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
|
||||
# Test with TestClient (synchronous)
|
||||
client = TestClient(app)
|
||||
response = client.get("/api/health")
|
||||
|
||||
# Should have correlation ID header in response
|
||||
assert "X-Correlation-ID" in response.headers
|
||||
correlation_id = response.headers["X-Correlation-ID"]
|
||||
# UUID4 format: 8-4-4-4-12 hex digits
|
||||
assert len(correlation_id) == 36
|
||||
assert correlation_id.count("-") == 4
|
||||
|
||||
|
||||
def test_correlation_middleware_preserves_header_from_request() -> None:
|
||||
"""Correlation middleware preserves X-Correlation-ID header from client request."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
|
||||
client = TestClient(app)
|
||||
test_correlation_id = "550e8400-e29b-41d4-a716-446655440000"
|
||||
response = client.get("/api/health", headers={"X-Correlation-ID": test_correlation_id})
|
||||
|
||||
# Should return the same correlation ID in response
|
||||
assert response.headers["X-Correlation-ID"] == test_correlation_id
|
||||
|
||||
|
||||
def test_correlation_middleware_stores_in_request_state() -> None:
|
||||
"""Correlation middleware stores correlation ID in request.state for handlers."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
client = TestClient(app)
|
||||
|
||||
# Make a request and verify correlation ID is available to handlers
|
||||
test_correlation_id = "550e8400-e29b-41d4-a716-446655440000"
|
||||
response = client.get("/api/health", headers={"X-Correlation-ID": test_correlation_id})
|
||||
|
||||
# The health endpoint should return 200, proving the correlation ID was processed
|
||||
assert response.status_code == 200
|
||||
# Response should have correlation ID header (proves it was stored and added)
|
||||
assert response.headers["X-Correlation-ID"] == test_correlation_id
|
||||
|
||||
|
||||
def test_correlation_id_in_response_headers() -> None:
|
||||
"""Correlation ID is included in all response headers."""
|
||||
settings = Settings(
|
||||
database_path="/tmp/test.db",
|
||||
fail2ban_socket="/tmp/fake_fail2ban.sock",
|
||||
fail2ban_config_dir="/tmp/fail2ban",
|
||||
session_secret="test-secret-key-do-not-use-in-production",
|
||||
session_duration_minutes=60,
|
||||
timezone="UTC",
|
||||
log_level="debug",
|
||||
)
|
||||
|
||||
app = create_app(settings=settings)
|
||||
client = TestClient(app)
|
||||
|
||||
# Test without providing header (should generate one)
|
||||
response = client.get("/api/health")
|
||||
assert "X-Correlation-ID" in response.headers
|
||||
|
||||
# Test with providing header (should preserve it)
|
||||
test_id = "test-correlation-id-12345"
|
||||
response = client.get("/api/health", headers={"X-Correlation-ID": test_id})
|
||||
assert response.headers["X-Correlation-ID"] == test_id
|
||||
|
||||
Reference in New Issue
Block a user