|  | 
| 6 | 6 | 
 | 
| 7 | 7 | import json | 
| 8 | 8 | import logging | 
| 9 |  | -import os | 
| 10 | 9 | from datetime import date, datetime, timezone | 
| 11 | 10 | from typing import Any, Dict, Mapping, Optional | 
| 12 | 11 | 
 | 
| 13 | 12 | import opentelemetry.trace as trace_api | 
| 14 |  | -from opentelemetry import propagate | 
| 15 |  | -from opentelemetry.baggage.propagation import W3CBaggagePropagator | 
| 16 |  | -from opentelemetry.propagators.composite import CompositePropagator | 
| 17 |  | -from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider | 
| 18 |  | -from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor | 
| 19 | 13 | from opentelemetry.trace import Span, StatusCode | 
| 20 |  | -from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator | 
| 21 | 14 | 
 | 
| 22 | 15 | from ..agent.agent_result import AgentResult | 
| 23 |  | -from ..telemetry import get_otel_resource | 
| 24 | 16 | from ..types.content import Message, Messages | 
| 25 | 17 | from ..types.streaming import Usage | 
| 26 | 18 | from ..types.tools import ToolResult, ToolUse | 
| 27 | 19 | from ..types.traces import AttributeValue | 
| 28 | 20 | 
 | 
| 29 | 21 | logger = logging.getLogger(__name__) | 
| 30 | 22 | 
 | 
| 31 |  | -HAS_OTEL_EXPORTER_MODULE = False | 
| 32 |  | -OTEL_EXPORTER_MODULE_ERROR = ( | 
| 33 |  | -    "opentelemetry-exporter-otlp-proto-http not detected;" | 
| 34 |  | -    "please install strands-agents with the optional 'otel' target" | 
| 35 |  | -    "otel http exporting is currently DISABLED" | 
| 36 |  | -) | 
| 37 |  | -try: | 
| 38 |  | -    from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter | 
| 39 |  | - | 
| 40 |  | -    HAS_OTEL_EXPORTER_MODULE = True | 
| 41 |  | -except ImportError: | 
| 42 |  | -    pass | 
| 43 |  | - | 
| 44 | 23 | 
 | 
| 45 | 24 | class JSONEncoder(json.JSONEncoder): | 
| 46 | 25 |     """Custom JSON encoder that handles non-serializable types.""" | 
| @@ -106,119 +85,18 @@ class Tracer: | 
| 106 | 85 |     def __init__( | 
| 107 | 86 |         self, | 
| 108 | 87 |         service_name: str = "strands-agents", | 
| 109 |  | -        otlp_endpoint: Optional[str] = None, | 
| 110 |  | -        otlp_headers: Optional[Dict[str, str]] = None, | 
| 111 |  | -        enable_console_export: Optional[bool] = None, | 
| 112 | 88 |     ): | 
| 113 | 89 |         """Initialize the tracer. | 
| 114 | 90 | 
 | 
| 115 | 91 |         Args: | 
| 116 | 92 |             service_name: Name of the service for OpenTelemetry. | 
| 117 |  | -            otlp_endpoint: OTLP endpoint URL for sending traces. | 
| 118 |  | -            otlp_headers: Headers to include with OTLP requests. | 
| 119 |  | -            enable_console_export: Whether to also export traces to console. | 
| 120 | 93 |         """ | 
| 121 |  | -        # Check environment variables first | 
| 122 |  | -        env_endpoint = os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT") | 
| 123 |  | -        env_console_export_str = os.environ.get("STRANDS_OTEL_ENABLE_CONSOLE_EXPORT") | 
| 124 |  | - | 
| 125 |  | -        # Constructor parameters take precedence over environment variables | 
| 126 |  | -        self.otlp_endpoint = otlp_endpoint or env_endpoint | 
| 127 |  | - | 
| 128 |  | -        if enable_console_export is not None: | 
| 129 |  | -            self.enable_console_export = enable_console_export | 
| 130 |  | -        elif env_console_export_str: | 
| 131 |  | -            self.enable_console_export = env_console_export_str.lower() in ("true", "1", "yes") | 
| 132 |  | -        else: | 
| 133 |  | -            self.enable_console_export = False | 
| 134 |  | - | 
| 135 |  | -        # Parse headers from environment if available | 
| 136 |  | -        env_headers = os.environ.get("OTEL_EXPORTER_OTLP_HEADERS") | 
| 137 |  | -        if env_headers: | 
| 138 |  | -            try: | 
| 139 |  | -                headers_dict = {} | 
| 140 |  | -                # Parse comma-separated key-value pairs (format: "key1=value1,key2=value2") | 
| 141 |  | -                for pair in env_headers.split(","): | 
| 142 |  | -                    if "=" in pair: | 
| 143 |  | -                        key, value = pair.split("=", 1) | 
| 144 |  | -                        headers_dict[key.strip()] = value.strip() | 
| 145 |  | -                otlp_headers = headers_dict | 
| 146 |  | -            except Exception as e: | 
| 147 |  | -                logger.warning("error=<%s> | failed to parse OTEL_EXPORTER_OTLP_HEADERS", e) | 
| 148 |  | - | 
| 149 | 94 |         self.service_name = service_name | 
| 150 |  | -        self.otlp_headers = otlp_headers or {} | 
| 151 | 95 |         self.tracer_provider: Optional[trace_api.TracerProvider] = None | 
| 152 | 96 |         self.tracer: Optional[trace_api.Tracer] = None | 
| 153 |  | -        propagate.set_global_textmap( | 
| 154 |  | -            CompositePropagator( | 
| 155 |  | -                [ | 
| 156 |  | -                    W3CBaggagePropagator(), | 
| 157 |  | -                    TraceContextTextMapPropagator(), | 
| 158 |  | -                ] | 
| 159 |  | -            ) | 
| 160 |  | -        ) | 
| 161 |  | -        if self.otlp_endpoint or self.enable_console_export: | 
| 162 |  | -            # Create our own tracer provider | 
| 163 |  | -            self._initialize_tracer() | 
| 164 |  | - | 
| 165 |  | -    def _initialize_tracer(self) -> None: | 
| 166 |  | -        """Initialize the OpenTelemetry tracer.""" | 
| 167 |  | -        logger.info("initializing tracer") | 
| 168 | 97 | 
 | 
| 169 |  | -        if self._is_initialized(): | 
| 170 |  | -            self.tracer_provider = trace_api.get_tracer_provider() | 
| 171 |  | -            self.tracer = self.tracer_provider.get_tracer(self.service_name) | 
| 172 |  | -            return | 
| 173 |  | - | 
| 174 |  | -        resource = get_otel_resource() | 
| 175 |  | - | 
| 176 |  | -        # Create tracer provider | 
| 177 |  | -        self.tracer_provider = SDKTracerProvider(resource=resource) | 
| 178 |  | - | 
| 179 |  | -        # Add console exporter if enabled | 
| 180 |  | -        if self.enable_console_export and self.tracer_provider: | 
| 181 |  | -            logger.info("enabling console export") | 
| 182 |  | -            console_processor = SimpleSpanProcessor(ConsoleSpanExporter()) | 
| 183 |  | -            self.tracer_provider.add_span_processor(console_processor) | 
| 184 |  | - | 
| 185 |  | -        # Add OTLP exporter if endpoint is provided | 
| 186 |  | -        if HAS_OTEL_EXPORTER_MODULE and self.otlp_endpoint and self.tracer_provider: | 
| 187 |  | -            try: | 
| 188 |  | -                # Ensure endpoint has the right format | 
| 189 |  | -                endpoint = self.otlp_endpoint | 
| 190 |  | -                if not endpoint.endswith("/v1/traces") and not endpoint.endswith("/traces"): | 
| 191 |  | -                    if not endpoint.endswith("/"): | 
| 192 |  | -                        endpoint += "/" | 
| 193 |  | -                    endpoint += "v1/traces" | 
| 194 |  | - | 
| 195 |  | -                # Set default content type header if not provided | 
| 196 |  | -                headers = self.otlp_headers.copy() | 
| 197 |  | -                if "Content-Type" not in headers: | 
| 198 |  | -                    headers["Content-Type"] = "application/x-protobuf" | 
| 199 |  | - | 
| 200 |  | -                # Create OTLP exporter and processor | 
| 201 |  | -                otlp_exporter = OTLPSpanExporter( | 
| 202 |  | -                    endpoint=endpoint, | 
| 203 |  | -                    headers=headers, | 
| 204 |  | -                ) | 
| 205 |  | - | 
| 206 |  | -                batch_processor = BatchSpanProcessor(otlp_exporter) | 
| 207 |  | -                self.tracer_provider.add_span_processor(batch_processor) | 
| 208 |  | -                logger.info("endpoint=<%s> | OTLP exporter configured with endpoint", endpoint) | 
| 209 |  | - | 
| 210 |  | -            except Exception as e: | 
| 211 |  | -                logger.exception("error=<%s> | Failed to configure OTLP exporter", e) | 
| 212 |  | -        elif self.otlp_endpoint and self.tracer_provider: | 
| 213 |  | -            raise ModuleNotFoundError(OTEL_EXPORTER_MODULE_ERROR) | 
| 214 |  | - | 
| 215 |  | -        # Set as global tracer provider | 
| 216 |  | -        trace_api.set_tracer_provider(self.tracer_provider) | 
| 217 |  | -        self.tracer = trace_api.get_tracer(self.service_name) | 
| 218 |  | - | 
| 219 |  | -    def _is_initialized(self) -> bool: | 
| 220 |  | -        tracer_provider = trace_api.get_tracer_provider() | 
| 221 |  | -        return isinstance(tracer_provider, SDKTracerProvider) | 
|  | 98 | +        self.tracer_provider = trace_api.get_tracer_provider() | 
|  | 99 | +        self.tracer = self.tracer_provider.get_tracer(self.service_name) | 
| 222 | 100 | 
 | 
| 223 | 101 |     def _start_span( | 
| 224 | 102 |         self, | 
| @@ -571,32 +449,20 @@ def end_agent_span( | 
| 571 | 449 | 
 | 
| 572 | 450 | def get_tracer( | 
| 573 | 451 |     service_name: str = "strands-agents", | 
| 574 |  | -    otlp_endpoint: Optional[str] = None, | 
| 575 |  | -    otlp_headers: Optional[Dict[str, str]] = None, | 
| 576 |  | -    enable_console_export: Optional[bool] = None, | 
| 577 | 452 | ) -> Tracer: | 
| 578 | 453 |     """Get or create the global tracer. | 
| 579 | 454 | 
 | 
| 580 | 455 |     Args: | 
| 581 | 456 |         service_name: Name of the service for OpenTelemetry. | 
| 582 |  | -        otlp_endpoint: OTLP endpoint URL for sending traces. | 
| 583 |  | -        otlp_headers: Headers to include with OTLP requests. | 
| 584 |  | -        enable_console_export: Whether to also export traces to console. | 
| 585 |  | -        tracer_provider: Optional existing TracerProvider to use instead of creating a new one. | 
| 586 | 457 | 
 | 
| 587 | 458 |     Returns: | 
| 588 | 459 |         The global tracer instance. | 
| 589 | 460 |     """ | 
| 590 | 461 |     global _tracer_instance | 
| 591 | 462 | 
 | 
| 592 |  | -    if ( | 
| 593 |  | -        _tracer_instance is None or (otlp_endpoint and _tracer_instance.otlp_endpoint != otlp_endpoint)  # type: ignore[unreachable] | 
| 594 |  | -    ): | 
|  | 463 | +    if not _tracer_instance: | 
| 595 | 464 |         _tracer_instance = Tracer( | 
| 596 | 465 |             service_name=service_name, | 
| 597 |  | -            otlp_endpoint=otlp_endpoint, | 
| 598 |  | -            otlp_headers=otlp_headers, | 
| 599 |  | -            enable_console_export=enable_console_export, | 
| 600 | 466 |         ) | 
| 601 | 467 | 
 | 
| 602 | 468 |     return _tracer_instance | 
|  | 
0 commit comments