group-wbl/.venv/lib/python3.13/site-packages/langsmith/integrations/otel/processor.py
2026-01-09 09:12:25 +08:00

223 lines
7.3 KiB
Python

"""OpenTelemetry span processor and exporter for LangSmith."""
import logging
import warnings
from typing import Optional
from urllib.parse import urljoin
from langsmith import utils as ls_utils
try:
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace.export import BatchSpanProcessor
OTEL_AVAILABLE = True
except ImportError:
warnings.warn(
"OpenTelemetry packages are not installed. "
"Install optional OpenTelemetry dependencies with: "
"pip install langsmith[otel]",
UserWarning,
stacklevel=2,
)
class OTLPSpanExporter: # type: ignore[no-redef]
"""Mock otlp span exporter class."""
def __init__(self, *args, **kwargs):
"""Mock init method."""
raise ImportError(
"OpenTelemetry packages are not installed. "
"Install optional OpenTelemetry dependencies with: "
"pip install langsmith[otel]"
)
class BatchSpanProcessor: # type: ignore[no-redef]
"""Mock batch span processor class."""
def __init__(self, *args, **kwargs):
"""Mock init method."""
raise ImportError(
"OpenTelemetry packages are not installed. "
"Install optional OpenTelemetry dependencies with: "
"pip install langsmith[otel]"
)
class trace:
"""Mock trace class."""
@staticmethod
def get_tracer_provider():
"""Mock get tracer provider method."""
raise ImportError(
"OpenTelemetry packages are not installed. "
"Install optional OpenTelemetry dependencies with: "
"pip install langsmith[otel]"
)
OTEL_AVAILABLE = False
class OtelExporter(OTLPSpanExporter):
"""A subclass of `OTLPSpanExporter` configured for LangSmith.
Environment Variables:
- `LANGSMITH_API_KEY`: Your LangSmith API key.
- `LANGSMITH_ENDPOINT`: Base URL for LangSmith API (defaults to `https://api.smith.langchain.com`).
- `LANGSMITH_PROJECT`: Project identifier.
"""
def __init__(
self,
url: Optional[str] = None,
api_key: Optional[str] = None,
project: Optional[str] = None,
headers: Optional[dict[str, str]] = None,
**kwargs,
):
"""Initialize the `OtelExporter`.
Args:
url: OTLP endpoint URL. Defaults to `{LANGSMITH_ENDPOINT}/otel/v1/traces`.
api_key: LangSmith API key. Defaults to `LANGSMITH_API_KEY` env var.
parent: Parent identifier (e.g., `'project_name:test'`).
Defaults to `LANGSMITH_PARENT` env var.
headers: Additional headers to include in requests.
**kwargs: Additional arguments passed to `OTLPSpanExporter`.
"""
base_url = ls_utils.get_api_url(None)
# Ensure base_url ends with / for proper joining
if not base_url.endswith("/"):
base_url += "/"
endpoint = url or urljoin(base_url, "otel/v1/traces")
api_key = api_key or ls_utils.get_api_key(None)
project = project or ls_utils.get_tracer_project()
headers = headers or {}
if not api_key:
raise ValueError(
"API key is required. Provide it via api_key parameter or "
"LANGSMITH_API_KEY environment variable."
)
if not project:
project = "default"
logging.info(
"No project specified, using default. "
"Configure with LANGSMITH_PROJECT environment variable or "
"project parameter."
)
exporter_headers = {
"x-api-key": api_key,
**headers,
}
if project:
exporter_headers["Langsmith-Project"] = project
self.project = project
super().__init__(endpoint=endpoint, headers=exporter_headers, **kwargs)
class OtelSpanProcessor:
"""A span processor for adding LangSmith to OpenTelemetry setups.
This class combines the `OtelExporter` and `BatchSpanProcessor`
into a single processor that can be added to any `TracerProvider`.
Use this when:
1. You already have OpenTelemetry initialized with other tools
2. You want to add LangSmith alongside existing OTEL exporters
Examples:
# Fresh OpenTelemetry setup (LangSmith only):
from langsmith.integrations.otel import configure
configure(api_key="your-key", project="your-project")
# Add LangSmith to existing OpenTelemetry setup:
from opentelemetry import trace
from langsmith.integrations.otel.processor import OtelSpanProcessor
# Get your existing TracerProvider (already set by other tools)
provider = trace.get_tracer_provider()
# Add LangSmith processor alongside existing processors
langsmith_processor = OtelSpanProcessor(
project="your-project",
)
provider.add_span_processor(langsmith_processor)
"""
def __init__(
self,
api_key: Optional[str] = None,
project: Optional[str] = None,
url: Optional[str] = None,
headers: Optional[dict[str, str]] = None,
SpanProcessor: Optional[type] = None,
):
"""Initialize the `OtelSpanProcessor`.
Args:
api_key: LangSmith API key. Defaults to `LANGSMITH_API_KEY` env var.
project: Project identifier. Defaults to `LANGSMITH_PROJECT` env var.
url: Base URL for LangSmith API. Defaults to `LANGSMITH_ENDPOINT` env var
or `https://api.smith.langchain.com`.
headers: Additional headers to include in requests.
SpanProcessor: Optional span processor class. Defaults to
`BatchSpanProcessor`.
"""
# Create the exporter
# Convert url to the full endpoint URL that OtelExporter expects
exporter_url = None
if url:
exporter_url = f"{url.rstrip('/')}/otel/v1/traces"
self._exporter = OtelExporter(
url=exporter_url, api_key=api_key, project=project, headers=headers
)
# Create the processor chain
if not OTEL_AVAILABLE:
raise ImportError(
"OpenTelemetry packages are not installed. "
"Install optional OpenTelemetry dependencies with: "
"pip install langsmith[otel]"
)
if SpanProcessor is None:
SpanProcessor = BatchSpanProcessor
self._processor = SpanProcessor(self._exporter)
def on_start(self, span, parent_context=None):
"""Forward span start events to the inner processor."""
self._processor.on_start(span, parent_context)
def on_end(self, span):
"""Forward span end events to the inner processor."""
self._processor.on_end(span)
def shutdown(self):
"""Shutdown processor."""
self._processor.shutdown()
def force_flush(self, timeout_millis=30000):
"""Force flush the inner processor."""
return self._processor.force_flush(timeout_millis)
@property
def exporter(self):
"""The underlying OtelExporter."""
return self._exporter
@property
def processor(self):
"""The underlying span processor."""
return self._processor