fa45d8aa5f
- health_checklist.json: 192.168.1.122→node122
- ocr_client.py: docstring IP→node122
- docs/market-data-requirements.md: IP→node122
- 所有API调用通过ProxyHandler({})绕过系统代理
Privoxy对node122:18003返回500,直连正常
345 lines
14 KiB
Python
345 lines
14 KiB
Python
"""Provider / exporter factory + the Baggage span processor."""
|
|
|
|
from typing import TYPE_CHECKING, Any, Callable, Iterable
|
|
|
|
from opentelemetry import baggage, metrics
|
|
from opentelemetry.context import Context
|
|
from opentelemetry.metrics import MeterProvider, NoOpMeterProvider
|
|
from opentelemetry.sdk.metrics import MeterProvider as SDKMeterProvider
|
|
from opentelemetry.sdk.resources import Resource
|
|
from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor, TracerProvider
|
|
from opentelemetry.sdk.trace.export import (
|
|
BatchSpanProcessor,
|
|
ConsoleSpanExporter,
|
|
SimpleSpanProcessor,
|
|
SpanExporter,
|
|
)
|
|
from opentelemetry.sdk.trace.export.in_memory_span_exporter import (
|
|
InMemorySpanExporter,
|
|
)
|
|
from opentelemetry.trace import Span, SpanKind, Tracer
|
|
|
|
from litellm._version import version as litellm_version
|
|
from litellm.integrations.otel.model.config import ExporterSpec, OpenTelemetryV2Config
|
|
from litellm.integrations.otel.model.semconv import LiteLLM
|
|
from litellm.integrations.otel.model.spans import LiteLLMSpanKind
|
|
|
|
# Re-exported so ``providers.parse_headers`` remains a stable entry point.
|
|
from litellm.integrations.otel.model.utils import parse_headers as parse_headers
|
|
|
|
if TYPE_CHECKING:
|
|
from opentelemetry.metrics import Meter
|
|
from opentelemetry.sdk.metrics.export import MetricReader
|
|
|
|
_SPAN_KIND_BY_ROLE_KIND: dict[LiteLLMSpanKind, SpanKind] = {
|
|
LiteLLMSpanKind.SERVER: SpanKind.SERVER,
|
|
LiteLLMSpanKind.CLIENT: SpanKind.CLIENT,
|
|
LiteLLMSpanKind.INTERNAL: SpanKind.INTERNAL,
|
|
LiteLLMSpanKind.PRODUCER: SpanKind.PRODUCER,
|
|
LiteLLMSpanKind.CONSUMER: SpanKind.CONSUMER,
|
|
}
|
|
|
|
|
|
def to_otel_span_kind(kind: LiteLLMSpanKind) -> SpanKind:
|
|
return _SPAN_KIND_BY_ROLE_KIND[kind]
|
|
|
|
|
|
# Custom exporter factories keyed by ``ExporterSpec.kind``. A preset registers
|
|
# one here when its destination needs construction logic the built-in kinds
|
|
# can't express — e.g. an exporter that fetches an auth token lazily on its
|
|
# first export (off the event loop) instead of blocking at config-build time.
|
|
# Keeping the registry here lets this module stay vendor-agnostic: the factory
|
|
# lives with the integration that needs it.
|
|
_EXPORTER_FACTORIES: dict[str, Callable[[ExporterSpec], SpanExporter]] = {}
|
|
|
|
|
|
def register_exporter_factory(
|
|
kind: str, factory: Callable[[ExporterSpec], SpanExporter]
|
|
) -> None:
|
|
"""Register a custom exporter ``factory`` for the exporter ``kind``."""
|
|
_EXPORTER_FACTORIES[kind.lower()] = factory
|
|
|
|
|
|
class LiteLLMBaggageSpanProcessor(SpanProcessor):
|
|
"""Stamps an allowlisted set of Baggage entries onto every span at start."""
|
|
|
|
def __init__(
|
|
self,
|
|
allowed_keys: Iterable[str],
|
|
allowed_prefixes: tuple[str, ...] = (LiteLLM.METADATA_PREFIX,),
|
|
) -> None:
|
|
self._allowed_keys = frozenset(allowed_keys)
|
|
self._allowed_prefixes = tuple(allowed_prefixes)
|
|
|
|
def _is_allowed(self, key: str) -> bool:
|
|
return key in self._allowed_keys or any(
|
|
key.startswith(prefix) for prefix in self._allowed_prefixes
|
|
)
|
|
|
|
def on_start(self, span: Span, parent_context: Context | None = None) -> None:
|
|
for key, value in baggage.get_all(parent_context).items():
|
|
if self._is_allowed(key) and isinstance(value, (str, bool, int, float)):
|
|
span.set_attribute(key, value)
|
|
|
|
def on_end(self, span: ReadableSpan) -> None: # noqa: D401 - no-op
|
|
return None
|
|
|
|
def shutdown(self) -> None:
|
|
return None
|
|
|
|
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
return True
|
|
|
|
|
|
def _otlp_traces_endpoint(endpoint: str | None) -> str | None:
|
|
"""Point an OTLP/HTTP base endpoint at the ``/v1/traces`` signal path.
|
|
|
|
``OTEL_EXPORTER_OTLP_ENDPOINT`` is a base URL (e.g. ``http://host:4318``).
|
|
The OTLP/HTTP exporter only appends the ``/v1/traces`` path when it reads
|
|
that env var itself; when an endpoint is passed explicitly it is used
|
|
verbatim, so a base URL would POST to the root and the collector returns
|
|
404. Append the signal path here (leaving an already-correct path intact).
|
|
"""
|
|
if not endpoint:
|
|
return endpoint
|
|
endpoint = endpoint.rstrip("/")
|
|
# Splunk Observability uses ``/v2/trace/otlp``; never rewrite it.
|
|
if endpoint.endswith("/v1/traces") or "/v2/trace/otlp" in endpoint:
|
|
return endpoint
|
|
for other_signal in ("/v1/logs", "/v1/metrics"):
|
|
if endpoint.endswith(other_signal):
|
|
return endpoint[: -len(other_signal)] + "/v1/traces"
|
|
return endpoint + "/v1/traces"
|
|
|
|
|
|
def _exporter_from_spec(spec: ExporterSpec) -> SpanExporter:
|
|
kind = (spec.kind or "console").lower()
|
|
factory = _EXPORTER_FACTORIES.get(kind)
|
|
if factory is not None:
|
|
return factory(spec)
|
|
if kind in ("in_memory", "inmemory", "memory"):
|
|
return InMemorySpanExporter()
|
|
if kind in ("otlp_http", "http", "http/protobuf", "http/json"):
|
|
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
|
|
OTLPSpanExporter as HTTPExporter,
|
|
)
|
|
|
|
return HTTPExporter(
|
|
endpoint=_otlp_traces_endpoint(spec.endpoint),
|
|
headers=parse_headers(spec.headers),
|
|
)
|
|
if kind in ("otlp_grpc", "grpc"):
|
|
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
|
|
OTLPSpanExporter as GRPCExporter,
|
|
)
|
|
|
|
return GRPCExporter(endpoint=spec.endpoint, headers=parse_headers(spec.headers))
|
|
return ConsoleSpanExporter()
|
|
|
|
|
|
def _processor_for(exporter: SpanExporter, use_simple: bool | None) -> SpanProcessor:
|
|
"""Pick a Simple or Batch span processor for ``exporter``.
|
|
|
|
When ``use_simple`` is unset, default to Simple for console and in-memory
|
|
exporters (spans export synchronously, which tests rely on) and Batch for
|
|
everything else (the right export semantics for production).
|
|
"""
|
|
if use_simple is None:
|
|
use_simple = isinstance(exporter, (ConsoleSpanExporter, InMemorySpanExporter))
|
|
return SimpleSpanProcessor(exporter) if use_simple else BatchSpanProcessor(exporter)
|
|
|
|
|
|
def build_span_exporter(config: OpenTelemetryV2Config) -> SpanExporter:
|
|
"""Build a single exporter from the top-level config fields.
|
|
|
|
Convenience for the common single-exporter case (and for tests): reads the
|
|
``exporter`` / ``endpoint`` / ``headers`` fields. To configure multiple
|
|
exporters, populate ``config.exporters`` directly.
|
|
"""
|
|
return _exporter_from_spec(
|
|
ExporterSpec(
|
|
kind=config.exporter, endpoint=config.endpoint, headers=config.headers
|
|
)
|
|
)
|
|
|
|
|
|
def _otlp_metrics_endpoint(endpoint: str | None) -> str | None:
|
|
"""Point an OTLP/HTTP base endpoint at the ``/v1/metrics`` signal path.
|
|
|
|
The OTLP/HTTP exporter only appends ``/v1/metrics`` when it reads
|
|
``OTEL_EXPORTER_OTLP_ENDPOINT`` itself; an explicitly passed endpoint is used
|
|
verbatim, so a base URL would POST to the root. Mirror ``_otlp_traces_endpoint``
|
|
for the metrics signal (rewriting a sibling signal path when present).
|
|
"""
|
|
if not endpoint:
|
|
return endpoint
|
|
endpoint = endpoint.rstrip("/")
|
|
if endpoint.endswith("/v1/metrics"):
|
|
return endpoint
|
|
for other_signal in ("/v1/traces", "/v1/logs"):
|
|
if endpoint.endswith(other_signal):
|
|
return endpoint[: -len(other_signal)] + "/v1/metrics"
|
|
return endpoint + "/v1/metrics"
|
|
|
|
|
|
def build_metric_reader(config: OpenTelemetryV2Config) -> "MetricReader":
|
|
"""Build a metric reader mirroring v1's exporter selection.
|
|
|
|
``console`` (and any unrecognized kind) exports to the console; ``otlp_http``
|
|
and ``otlp_grpc`` export over OTLP with the configured endpoint/headers. The
|
|
reader exports on a 5s period, matching v1.
|
|
"""
|
|
from opentelemetry.sdk.metrics.export import (
|
|
ConsoleMetricExporter,
|
|
PeriodicExportingMetricReader,
|
|
)
|
|
|
|
kind = (config.exporter or "console").lower()
|
|
if kind in ("otlp_http", "http", "http/protobuf", "http/json"):
|
|
from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
|
|
OTLPMetricExporter as HTTPMetricExporter,
|
|
)
|
|
from opentelemetry.sdk.metrics import Histogram
|
|
from opentelemetry.sdk.metrics.export import AggregationTemporality
|
|
|
|
exporter: Any = HTTPMetricExporter(
|
|
endpoint=_otlp_metrics_endpoint(config.endpoint),
|
|
headers=parse_headers(config.headers),
|
|
preferred_temporality={Histogram: AggregationTemporality.DELTA},
|
|
)
|
|
elif kind in ("otlp_grpc", "grpc"):
|
|
from opentelemetry.sdk.metrics import Histogram
|
|
from opentelemetry.sdk.metrics.export import AggregationTemporality
|
|
|
|
try:
|
|
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
|
|
OTLPMetricExporter as GRPCMetricExporter,
|
|
)
|
|
except ImportError as exc:
|
|
raise ImportError(
|
|
"OpenTelemetry OTLP gRPC metric exporter is not available. Install "
|
|
"`opentelemetry-exporter-otlp` and `grpcio` (or `litellm[grpc]`)."
|
|
) from exc
|
|
|
|
exporter = GRPCMetricExporter(
|
|
endpoint=config.endpoint,
|
|
headers=parse_headers(config.headers),
|
|
preferred_temporality={Histogram: AggregationTemporality.DELTA},
|
|
)
|
|
else:
|
|
exporter = ConsoleMetricExporter()
|
|
|
|
return PeriodicExportingMetricReader(exporter, export_interval_millis=5000)
|
|
|
|
|
|
def build_meter_provider(
|
|
config: OpenTelemetryV2Config,
|
|
metric_reader: "MetricReader | None" = None,
|
|
) -> SDKMeterProvider:
|
|
"""Build the :class:`MeterProvider` for GenAI metrics.
|
|
|
|
``metric_reader`` is an explicit override (tests inject an
|
|
``InMemoryMetricReader``); otherwise the reader is selected from the config's
|
|
exporter kind via :func:`build_metric_reader`.
|
|
"""
|
|
reader = metric_reader if metric_reader is not None else build_metric_reader(config)
|
|
return SDKMeterProvider(metric_readers=[reader], resource=build_resource(config))
|
|
|
|
|
|
def resolve_meter_provider(
|
|
config: OpenTelemetryV2Config,
|
|
meter_provider: MeterProvider | None = None,
|
|
) -> MeterProvider:
|
|
"""Resolve the :class:`MeterProvider` GenAI metrics record through.
|
|
|
|
An injected provider wins (DI/tests). Otherwise reuse whatever the operator has
|
|
configured as the global, whether a real SDK provider or an explicit
|
|
``NoOpMeterProvider``, so the GenAI histograms ride the operator's
|
|
readers/exporters and an explicit opt-out is honored. Only when the global is
|
|
still the default proxy placeholder does V2 build one from the config and
|
|
publish it as the global, mirroring how V2 owns trace export. The built
|
|
provider is the one returned, so its reader thread is always live, never
|
|
orphaned.
|
|
"""
|
|
if meter_provider is not None:
|
|
return meter_provider
|
|
|
|
existing = metrics.get_meter_provider()
|
|
if isinstance(existing, (SDKMeterProvider, NoOpMeterProvider)):
|
|
return existing
|
|
|
|
provider = build_meter_provider(config)
|
|
metrics.set_meter_provider(provider)
|
|
return provider
|
|
|
|
|
|
def get_meter(provider: MeterProvider, name: str = "litellm") -> "Meter":
|
|
return provider.get_meter(name, litellm_version)
|
|
|
|
|
|
def build_resource(config: OpenTelemetryV2Config) -> Resource:
|
|
attributes: dict[str, str] = {"service.name": config.service_name}
|
|
if config.deployment_environment:
|
|
attributes["deployment.environment"] = config.deployment_environment
|
|
attributes.update(config.resource_attributes)
|
|
return Resource.create(attributes)
|
|
|
|
|
|
def build_tracer_provider(
|
|
config: OpenTelemetryV2Config,
|
|
exporter: SpanExporter | None = None,
|
|
baggage_processor: SpanProcessor | None = None,
|
|
use_simple_processor: bool | None = None,
|
|
) -> TracerProvider:
|
|
"""Build the shared :class:`TracerProvider`.
|
|
|
|
Attach the Baggage processor first (so identity attributes land on each
|
|
span before any export decision), then add one ``SpanProcessor`` per
|
|
``config.exporters`` entry — this is what fans spans out to multiple
|
|
backends. ``exporter`` and ``use_simple_processor`` are explicit overrides:
|
|
pass a single exporter to attach exactly that one (used by tests).
|
|
"""
|
|
provider = TracerProvider(resource=build_resource(config))
|
|
if baggage_processor is None:
|
|
baggage_processor = LiteLLMBaggageSpanProcessor(
|
|
allowed_keys=config.baggage_promoted_keys
|
|
)
|
|
provider.add_span_processor(baggage_processor)
|
|
|
|
if exporter is not None:
|
|
provider.add_span_processor(_processor_for(exporter, use_simple_processor))
|
|
return provider
|
|
|
|
# ``config._normalize`` guarantees at least one spec (it folds the top-level
|
|
# ``exporter``/``endpoint``/``headers`` fields in when ``exporters`` is empty).
|
|
for spec in config.exporters:
|
|
exp = _exporter_from_spec(spec)
|
|
provider.add_span_processor(
|
|
_processor_for(
|
|
exp,
|
|
(
|
|
spec.use_simple_processor
|
|
if spec.use_simple_processor is not None
|
|
else use_simple_processor
|
|
),
|
|
)
|
|
)
|
|
return provider
|
|
|
|
|
|
def get_tracer(provider: TracerProvider, name: str = "litellm") -> Tracer:
|
|
# Stamp the instrumentation scope with the LiteLLM package version so every
|
|
# emitted span carries a deterministic ``scope.version`` (the standard OTel
|
|
# location for the emitting library's version) for downstream consumers.
|
|
return provider.get_tracer(name, litellm_version)
|
|
|
|
|
|
def in_memory_provider(
|
|
config: OpenTelemetryV2Config | None = None,
|
|
) -> tuple[TracerProvider, InMemorySpanExporter]:
|
|
"""Convenience for tests: a provider exporting to an in-memory buffer."""
|
|
cfg = config or OpenTelemetryV2Config(exporter="in_memory")
|
|
exporter = InMemorySpanExporter()
|
|
provider = build_tracer_provider(cfg, exporter=exporter)
|
|
return provider, exporter
|