Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/strands/telemetry/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@
import logging
from importlib.metadata import version

import opentelemetry.metrics as metrics_api
import opentelemetry.sdk.metrics as metrics_sdk
import opentelemetry.trace as trace_api
from opentelemetry import propagate
from opentelemetry.baggage.propagation import W3CBaggagePropagator
from opentelemetry.propagators.composite import CompositePropagator
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter, PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider as SDKTracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter, SimpleSpanProcessor
Expand Down Expand Up @@ -65,6 +68,9 @@ class StrandsTelemetry:
>>> telemetry.setup_console_exporter()
>>> telemetry.setup_otlp_exporter()

To setup global meter provider
>>> telemetry.setup_meter(enable_console_exporter=True, enable_otlp_exporter=True) # default are False

Note:
- The tracer provider is automatically initialized upon instantiation
- When no tracer_provider is provided, the instance sets itself as the global provider
Expand All @@ -86,15 +92,15 @@ def __init__(
The instance is ready to use immediately after initialization, though
trace exporters must be configured separately using the setup methods.
"""
self.resource = get_otel_resource()
if tracer_provider:
self.tracer_provider = tracer_provider
else:
self.resource = get_otel_resource()
self._initialize_tracer()

def _initialize_tracer(self) -> None:
"""Initialize the OpenTelemetry tracer."""
logger.info("initializing tracer")
logger.info("Initializing tracer")

# Create tracer provider
self.tracer_provider = SDKTracerProvider(resource=self.resource)
Expand All @@ -115,7 +121,7 @@ def _initialize_tracer(self) -> None:
def setup_console_exporter(self) -> "StrandsTelemetry":
"""Set up console exporter for the tracer provider."""
try:
logger.info("enabling console export")
logger.info("Enabling console export")
console_processor = SimpleSpanProcessor(ConsoleSpanExporter())
self.tracer_provider.add_span_processor(console_processor)
except Exception as e:
Expand All @@ -134,3 +140,30 @@ def setup_otlp_exporter(self) -> "StrandsTelemetry":
except Exception as e:
logger.exception("error=<%s> | Failed to configure OTLP exporter", e)
return self

def setup_meter(
self, enable_console_exporter: bool = False, enable_otlp_exporter: bool = False
) -> "StrandsTelemetry":
"""Initialize the OpenTelemetry Meter."""
logger.info("Initializing meter")
metrics_readers = []
try:
if enable_console_exporter:
logger.info("Enabling console metrics exporter")
console_reader = PeriodicExportingMetricReader(ConsoleMetricExporter())
metrics_readers.append(console_reader)
if enable_otlp_exporter:
logger.info("Enabling OTLP metrics exporter")
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter

otlp_reader = PeriodicExportingMetricReader(OTLPMetricExporter())
metrics_readers.append(otlp_reader)
except Exception as e:
logger.exception("error=<%s> | Failed to configure OTLP metrics exporter", e)

self.meter_provider = metrics_sdk.MeterProvider(resource=self.resource, metric_readers=metrics_readers)

# Set as global tracer provider
metrics_api.set_meter_provider(self.meter_provider)
logger.info("Strands Meter configured")
return self
19 changes: 5 additions & 14 deletions src/strands/telemetry/tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,9 @@ class Tracer:

def __init__(
self,
service_name: str = "strands-agents",
):
"""Initialize the tracer.

Args:
service_name: Name of the service for OpenTelemetry.
"""
self.service_name = service_name
) -> None:
"""Initialize the tracer."""
self.service_name = __name__
self.tracer_provider: Optional[trace_api.TracerProvider] = None
self.tracer: Optional[trace_api.Tracer] = None

Expand Down Expand Up @@ -505,9 +500,7 @@ def end_agent_span(
_tracer_instance = None


def get_tracer(
service_name: str = "strands-agents",
) -> Tracer:
def get_tracer() -> Tracer:
"""Get or create the global tracer.

Args:
Expand All @@ -519,9 +512,7 @@ def get_tracer(
global _tracer_instance

if not _tracer_instance:
_tracer_instance = Tracer(
service_name=service_name,
)
_tracer_instance = Tracer()

return _tracer_instance

Expand Down
74 changes: 74 additions & 0 deletions tests/strands/telemetry/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ def mock_set_tracer_provider():
yield mock_set


@pytest.fixture
def mock_meter_provider():
with mock.patch("strands.telemetry.config.metrics_sdk.MeterProvider") as mock_meter_provider:
yield mock_meter_provider


@pytest.fixture
def mock_metrics_api():
with mock.patch("strands.telemetry.config.metrics_api") as mock_metrics_api:
yield mock_metrics_api


@pytest.fixture
def mock_set_global_textmap():
with mock.patch("strands.telemetry.config.propagate.set_global_textmap") as mock_set_global_textmap:
Expand All @@ -45,6 +57,26 @@ def mock_console_exporter():
yield mock_console_exporter


@pytest.fixture
def mock_reader():
with mock.patch("strands.telemetry.config.PeriodicExportingMetricReader") as mock_reader:
yield mock_reader


@pytest.fixture
def mock_console_metrics_exporter():
with mock.patch("strands.telemetry.config.ConsoleMetricExporter") as mock_console_metrics_exporter:
yield mock_console_metrics_exporter


@pytest.fixture
def mock_otlp_metrics_exporter():
with mock.patch(
"opentelemetry.exporter.otlp.proto.http.metric_exporter.OTLPMetricExporter"
) as mock_otlp_metrics_exporter:
yield mock_otlp_metrics_exporter


@pytest.fixture
def mock_otlp_exporter():
with mock.patch("opentelemetry.exporter.otlp.proto.http.trace_exporter.OTLPSpanExporter") as mock_otlp_exporter:
Expand Down Expand Up @@ -88,6 +120,48 @@ def test_init_default(mock_resource, mock_tracer_provider, mock_set_tracer_provi
mock_set_global_textmap.assert_called()


def test_setup_meter_with_console_exporter(
mock_resource,
mock_reader,
mock_console_metrics_exporter,
mock_otlp_metrics_exporter,
mock_metrics_api,
mock_meter_provider,
):
"""Test add console metrics exporter"""
mock_metrics_api.MeterProvider.return_value = mock_meter_provider

telemetry = StrandsTelemetry()
telemetry.setup_meter(enable_console_exporter=True)

mock_console_metrics_exporter.assert_called_once()
mock_reader.assert_called_once_with(mock_console_metrics_exporter.return_value)
mock_otlp_metrics_exporter.assert_not_called()

mock_metrics_api.set_meter_provider.assert_called_once()


def test_setup_meter_with_console_and_otlp_exporter(
mock_resource,
mock_reader,
mock_console_metrics_exporter,
mock_otlp_metrics_exporter,
mock_metrics_api,
mock_meter_provider,
):
"""Test add console and otlp metrics exporter"""
mock_metrics_api.MeterProvider.return_value = mock_meter_provider

telemetry = StrandsTelemetry()
telemetry.setup_meter(enable_console_exporter=True, enable_otlp_exporter=True)

mock_console_metrics_exporter.assert_called_once()
mock_otlp_metrics_exporter.assert_called_once()
assert mock_reader.call_count == 2

mock_metrics_api.set_meter_provider.assert_called_once()


def test_setup_console_exporter(mock_resource, mock_tracer_provider, mock_console_exporter, mock_simple_processor):
"""Test add console exporter"""

Expand Down
13 changes: 1 addition & 12 deletions tests/strands/telemetry/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def test_init_default():
"""Test initializing the Tracer with default parameters."""
tracer = Tracer()

assert tracer.service_name == "strands-agents"
assert tracer.service_name == "strands.telemetry.tracer"
assert tracer.tracer_provider is not None
assert tracer.tracer is not None

Expand Down Expand Up @@ -347,17 +347,6 @@ def test_get_tracer_new_endpoint():
assert tracer1 is tracer2


def test_get_tracer_parameters():
"""Test that get_tracer passes parameters correctly."""
# Reset the singleton first
with mock.patch("strands.telemetry.tracer._tracer_instance", None):
tracer = get_tracer(
service_name="test-service",
)

assert tracer.service_name == "test-service"


def test_initialize_tracer_with_custom_tracer_provider(mock_get_tracer_provider):
"""Test initializing the tracer with NoOpTracerProvider."""
tracer = Tracer()
Expand Down
Loading