feat: implement 15 production items (SSE, security, observability, features, infra)
Performance: - SSE dashboard streaming endpoint (GET /v1/admin/status/stream) - Web Worker for markdown rendering (offload from main thread) - IndexedDB chat persistence (replace localStorage, 500msg support) Security: - CSRF protection middleware (Origin/Referer validation) - Content Security Policy + security headers middleware - API key rotation endpoint (POST /v1/admin/keys/rotate) Observability: - OpenTelemetry tracing with graceful NoOp fallback - Structured error codes (FAGI-xxxx taxonomy with ErrorResponse schema) - Audit log export (CSV + JSON at /v1/admin/audit/export/*) Features: - Multi-session management hook (parallel conversations) - Conversation export (markdown/JSON/text download + clipboard) - Head customization UI (enable/disable + weight sliders for 12 heads) Infrastructure: - Kubernetes Helm chart (Deployment, Service, HPA, Ingress) - Database migration versioning (generate, verify commands) - Blue-green deployment manifests (color-based traffic switching) Tests: 598 Python + 56 frontend = 654 total, 0 ruff errors Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
This commit is contained in:
124
fusionagi/api/otel.py
Normal file
124
fusionagi/api/otel.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""OpenTelemetry tracing integration.
|
||||
|
||||
Provides OTel-compatible tracing when opentelemetry SDK is installed.
|
||||
Falls back gracefully to no-op when unavailable.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from typing import Any, Generator
|
||||
|
||||
from fusionagi._logger import logger
|
||||
|
||||
_tracer: Any = None
|
||||
_initialized = False
|
||||
|
||||
|
||||
class NoOpSpan:
|
||||
"""No-op span for when OTel is unavailable."""
|
||||
|
||||
def set_attribute(self, key: str, value: Any) -> None:
|
||||
pass
|
||||
|
||||
def set_status(self, status: Any) -> None:
|
||||
pass
|
||||
|
||||
def record_exception(self, exception: Exception) -> None:
|
||||
pass
|
||||
|
||||
def end(self) -> None:
|
||||
pass
|
||||
|
||||
def __enter__(self) -> "NoOpSpan":
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class NoOpTracer:
|
||||
"""No-op tracer for when OTel is unavailable."""
|
||||
|
||||
def start_span(self, name: str, **kwargs: Any) -> NoOpSpan:
|
||||
return NoOpSpan()
|
||||
|
||||
@contextmanager
|
||||
def start_as_current_span(self, name: str, **kwargs: Any) -> Generator[NoOpSpan, None, None]:
|
||||
yield NoOpSpan()
|
||||
|
||||
|
||||
def init_otel(service_name: str = "fusionagi") -> Any:
|
||||
"""Initialize OpenTelemetry tracing.
|
||||
|
||||
Configures OTLP exporter if ``OTEL_EXPORTER_OTLP_ENDPOINT`` is set.
|
||||
Falls back to no-op tracer if opentelemetry is not installed.
|
||||
|
||||
Args:
|
||||
service_name: Service name for traces.
|
||||
|
||||
Returns:
|
||||
Configured tracer instance.
|
||||
"""
|
||||
global _tracer, _initialized
|
||||
|
||||
if _initialized:
|
||||
return _tracer
|
||||
|
||||
_initialized = True
|
||||
|
||||
try:
|
||||
from opentelemetry import trace
|
||||
from opentelemetry.sdk.resources import Resource
|
||||
from opentelemetry.sdk.trace import TracerProvider
|
||||
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
||||
|
||||
resource = Resource.create({"service.name": service_name})
|
||||
provider = TracerProvider(resource=resource)
|
||||
|
||||
endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
||||
if endpoint:
|
||||
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
||||
exporter = OTLPSpanExporter(endpoint=endpoint)
|
||||
provider.add_span_processor(BatchSpanProcessor(exporter))
|
||||
logger.info("OTel: OTLP exporter configured", extra={"endpoint": endpoint})
|
||||
else:
|
||||
logger.info("OTel: no OTLP endpoint configured, using in-memory tracing")
|
||||
|
||||
trace.set_tracer_provider(provider)
|
||||
_tracer = trace.get_tracer(service_name)
|
||||
logger.info("OTel: tracing initialized", extra={"service": service_name})
|
||||
|
||||
except ImportError:
|
||||
logger.info("OTel: opentelemetry not installed, using no-op tracer")
|
||||
_tracer = NoOpTracer()
|
||||
|
||||
return _tracer
|
||||
|
||||
|
||||
def get_tracer() -> Any:
|
||||
"""Return the global tracer (initializes on first call)."""
|
||||
global _tracer
|
||||
if _tracer is None:
|
||||
init_otel()
|
||||
return _tracer
|
||||
|
||||
|
||||
@contextmanager
|
||||
def trace_span(name: str, attributes: dict[str, Any] | None = None) -> Generator[Any, None, None]:
|
||||
"""Context manager for creating a traced span.
|
||||
|
||||
Args:
|
||||
name: Span name.
|
||||
attributes: Optional span attributes.
|
||||
|
||||
Yields:
|
||||
Active span (OTel or NoOp).
|
||||
"""
|
||||
tracer = get_tracer()
|
||||
with tracer.start_as_current_span(name) as span:
|
||||
if attributes:
|
||||
for k, v in attributes.items():
|
||||
span.set_attribute(k, str(v) if not isinstance(v, (str, int, float, bool)) else v)
|
||||
yield span
|
||||
Reference in New Issue
Block a user