-
Notifications
You must be signed in to change notification settings - Fork 43
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(telemetry): add opentelemetry support #346
base: main
Are you sure you want to change the base?
Changes from all commits
f8dccc5
d0f104d
99a8c85
269316e
66b2999
2311142
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,15 @@ | ||
#[tokio::main] | ||
async fn main() -> color_eyre::Result<()> { | ||
color_eyre::install()?; | ||
orb_telemetry::TelemetryConfig::new() | ||
|
||
let _telemetry_guard = orb_telemetry::TelemetryConfig::new( | ||
orb_attest::SYSLOG_IDENTIFIER, | ||
"1.0.0", | ||
"orb" | ||
) | ||
.with_journald(orb_attest::SYSLOG_IDENTIFIER) | ||
.with_opentelemetry(orb_telemetry::OpenTelemetryConfig::default()) | ||
.init(); | ||
|
||
orb_attest::main().await | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,42 +2,104 @@ use std::io::IsTerminal as _; | |
|
||
use tracing::level_filters::LevelFilter; | ||
use tracing_subscriber::{ | ||
layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter, | ||
layer::SubscriberExt as _, | ||
EnvFilter, | ||
util::SubscriberInitExt, | ||
}; | ||
|
||
use opentelemetry::{global, KeyValue}; | ||
use opentelemetry::trace::TracerProvider; | ||
use opentelemetry_otlp::WithExportConfig; | ||
use opentelemetry_sdk::{ | ||
trace::{self, Sampler}, | ||
runtime::Tokio, | ||
Resource, | ||
}; | ||
use opentelemetry_sdk::propagation::TraceContextPropagator; | ||
|
||
/// Configuration for OpenTelemetry tracing. | ||
#[derive(Debug, Clone)] | ||
pub struct OpenTelemetryConfig { | ||
/// The endpoint to send OTLP data to | ||
pub endpoint: String, | ||
} | ||
|
||
impl Default for OpenTelemetryConfig { | ||
fn default() -> Self { | ||
Self { | ||
endpoint: "http://localhost:4317".to_string(), | ||
} | ||
} | ||
} | ||
|
||
impl OpenTelemetryConfig { | ||
/// Creates a new OpenTelemetry configuration with a custom endpoint. | ||
pub fn new(endpoint: impl Into<String>) -> Self { | ||
Self { | ||
endpoint: endpoint.into(), | ||
} | ||
} | ||
} | ||
|
||
/// A struct controlling how telemetry will be configured (logging + optional OpenTelemetry). | ||
#[derive(Debug)] | ||
pub struct TelemetryConfig { | ||
syslog_identifier: Option<String>, | ||
global_filter: EnvFilter, | ||
service_name: String, | ||
service_version: String, | ||
environment: String, | ||
otel: Option<OpenTelemetryConfig>, | ||
} | ||
|
||
/// Handles cleanup of telemetry resources on drop. | ||
#[must_use] | ||
pub struct TelemetryShutdownHandler; | ||
|
||
impl Drop for TelemetryShutdownHandler { | ||
fn drop(&mut self) { | ||
global::shutdown_tracer_provider(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what happens if we aren't using opentelemetry at all (for example, if we only did |
||
} | ||
} | ||
|
||
impl TelemetryConfig { | ||
/// Provides all required arguments for telemetry configuration. | ||
/// - `log_identifier` will be used for journald, if appropriate. | ||
#[expect(clippy::new_without_default, reason = "may add required args later")] | ||
#[must_use] | ||
pub fn new() -> Self { | ||
/// Creates a new telemetry configuration with mandatory service identification. | ||
/// | ||
/// # Arguments | ||
/// * `service_name` - The name of the service (e.g., "user-service") | ||
/// * `service_version` - The version of the service (e.g., "1.0.0") | ||
/// * `environment` - The deployment environment (e.g., "production", but for orbs it's always "orb") | ||
pub fn new( | ||
service_name: impl Into<String>, | ||
service_version: impl Into<String>, | ||
environment: impl Into<String>, | ||
) -> Self { | ||
Self { | ||
syslog_identifier: None, | ||
global_filter: EnvFilter::builder() | ||
.with_default_directive(LevelFilter::INFO.into()) | ||
.from_env_lossy(), | ||
// Spans from dependencies are emitted only at the error level | ||
.parse_lossy("info,zbus=error,h2=error,hyper=error,tonic=error,tower_http=error"), | ||
service_name: service_name.into(), | ||
service_version: service_version.into(), | ||
environment: environment.into(), | ||
otel: None, | ||
} | ||
} | ||
|
||
/// Enables journald, and uses the provided syslog identifier. | ||
/// | ||
/// If you run the application in a tty, stderr will be used instead. | ||
#[must_use] | ||
pub fn with_journald(self, syslog_identifier: &str) -> Self { | ||
pub fn with_journald(self, syslog_identifier: impl Into<String>) -> Self { | ||
Self { | ||
syslog_identifier: Some(syslog_identifier.to_owned()), | ||
syslog_identifier: Some(syslog_identifier.into()), | ||
..self | ||
} | ||
} | ||
|
||
/// Override the global filter to a custom filter. | ||
/// Only do this if actually necessary to deviate from the orb's defaults. | ||
/// Only do this if you actually need to deviate from the defaults. | ||
#[must_use] | ||
pub fn with_global_filter(self, filter: EnvFilter) -> Self { | ||
Self { | ||
|
@@ -46,13 +108,79 @@ impl TelemetryConfig { | |
} | ||
} | ||
|
||
pub fn try_init(self) -> Result<(), tracing_subscriber::util::TryInitError> { | ||
/// Enable OpenTelemetry/OTLP tracing with the specified configuration. | ||
#[must_use] | ||
pub fn with_opentelemetry(self, config: OpenTelemetryConfig) -> Self { | ||
Self { | ||
otel: Some(config), | ||
..self | ||
} | ||
} | ||
|
||
/// Initialize the OpenTelemetry TracerProvider and set it globally. | ||
fn init_opentelemetry(&self) | ||
-> Result<(trace::TracerProvider, trace::Tracer), Box<dyn std::error::Error>> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: always use But in reality, instead of using |
||
{ | ||
// Build an OpenTelemetry Resource with service metadata | ||
let resource = Resource::new(vec![ | ||
KeyValue::new("service.name", self.service_name.clone()), | ||
KeyValue::new("service.version", self.service_version.clone()), | ||
KeyValue::new("deployment.environment", self.environment.clone()), | ||
]); | ||
|
||
let config = self.otel.as_ref().expect("OpenTelemetry config must be present"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps this is a code smell that indicates we should actually have this as a method on |
||
|
||
let exporter = opentelemetry_otlp::new_exporter() | ||
.tonic() | ||
.with_endpoint(&config.endpoint) | ||
.build_span_exporter()?; | ||
|
||
let trace_config = trace::config() | ||
.with_resource(resource) | ||
.with_sampler(Sampler::AlwaysOn); | ||
|
||
// Create a new tracer provider builder | ||
let tracer_provider = trace::TracerProvider::builder() | ||
.with_config(trace_config) | ||
.with_batch_exporter(exporter, Tokio) | ||
.build(); | ||
|
||
// Create a concrete tracer from the provider: | ||
let tracer = tracer_provider.tracer("telemetry"); | ||
|
||
// Now set the global tracer provider (if desired) | ||
global::set_tracer_provider(tracer_provider.clone()); | ||
global::set_text_map_propagator(TraceContextPropagator::new()); | ||
|
||
Ok((tracer_provider, tracer)) | ||
} | ||
|
||
|
||
/// Try to initialize telemetry (journald/stderr + optional OTLP). | ||
/// Returns an error if something goes wrong setting up the subscriber stack. | ||
pub fn try_init(self) -> Result<(TelemetryShutdownHandler, Result<(), tracing_subscriber::util::TryInitError>), Box<dyn std::error::Error>> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ths type is quite strange. I think you can simplify it by using pub fn try_init(self) -> Result<TelemetryShutdownHandler, OrbTelemetryErr> {
// ...
} |
||
// Set up the tracer provider if OTLP was requested | ||
let tracer = if let Some(_otel_config) = self.otel.as_ref() { | ||
match self.init_opentelemetry() { | ||
Ok((_provider, tracer)) => Some(tracer), | ||
Err(err) => { | ||
eprintln!("Failed to initialize OTLP exporter: {err}"); | ||
None | ||
} | ||
} | ||
} else { | ||
None | ||
}; | ||
|
||
// Base journald/stderr logging setup | ||
let registry = tracing_subscriber::registry(); | ||
// The type is only there to get it to compile. | ||
|
||
// If tokio_unstable is enabled, we can gather runtime metrics | ||
let tokio_console_layer: Option<tracing_subscriber::layer::Identity> = None; | ||
#[cfg(tokio_unstable)] | ||
let tokio_console_layer = console_subscriber::spawn(); | ||
// Checking for a terminal helps detect if we are running under systemd. | ||
|
||
// If we're not attached to a terminal, assume journald is the intended output | ||
let journald_layer = if !std::io::stderr().is_terminal() { | ||
self.syslog_identifier.and_then(|syslog_identifier| { | ||
tracing_journald::layer() | ||
|
@@ -68,24 +196,34 @@ impl TelemetryConfig { | |
} else { | ||
None | ||
}; | ||
|
||
// If journald is not available or we're in a TTY, fallback to stderr | ||
let stderr_layer = journald_layer | ||
.is_none() | ||
.then(|| tracing_subscriber::fmt::layer().with_writer(std::io::stderr)); | ||
assert!(stderr_layer.is_some() || journald_layer.is_some()); | ||
registry | ||
|
||
// If OTLP tracing is available, attach a tracing-opentelemetry layer | ||
let otlp_layer = tracer.map(|tracer| { | ||
tracing_opentelemetry::layer().with_tracer(tracer) | ||
}); | ||
|
||
// Build the final subscriber | ||
let init_result = registry | ||
.with(tokio_console_layer) | ||
.with(stderr_layer) | ||
.with(journald_layer) | ||
.with(otlp_layer) | ||
.with(self.global_filter) | ||
.try_init() | ||
.try_init(); | ||
|
||
Ok((TelemetryShutdownHandler, init_result)) | ||
} | ||
|
||
/// Initializes the telemetry config. Call this only once, at the beginning of the | ||
/// program. | ||
/// | ||
/// Calling this more than once or when another tracing subscriber is registered | ||
/// will cause a panic. | ||
pub fn init(self) { | ||
self.try_init().expect("failed to initialize orb-telemetry") | ||
/// Initializes telemetry, panicking if something goes wrong. | ||
/// Returns a shutdown handler that will clean up resources when dropped. | ||
pub fn init(self) -> TelemetryShutdownHandler { | ||
let (handler, result) = self.try_init().expect("failed to create shutdown handler"); | ||
result.expect("failed to initialize telemetry"); | ||
handler | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these three fields seem like they should be a part of the
OpenTelemetryConfig
struct instead of the main telemetry struct. They won't be used by anything other than opentelemetry.