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>
125 lines
3.4 KiB
Python
125 lines
3.4 KiB
Python
"""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
|