1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
//! Feature-gated OpenTelemetry integration.
//!
//! When the `otel` feature is enabled, this module provides a convenience
//! function to initialize a `tracing` layer that bridges spans to an
//! OpenTelemetry OTLP exporter. The agent loop already emits `tracing` spans
//! (`agent.run`, `agent.turn`, `agent.llm_call`, `agent.tool`), so enabling
//! this layer is all that's needed to export them to an `OTel`-compatible backend.
use std::error::Error;
use std::fmt;
use opentelemetry::trace::TracerProvider as _;
use opentelemetry_otlp::WithExportConfig;
use opentelemetry_sdk::Resource;
use opentelemetry_sdk::trace::SdkTracerProvider;
use tracing_subscriber::Layer;
/// Error returned when [`init_otel_layer`] fails to build the OTLP exporter.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OtelInitError {
message: String,
}
impl OtelInitError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
}
impl fmt::Display for OtelInitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl Error for OtelInitError {}
/// Configuration for the convenience `OTel` initialization helper.
///
/// Construct with [`OtelInitConfig::new`] or [`OtelInitConfig::default`],
/// then chain `with_*` builders.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct OtelInitConfig {
/// Service name reported to the `OTel` backend.
pub service_name: String,
/// OTLP gRPC endpoint. Defaults to `"http://localhost:4317"`.
pub endpoint: Option<String>,
}
impl OtelInitConfig {
/// Create a config with the given service name and the default endpoint.
#[must_use]
pub fn new(service_name: impl Into<String>) -> Self {
Self {
service_name: service_name.into(),
endpoint: None,
}
}
/// Set the OTLP gRPC endpoint.
#[must_use]
pub fn with_endpoint(mut self, endpoint: impl Into<String>) -> Self {
self.endpoint = Some(endpoint.into());
self
}
}
impl Default for OtelInitConfig {
fn default() -> Self {
Self::new("swink-agent")
}
}
/// Initialize a `tracing` [`Layer`] that exports spans to an OTLP gRPC
/// endpoint via `tracing-opentelemetry`.
///
/// Compose the returned layer into a `tracing_subscriber::Registry`:
///
/// ```ignore
/// use tracing_subscriber::prelude::*;
/// use swink_agent::otel::{OtelInitConfig, init_otel_layer};
///
/// let otel_layer = init_otel_layer(OtelInitConfig::default()).expect("otel init");
/// tracing_subscriber::registry()
/// .with(otel_layer)
/// .init();
/// ```
///
/// # Errors
///
/// Returns [`OtelInitError`] if the OTLP gRPC exporter fails to build (e.g.
/// an invalid endpoint configuration).
pub fn init_otel_layer<S>(config: OtelInitConfig) -> Result<impl Layer<S>, OtelInitError>
where
S: tracing::Subscriber + for<'span> tracing_subscriber::registry::LookupSpan<'span>,
{
let endpoint = config
.endpoint
.unwrap_or_else(|| "http://localhost:4317".to_string());
let exporter = opentelemetry_otlp::SpanExporter::builder()
.with_tonic()
.with_endpoint(endpoint)
.build()
.map_err(|err| OtelInitError::new(format!("failed to build OTLP exporter: {err}")))?;
let provider = SdkTracerProvider::builder()
.with_batch_exporter(exporter)
.with_resource(
Resource::builder()
.with_service_name(config.service_name)
.build(),
)
.build();
let tracer = provider.tracer("swink-agent");
Ok(tracing_opentelemetry::layer().with_tracer(tracer))
}
// ─── Send + Sync assertion ──────────────────────────────────────────────────
const fn _assert_send_sync() {
const fn assert<T: Send + Sync>() {}
assert::<OtelInitConfig>();
}
// ─── Tests ──────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn otel_init_config_defaults() {
let config = OtelInitConfig::default();
assert_eq!(config.service_name, "swink-agent");
assert!(config.endpoint.is_none());
}
}