223 lines
7.3 KiB
Python
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
|