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) --- # --- Observability (leave empty to disable) ---
SENTRY_DSN= SENTRY_DSN=
# OTLP endpoint for traces/metrics/logs. # 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 # profile: obs → configure your own otel-collector/tempo, or leave empty
OTEL_EXPORTER_OTLP_ENDPOINT= 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 OTEL_SERVICE_NAME=contract-check
# --- OpenObserve (profile: observer) --- # --- OpenObserve (profile: observer) ---

View file

@ -1,49 +1,87 @@
"""OpenTelemetry initialization. """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 `OTEL_EXPORTER_OTLP_ENDPOINT` is set; otherwise no-op. httpx auto-instrumentation
is wired so Ollama Cloud calls appear as spans. Service entrypoints call is wired so Ollama Cloud calls appear as spans. Service entrypoints call
`setup_telemetry()` early and `shutdown_telemetry()` on exit. `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 This module imports `opentelemetry` only services in the `obs` group import it
(see docs/ARCHITECTURE.md §4). mypy resolves it via the dev group. (see docs/ARCHITECTURE.md §4). mypy resolves it via the dev group.
""" """
from __future__ import annotations from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from src.contract_check.core.config import get_settings from src.contract_check.core.config import get_settings
from src.contract_check.core.logging import get_logger from src.contract_check.core.logging import get_logger
if TYPE_CHECKING: if TYPE_CHECKING:
from opentelemetry.sdk._logs import LoggerProvider
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace import TracerProvider
log = get_logger(__name__) 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: def setup_telemetry(service_name: str | None = None) -> None:
"""Initialize OTel tracing. Safe to call multiple times / when disabled.""" """Initialize OTel tracing/logging/metrics. Safe to call when disabled."""
global _provider global _trace_provider, _log_provider, _meter_provider
settings = get_settings() settings = get_settings()
endpoint = settings.otel_exporter_otlp_endpoint endpoint = settings.otel_exporter_otlp_endpoint
if not endpoint: if not endpoint:
log.debug("otel_disabled", reason="no endpoint configured") log.debug("otel_disabled", reason="no endpoint configured")
return 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.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.resources import Resource
from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.trace import set_tracer_provider from opentelemetry.trace import set_tracer_provider
resource = Resource.create({"service.name": service_name or settings.otel_service_name}) resource = Resource.create({"service.name": service_name or settings.otel_service_name})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint))) # Traces
set_tracer_provider(provider) trace_provider = TracerProvider(resource=resource)
_provider = provider 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: try:
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor 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.warning("otel_httpx_instrument_failed", error=str(exc))
log.info("otel_initialized", endpoint=endpoint) 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: def shutdown_telemetry() -> None:
"""Flush and shut down the tracer provider if it was initialized.""" """Flush and shut down all OTel providers if initialized."""
global _provider global _trace_provider, _log_provider, _meter_provider
if _provider is None: for provider in (_trace_provider, _log_provider, _meter_provider):
return if provider is not None:
try: try:
_provider.shutdown() provider.shutdown()
finally: except Exception as exc: # pragma: no cover - shutdown must not raise
_provider = None 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: def get_tracer(name: str | None = None) -> Any:
@ -71,3 +118,10 @@ def get_tracer(name: str | None = None) -> Any:
from opentelemetry import trace from opentelemetry import trace
return trace.get_tracer(name or "contract_check") 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")