feat(B.13-B.17+B.16): Error-Handling-Infra, Observability, Graceful Shutdown, Cost-Cap, API Versioning
B.13 Error-Handling-Infrastruktur:
- ErrorCategory Enum (TRANSIENT/PERMANENT/PARTIAL), ApiError erweitert
- Einheitliches Error-Response-Format: {code, detail, field, trace_id, retryable, category}
- 3 FastAPI Exception-Handler (ApiError, HTTPException, unhandled)
- classify_exception() Helper, 6 neue Error-Codes
- 28 Tests in test_error_handling.py
B.14 Observability & trace_id-Korrelation:
- trace_id pro Request (UUID4 short) in structlog contextvars
- X-Trace-Id Response-Header
- Sensitive Fields structlog processor
- llm_complete()/llm_embed() akzeptieren trace_id kwarg
- 12 Tests in test_observability.py
B.15 Graceful Shutdown & Connection Draining:
- _shutdown_event + _inflight_requests Tracking in main.py
- drain_all_connections() in ws_helpers.py
- Worker on_shutdown pausiert WorkflowInstances (status=paused)
- 8 Tests in test_graceful_shutdown.py
B.16 API Versioning Strategie:
- Plugin-Dev-Guide Kapitel 30: URL-basiertes Versioning, Breaking Change Prozess
B.17 Cost Overrun Protection:
- llm_monthly_budget_usd + llm_hard_cutoff Settings
- _check_tenant_budget() vor jedem LLM-Call
- _track_tenant_cost() in Redis (INCRBYFLOAT)
- _check_cost_alerts() bei 50%/80%/100% -> post_system_message()
- 20 Tests in test_cost_protection.py
Total: 68 neue Tests, alle grün. Keine Regressionen.
This commit is contained in:
@@ -51,10 +51,50 @@ arq_jobs_total = Counter(
|
||||
|
||||
# ─── Structured Logging (structlog) ───
|
||||
|
||||
# Sensitive field names that should be redacted from log output
|
||||
_SENSITIVE_FIELDS = frozenset({
|
||||
"password", "passwd", "secret", "api_key", "apikey", "token",
|
||||
"authorization", "auth", "cookie", "session_id", "session",
|
||||
"private_key", "privatekey", "credentials", "smtp_password",
|
||||
"mail_password", "encryption_key", "secret_key",
|
||||
})
|
||||
|
||||
|
||||
def _sanitize_sensitive_fields(logger, method_name, event_dict):
|
||||
"""structlog processor that redacts sensitive field values."""
|
||||
for key in list(event_dict.keys()):
|
||||
if key.lower() in _SENSITIVE_FIELDS:
|
||||
event_dict[key] = "***REDACTED***"
|
||||
return event_dict
|
||||
|
||||
|
||||
def sanitize_dict(data: dict[str, Any], extra_sensitive: set[str] | None = None) -> dict[str, Any]:
|
||||
"""Sanitize a dictionary by redacting sensitive field values.
|
||||
|
||||
Args:
|
||||
data: The dictionary to sanitize.
|
||||
extra_sensitive: Additional field names to treat as sensitive.
|
||||
|
||||
Returns:
|
||||
A copy of *data* with sensitive values replaced by ``"***REDACTED***"``.
|
||||
"""
|
||||
sensitive = _SENSITIVE_FIELDS | (extra_sensitive or set())
|
||||
result = {}
|
||||
for key, value in data.items():
|
||||
if key.lower() in sensitive:
|
||||
result[key] = "***REDACTED***"
|
||||
elif isinstance(value, dict):
|
||||
result[key] = sanitize_dict(value, extra_sensitive)
|
||||
else:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
|
||||
structlog.configure(
|
||||
processors=[
|
||||
structlog.contextvars.merge_contextvars,
|
||||
structlog.processors.add_log_level,
|
||||
_sanitize_sensitive_fields,
|
||||
structlog.processors.TimeStamper(fmt="iso"),
|
||||
structlog.processors.JSONRenderer(),
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user