OTLP was added.

This commit is contained in:
febux 2026-09-03 02:36:56 +03:00
parent 70f793e432
commit 721bc99b7a
2 changed files with 74 additions and 17 deletions

View file

@ -49,9 +49,12 @@ REFUND_FULL_USAGE_THRESHOLD=0.20 # ≤20% consumed + within window ⇒ full ref
# --- Observability (leave empty to disable) ---
SENTRY_DSN=
# OTLP endpoint for traces/metrics/logs.
# profile: observer → http://openobserve:5081 (OTLP/gRPC)
# profile: observer → http://openobserve:5080/api/default (OTLP/HTTP)
# profile: obs → configure your own otel-collector/tempo, or leave empty
OTEL_EXPORTER_OTLP_ENDPOINT=
# Required when sending to OpenObserve. Generate with: echo -n 'user:pass' | base64
# Default value is for root@example.com:Complexpass#123 (percent-encoded space).
OTEL_EXPORTER_OTLP_HEADERS=Authorization=Basic%20cm9vdEBleGFtcGxlLmNvbTpDb21wbGV4cGFzcyMxMjM=
OTEL_SERVICE_NAME=contract-check
# --- OpenObserve (profile: observer) ---

View file

@ -1,49 +1,87 @@
"""OpenTelemetry initialization.
Sets up a tracer exporting OTLP to the collector when
Sets up OTLP exporters for traces, logs, and metrics when
`OTEL_EXPORTER_OTLP_ENDPOINT` is set; otherwise no-op. httpx auto-instrumentation
is wired so Ollama Cloud calls appear as spans. Service entrypoints call
`setup_telemetry()` early and `shutdown_telemetry()` on exit.
Auth headers are taken from `OTEL_EXPORTER_OTLP_HEADERS` by the OTLP exporters
(no code-level parsing needed).
This module imports `opentelemetry` only services in the `obs` group import it
(see docs/ARCHITECTURE.md §4). mypy resolves it via the dev group.
"""
from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any
from src.contract_check.core.config import get_settings
from src.contract_check.core.logging import get_logger
if TYPE_CHECKING:
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.trace import TracerProvider
log = get_logger(__name__)
_provider: TracerProvider | None = None
_trace_provider: TracerProvider | None = None
_log_provider: LoggerProvider | None = None
_meter_provider: MeterProvider | None = None
def setup_telemetry(service_name: str | None = None) -> None:
"""Initialize OTel tracing. Safe to call multiple times / when disabled."""
global _provider
"""Initialize OTel tracing/logging/metrics. Safe to call when disabled."""
global _trace_provider, _log_provider, _meter_provider
settings = get_settings()
endpoint = settings.otel_exporter_otlp_endpoint
if not endpoint:
log.debug("otel_disabled", reason="no endpoint configured")
return
from opentelemetry._logs import set_logger_provider
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.metrics import set_meter_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.trace import set_tracer_provider
resource = Resource.create({"service.name": service_name or settings.otel_service_name})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
set_tracer_provider(provider)
_provider = provider
# Traces
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
set_tracer_provider(trace_provider)
_trace_provider = trace_provider
# Logs (stdlib logging → OTLP). Structlog's PrintLogger still writes to stderr;
# this captures stdlib/third-party logs and anything routed through the root logger.
log_provider = LoggerProvider(resource=resource)
log_provider.add_log_record_processor(
BatchLogRecordProcessor(OTLPLogExporter(endpoint=endpoint))
)
set_logger_provider(log_provider)
_log_provider = log_provider
otel_log_handler = LoggingHandler(logger_provider=log_provider)
otel_log_handler.setLevel(logging.INFO)
logging.getLogger().addHandler(otel_log_handler)
# Metrics infrastructure (OTel metrics → OTLP). Existing prometheus_client
# metrics are still scraped by Prometheus; use remote_write for those.
metric_reader = PeriodicExportingMetricReader(OTLPMetricExporter(endpoint=endpoint))
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
set_meter_provider(meter_provider)
_meter_provider = meter_provider
try:
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
@ -53,17 +91,26 @@ def setup_telemetry(service_name: str | None = None) -> None:
log.warning("otel_httpx_instrument_failed", error=str(exc))
log.info("otel_initialized", endpoint=endpoint)
# Stdlib log so the OTel handler exports a verification record too.
logging.getLogger("contract_check.telemetry").info(
"OpenTelemetry initialized for %s at %s",
service_name or settings.otel_service_name,
endpoint,
)
def shutdown_telemetry() -> None:
"""Flush and shut down the tracer provider if it was initialized."""
global _provider
if _provider is None:
return
"""Flush and shut down all OTel providers if initialized."""
global _trace_provider, _log_provider, _meter_provider
for provider in (_trace_provider, _log_provider, _meter_provider):
if provider is not None:
try:
_provider.shutdown()
finally:
_provider = None
provider.shutdown()
except Exception as exc: # pragma: no cover - shutdown must not raise
log.warning("otel_shutdown_failed", error=str(exc))
_trace_provider = None
_log_provider = None
_meter_provider = None
def get_tracer(name: str | None = None) -> Any:
@ -71,3 +118,10 @@ def get_tracer(name: str | None = None) -> Any:
from opentelemetry import trace
return trace.get_tracer(name or "contract_check")
def get_meter(name: str | None = None) -> Any:
"""Return a meter. Returns a no-op-safe meter when OTel is absent."""
from opentelemetry import metrics
return metrics.get_meter(name or "contract_check")