use opentelemetry::global;
use opentelemetry::trace::TracerProvider;
use opentelemetry_sdk::{
propagation::TraceContextPropagator,
trace::SdkTracerProvider,
Resource,
};
#[derive(Debug, Clone)]
pub struct OtelResource {
pub service_name: String,
pub service_version: String,
pub sdk_name: String,
pub sdk_language: String,
}
impl Default for OtelResource {
fn default() -> Self {
Self {
service_name: "strands-agents".to_string(),
service_version: env!("CARGO_PKG_VERSION").to_string(),
sdk_name: "opentelemetry".to_string(),
sdk_language: "rust".to_string(),
}
}
}
impl OtelResource {
pub fn new() -> Self {
Self::default()
}
pub fn attributes(&self) -> std::collections::HashMap<String, String> {
let mut attrs = std::collections::HashMap::new();
attrs.insert("service.name".to_string(), self.service_name.clone());
attrs.insert("service.version".to_string(), self.service_version.clone());
attrs.insert("telemetry.sdk.name".to_string(), self.sdk_name.clone());
attrs.insert("telemetry.sdk.language".to_string(), self.sdk_language.clone());
attrs
}
fn to_otel_resource(&self) -> Resource {
Resource::builder()
.with_service_name(self.service_name.clone())
.with_attributes([
opentelemetry::KeyValue::new("service.version", self.service_version.clone()),
opentelemetry::KeyValue::new("telemetry.sdk.name", self.sdk_name.clone()),
opentelemetry::KeyValue::new("telemetry.sdk.language", self.sdk_language.clone()),
])
.build()
}
}
pub struct StrandsTelemetry {
pub resource: OtelResource,
tracer_provider: Option<SdkTracerProvider>,
console_enabled: bool,
otlp_enabled: bool,
meter_enabled: bool,
}
impl Default for StrandsTelemetry {
fn default() -> Self {
Self::new()
}
}
impl StrandsTelemetry {
pub fn new() -> Self {
let mut instance = Self {
resource: OtelResource::default(),
tracer_provider: None,
console_enabled: false,
otlp_enabled: false,
meter_enabled: false,
};
instance.initialize_tracer();
instance
}
pub fn with_tracer_provider(tracer_provider: SdkTracerProvider) -> Self {
Self {
resource: OtelResource::default(),
tracer_provider: Some(tracer_provider),
console_enabled: false,
otlp_enabled: false,
meter_enabled: false,
}
}
fn initialize_tracer(&mut self) {
tracing::info!("Initializing tracer");
let resource = self.resource.to_otel_resource();
let tracer_provider = SdkTracerProvider::builder()
.with_resource(resource)
.build();
global::set_tracer_provider(tracer_provider.clone());
let propagator = TraceContextPropagator::new();
global::set_text_map_propagator(propagator);
self.tracer_provider = Some(tracer_provider);
}
#[cfg(feature = "otel-stdout")]
pub fn setup_console_exporter(mut self) -> Self {
use opentelemetry_stdout::SpanExporter as StdoutSpanExporter;
tracing::info!("Enabling console export");
self.console_enabled = true;
let exporter = StdoutSpanExporter::default();
let new_provider = SdkTracerProvider::builder()
.with_simple_exporter(exporter)
.with_resource(self.resource.to_otel_resource())
.build();
global::set_tracer_provider(new_provider.clone());
self.tracer_provider = Some(new_provider);
self
}
#[cfg(not(feature = "otel-stdout"))]
pub fn setup_console_exporter(mut self) -> Self {
tracing::warn!("otel-stdout feature not enabled, console exporter not available");
self.console_enabled = false;
self
}
#[cfg(feature = "otel-otlp")]
pub fn setup_otlp_exporter(mut self) -> Self {
use opentelemetry_otlp::SpanExporter;
tracing::info!("Enabling OTLP export");
self.otlp_enabled = true;
match SpanExporter::builder().with_tonic().build() {
Ok(exporter) => {
let new_provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(self.resource.to_otel_resource())
.build();
global::set_tracer_provider(new_provider.clone());
self.tracer_provider = Some(new_provider);
tracing::info!("OTLP exporter configured");
}
Err(e) => {
tracing::error!("error=<{}> | Failed to configure OTLP exporter", e);
}
}
self
}
#[cfg(not(feature = "otel-otlp"))]
pub fn setup_otlp_exporter(mut self) -> Self {
tracing::warn!("otel-otlp feature not enabled, OTLP exporter not available");
self.otlp_enabled = false;
self
}
#[cfg(all(feature = "otel-stdout", feature = "otel-otlp"))]
pub fn setup_meter(mut self, enable_console: bool, enable_otlp: bool) -> Self {
use opentelemetry_sdk::metrics::SdkMeterProvider;
tracing::info!("Initializing meter");
self.meter_enabled = true;
let resource = self.resource.to_otel_resource();
let mut builder = SdkMeterProvider::builder().with_resource(resource);
if enable_console {
tracing::info!("Enabling console metrics exporter");
let exporter = opentelemetry_stdout::MetricExporter::default();
builder = builder.with_periodic_exporter(exporter);
}
if enable_otlp {
tracing::info!("Enabling OTLP metrics exporter");
match opentelemetry_otlp::MetricExporter::builder()
.with_tonic()
.build()
{
Ok(exporter) => {
builder = builder.with_periodic_exporter(exporter);
}
Err(e) => {
tracing::error!("error=<{}> | Failed to configure OTLP metrics exporter", e);
}
}
}
let meter_provider = builder.build();
opentelemetry::global::set_meter_provider(meter_provider);
tracing::info!("Strands Meter configured");
self
}
#[cfg(all(feature = "otel-stdout", not(feature = "otel-otlp")))]
pub fn setup_meter(mut self, enable_console: bool, _enable_otlp: bool) -> Self {
use opentelemetry_sdk::metrics::SdkMeterProvider;
tracing::info!("Initializing meter");
self.meter_enabled = true;
let resource = self.resource.to_otel_resource();
let mut builder = SdkMeterProvider::builder().with_resource(resource);
if enable_console {
tracing::info!("Enabling console metrics exporter");
let exporter = opentelemetry_stdout::MetricExporter::default();
builder = builder.with_periodic_exporter(exporter);
}
let meter_provider = builder.build();
opentelemetry::global::set_meter_provider(meter_provider);
tracing::info!("Strands Meter configured");
self
}
#[cfg(all(not(feature = "otel-stdout"), feature = "otel-otlp"))]
pub fn setup_meter(mut self, _enable_console: bool, enable_otlp: bool) -> Self {
use opentelemetry_sdk::metrics::SdkMeterProvider;
tracing::info!("Initializing meter");
self.meter_enabled = true;
let resource = self.resource.to_otel_resource();
let mut builder = SdkMeterProvider::builder().with_resource(resource);
if enable_otlp {
tracing::info!("Enabling OTLP metrics exporter");
match opentelemetry_otlp::MetricExporter::builder()
.with_tonic()
.build()
{
Ok(exporter) => {
builder = builder.with_periodic_exporter(exporter);
}
Err(e) => {
tracing::error!("error=<{}> | Failed to configure OTLP metrics exporter", e);
}
}
}
let meter_provider = builder.build();
opentelemetry::global::set_meter_provider(meter_provider);
tracing::info!("Strands Meter configured");
self
}
#[cfg(not(any(feature = "otel-stdout", feature = "otel-otlp")))]
pub fn setup_meter(mut self, _enable_console: bool, _enable_otlp: bool) -> Self {
tracing::warn!("Neither otel-stdout nor otel-otlp features enabled, meter not available");
self.meter_enabled = false;
self
}
pub fn is_console_enabled(&self) -> bool {
self.console_enabled
}
pub fn is_otlp_enabled(&self) -> bool {
self.otlp_enabled
}
pub fn is_meter_enabled(&self) -> bool {
self.meter_enabled
}
pub fn resource(&self) -> &OtelResource {
&self.resource
}
pub fn tracer_provider(&self) -> Option<&SdkTracerProvider> {
self.tracer_provider.as_ref()
}
pub fn tracer(&self, name: &'static str) -> Option<opentelemetry_sdk::trace::Tracer> {
self.tracer_provider.as_ref().map(|p| p.tracer(name))
}
}
pub struct StrandsTelemetryBuilder {
resource: OtelResource,
enable_console: bool,
enable_otlp: bool,
enable_console_metrics: bool,
enable_otlp_metrics: bool,
}
impl Default for StrandsTelemetryBuilder {
fn default() -> Self {
Self::new()
}
}
impl StrandsTelemetryBuilder {
pub fn new() -> Self {
Self {
resource: OtelResource::default(),
enable_console: false,
enable_otlp: false,
enable_console_metrics: false,
enable_otlp_metrics: false,
}
}
pub fn with_resource(mut self, resource: OtelResource) -> Self {
self.resource = resource;
self
}
pub fn with_console_exporter(mut self) -> Self {
self.enable_console = true;
self
}
pub fn with_otlp_exporter(mut self) -> Self {
self.enable_otlp = true;
self
}
pub fn with_console_metrics(mut self) -> Self {
self.enable_console_metrics = true;
self
}
pub fn with_otlp_metrics(mut self) -> Self {
self.enable_otlp_metrics = true;
self
}
pub fn build(self) -> StrandsTelemetry {
let mut telemetry = StrandsTelemetry {
resource: self.resource,
tracer_provider: None,
console_enabled: false,
otlp_enabled: false,
meter_enabled: false,
};
telemetry.initialize_tracer();
if self.enable_console {
telemetry = telemetry.setup_console_exporter();
}
if self.enable_otlp {
telemetry = telemetry.setup_otlp_exporter();
}
if self.enable_console_metrics || self.enable_otlp_metrics {
telemetry = telemetry.setup_meter(self.enable_console_metrics, self.enable_otlp_metrics);
}
telemetry
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_otel_resource_default() {
let resource = OtelResource::default();
assert_eq!(resource.service_name, "strands-agents");
assert_eq!(resource.sdk_language, "rust");
}
#[test]
fn test_otel_resource_attributes() {
let resource = OtelResource::default();
let attrs = resource.attributes();
assert!(attrs.contains_key("service.name"));
assert!(attrs.contains_key("telemetry.sdk.language"));
}
#[test]
fn test_strands_telemetry_builder() {
let telemetry = StrandsTelemetryBuilder::new()
.with_console_exporter()
.build();
assert!(telemetry.tracer_provider().is_some());
}
#[test]
fn test_strands_telemetry_new() {
let telemetry = StrandsTelemetry::new();
assert!(telemetry.tracer_provider().is_some());
assert!(!telemetry.is_console_enabled());
assert!(!telemetry.is_otlp_enabled());
}
#[test]
fn test_strands_telemetry_chaining() {
let telemetry = StrandsTelemetry::new()
.setup_console_exporter();
assert!(telemetry.tracer_provider().is_some());
}
#[tokio::test]
#[cfg(feature = "otel-otlp")]
async fn test_strands_telemetry_otlp() {
let telemetry = StrandsTelemetry::new()
.setup_otlp_exporter();
assert!(telemetry.tracer_provider().is_some());
}
}