From dddabbc616ed437d7d3bea82168bd30ce59d910a Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Fri, 5 Sep 2025 12:21:16 -0400 Subject: [PATCH 1/9] feat(tracing): add type-state builder pattern for configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a fluent builder pattern with compile-time guarantees for configuring tracing/logging features. Key improvements: - Type-state pattern ensures mutually exclusive telemetry backends (OTLP vs Google Cloud) - Compile-time validation of configuration flow - Support for custom service names in telemetry - Flexible filter configuration (custom, env var, or defaults) - Maintains backward compatibility with existing init() function The builder enforces single-direction configuration flow and prevents invalid combinations at compile time, making the API more intuitive and error-proof. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- Cargo.lock | 1 + crates/tracing/Cargo.toml | 3 + crates/tracing/examples/builder_usage.rs | 60 +++++ crates/tracing/src/builder.rs | 277 +++++++++++++++++++++++ crates/tracing/src/gcloud.rs | 21 +- crates/tracing/src/lib.rs | 74 +++--- crates/tracing/src/otlp.rs | 16 +- 7 files changed, 395 insertions(+), 57 deletions(-) create mode 100644 crates/tracing/examples/builder_usage.rs create mode 100644 crates/tracing/src/builder.rs diff --git a/Cargo.lock b/Cargo.lock index d76dd4a99..0b38f441b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6524,6 +6524,7 @@ dependencies = [ "rustls 0.23.31", "serde", "thiserror 1.0.69", + "tokio", "tower-http", "tracing", "tracing-log 0.1.4", diff --git a/crates/tracing/Cargo.toml b/crates/tracing/Cargo.toml index d28bd662f..c4e5ffaae 100644 --- a/crates/tracing/Cargo.toml +++ b/crates/tracing/Cargo.toml @@ -29,3 +29,6 @@ tracing-opentelemetry.workspace = true bytes.workspace = true chrono.workspace = true http-body-util = "0.1.3" + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/tracing/examples/builder_usage.rs b/crates/tracing/examples/builder_usage.rs new file mode 100644 index 000000000..b00b87a14 --- /dev/null +++ b/crates/tracing/examples/builder_usage.rs @@ -0,0 +1,60 @@ +use katana_tracing::{LogFormat, TracingBuilder}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Example 1: Basic configuration without telemetry + TracingBuilder::new() + .with_log_format(LogFormat::Json) + .with_default_filter()? + .configure() + .build() + .await?; + + tracing::info!("Basic logging initialized"); + + // Note: In a real application, you can only initialize tracing once. + // The examples below show different configuration options. + + // Example 2: With OTLP telemetry + // TracingBuilder::new() + // .with_log_format(LogFormat::Full) + // .with_env_filter_or_default()? + // .with_service_name("my-katana-node") + // .configure() + // .with_telemetry() + // .otlp() + // .with_endpoint("http://localhost:4317") + // .build() + // .await?; + + // Example 3: With Google Cloud telemetry + // TracingBuilder::new() + // .with_log_format(LogFormat::Json) + // .with_filter("debug")? + // .with_service_name("katana-prod") + // .configure() + // .with_telemetry() + // .gcloud() + // .with_project_id("my-project") + // .build() + // .await?; + + // Example 4: Using environment filter + // TracingBuilder::new() + // .with_env_filter()? // Uses RUST_LOG environment variable + // .configure() + // .build() + // .await?; + + // Example 5: Custom filter with telemetry + // TracingBuilder::new() + // .with_filter("katana=debug,tower=info")? + // .with_service_name("custom-service") + // .configure() + // .with_telemetry() + // .otlp() + // .build() // Uses default OTLP endpoint + // .await?; + + Ok(()) +} diff --git a/crates/tracing/src/builder.rs b/crates/tracing/src/builder.rs new file mode 100644 index 000000000..5e80f178c --- /dev/null +++ b/crates/tracing/src/builder.rs @@ -0,0 +1,277 @@ +use std::marker::PhantomData; + +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, Layer}; + +use crate::fmt::LocalTime; +use crate::{gcloud, otlp, Error, LogFormat}; + +// Type-state markers for the builder pattern +pub struct Initial; +pub struct Configured; +#[allow(dead_code)] +pub struct WithTelemetry { + _phantom: PhantomData, +} + +// Telemetry type markers +#[allow(dead_code)] +pub struct NoTelemetry; +#[allow(dead_code)] +pub struct Otlp; +#[allow(dead_code)] +pub struct Gcloud; + +// Main builder struct with type-state +pub struct TracingBuilder { + log_format: LogFormat, + filter: Option, + service_name: String, + _state: PhantomData, +} + +// Configuration that will be used during build +enum TelemetryConfig { + None, + Otlp(otlp::OtlpConfig), + Gcloud(gcloud::GcloudConfig), +} + +// Builder for OTLP telemetry configuration +pub struct OtlpTelemetryBuilder { + endpoint: Option, + parent_builder: TracingBuilder, +} + +// Builder for Google Cloud telemetry configuration +pub struct GcloudTelemetryBuilder { + project_id: Option, + parent_builder: TracingBuilder, +} + +impl TracingBuilder { + /// Create a new tracing builder with default settings + pub fn new() -> Self { + Self { + log_format: LogFormat::Full, + filter: None, + service_name: "katana".to_string(), + _state: PhantomData, + } + } + + /// Set the log format + pub fn with_log_format(mut self, format: LogFormat) -> Self { + self.log_format = format; + self + } + + /// Set a custom filter from a string + pub fn with_filter(mut self, filter: &str) -> Result { + self.filter = Some(EnvFilter::try_new(filter)?); + Ok(self) + } + + /// Use the default filter + pub fn with_default_filter(mut self) -> Result { + const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,\ + pipeline=debug,stage=debug,tasks=debug,executor=trace,\ + forking::backend=trace,blockifier=off,\ + jsonrpsee_server=off,hyper=off,messaging=debug,\ + node=error,explorer=info,rpc=trace,pool=trace,info"; + + self.filter = Some(EnvFilter::try_new(DEFAULT_LOG_FILTER)?); + Ok(self) + } + + /// Use filter from environment variable (RUST_LOG) + pub fn with_env_filter(mut self) -> Result { + self.filter = Some(EnvFilter::try_from_default_env()?); + Ok(self) + } + + /// Use filter from environment with fallback to default + pub fn with_env_filter_or_default(mut self) -> Result { + const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,\ + pipeline=debug,stage=debug,tasks=debug,executor=trace,\ + forking::backend=trace,blockifier=off,\ + jsonrpsee_server=off,hyper=off,messaging=debug,\ + node=error,explorer=info,rpc=trace,pool=trace,info"; + + let default_filter = EnvFilter::try_new(DEFAULT_LOG_FILTER); + self.filter = Some(EnvFilter::try_from_default_env().or(default_filter)?); + Ok(self) + } + + /// Set the service name (default: "katana") + pub fn with_service_name(mut self, name: impl Into) -> Self { + self.service_name = name.into(); + self + } + + /// Finalize the basic configuration and prepare for optional telemetry + pub fn configure(self) -> TracingBuilder { + TracingBuilder { + log_format: self.log_format, + filter: self.filter, + service_name: self.service_name, + _state: PhantomData, + } + } +} + +impl TracingBuilder { + /// Add telemetry configuration + pub fn with_telemetry(self) -> TelemetrySelector { + TelemetrySelector { parent_builder: self } + } + + /// Build the tracing subscriber without telemetry + pub async fn build(self) -> Result<(), Error> { + self.build_with_telemetry(TelemetryConfig::None).await + } + + async fn build_with_telemetry(self, telemetry_config: TelemetryConfig) -> Result<(), Error> { + let filter = self.filter.unwrap_or_else(|| { + const DEFAULT_LOG_FILTER: &str = + "katana_db::mdbx=trace,cairo_native::compiler=off,pipeline=debug,stage=debug,\ + tasks=debug,executor=trace,forking::backend=trace,blockifier=off,\ + jsonrpsee_server=off,hyper=off,messaging=debug,node=error,explorer=info,\ + rpc=trace,pool=trace,info"; + + EnvFilter::try_new(DEFAULT_LOG_FILTER).expect("default filter should be valid") + }); + + match telemetry_config { + TelemetryConfig::None => { + let fmt = match self.log_format { + LogFormat::Full => { + tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() + } + LogFormat::Json => { + tracing_subscriber::fmt::layer().json().with_timer(LocalTime::new()).boxed() + } + }; + + tracing_subscriber::registry().with(filter).with(fmt).init(); + } + TelemetryConfig::Otlp(cfg) => { + let tracer = otlp::init_tracer_with_service(&cfg, &self.service_name)?; + let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); + + let fmt = match self.log_format { + LogFormat::Full => { + tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() + } + LogFormat::Json => { + tracing_subscriber::fmt::layer().json().with_timer(LocalTime::new()).boxed() + } + }; + + tracing_subscriber::registry().with(filter).with(telemetry).with(fmt).init(); + } + TelemetryConfig::Gcloud(cfg) => { + let tracer = gcloud::init_tracer_with_service(&cfg, &self.service_name).await?; + let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); + + let fmt = match self.log_format { + LogFormat::Full => { + tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() + } + LogFormat::Json => { + tracing_subscriber::fmt::layer().json().with_timer(LocalTime::new()).boxed() + } + }; + + tracing_subscriber::registry().with(filter).with(telemetry).with(fmt).init(); + } + } + + Ok(()) + } +} + +/// Selector for choosing telemetry backend +pub struct TelemetrySelector { + parent_builder: TracingBuilder, +} + +impl TelemetrySelector { + /// Configure OTLP telemetry + pub fn otlp(self) -> OtlpTelemetryBuilder { + OtlpTelemetryBuilder { endpoint: None, parent_builder: self.parent_builder } + } + + /// Configure Google Cloud telemetry + pub fn gcloud(self) -> GcloudTelemetryBuilder { + GcloudTelemetryBuilder { project_id: None, parent_builder: self.parent_builder } + } +} + +impl OtlpTelemetryBuilder { + /// Set the OTLP endpoint + pub fn with_endpoint(mut self, endpoint: impl Into) -> Self { + self.endpoint = Some(endpoint.into()); + self + } + + /// Build the tracing subscriber with OTLP telemetry + pub async fn build(self) -> Result<(), Error> { + let config = otlp::OtlpConfig { endpoint: self.endpoint }; + self.parent_builder.build_with_telemetry(TelemetryConfig::Otlp(config)).await + } +} + +impl GcloudTelemetryBuilder { + /// Set the Google Cloud project ID + pub fn with_project_id(mut self, project_id: impl Into) -> Self { + self.project_id = Some(project_id.into()); + self + } + + /// Build the tracing subscriber with Google Cloud telemetry + pub async fn build(self) -> Result<(), Error> { + let config = gcloud::GcloudConfig { project_id: self.project_id }; + self.parent_builder.build_with_telemetry(TelemetryConfig::Gcloud(config)).await + } +} + +impl Default for TracingBuilder { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_builder_type_safety() { + // This test ensures the builder pattern compiles correctly + // and demonstrates the type-safe flow + + // The following should compile: + let _builder = TracingBuilder::new() + .with_log_format(LogFormat::Json) + .with_service_name("test-service") + .configure(); + + // Test that telemetry selection is exclusive + // (can't test runtime behavior without async runtime in unit tests) + } + + #[tokio::test] + async fn test_builder_without_telemetry() { + // Note: This will fail if tracing is already initialized + // In practice, this would be tested with a custom registry + + // Just ensure the builder compiles and doesn't panic + let result = + TracingBuilder::new().with_log_format(LogFormat::Json).configure().build().await; + + // The second initialization should fail + assert!(result.is_ok() || result.is_err()); + } +} diff --git a/crates/tracing/src/gcloud.rs b/crates/tracing/src/gcloud.rs index eb93a3aa2..a52e9ca82 100644 --- a/crates/tracing/src/gcloud.rs +++ b/crates/tracing/src/gcloud.rs @@ -36,16 +36,16 @@ pub struct GcloudConfig { pub project_id: Option, } -/// Initialize Google Cloud Trace exporter and OpenTelemetry propagators for Google Cloud trace -/// context support. -/// -/// Make sure to set `GOOGLE_APPLICATION_CREDENTIALS` env var to authenticate to gcloud -pub(crate) async fn init_tracer(gcloud_config: &GcloudConfig) -> Result { +/// Initialize Google Cloud Trace exporter with custom service name +pub(crate) async fn init_tracer_with_service( + gcloud_config: &GcloudConfig, + service_name: &str, +) -> Result { rustls::crypto::ring::default_provider() .install_default() .map_err(|_| Error::InstallCryptoFailed)?; - let resource = Resource::builder().with_service_name("katana").build(); + let resource = Resource::builder().with_service_name(service_name.to_string()).build(); let mut trace_exporter = if let Some(project_id) = &gcloud_config.project_id { GcpCloudTraceExporterBuilder::new(project_id.clone()) @@ -70,3 +70,12 @@ pub(crate) async fn init_tracer(gcloud_config: &GcloudConfig) -> Result Result { + init_tracer_with_service(gcloud_config, "katana").await +} diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index 3ce83f193..d8a7e3855 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -1,17 +1,16 @@ use opentelemetry_gcloud_trace::errors::GcloudTraceError; use tracing::subscriber::SetGlobalDefaultError; use tracing_log::log::SetLoggerError; -use tracing_subscriber::layer::SubscriberExt; -use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::{filter, EnvFilter, Layer}; +use tracing_subscriber::filter; +mod builder; mod fmt; pub mod gcloud; pub mod otlp; +pub use builder::TracingBuilder; pub use fmt::LogFormat; -use crate::fmt::LocalTime; #[derive(Debug, Clone)] pub enum TracerConfig { @@ -26,6 +25,9 @@ pub enum Error { #[error("failed to parse environment filter: {0}")] EnvFilterParse(#[from] filter::ParseError), + + #[error("failed to parse environment filter from env: {0}")] + EnvFilterFromEnv(#[from] filter::FromEnvError), #[error("failed to set global dispatcher: {0}")] SetGlobalDefault(#[from] SetGlobalDefaultError), @@ -43,51 +45,29 @@ pub enum Error { OtelSdk(#[from] opentelemetry_sdk::error::OTelSdkError), } +/// Initialize tracing with the given configuration. +/// +/// This function is maintained for backward compatibility. +/// For new code, consider using [`TracingBuilder`] for more flexibility. pub async fn init(format: LogFormat, telemetry_config: Option) -> Result<(), Error> { - const DEFAULT_LOG_FILTER: &str = - "katana_db::mdbx=trace,cairo_native::compiler=off,pipeline=debug,stage=debug,tasks=debug,\ - executor=trace,forking::backend=trace,blockifier=off,jsonrpsee_server=off,hyper=off,\ - messaging=debug,node=error,explorer=info,rpc=trace,pool=trace,info"; - - let default_filter = EnvFilter::try_new(DEFAULT_LOG_FILTER); - let filter = EnvFilter::try_from_default_env().or(default_filter)?; - - // Initialize tracing subscriber with optional telemetry - if let Some(telemetry_config) = telemetry_config { - // Initialize telemetry layer based on exporter type - let telemetry = match telemetry_config { - TracerConfig::Gcloud(cfg) => { - let tracer = gcloud::init_tracer(&cfg).await?; - tracing_opentelemetry::layer().with_tracer(tracer) - } - TracerConfig::Otlp(cfg) => { - let tracer = otlp::init_tracer(&cfg)?; - tracing_opentelemetry::layer().with_tracer(tracer) - } - }; - - let fmt = match format { - LogFormat::Full => { - tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() + let builder = + TracingBuilder::new().with_log_format(format).with_env_filter_or_default()?.configure(); + + match telemetry_config { + Some(TracerConfig::Otlp(cfg)) => { + let mut otlp_builder = builder.with_telemetry().otlp(); + if let Some(endpoint) = cfg.endpoint { + otlp_builder = otlp_builder.with_endpoint(endpoint); } - LogFormat::Json => { - tracing_subscriber::fmt::layer().json().with_timer(LocalTime::new()).boxed() + otlp_builder.build().await + } + Some(TracerConfig::Gcloud(cfg)) => { + let mut gcloud_builder = builder.with_telemetry().gcloud(); + if let Some(project_id) = cfg.project_id { + gcloud_builder = gcloud_builder.with_project_id(project_id); } - }; - - tracing_subscriber::registry().with(filter).with(telemetry).with(fmt).init(); - } else { - let fmt = match format { - LogFormat::Full => { - tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() - } - LogFormat::Json => { - tracing_subscriber::fmt::layer().json().with_timer(LocalTime::new()).boxed() - } - }; - - tracing_subscriber::registry().with(filter).with(fmt).init(); + gcloud_builder.build().await + } + None => builder.build().await, } - - Ok(()) } diff --git a/crates/tracing/src/otlp.rs b/crates/tracing/src/otlp.rs index 7582c3ac4..f8b0188e1 100644 --- a/crates/tracing/src/otlp.rs +++ b/crates/tracing/src/otlp.rs @@ -11,11 +11,14 @@ pub struct OtlpConfig { pub endpoint: Option, } -/// Initialize OTLP tracer -pub fn init_tracer(otlp_config: &OtlpConfig) -> Result { +/// Initialize OTLP tracer with custom service name +pub fn init_tracer_with_service( + otlp_config: &OtlpConfig, + service_name: &str, +) -> Result { use opentelemetry_otlp::WithExportConfig; - let resource = Resource::builder().with_service_name("katana").build(); + let resource = Resource::builder().with_service_name(service_name.to_string()).build(); let mut exporter_builder = SpanExporterBuilder::new().with_tonic(); @@ -33,5 +36,10 @@ pub fn init_tracer(otlp_config: &OtlpConfig) -> Result Result { + init_tracer_with_service(otlp_config, "katana") } From cef71cdef663e40bb7d9f3a5984d010506e6931e Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Fri, 5 Sep 2025 12:32:33 -0400 Subject: [PATCH 2/9] refactor(tracing): simplify builder API by removing configure() step MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unnecessary configure() method from TracingBuilder - Make telemetry methods (otlp() and gcloud()) directly available on TracingBuilder - Simplify the API flow for better developer experience - Remove unused type-state markers and PhantomData since they're no longer needed - Update examples to reflect the cleaner API The new API is more intuitive: TracingBuilder::new() .with_service_name("my-service") .otlp() .with_endpoint("http://localhost:4317") .build() Instead of: TracingBuilder::new() .with_service_name("my-service") .configure() .with_telemetry() .otlp() .with_endpoint("http://localhost:4317") .build() 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- crates/tracing/examples/builder_usage.rs | 10 +-- crates/tracing/src/builder.rs | 103 ++++++++--------------- crates/tracing/src/lib.rs | 7 +- 3 files changed, 40 insertions(+), 80 deletions(-) diff --git a/crates/tracing/examples/builder_usage.rs b/crates/tracing/examples/builder_usage.rs index b00b87a14..dc095a1b2 100644 --- a/crates/tracing/examples/builder_usage.rs +++ b/crates/tracing/examples/builder_usage.rs @@ -6,7 +6,6 @@ async fn main() -> Result<(), Box> { TracingBuilder::new() .with_log_format(LogFormat::Json) .with_default_filter()? - .configure() .build() .await?; @@ -20,8 +19,6 @@ async fn main() -> Result<(), Box> { // .with_log_format(LogFormat::Full) // .with_env_filter_or_default()? // .with_service_name("my-katana-node") - // .configure() - // .with_telemetry() // .otlp() // .with_endpoint("http://localhost:4317") // .build() @@ -32,8 +29,6 @@ async fn main() -> Result<(), Box> { // .with_log_format(LogFormat::Json) // .with_filter("debug")? // .with_service_name("katana-prod") - // .configure() - // .with_telemetry() // .gcloud() // .with_project_id("my-project") // .build() @@ -42,16 +37,13 @@ async fn main() -> Result<(), Box> { // Example 4: Using environment filter // TracingBuilder::new() // .with_env_filter()? // Uses RUST_LOG environment variable - // .configure() // .build() // .await?; - // Example 5: Custom filter with telemetry + // Example 5: Custom filter with OTLP telemetry (using default endpoint) // TracingBuilder::new() // .with_filter("katana=debug,tower=info")? // .with_service_name("custom-service") - // .configure() - // .with_telemetry() // .otlp() // .build() // Uses default OTLP endpoint // .await?; diff --git a/crates/tracing/src/builder.rs b/crates/tracing/src/builder.rs index 5e80f178c..6b58ac3c8 100644 --- a/crates/tracing/src/builder.rs +++ b/crates/tracing/src/builder.rs @@ -1,4 +1,3 @@ -use std::marker::PhantomData; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -7,28 +6,11 @@ use tracing_subscriber::{EnvFilter, Layer}; use crate::fmt::LocalTime; use crate::{gcloud, otlp, Error, LogFormat}; -// Type-state markers for the builder pattern -pub struct Initial; -pub struct Configured; -#[allow(dead_code)] -pub struct WithTelemetry { - _phantom: PhantomData, -} - -// Telemetry type markers -#[allow(dead_code)] -pub struct NoTelemetry; -#[allow(dead_code)] -pub struct Otlp; -#[allow(dead_code)] -pub struct Gcloud; - -// Main builder struct with type-state -pub struct TracingBuilder { +// Main builder struct +pub struct TracingBuilder { log_format: LogFormat, filter: Option, service_name: String, - _state: PhantomData, } // Configuration that will be used during build @@ -41,23 +23,22 @@ enum TelemetryConfig { // Builder for OTLP telemetry configuration pub struct OtlpTelemetryBuilder { endpoint: Option, - parent_builder: TracingBuilder, + parent_builder: TracingBuilder, } // Builder for Google Cloud telemetry configuration pub struct GcloudTelemetryBuilder { project_id: Option, - parent_builder: TracingBuilder, + parent_builder: TracingBuilder, } -impl TracingBuilder { +impl TracingBuilder { /// Create a new tracing builder with default settings pub fn new() -> Self { Self { log_format: LogFormat::Full, filter: None, service_name: "katana".to_string(), - _state: PhantomData, } } @@ -110,21 +91,20 @@ impl TracingBuilder { self } - /// Finalize the basic configuration and prepare for optional telemetry - pub fn configure(self) -> TracingBuilder { - TracingBuilder { - log_format: self.log_format, - filter: self.filter, - service_name: self.service_name, - _state: PhantomData, + /// Configure OTLP telemetry + pub fn otlp(self) -> OtlpTelemetryBuilder { + OtlpTelemetryBuilder { + endpoint: None, + parent_builder: self, } } -} -impl TracingBuilder { - /// Add telemetry configuration - pub fn with_telemetry(self) -> TelemetrySelector { - TelemetrySelector { parent_builder: self } + /// Configure Google Cloud telemetry + pub fn gcloud(self) -> GcloudTelemetryBuilder { + GcloudTelemetryBuilder { + project_id: None, + parent_builder: self, + } } /// Build the tracing subscriber without telemetry @@ -192,23 +172,6 @@ impl TracingBuilder { } } -/// Selector for choosing telemetry backend -pub struct TelemetrySelector { - parent_builder: TracingBuilder, -} - -impl TelemetrySelector { - /// Configure OTLP telemetry - pub fn otlp(self) -> OtlpTelemetryBuilder { - OtlpTelemetryBuilder { endpoint: None, parent_builder: self.parent_builder } - } - - /// Configure Google Cloud telemetry - pub fn gcloud(self) -> GcloudTelemetryBuilder { - GcloudTelemetryBuilder { project_id: None, parent_builder: self.parent_builder } - } -} - impl OtlpTelemetryBuilder { /// Set the OTLP endpoint pub fn with_endpoint(mut self, endpoint: impl Into) -> Self { @@ -218,7 +181,9 @@ impl OtlpTelemetryBuilder { /// Build the tracing subscriber with OTLP telemetry pub async fn build(self) -> Result<(), Error> { - let config = otlp::OtlpConfig { endpoint: self.endpoint }; + let config = otlp::OtlpConfig { + endpoint: self.endpoint, + }; self.parent_builder.build_with_telemetry(TelemetryConfig::Otlp(config)).await } } @@ -232,12 +197,14 @@ impl GcloudTelemetryBuilder { /// Build the tracing subscriber with Google Cloud telemetry pub async fn build(self) -> Result<(), Error> { - let config = gcloud::GcloudConfig { project_id: self.project_id }; + let config = gcloud::GcloudConfig { + project_id: self.project_id, + }; self.parent_builder.build_with_telemetry(TelemetryConfig::Gcloud(config)).await } } -impl Default for TracingBuilder { +impl Default for TracingBuilder { fn default() -> Self { Self::new() } @@ -250,28 +217,30 @@ mod tests { #[test] fn test_builder_type_safety() { // This test ensures the builder pattern compiles correctly - // and demonstrates the type-safe flow - + // and demonstrates the fluent API flow + // The following should compile: let _builder = TracingBuilder::new() .with_log_format(LogFormat::Json) - .with_service_name("test-service") - .configure(); + .with_service_name("test-service"); - // Test that telemetry selection is exclusive - // (can't test runtime behavior without async runtime in unit tests) + // Test that telemetry selection works + let _otlp_builder = TracingBuilder::new().otlp(); + let _gcloud_builder = TracingBuilder::new().gcloud(); } #[tokio::test] async fn test_builder_without_telemetry() { // Note: This will fail if tracing is already initialized // In practice, this would be tested with a custom registry - + // Just ensure the builder compiles and doesn't panic - let result = - TracingBuilder::new().with_log_format(LogFormat::Json).configure().build().await; - + let result = TracingBuilder::new() + .with_log_format(LogFormat::Json) + .build() + .await; + // The second initialization should fail assert!(result.is_ok() || result.is_err()); } -} +} \ No newline at end of file diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index d8a7e3855..feb9593f2 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -50,19 +50,18 @@ pub enum Error { /// This function is maintained for backward compatibility. /// For new code, consider using [`TracingBuilder`] for more flexibility. pub async fn init(format: LogFormat, telemetry_config: Option) -> Result<(), Error> { - let builder = - TracingBuilder::new().with_log_format(format).with_env_filter_or_default()?.configure(); + let builder = TracingBuilder::new().with_log_format(format).with_env_filter_or_default()?; match telemetry_config { Some(TracerConfig::Otlp(cfg)) => { - let mut otlp_builder = builder.with_telemetry().otlp(); + let mut otlp_builder = builder.otlp(); if let Some(endpoint) = cfg.endpoint { otlp_builder = otlp_builder.with_endpoint(endpoint); } otlp_builder.build().await } Some(TracerConfig::Gcloud(cfg)) => { - let mut gcloud_builder = builder.with_telemetry().gcloud(); + let mut gcloud_builder = builder.gcloud(); if let Some(project_id) = cfg.project_id { gcloud_builder = gcloud_builder.with_project_id(project_id); } From e6027a1f3d56dcbe47a85821c24407ba4f6a686c Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Fri, 5 Sep 2025 12:49:26 -0400 Subject: [PATCH 3/9] feat(tracing): add type-state pattern for log format selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement compile-time enforcement for log format selection using type-state pattern: - Log format must now be chosen first via .json() or .full() methods - Filter and service configuration only available after format is selected - Telemetry methods (otlp/gcloud) only accessible after format is set - Maintains backward compatibility through internal with_format() helper This ensures users cannot accidentally build without specifying a format, catching configuration errors at compile time rather than runtime. Example: TracingBuilder::new() .json() // Must choose format first .with_service_name("my-service") .build() The compiler will reject: TracingBuilder::new() .with_service_name("my-service") // Error: method not found .build() 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- crates/tracing/examples/builder_usage.rs | 10 ++- crates/tracing/src/builder.rs | 94 +++++++++++++++++------- crates/tracing/src/lib.rs | 2 +- 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/crates/tracing/examples/builder_usage.rs b/crates/tracing/examples/builder_usage.rs index dc095a1b2..772ceb86d 100644 --- a/crates/tracing/examples/builder_usage.rs +++ b/crates/tracing/examples/builder_usage.rs @@ -1,10 +1,10 @@ -use katana_tracing::{LogFormat, TracingBuilder}; +use katana_tracing::TracingBuilder; #[tokio::main] async fn main() -> Result<(), Box> { // Example 1: Basic configuration without telemetry TracingBuilder::new() - .with_log_format(LogFormat::Json) + .json() // Must choose format first .with_default_filter()? .build() .await?; @@ -16,7 +16,7 @@ async fn main() -> Result<(), Box> { // Example 2: With OTLP telemetry // TracingBuilder::new() - // .with_log_format(LogFormat::Full) + // .full() // Choose format first // .with_env_filter_or_default()? // .with_service_name("my-katana-node") // .otlp() @@ -26,7 +26,7 @@ async fn main() -> Result<(), Box> { // Example 3: With Google Cloud telemetry // TracingBuilder::new() - // .with_log_format(LogFormat::Json) + // .json() // Choose format first // .with_filter("debug")? // .with_service_name("katana-prod") // .gcloud() @@ -36,12 +36,14 @@ async fn main() -> Result<(), Box> { // Example 4: Using environment filter // TracingBuilder::new() + // .full() // Must choose format // .with_env_filter()? // Uses RUST_LOG environment variable // .build() // .await?; // Example 5: Custom filter with OTLP telemetry (using default endpoint) // TracingBuilder::new() + // .json() // .with_filter("katana=debug,tower=info")? // .with_service_name("custom-service") // .otlp() diff --git a/crates/tracing/src/builder.rs b/crates/tracing/src/builder.rs index 6b58ac3c8..a10c903d8 100644 --- a/crates/tracing/src/builder.rs +++ b/crates/tracing/src/builder.rs @@ -1,3 +1,4 @@ +use std::marker::PhantomData; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; @@ -6,11 +7,16 @@ use tracing_subscriber::{EnvFilter, Layer}; use crate::fmt::LocalTime; use crate::{gcloud, otlp, Error, LogFormat}; -// Main builder struct -pub struct TracingBuilder { - log_format: LogFormat, +// Type-state markers for log format +pub struct NoFormat; +pub struct WithFormat; + +// Main builder struct with type-state for format +pub struct TracingBuilder { + log_format: Option, filter: Option, service_name: String, + _format: PhantomData, } // Configuration that will be used during build @@ -23,31 +29,48 @@ enum TelemetryConfig { // Builder for OTLP telemetry configuration pub struct OtlpTelemetryBuilder { endpoint: Option, - parent_builder: TracingBuilder, + parent_builder: TracingBuilder, } // Builder for Google Cloud telemetry configuration pub struct GcloudTelemetryBuilder { project_id: Option, - parent_builder: TracingBuilder, + parent_builder: TracingBuilder, } -impl TracingBuilder { - /// Create a new tracing builder with default settings +impl TracingBuilder { + /// Create a new tracing builder pub fn new() -> Self { Self { - log_format: LogFormat::Full, + log_format: None, filter: None, service_name: "katana".to_string(), + _format: PhantomData, } } - /// Set the log format - pub fn with_log_format(mut self, format: LogFormat) -> Self { - self.log_format = format; - self + /// Set the log format to full (human-readable with colors) + pub fn full(self) -> TracingBuilder { + TracingBuilder { + log_format: Some(LogFormat::Full), + filter: self.filter, + service_name: self.service_name, + _format: PhantomData, + } + } + + /// Set the log format to JSON + pub fn json(self) -> TracingBuilder { + TracingBuilder { + log_format: Some(LogFormat::Json), + filter: self.filter, + service_name: self.service_name, + _format: PhantomData, + } } +} +impl TracingBuilder { /// Set a custom filter from a string pub fn with_filter(mut self, filter: &str) -> Result { self.filter = Some(EnvFilter::try_new(filter)?); @@ -123,9 +146,11 @@ impl TracingBuilder { EnvFilter::try_new(DEFAULT_LOG_FILTER).expect("default filter should be valid") }); + let log_format = self.log_format.expect("log format must be set"); + match telemetry_config { TelemetryConfig::None => { - let fmt = match self.log_format { + let fmt = match log_format { LogFormat::Full => { tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() } @@ -140,7 +165,7 @@ impl TracingBuilder { let tracer = otlp::init_tracer_with_service(&cfg, &self.service_name)?; let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - let fmt = match self.log_format { + let fmt = match log_format { LogFormat::Full => { tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() } @@ -155,7 +180,7 @@ impl TracingBuilder { let tracer = gcloud::init_tracer_with_service(&cfg, &self.service_name).await?; let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - let fmt = match self.log_format { + let fmt = match log_format { LogFormat::Full => { tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() } @@ -204,39 +229,58 @@ impl GcloudTelemetryBuilder { } } -impl Default for TracingBuilder { +impl Default for TracingBuilder { fn default() -> Self { Self::new() } } +// Helper function for backward compatibility - creates a builder with a format already set +impl TracingBuilder { + /// Create a builder with a pre-selected format (for backward compatibility) + pub(crate) fn with_format(format: LogFormat) -> TracingBuilder { + match format { + LogFormat::Full => Self::new().full(), + LogFormat::Json => Self::new().json(), + } + } +} + #[cfg(test)] mod tests { use super::*; #[test] - fn test_builder_type_safety() { - // This test ensures the builder pattern compiles correctly - // and demonstrates the fluent API flow + fn test_builder_requires_format() { + // This demonstrates that you must choose a format before building // The following should compile: let _builder = TracingBuilder::new() - .with_log_format(LogFormat::Json) + .json() .with_service_name("test-service"); - // Test that telemetry selection works - let _otlp_builder = TracingBuilder::new().otlp(); - let _gcloud_builder = TracingBuilder::new().gcloud(); + let _builder = TracingBuilder::new() + .full() + .with_service_name("test-service"); + + // The following would NOT compile (commented out): + // let _builder = TracingBuilder::new() + // .with_service_name("test-service") // Error: method not found + // .build(); + + // Test that telemetry selection works after format is set + let _otlp_builder = TracingBuilder::new().json().otlp(); + let _gcloud_builder = TracingBuilder::new().full().gcloud(); } #[tokio::test] - async fn test_builder_without_telemetry() { + async fn test_builder_with_format() { // Note: This will fail if tracing is already initialized // In practice, this would be tested with a custom registry // Just ensure the builder compiles and doesn't panic let result = TracingBuilder::new() - .with_log_format(LogFormat::Json) + .json() .build() .await; diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index feb9593f2..553c3fda5 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -50,7 +50,7 @@ pub enum Error { /// This function is maintained for backward compatibility. /// For new code, consider using [`TracingBuilder`] for more flexibility. pub async fn init(format: LogFormat, telemetry_config: Option) -> Result<(), Error> { - let builder = TracingBuilder::new().with_log_format(format).with_env_filter_or_default()?; + let builder = builder::TracingBuilder::with_format(format).with_env_filter_or_default()?; match telemetry_config { Some(TracerConfig::Otlp(cfg)) => { From 148d805101a646a777589207897dfd9e6f91b431 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sat, 6 Sep 2025 17:43:09 -0400 Subject: [PATCH 4/9] human takeover --- crates/cli/src/options.rs | 2 +- crates/tracing/Cargo.toml | 2 +- crates/tracing/examples/builder_usage.rs | 54 ---- crates/tracing/src/builder.rs | 309 ++++++++--------------- crates/tracing/src/lib.rs | 41 +-- 5 files changed, 124 insertions(+), 284 deletions(-) delete mode 100644 crates/tracing/examples/builder_usage.rs diff --git a/crates/cli/src/options.rs b/crates/cli/src/options.rs index ee5b33885..10d2db095 100644 --- a/crates/cli/src/options.rs +++ b/crates/cli/src/options.rs @@ -628,7 +628,7 @@ impl TracerOptions { /// Get the tracer configuration based on the options pub fn config(&self) -> Option { if self.tracer_gcloud { - Some(TracerConfig::Gcloud(gcloud::GcloudConfig { + Some(TracerConfig::GCloud(gcloud::GcloudConfig { project_id: self.gcloud_project_id.clone(), })) } else if self.tracer_otlp { diff --git a/crates/tracing/Cargo.toml b/crates/tracing/Cargo.toml index c4e5ffaae..1b6f157d2 100644 --- a/crates/tracing/Cargo.toml +++ b/crates/tracing/Cargo.toml @@ -31,4 +31,4 @@ chrono.workspace = true http-body-util = "0.1.3" [dev-dependencies] -tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } +tokio = { workspace = true, features = [ "macros", "rt-multi-thread" ] } diff --git a/crates/tracing/examples/builder_usage.rs b/crates/tracing/examples/builder_usage.rs deleted file mode 100644 index 772ceb86d..000000000 --- a/crates/tracing/examples/builder_usage.rs +++ /dev/null @@ -1,54 +0,0 @@ -use katana_tracing::TracingBuilder; - -#[tokio::main] -async fn main() -> Result<(), Box> { - // Example 1: Basic configuration without telemetry - TracingBuilder::new() - .json() // Must choose format first - .with_default_filter()? - .build() - .await?; - - tracing::info!("Basic logging initialized"); - - // Note: In a real application, you can only initialize tracing once. - // The examples below show different configuration options. - - // Example 2: With OTLP telemetry - // TracingBuilder::new() - // .full() // Choose format first - // .with_env_filter_or_default()? - // .with_service_name("my-katana-node") - // .otlp() - // .with_endpoint("http://localhost:4317") - // .build() - // .await?; - - // Example 3: With Google Cloud telemetry - // TracingBuilder::new() - // .json() // Choose format first - // .with_filter("debug")? - // .with_service_name("katana-prod") - // .gcloud() - // .with_project_id("my-project") - // .build() - // .await?; - - // Example 4: Using environment filter - // TracingBuilder::new() - // .full() // Must choose format - // .with_env_filter()? // Uses RUST_LOG environment variable - // .build() - // .await?; - - // Example 5: Custom filter with OTLP telemetry (using default endpoint) - // TracingBuilder::new() - // .json() - // .with_filter("katana=debug,tower=info")? - // .with_service_name("custom-service") - // .otlp() - // .build() // Uses default OTLP endpoint - // .await?; - - Ok(()) -} diff --git a/crates/tracing/src/builder.rs b/crates/tracing/src/builder.rs index a10c903d8..2945e89d9 100644 --- a/crates/tracing/src/builder.rs +++ b/crates/tracing/src/builder.rs @@ -2,75 +2,65 @@ use std::marker::PhantomData; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::{EnvFilter, Layer}; +use tracing_subscriber::{fmt, EnvFilter, Layer}; use crate::fmt::LocalTime; -use crate::{gcloud, otlp, Error, LogFormat}; - -// Type-state markers for log format -pub struct NoFormat; -pub struct WithFormat; +use crate::{gcloud, otlp, Error, LogFormat, TracerConfig}; + +const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,pipeline=debug,\ + stage=debug,tasks=debug,executor=trace,forking::backend=trace,\ + blockifier=off,jsonrpsee_server=off,hyper=off,messaging=debug,\ + node=error,explorer=info,rpc=trace,pool=trace,info"; + +/// Identity type-state markers for [`TracingBuilder`]. +#[derive(Debug)] +pub struct Identity; + +/// [`TracingBuilder`] type-state markers for full log format [`LogFormat::Full`]. +#[derive(Debug)] +pub struct FullFormat; +/// [`TracingBuilder`] type-state markers for JSON log format [`LogFormat::Json`]. +#[derive(Debug)] +pub struct JsonFormat; + +/// [`TracingBuilder`] type-state markers for OTLP tracer. +#[derive(Debug)] +pub struct OtlpTracer; +/// [`TracingBuilder`] type-state markers for GCloud tracer. +#[derive(Debug)] +pub struct GCloudTracer; // Main builder struct with type-state for format -pub struct TracingBuilder { +#[derive(Debug)] +pub struct TracingBuilder { + service_name: String, log_format: Option, filter: Option, - service_name: String, - _format: PhantomData, -} - -// Configuration that will be used during build -enum TelemetryConfig { - None, - Otlp(otlp::OtlpConfig), - Gcloud(gcloud::GcloudConfig), -} - -// Builder for OTLP telemetry configuration -pub struct OtlpTelemetryBuilder { - endpoint: Option, - parent_builder: TracingBuilder, -} - -// Builder for Google Cloud telemetry configuration -pub struct GcloudTelemetryBuilder { - project_id: Option, - parent_builder: TracingBuilder, + tracer: Option, + _format: PhantomData, + _telemetry: PhantomData, } -impl TracingBuilder { +impl TracingBuilder { /// Create a new tracing builder pub fn new() -> Self { Self { + service_name: "katana".to_string(), log_format: None, filter: None, - service_name: "katana".to_string(), - _format: PhantomData, - } - } - - /// Set the log format to full (human-readable with colors) - pub fn full(self) -> TracingBuilder { - TracingBuilder { - log_format: Some(LogFormat::Full), - filter: self.filter, - service_name: self.service_name, + tracer: None, _format: PhantomData, + _telemetry: PhantomData, } } +} - /// Set the log format to JSON - pub fn json(self) -> TracingBuilder { - TracingBuilder { - log_format: Some(LogFormat::Json), - filter: self.filter, - service_name: self.service_name, - _format: PhantomData, - } +impl TracingBuilder { + pub fn service_name(mut self, service_name: S) -> Self { + self.service_name = service_name.to_string(); + self } -} -impl TracingBuilder { /// Set a custom filter from a string pub fn with_filter(mut self, filter: &str) -> Result { self.filter = Some(EnvFilter::try_new(filter)?); @@ -79,12 +69,6 @@ impl TracingBuilder { /// Use the default filter pub fn with_default_filter(mut self) -> Result { - const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,\ - pipeline=debug,stage=debug,tasks=debug,executor=trace,\ - forking::backend=trace,blockifier=off,\ - jsonrpsee_server=off,hyper=off,messaging=debug,\ - node=error,explorer=info,rpc=trace,pool=trace,info"; - self.filter = Some(EnvFilter::try_new(DEFAULT_LOG_FILTER)?); Ok(self) } @@ -97,194 +81,99 @@ impl TracingBuilder { /// Use filter from environment with fallback to default pub fn with_env_filter_or_default(mut self) -> Result { - const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,\ - pipeline=debug,stage=debug,tasks=debug,executor=trace,\ - forking::backend=trace,blockifier=off,\ - jsonrpsee_server=off,hyper=off,messaging=debug,\ - node=error,explorer=info,rpc=trace,pool=trace,info"; - let default_filter = EnvFilter::try_new(DEFAULT_LOG_FILTER); self.filter = Some(EnvFilter::try_from_default_env().or(default_filter)?); Ok(self) } - /// Set the service name (default: "katana") - pub fn with_service_name(mut self, name: impl Into) -> Self { - self.service_name = name.into(); - self - } - - /// Configure OTLP telemetry - pub fn otlp(self) -> OtlpTelemetryBuilder { - OtlpTelemetryBuilder { - endpoint: None, - parent_builder: self, - } - } - - /// Configure Google Cloud telemetry - pub fn gcloud(self) -> GcloudTelemetryBuilder { - GcloudTelemetryBuilder { - project_id: None, - parent_builder: self, - } - } - - /// Build the tracing subscriber without telemetry - pub async fn build(self) -> Result<(), Error> { - self.build_with_telemetry(TelemetryConfig::None).await - } - - async fn build_with_telemetry(self, telemetry_config: TelemetryConfig) -> Result<(), Error> { + pub async fn try_init(self) -> Result<(), Error> { let filter = self.filter.unwrap_or_else(|| { - const DEFAULT_LOG_FILTER: &str = - "katana_db::mdbx=trace,cairo_native::compiler=off,pipeline=debug,stage=debug,\ - tasks=debug,executor=trace,forking::backend=trace,blockifier=off,\ - jsonrpsee_server=off,hyper=off,messaging=debug,node=error,explorer=info,\ - rpc=trace,pool=trace,info"; - EnvFilter::try_new(DEFAULT_LOG_FILTER).expect("default filter should be valid") }); - let log_format = self.log_format.expect("log format must be set"); + let log_format = self.log_format.unwrap_or(LogFormat::Full); - match telemetry_config { - TelemetryConfig::None => { - let fmt = match log_format { - LogFormat::Full => { - tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() - } - LogFormat::Json => { - tracing_subscriber::fmt::layer().json().with_timer(LocalTime::new()).boxed() - } - }; + let fmt = match log_format { + LogFormat::Full => fmt::layer().with_timer(LocalTime::new()).boxed(), + LogFormat::Json => fmt::layer().json().with_timer(LocalTime::new()).boxed(), + }; - tracing_subscriber::registry().with(filter).with(fmt).init(); - } - TelemetryConfig::Otlp(cfg) => { + let registry = tracing_subscriber::registry().with(filter).with(fmt); + + match self.tracer { + Some(TracerConfig::Otlp(cfg)) => { let tracer = otlp::init_tracer_with_service(&cfg, &self.service_name)?; let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - - let fmt = match log_format { - LogFormat::Full => { - tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() - } - LogFormat::Json => { - tracing_subscriber::fmt::layer().json().with_timer(LocalTime::new()).boxed() - } - }; - - tracing_subscriber::registry().with(filter).with(telemetry).with(fmt).init(); + registry.with(telemetry).init(); } - TelemetryConfig::Gcloud(cfg) => { + Some(TracerConfig::GCloud(cfg)) => { let tracer = gcloud::init_tracer_with_service(&cfg, &self.service_name).await?; let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - - let fmt = match log_format { - LogFormat::Full => { - tracing_subscriber::fmt::layer().with_timer(LocalTime::new()).boxed() - } - LogFormat::Json => { - tracing_subscriber::fmt::layer().json().with_timer(LocalTime::new()).boxed() - } - }; - - tracing_subscriber::registry().with(filter).with(telemetry).with(fmt).init(); + registry.with(telemetry).init(); } + None => registry.init(), } Ok(()) } -} - -impl OtlpTelemetryBuilder { - /// Set the OTLP endpoint - pub fn with_endpoint(mut self, endpoint: impl Into) -> Self { - self.endpoint = Some(endpoint.into()); - self - } - /// Build the tracing subscriber with OTLP telemetry - pub async fn build(self) -> Result<(), Error> { - let config = otlp::OtlpConfig { - endpoint: self.endpoint, - }; - self.parent_builder.build_with_telemetry(TelemetryConfig::Otlp(config)).await + pub async fn init(self) { + self.try_init().await.expect("failed to initialize global tracer") } } -impl GcloudTelemetryBuilder { - /// Set the Google Cloud project ID - pub fn with_project_id(mut self, project_id: impl Into) -> Self { - self.project_id = Some(project_id.into()); - self +impl TracingBuilder { + /// Set the log format to full (human-readable with colors) + pub fn full(self) -> TracingBuilder { + TracingBuilder { + service_name: self.service_name, + log_format: Some(LogFormat::Full), + tracer: self.tracer, + filter: self.filter, + _format: PhantomData, + _telemetry: PhantomData, + } } - /// Build the tracing subscriber with Google Cloud telemetry - pub async fn build(self) -> Result<(), Error> { - let config = gcloud::GcloudConfig { - project_id: self.project_id, - }; - self.parent_builder.build_with_telemetry(TelemetryConfig::Gcloud(config)).await + /// Set the log format to JSON + pub fn json(self) -> TracingBuilder { + TracingBuilder { + service_name: self.service_name, + log_format: Some(LogFormat::Json), + tracer: self.tracer, + filter: self.filter, + _format: PhantomData, + _telemetry: PhantomData, + } } } -impl Default for TracingBuilder { - fn default() -> Self { - Self::new() +impl TracingBuilder { + pub fn with_otlp(self, config: otlp::OtlpConfig) -> TracingBuilder { + TracingBuilder { + service_name: self.service_name, + filter: self.filter, + log_format: self.log_format, + tracer: Some(TracerConfig::Otlp(config)), + _format: PhantomData, + _telemetry: PhantomData, + } } -} -// Helper function for backward compatibility - creates a builder with a format already set -impl TracingBuilder { - /// Create a builder with a pre-selected format (for backward compatibility) - pub(crate) fn with_format(format: LogFormat) -> TracingBuilder { - match format { - LogFormat::Full => Self::new().full(), - LogFormat::Json => Self::new().json(), + pub fn with_gcloud(self, config: gcloud::GcloudConfig) -> TracingBuilder { + TracingBuilder { + service_name: self.service_name, + filter: self.filter, + log_format: self.log_format, + tracer: Some(TracerConfig::GCloud(config)), + _format: PhantomData, + _telemetry: PhantomData, } } } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_builder_requires_format() { - // This demonstrates that you must choose a format before building - - // The following should compile: - let _builder = TracingBuilder::new() - .json() - .with_service_name("test-service"); - - let _builder = TracingBuilder::new() - .full() - .with_service_name("test-service"); - - // The following would NOT compile (commented out): - // let _builder = TracingBuilder::new() - // .with_service_name("test-service") // Error: method not found - // .build(); - - // Test that telemetry selection works after format is set - let _otlp_builder = TracingBuilder::new().json().otlp(); - let _gcloud_builder = TracingBuilder::new().full().gcloud(); - } - - #[tokio::test] - async fn test_builder_with_format() { - // Note: This will fail if tracing is already initialized - // In practice, this would be tested with a custom registry - - // Just ensure the builder compiles and doesn't panic - let result = TracingBuilder::new() - .json() - .build() - .await; - - // The second initialization should fail - assert!(result.is_ok() || result.is_err()); +impl Default for TracingBuilder { + fn default() -> Self { + Self::new() } -} \ No newline at end of file +} diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index 553c3fda5..cb5edea51 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -11,11 +11,10 @@ pub mod otlp; pub use builder::TracingBuilder; pub use fmt::LogFormat; - #[derive(Debug, Clone)] pub enum TracerConfig { Otlp(otlp::OtlpConfig), - Gcloud(gcloud::GcloudConfig), + GCloud(gcloud::GcloudConfig), } #[derive(Debug, thiserror::Error)] @@ -25,7 +24,7 @@ pub enum Error { #[error("failed to parse environment filter: {0}")] EnvFilterParse(#[from] filter::ParseError), - + #[error("failed to parse environment filter from env: {0}")] EnvFilterFromEnv(#[from] filter::FromEnvError), @@ -50,23 +49,29 @@ pub enum Error { /// This function is maintained for backward compatibility. /// For new code, consider using [`TracingBuilder`] for more flexibility. pub async fn init(format: LogFormat, telemetry_config: Option) -> Result<(), Error> { - let builder = builder::TracingBuilder::with_format(format).with_env_filter_or_default()?; + match format { + LogFormat::Full => match telemetry_config { + Some(TracerConfig::Otlp(cfg)) => { + TracingBuilder::new().full().with_otlp(cfg).try_init().await + } + + Some(TracerConfig::GCloud(cfg)) => { + TracingBuilder::new().full().with_gcloud(cfg).try_init().await + } + + None => TracingBuilder::new().full().try_init().await, + }, - match telemetry_config { - Some(TracerConfig::Otlp(cfg)) => { - let mut otlp_builder = builder.otlp(); - if let Some(endpoint) = cfg.endpoint { - otlp_builder = otlp_builder.with_endpoint(endpoint); + LogFormat::Json => match telemetry_config { + Some(TracerConfig::Otlp(cfg)) => { + TracingBuilder::new().json().with_otlp(cfg).try_init().await } - otlp_builder.build().await - } - Some(TracerConfig::Gcloud(cfg)) => { - let mut gcloud_builder = builder.gcloud(); - if let Some(project_id) = cfg.project_id { - gcloud_builder = gcloud_builder.with_project_id(project_id); + + Some(TracerConfig::GCloud(cfg)) => { + TracingBuilder::new().json().with_gcloud(cfg).try_init().await } - gcloud_builder.build().await - } - None => builder.build().await, + + None => TracingBuilder::new().json().try_init().await, + }, } } From 9a7cdd52b958bcb8393e7d3a4b23f2124ccf21f6 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 9 Sep 2025 17:29:38 -0400 Subject: [PATCH 5/9] save --- crates/tracing/src/builder.rs | 174 +++++++++++++++------------------- crates/tracing/src/gcloud.rs | 132 ++++++++++++++++++++++++-- crates/tracing/src/lib.rs | 75 ++++++++++----- crates/tracing/src/otlp.rs | 87 +++++++++++++++-- 4 files changed, 330 insertions(+), 138 deletions(-) diff --git a/crates/tracing/src/builder.rs b/crates/tracing/src/builder.rs index 2945e89d9..76adcba9c 100644 --- a/crates/tracing/src/builder.rs +++ b/crates/tracing/src/builder.rs @@ -1,66 +1,84 @@ use std::marker::PhantomData; -use tracing_subscriber::layer::SubscriberExt; +use opentelemetry::trace::noop::NoopTracer; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::fmt::format::{self}; +use tracing_subscriber::layer::{Layered, SubscriberExt}; use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::{fmt, EnvFilter, Layer}; +use tracing_subscriber::{fmt, EnvFilter, Layer, Registry}; use crate::fmt::LocalTime; -use crate::{gcloud, otlp, Error, LogFormat, TracerConfig}; +use crate::{Error, GCloudTracingBuilder, LogFormat, TelemetryTracer}; + +type DefaultFormat = format::Full; + +type Subscriber = Layered< + OpenTelemetryLayer< + Layered< + Box> + Send + Sync + 'static>, + Layered, + >, + Tracer, + >, + Layered< + Box> + Send + Sync + 'static>, + Layered, + >, +>; + +struct TracingSubscriber { + subscriber: Subscriber, + _fmt: PhantomData, +} + +impl TracingSubscriber { + fn init(self) { + self.subscriber.init(); + } +} const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,pipeline=debug,\ stage=debug,tasks=debug,executor=trace,forking::backend=trace,\ blockifier=off,jsonrpsee_server=off,hyper=off,messaging=debug,\ node=error,explorer=info,rpc=trace,pool=trace,info"; -/// Identity type-state markers for [`TracingBuilder`]. -#[derive(Debug)] -pub struct Identity; - -/// [`TracingBuilder`] type-state markers for full log format [`LogFormat::Full`]. -#[derive(Debug)] -pub struct FullFormat; -/// [`TracingBuilder`] type-state markers for JSON log format [`LogFormat::Json`]. -#[derive(Debug)] -pub struct JsonFormat; - -/// [`TracingBuilder`] type-state markers for OTLP tracer. -#[derive(Debug)] -pub struct OtlpTracer; -/// [`TracingBuilder`] type-state markers for GCloud tracer. -#[derive(Debug)] -pub struct GCloudTracer; +// /// Identity type-state markers for [`TracingBuilder`]. +// #[derive(Debug)] +// pub struct Identity; // Main builder struct with type-state for format #[derive(Debug)] -pub struct TracingBuilder { - service_name: String, - log_format: Option, +pub struct TracingBuilder { + log_format: LogFormat, filter: Option, - tracer: Option, + tracer: Telemetry, _format: PhantomData, - _telemetry: PhantomData, } impl TracingBuilder { /// Create a new tracing builder pub fn new() -> Self { Self { - service_name: "katana".to_string(), - log_format: None, filter: None, - tracer: None, + log_format: LogFormat::Full, + tracer: NoopTracer::new(), _format: PhantomData, - _telemetry: PhantomData, } } } -impl TracingBuilder { - pub fn service_name(mut self, service_name: S) -> Self { - self.service_name = service_name.to_string(); - self +impl TracingBuilder { + pub fn with_telemetry(self, tracer: T) -> TracingBuilder { + TracingBuilder { + filter: self.filter, + log_format: self.log_format, + tracer, + _format: PhantomData, + } } +} +impl TracingBuilder { /// Set a custom filter from a string pub fn with_filter(mut self, filter: &str) -> Result { self.filter = Some(EnvFilter::try_new(filter)?); @@ -85,89 +103,36 @@ impl TracingBuilder { self.filter = Some(EnvFilter::try_from_default_env().or(default_filter)?); Ok(self) } +} - pub async fn try_init(self) -> Result<(), Error> { +impl TracingBuilder { + /// Try to initialize the tracing subscriber without telemetry + pub fn build(self) -> Result, Error> { let filter = self.filter.unwrap_or_else(|| { EnvFilter::try_new(DEFAULT_LOG_FILTER).expect("default filter should be valid") }); - let log_format = self.log_format.unwrap_or(LogFormat::Full); - - let fmt = match log_format { - LogFormat::Full => fmt::layer().with_timer(LocalTime::new()).boxed(), - LogFormat::Json => fmt::layer().json().with_timer(LocalTime::new()).boxed(), + let fmt_layer = fmt::layer().with_timer(LocalTime::new()); + let fmt_layer = match self.log_format { + LogFormat::Full => fmt_layer.boxed(), + LogFormat::Json => fmt_layer.json().boxed(), }; - let registry = tracing_subscriber::registry().with(filter).with(fmt); - - match self.tracer { - Some(TracerConfig::Otlp(cfg)) => { - let tracer = otlp::init_tracer_with_service(&cfg, &self.service_name)?; - let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - registry.with(telemetry).init(); - } - Some(TracerConfig::GCloud(cfg)) => { - let tracer = gcloud::init_tracer_with_service(&cfg, &self.service_name).await?; - let telemetry = tracing_opentelemetry::layer().with_tracer(tracer); - registry.with(telemetry).init(); - } - None => registry.init(), - } - - Ok(()) - } + let telem = tracing_opentelemetry::layer().with_tracer(self.tracer); + let subscriber = tracing_subscriber::registry().with(filter).with(fmt_layer).with(telem); - pub async fn init(self) { - self.try_init().await.expect("failed to initialize global tracer") + Ok(TracingSubscriber { subscriber, _fmt: PhantomData }) } } -impl TracingBuilder { - /// Set the log format to full (human-readable with colors) - pub fn full(self) -> TracingBuilder { - TracingBuilder { - service_name: self.service_name, - log_format: Some(LogFormat::Full), - tracer: self.tracer, - filter: self.filter, - _format: PhantomData, - _telemetry: PhantomData, - } - } - +impl TracingBuilder { /// Set the log format to JSON - pub fn json(self) -> TracingBuilder { + pub fn json(self) -> TracingBuilder { TracingBuilder { - service_name: self.service_name, - log_format: Some(LogFormat::Json), + log_format: LogFormat::Json, tracer: self.tracer, filter: self.filter, _format: PhantomData, - _telemetry: PhantomData, - } - } -} - -impl TracingBuilder { - pub fn with_otlp(self, config: otlp::OtlpConfig) -> TracingBuilder { - TracingBuilder { - service_name: self.service_name, - filter: self.filter, - log_format: self.log_format, - tracer: Some(TracerConfig::Otlp(config)), - _format: PhantomData, - _telemetry: PhantomData, - } - } - - pub fn with_gcloud(self, config: gcloud::GcloudConfig) -> TracingBuilder { - TracingBuilder { - service_name: self.service_name, - filter: self.filter, - log_format: self.log_format, - tracer: Some(TracerConfig::GCloud(config)), - _format: PhantomData, - _telemetry: PhantomData, } } } @@ -177,3 +142,12 @@ impl Default for TracingBuilder { Self::new() } } + +#[cfg(test)] +#[tokio::test] +async fn foo() { + let builder = TracingBuilder::new().build().unwrap(); + + let gcloud = GCloudTracingBuilder::new().build().await.unwrap(); + let builder = TracingBuilder::new().json().with_telemetry(gcloud).build().unwrap(); +} diff --git a/crates/tracing/src/gcloud.rs b/crates/tracing/src/gcloud.rs index a52e9ca82..f97dba232 100644 --- a/crates/tracing/src/gcloud.rs +++ b/crates/tracing/src/gcloud.rs @@ -1,19 +1,26 @@ use http::Request; +use opentelemetry::trace::Tracer; use opentelemetry_gcloud_trace::{GcpCloudTraceExporterBuilder, SdkTracer}; use opentelemetry_http::HeaderExtractor; use opentelemetry_sdk::Resource; use opentelemetry_stackdriver::google_trace_context_propagator::GoogleTraceContextPropagator; use tower_http::trace::MakeSpan; -use tracing::Span; -use tracing_opentelemetry::OpenTelemetrySpanExt; +use tracing_opentelemetry::{OpenTelemetryLayer, OpenTelemetrySpanExt, PreSampledTracer}; +use tracing_subscriber::Registry; use crate::Error; +/// Wrapper type for SdkTracer that implements the Tracer trait +#[derive(Debug, Clone)] +pub struct GCloudTracer { + tracer: SdkTracer, +} + #[derive(Debug, Clone, Default)] pub struct GoogleStackDriverMakeSpan; impl MakeSpan for GoogleStackDriverMakeSpan { - fn make_span(&mut self, request: &Request) -> Span { + fn make_span(&mut self, request: &Request) -> tracing::Span { // Extract trace context from HTTP headers let cx = opentelemetry::global::get_text_map_propagator(|propagator| { propagator.extract(&HeaderExtractor(request.headers())) @@ -36,11 +43,122 @@ pub struct GcloudConfig { pub project_id: Option, } -/// Initialize Google Cloud Trace exporter with custom service name +/// Builder for creating an OpenTelemetry layer with Google Cloud Trace exporter +#[derive(Debug, Clone)] +pub struct GCloudTracingBuilder { + service_name: String, + project_id: Option, + resource: Option, +} + +impl GCloudTracingBuilder { + /// Create a new Google Cloud tracing builder + pub fn new() -> Self { + Self { service_name: "katana".to_string(), project_id: None, resource: None } + } + + /// Set the service name + pub fn service_name(mut self, name: impl Into) -> Self { + self.service_name = name.into(); + self + } + + /// Set the Google Cloud project ID + pub fn project_id(mut self, project_id: impl Into) -> Self { + self.project_id = Some(project_id.into()); + self + } + + /// Set a custom resource + pub fn resource(mut self, resource: Resource) -> Self { + self.resource = Some(resource); + self + } + + /// Build the OpenTelemetry layer (async because GCloud SDK requires it) + pub async fn build(self) -> Result { + // Install crypto provider + rustls::crypto::ring::default_provider() + .install_default() + .map_err(|_| Error::InstallCryptoFailed)?; + + // Build resource with service name + let resource = self.resource.unwrap_or_else(|| { + Resource::builder().with_service_name(self.service_name.clone()).build() + }); + + // Create trace exporter + let mut trace_exporter = if let Some(project_id) = self.project_id { + GcpCloudTraceExporterBuilder::new(project_id) + } else { + // Default will attempt to find project ID from environment variables in the following + // order: + // - GCP_PROJECT + // - PROJECT_ID + // - GCP_PROJECT_ID + GcpCloudTraceExporterBuilder::for_default_project_id().await? + }; + + trace_exporter = trace_exporter.with_resource(resource); + + // Create provider and install + let tracer_provider = trace_exporter.create_provider().await?; + let tracer = trace_exporter.install(&tracer_provider).await?; + + // Set the Google Cloud trace context propagator globally + // This will handle both extraction and injection of X-Cloud-Trace-Context headers + opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); + opentelemetry::global::set_tracer_provider(tracer_provider.clone()); + + // Return the layer + Ok(GCloudTracer { tracer }) + } +} + +impl Default for GCloudTracingBuilder { + fn default() -> Self { + Self::new() + } +} + +impl Tracer for GCloudTracer { + type Span = ::Span; + + #[inline] + fn build_with_context( + &self, + builder: opentelemetry::trace::SpanBuilder, + parent_cx: &opentelemetry::Context, + ) -> Self::Span { + self.tracer.build_with_context(builder, parent_cx) + } +} + +impl PreSampledTracer for GCloudTracer { + #[inline] + fn new_span_id(&self) -> opentelemetry::trace::SpanId { + self.tracer.new_span_id() + } + + #[inline] + fn new_trace_id(&self) -> opentelemetry::trace::TraceId { + self.tracer.new_trace_id() + } + + #[inline] + fn sampled_context( + &self, + data: &mut tracing_opentelemetry::OtelData, + ) -> opentelemetry::Context { + self.tracer.sampled_context(data) + } +} + +/// Initialize Google Cloud Trace exporter with custom service name (backward compatibility) pub(crate) async fn init_tracer_with_service( gcloud_config: &GcloudConfig, service_name: &str, -) -> Result { +) -> Result { rustls::crypto::ring::default_provider() .install_default() .map_err(|_| Error::InstallCryptoFailed)?; @@ -68,7 +186,7 @@ pub(crate) async fn init_tracer_with_service( opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); opentelemetry::global::set_tracer_provider(tracer_provider.clone()); - Ok(tracer) + Ok(GCloudTracer::new(tracer)) } /// Initialize Google Cloud Trace exporter and OpenTelemetry propagators for Google Cloud trace @@ -76,6 +194,6 @@ pub(crate) async fn init_tracer_with_service( /// /// Make sure to set `GOOGLE_APPLICATION_CREDENTIALS` env var to authenticate to gcloud #[allow(dead_code)] -pub(crate) async fn init_tracer(gcloud_config: &GcloudConfig) -> Result { +pub(crate) async fn init_tracer(gcloud_config: &GcloudConfig) -> Result { init_tracer_with_service(gcloud_config, "katana").await } diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index cb5edea51..d384160cf 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -1,6 +1,8 @@ +use opentelemetry::trace::Tracer; use opentelemetry_gcloud_trace::errors::GcloudTraceError; use tracing::subscriber::SetGlobalDefaultError; use tracing_log::log::SetLoggerError; +use tracing_opentelemetry::PreSampledTracer; use tracing_subscriber::filter; mod builder; @@ -10,6 +12,13 @@ pub mod otlp; pub use builder::TracingBuilder; pub use fmt::LogFormat; +pub use gcloud::{GCloudTracingBuilder, GcloudConfig}; +pub use otlp::{OtlpConfig, OtlpTracingBuilder}; + +// pub type Tracer= Tracer + PreSampledTracer + Send + Sync + 'static; + +trait TelemetryTracer: Tracer + PreSampledTracer + Send + Sync + 'static {} +impl TelemetryTracer for T where T: Tracer + PreSampledTracer + Send + Sync + 'static {} #[derive(Debug, Clone)] pub enum TracerConfig { @@ -47,31 +56,51 @@ pub enum Error { /// Initialize tracing with the given configuration. /// /// This function is maintained for backward compatibility. -/// For new code, consider using [`TracingBuilder`] for more flexibility. +/// For new code, consider using [`TracingBuilder`] with the new telemetry builders. +/// +/// # Example +/// ```rust,ignore +/// use katana_tracing::{OtlpTracingBuilder, TracingBuilder}; +/// +/// // New API (recommended): +/// let otlp_layer = OtlpTracingBuilder::new() +/// .service_name("my-service") +/// .endpoint("http://localhost:4317") +/// .build()?; +/// +/// TracingBuilder::new() +/// .json() +/// .with_default_filter()? +/// .init_with_otlp_telemetry(otlp_layer)?; +/// ``` pub async fn init(format: LogFormat, telemetry_config: Option) -> Result<(), Error> { - match format { - LogFormat::Full => match telemetry_config { - Some(TracerConfig::Otlp(cfg)) => { - TracingBuilder::new().full().with_otlp(cfg).try_init().await + // Build the base tracing builder with format and filter + let builder = TracingBuilder::with_format(format).with_env_filter_or_default()?; + + // Build telemetry layer and initialize based on config type + match telemetry_config { + Some(TracerConfig::Otlp(cfg)) => { + // OTLP is synchronous + let mut otlp_builder = OtlpTracingBuilder::new().service_name("katana"); + if let Some(endpoint) = cfg.endpoint { + otlp_builder = otlp_builder.endpoint(endpoint); } - - Some(TracerConfig::GCloud(cfg)) => { - TracingBuilder::new().full().with_gcloud(cfg).try_init().await + let layer = otlp_builder.build()?; + builder.init_with_otlp_telemetry(layer)?; + } + Some(TracerConfig::GCloud(cfg)) => { + // GCloud is async + let mut gcloud_builder = GCloudTracingBuilder::new().service_name("katana"); + if let Some(project_id) = cfg.project_id { + gcloud_builder = gcloud_builder.project_id(project_id); } - - None => TracingBuilder::new().full().try_init().await, - }, - - LogFormat::Json => match telemetry_config { - Some(TracerConfig::Otlp(cfg)) => { - TracingBuilder::new().json().with_otlp(cfg).try_init().await - } - - Some(TracerConfig::GCloud(cfg)) => { - TracingBuilder::new().json().with_gcloud(cfg).try_init().await - } - - None => TracingBuilder::new().json().try_init().await, - }, + let layer = gcloud_builder.build().await?; + builder.init_with_gcloud_telemetry(layer)?; + } + None => { + builder.try_init()?; + } } + + Ok(()) } diff --git a/crates/tracing/src/otlp.rs b/crates/tracing/src/otlp.rs index f8b0188e1..3bb0a83d8 100644 --- a/crates/tracing/src/otlp.rs +++ b/crates/tracing/src/otlp.rs @@ -1,8 +1,9 @@ -use anyhow::Result; use opentelemetry::trace::TracerProvider; -use opentelemetry_otlp::SpanExporterBuilder; -use opentelemetry_sdk::trace::{RandomIdGenerator, SdkTracerProvider}; +use opentelemetry_otlp::{SpanExporterBuilder, WithExportConfig}; +use opentelemetry_sdk::trace::{RandomIdGenerator, SdkTracerProvider, Tracer}; use opentelemetry_sdk::Resource; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::Registry; use crate::Error; @@ -11,13 +12,83 @@ pub struct OtlpConfig { pub endpoint: Option, } -/// Initialize OTLP tracer with custom service name +/// Builder for creating an OpenTelemetry layer with OTLP exporter +#[derive(Debug, Clone)] +pub struct OtlpTracingBuilder { + service_name: String, + endpoint: Option, + resource: Option, +} + +impl OtlpTracingBuilder { + /// Create a new OTLP tracing builder + pub fn new() -> Self { + Self { service_name: "katana".to_string(), endpoint: None, resource: None } + } + + /// Set the service name + pub fn service_name(mut self, name: impl Into) -> Self { + self.service_name = name.into(); + self + } + + /// Set the OTLP endpoint + pub fn endpoint(mut self, endpoint: impl Into) -> Self { + self.endpoint = Some(endpoint.into()); + self + } + + /// Set a custom resource + pub fn resource(mut self, resource: Resource) -> Self { + self.resource = Some(resource); + self + } + + /// Build the OpenTelemetry layer + pub fn build(self) -> Result, Error> { + // Build resource with service name + let resource = self.resource.unwrap_or_else(|| { + Resource::builder().with_service_name(self.service_name.clone()).build() + }); + + // Configure exporter + let mut exporter_builder = SpanExporterBuilder::new().with_tonic(); + + if let Some(endpoint) = self.endpoint { + exporter_builder = exporter_builder.with_endpoint(endpoint); + } + + let exporter = exporter_builder.build()?; + + // Build provider + let provider = SdkTracerProvider::builder() + .with_id_generator(RandomIdGenerator::default()) + .with_batch_exporter(exporter) + .with_resource(resource) + .build(); + + // Set global provider + opentelemetry::global::set_tracer_provider(provider.clone()); + + // Create tracer + let tracer = provider.tracer(self.service_name); + + // Return the layer + Ok(tracing_opentelemetry::layer().with_tracer(tracer)) + } +} + +impl Default for OtlpTracingBuilder { + fn default() -> Self { + Self::new() + } +} + +/// Initialize OTLP tracer with custom service name (backward compatibility) pub fn init_tracer_with_service( otlp_config: &OtlpConfig, service_name: &str, -) -> Result { - use opentelemetry_otlp::WithExportConfig; - +) -> Result { let resource = Resource::builder().with_service_name(service_name.to_string()).build(); let mut exporter_builder = SpanExporterBuilder::new().with_tonic(); @@ -40,6 +111,6 @@ pub fn init_tracer_with_service( } /// Initialize OTLP tracer (backward compatibility) -pub fn init_tracer(otlp_config: &OtlpConfig) -> Result { +pub fn init_tracer(otlp_config: &OtlpConfig) -> Result { init_tracer_with_service(otlp_config, "katana") } From 81752a672bb604684fa7b5506ebfd26f139ae72d Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 10 Sep 2025 13:41:33 -0400 Subject: [PATCH 6/9] save --- crates/cli/src/args.rs | 13 +++- crates/tracing/src/builder.rs | 42 ++++++---- crates/tracing/src/gcloud.rs | 142 ++++++++++++++++++++-------------- crates/tracing/src/lib.rs | 21 +++-- crates/tracing/src/otlp.rs | 126 +++++++++++++++++++++--------- 5 files changed, 222 insertions(+), 122 deletions(-) diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 9b459f653..32aed8e1e 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -27,6 +27,7 @@ use katana_node::config::Config; use katana_node::Node; use katana_primitives::genesis::allocation::DevAllocationsGenerator; use katana_primitives::genesis::constant::DEFAULT_PREFUNDED_ACCOUNT_BALANCE; +use katana_tracing::LogFormat; use serde::{Deserialize, Serialize}; use tracing::info; use url::Url; @@ -129,7 +130,17 @@ impl NodeArgs { pub async fn execute(&self) -> Result<()> { // Initialize logging with tracer let tracer_config = self.tracer_config(); - katana_tracing::init(self.logging.log_format, tracer_config).await?; + + match self.logging.log_format { + LogFormat::Full => { + katana_tracing::TracingBuilder::new().build()?; + } + LogFormat::Json => { + katana_tracing::TracingBuilder::new().json().init(tracer_config)?; + } + } + + // katana_tracing::init(self.logging.log_format, tracer_config).await?; self.start_node().await } diff --git a/crates/tracing/src/builder.rs b/crates/tracing/src/builder.rs index 76adcba9c..70b278507 100644 --- a/crates/tracing/src/builder.rs +++ b/crates/tracing/src/builder.rs @@ -1,6 +1,5 @@ use std::marker::PhantomData; -use opentelemetry::trace::noop::NoopTracer; use tracing_opentelemetry::OpenTelemetryLayer; use tracing_subscriber::fmt::format::{self}; use tracing_subscriber::layer::{Layered, SubscriberExt}; @@ -8,9 +7,19 @@ use tracing_subscriber::util::SubscriberInitExt; use tracing_subscriber::{fmt, EnvFilter, Layer, Registry}; use crate::fmt::LocalTime; -use crate::{Error, GCloudTracingBuilder, LogFormat, TelemetryTracer}; +use crate::{Error, LogFormat, TelemetryTracer}; -type DefaultFormat = format::Full; +const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,pipeline=debug,\ + stage=debug,tasks=debug,executor=trace,forking::backend=trace,\ + blockifier=off,jsonrpsee_server=off,hyper=off,messaging=debug,\ + node=error,explorer=info,rpc=trace,pool=trace,info"; + +pub type NoopTracer = opentelemetry::trace::noop::NoopTracer; + +// Format trait markers +pub type DefaultFormat = format::Full; +pub type Full = format::Full; +pub type Json = format::Json; type Subscriber = Layered< OpenTelemetryLayer< @@ -28,25 +37,19 @@ type Subscriber = Layered< struct TracingSubscriber { subscriber: Subscriber, + tracer: Tracer, _fmt: PhantomData, } impl TracingSubscriber { - fn init(self) { - self.subscriber.init(); + pub fn init(self) { + use tracing_subscriber::registry; + + self.tracer.init().unwrap(); + registry().with(self.filter).with(self.fmt_layer).with(self.tracer).init(); } } -const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,pipeline=debug,\ - stage=debug,tasks=debug,executor=trace,forking::backend=trace,\ - blockifier=off,jsonrpsee_server=off,hyper=off,messaging=debug,\ - node=error,explorer=info,rpc=trace,pool=trace,info"; - -// /// Identity type-state markers for [`TracingBuilder`]. -// #[derive(Debug)] -// pub struct Identity; - -// Main builder struct with type-state for format #[derive(Debug)] pub struct TracingBuilder { log_format: LogFormat, @@ -146,8 +149,13 @@ impl Default for TracingBuilder { #[cfg(test)] #[tokio::test] async fn foo() { + use crate::{GCloudTracerBuilder, OtlpTracerBuilder}; + let builder = TracingBuilder::new().build().unwrap(); - let gcloud = GCloudTracingBuilder::new().build().await.unwrap(); - let builder = TracingBuilder::new().json().with_telemetry(gcloud).build().unwrap(); + let oltp = OtlpTracerBuilder::new().build().unwrap(); + let gcloud = GCloudTracerBuilder::new().build().await.unwrap(); + + let builder_w_otlp = TracingBuilder::new().json().with_telemetry(oltp).build().unwrap(); + let builder_w_gcloud = TracingBuilder::new().json().with_telemetry(gcloud).build().unwrap(); } diff --git a/crates/tracing/src/gcloud.rs b/crates/tracing/src/gcloud.rs index f97dba232..d37dcbe43 100644 --- a/crates/tracing/src/gcloud.rs +++ b/crates/tracing/src/gcloud.rs @@ -2,19 +2,12 @@ use http::Request; use opentelemetry::trace::Tracer; use opentelemetry_gcloud_trace::{GcpCloudTraceExporterBuilder, SdkTracer}; use opentelemetry_http::HeaderExtractor; -use opentelemetry_sdk::Resource; +use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; use opentelemetry_stackdriver::google_trace_context_propagator::GoogleTraceContextPropagator; use tower_http::trace::MakeSpan; -use tracing_opentelemetry::{OpenTelemetryLayer, OpenTelemetrySpanExt, PreSampledTracer}; -use tracing_subscriber::Registry; +use tracing_opentelemetry::{OpenTelemetrySpanExt, PreSampledTracer}; -use crate::Error; - -/// Wrapper type for SdkTracer that implements the Tracer trait -#[derive(Debug, Clone)] -pub struct GCloudTracer { - tracer: SdkTracer, -} +use crate::{Error, TelemetryTracer}; #[derive(Debug, Clone, Default)] pub struct GoogleStackDriverMakeSpan; @@ -45,13 +38,17 @@ pub struct GcloudConfig { /// Builder for creating an OpenTelemetry layer with Google Cloud Trace exporter #[derive(Debug, Clone)] -pub struct GCloudTracingBuilder { +pub struct GCloudTracerBuilder { service_name: String, project_id: Option, resource: Option, } -impl GCloudTracingBuilder { +///////////////////////////////////////////////////////////////////////////////// +// GCloudTracerBuilder implementations +///////////////////////////////////////////////////////////////////////////////// + +impl GCloudTracerBuilder { /// Create a new Google Cloud tracing builder pub fn new() -> Self { Self { service_name: "katana".to_string(), project_id: None, resource: None } @@ -105,22 +102,39 @@ impl GCloudTracingBuilder { let tracer_provider = trace_exporter.create_provider().await?; let tracer = trace_exporter.install(&tracer_provider).await?; - // Set the Google Cloud trace context propagator globally - // This will handle both extraction and injection of X-Cloud-Trace-Context headers - opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); - opentelemetry::global::set_tracer_provider(tracer_provider.clone()); + // // Set the Google Cloud trace context propagator globally + // // This will handle both extraction and injection of X-Cloud-Trace-Context headers + // opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); + // opentelemetry::global::set_tracer_provider(tracer_provider.clone()); // Return the layer - Ok(GCloudTracer { tracer }) + Ok(GCloudTracer { tracer, tracer_provider }) } } -impl Default for GCloudTracingBuilder { +impl Default for GCloudTracerBuilder { fn default() -> Self { Self::new() } } +/// Wrapper type for SdkTracer that implements the Tracer trait +#[derive(Debug, Clone)] +pub struct GCloudTracer { + tracer: SdkTracer, + tracer_provider: SdkTracerProvider, +} + +///////////////////////////////////////////////////////////////////////////////// +// GCloudTracer implementations +///////////////////////////////////////////////////////////////////////////////// + +impl GCloudTracer { + pub fn builder() -> GCloudTracerBuilder { + GCloudTracerBuilder::new() + } +} + impl Tracer for GCloudTracer { type Span = ::Span; @@ -154,46 +168,56 @@ impl PreSampledTracer for GCloudTracer { } } -/// Initialize Google Cloud Trace exporter with custom service name (backward compatibility) -pub(crate) async fn init_tracer_with_service( - gcloud_config: &GcloudConfig, - service_name: &str, -) -> Result { - rustls::crypto::ring::default_provider() - .install_default() - .map_err(|_| Error::InstallCryptoFailed)?; - - let resource = Resource::builder().with_service_name(service_name.to_string()).build(); - - let mut trace_exporter = if let Some(project_id) = &gcloud_config.project_id { - GcpCloudTraceExporterBuilder::new(project_id.clone()) - } else { - // Default will attempt to find project ID from environment variables in the following - // order: - // - GCP_PROJECT - // - PROJECT_ID - // - GCP_PROJECT_ID - GcpCloudTraceExporterBuilder::for_default_project_id().await? - }; - - trace_exporter = trace_exporter.with_resource(resource); - - let tracer_provider = trace_exporter.create_provider().await?; - let tracer = trace_exporter.install(&tracer_provider).await?; - - // Set the Google Cloud trace context propagator globally - // This will handle both extraction and injection of X-Cloud-Trace-Context headers - opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); - opentelemetry::global::set_tracer_provider(tracer_provider.clone()); - - Ok(GCloudTracer::new(tracer)) +impl TelemetryTracer for GCloudTracer { + fn init(&self) -> Result<(), Error> { + // Set the Google Cloud trace context propagator globally + // This will handle both extraction and injection of X-Cloud-Trace-Context headers + opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); + opentelemetry::global::set_tracer_provider(self.tracer_provider.clone()); + Ok(()) + } } -/// Initialize Google Cloud Trace exporter and OpenTelemetry propagators for Google Cloud trace -/// context support (backward compatibility). -/// -/// Make sure to set `GOOGLE_APPLICATION_CREDENTIALS` env var to authenticate to gcloud -#[allow(dead_code)] -pub(crate) async fn init_tracer(gcloud_config: &GcloudConfig) -> Result { - init_tracer_with_service(gcloud_config, "katana").await -} +// /// Initialize Google Cloud Trace exporter with custom service name (backward compatibility) +// pub(crate) async fn init_tracer_with_service( +// gcloud_config: &GcloudConfig, +// service_name: &str, +// ) -> Result { +// rustls::crypto::ring::default_provider() +// .install_default() +// .map_err(|_| Error::InstallCryptoFailed)?; + +// let resource = Resource::builder().with_service_name(service_name.to_string()).build(); + +// let mut trace_exporter = if let Some(project_id) = &gcloud_config.project_id { +// GcpCloudTraceExporterBuilder::new(project_id.clone()) +// } else { +// // Default will attempt to find project ID from environment variables in the following +// // order: +// // - GCP_PROJECT +// // - PROJECT_ID +// // - GCP_PROJECT_ID +// GcpCloudTraceExporterBuilder::for_default_project_id().await? +// }; + +// trace_exporter = trace_exporter.with_resource(resource); + +// let tracer_provider = trace_exporter.create_provider().await?; +// let tracer = trace_exporter.install(&tracer_provider).await?; + +// // Set the Google Cloud trace context propagator globally +// // This will handle both extraction and injection of X-Cloud-Trace-Context headers +// opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); +// opentelemetry::global::set_tracer_provider(tracer_provider.clone()); + +// Ok(GCloudTracer::new(tracer)) +// } + +// /// Initialize Google Cloud Trace exporter and OpenTelemetry propagators for Google Cloud trace +// /// context support (backward compatibility). +// /// +// /// Make sure to set `GOOGLE_APPLICATION_CREDENTIALS` env var to authenticate to gcloud +// #[allow(dead_code)] +// pub(crate) async fn init_tracer(gcloud_config: &GcloudConfig) -> Result { +// init_tracer_with_service(gcloud_config, "katana").await +// } diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index d384160cf..fa01bad6f 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -12,13 +12,20 @@ pub mod otlp; pub use builder::TracingBuilder; pub use fmt::LogFormat; -pub use gcloud::{GCloudTracingBuilder, GcloudConfig}; -pub use otlp::{OtlpConfig, OtlpTracingBuilder}; +pub use gcloud::{GCloudTracerBuilder, GcloudConfig}; +pub use otlp::{OtlpConfig, OtlpTracerBuilder}; -// pub type Tracer= Tracer + PreSampledTracer + Send + Sync + 'static; +use crate::builder::NoopTracer; -trait TelemetryTracer: Tracer + PreSampledTracer + Send + Sync + 'static {} -impl TelemetryTracer for T where T: Tracer + PreSampledTracer + Send + Sync + 'static {} +trait TelemetryTracer: Tracer + PreSampledTracer + Send + Sync + 'static { + fn init(&self) -> Result<(), Error>; +} + +impl TelemetryTracer for NoopTracer { + fn init(&self) -> Result<(), Error> { + Ok(()) + } +} #[derive(Debug, Clone)] pub enum TracerConfig { @@ -81,7 +88,7 @@ pub async fn init(format: LogFormat, telemetry_config: Option) -> match telemetry_config { Some(TracerConfig::Otlp(cfg)) => { // OTLP is synchronous - let mut otlp_builder = OtlpTracingBuilder::new().service_name("katana"); + let mut otlp_builder = OtlpTracerBuilder::new().service_name("katana"); if let Some(endpoint) = cfg.endpoint { otlp_builder = otlp_builder.endpoint(endpoint); } @@ -90,7 +97,7 @@ pub async fn init(format: LogFormat, telemetry_config: Option) -> } Some(TracerConfig::GCloud(cfg)) => { // GCloud is async - let mut gcloud_builder = GCloudTracingBuilder::new().service_name("katana"); + let mut gcloud_builder = GCloudTracerBuilder::new().service_name("katana"); if let Some(project_id) = cfg.project_id { gcloud_builder = gcloud_builder.project_id(project_id); } diff --git a/crates/tracing/src/otlp.rs b/crates/tracing/src/otlp.rs index 3bb0a83d8..8620268ca 100644 --- a/crates/tracing/src/otlp.rs +++ b/crates/tracing/src/otlp.rs @@ -1,11 +1,10 @@ -use opentelemetry::trace::TracerProvider; +use opentelemetry::trace::{Tracer, TracerProvider}; use opentelemetry_otlp::{SpanExporterBuilder, WithExportConfig}; -use opentelemetry_sdk::trace::{RandomIdGenerator, SdkTracerProvider, Tracer}; +use opentelemetry_sdk::trace::{RandomIdGenerator, SdkTracerProvider}; use opentelemetry_sdk::Resource; -use tracing_opentelemetry::OpenTelemetryLayer; -use tracing_subscriber::Registry; +use tracing_opentelemetry::PreSampledTracer; -use crate::Error; +use crate::{Error, TelemetryTracer}; #[derive(Debug, Clone)] pub struct OtlpConfig { @@ -14,13 +13,13 @@ pub struct OtlpConfig { /// Builder for creating an OpenTelemetry layer with OTLP exporter #[derive(Debug, Clone)] -pub struct OtlpTracingBuilder { +pub struct OtlpTracerBuilder { service_name: String, endpoint: Option, resource: Option, } -impl OtlpTracingBuilder { +impl OtlpTracerBuilder { /// Create a new OTLP tracing builder pub fn new() -> Self { Self { service_name: "katana".to_string(), endpoint: None, resource: None } @@ -45,7 +44,7 @@ impl OtlpTracingBuilder { } /// Build the OpenTelemetry layer - pub fn build(self) -> Result, Error> { + pub fn build(self) -> Result { // Build resource with service name let resource = self.resource.unwrap_or_else(|| { Resource::builder().with_service_name(self.service_name.clone()).build() @@ -61,56 +60,107 @@ impl OtlpTracingBuilder { let exporter = exporter_builder.build()?; // Build provider - let provider = SdkTracerProvider::builder() + let tracer_provider = SdkTracerProvider::builder() .with_id_generator(RandomIdGenerator::default()) .with_batch_exporter(exporter) .with_resource(resource) .build(); - // Set global provider - opentelemetry::global::set_tracer_provider(provider.clone()); - - // Create tracer - let tracer = provider.tracer(self.service_name); + // // Set global provider + // opentelemetry::global::set_tracer_provider(tracer_provider.clone()); + let tracer = tracer_provider.tracer(self.service_name); - // Return the layer - Ok(tracing_opentelemetry::layer().with_tracer(tracer)) + Ok(OtlpTracer { tracer, tracer_provider }) } } -impl Default for OtlpTracingBuilder { +impl Default for OtlpTracerBuilder { fn default() -> Self { Self::new() } } -/// Initialize OTLP tracer with custom service name (backward compatibility) -pub fn init_tracer_with_service( - otlp_config: &OtlpConfig, - service_name: &str, -) -> Result { - let resource = Resource::builder().with_service_name(service_name.to_string()).build(); - - let mut exporter_builder = SpanExporterBuilder::new().with_tonic(); +/// Wrapper type for SdkTracer that implements the Tracer trait +#[derive(Debug, Clone)] +pub struct OtlpTracer { + tracer: opentelemetry_sdk::trace::Tracer, + tracer_provider: SdkTracerProvider, +} - if let Some(endpoint) = &otlp_config.endpoint { - exporter_builder = exporter_builder.with_endpoint(endpoint); +impl OtlpTracer { + pub fn builder() -> OtlpTracerBuilder { + OtlpTracerBuilder::new() } +} - let exporter = exporter_builder.build()?; +impl Tracer for OtlpTracer { + type Span = ::Span; - let provider = SdkTracerProvider::builder() - .with_id_generator(RandomIdGenerator::default()) - .with_batch_exporter(exporter) - .with_resource(resource) - .build(); + #[inline] + fn build_with_context( + &self, + builder: opentelemetry::trace::SpanBuilder, + parent_cx: &opentelemetry::Context, + ) -> Self::Span { + self.tracer.build_with_context(builder, parent_cx) + } +} - opentelemetry::global::set_tracer_provider(provider.clone()); +impl PreSampledTracer for OtlpTracer { + #[inline] + fn new_span_id(&self) -> opentelemetry::trace::SpanId { + self.tracer.new_span_id() + } + + #[inline] + fn new_trace_id(&self) -> opentelemetry::trace::TraceId { + self.tracer.new_trace_id() + } - Ok(provider.tracer(service_name.to_string())) + #[inline] + fn sampled_context( + &self, + data: &mut tracing_opentelemetry::OtelData, + ) -> opentelemetry::Context { + self.tracer.sampled_context(data) + } } -/// Initialize OTLP tracer (backward compatibility) -pub fn init_tracer(otlp_config: &OtlpConfig) -> Result { - init_tracer_with_service(otlp_config, "katana") +impl TelemetryTracer for OtlpTracer { + fn init(&self) -> Result<(), Error> { + // Set global provider + opentelemetry::global::set_tracer_provider(self.tracer_provider.clone()); + Ok(()) + } } + +// /// Initialize OTLP tracer with custom service name (backward compatibility) +// pub fn init_tracer_with_service( +// otlp_config: &OtlpConfig, +// service_name: &str, +// ) -> Result { +// let resource = Resource::builder().with_service_name(service_name.to_string()).build(); + +// let mut exporter_builder = SpanExporterBuilder::new().with_tonic(); + +// if let Some(endpoint) = &otlp_config.endpoint { +// exporter_builder = exporter_builder.with_endpoint(endpoint); +// } + +// let exporter = exporter_builder.build()?; + +// let provider = SdkTracerProvider::builder() +// .with_id_generator(RandomIdGenerator::default()) +// .with_batch_exporter(exporter) +// .with_resource(resource) +// .build(); + +// opentelemetry::global::set_tracer_provider(provider.clone()); + +// Ok(provider.tracer(service_name.to_string())) +// } + +// /// Initialize OTLP tracer (backward compatibility) +// pub fn init_tracer(otlp_config: &OtlpConfig) -> Result { +// init_tracer_with_service(otlp_config, "katana") +// } From cc9bd75d9f078058a772ed08cae7d881911dc2f0 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Mon, 15 Sep 2025 19:07:47 -0400 Subject: [PATCH 7/9] wip --- crates/tracing/src/builder.rs | 93 +++++++++++++++++++++++++---------- 1 file changed, 67 insertions(+), 26 deletions(-) diff --git a/crates/tracing/src/builder.rs b/crates/tracing/src/builder.rs index 70b278507..3cb588f81 100644 --- a/crates/tracing/src/builder.rs +++ b/crates/tracing/src/builder.rs @@ -1,6 +1,7 @@ +use std::fmt::Debug; use std::marker::PhantomData; -use tracing_opentelemetry::OpenTelemetryLayer; +// use tracing_opentelemetry::OpenTelemetryLayer; use tracing_subscriber::fmt::format::{self}; use tracing_subscriber::layer::{Layered, SubscriberExt}; use tracing_subscriber::util::SubscriberInitExt; @@ -21,32 +22,47 @@ pub type DefaultFormat = format::Full; pub type Full = format::Full; pub type Json = format::Json; -type Subscriber = Layered< - OpenTelemetryLayer< - Layered< - Box> + Send + Sync + 'static>, - Layered, - >, - Tracer, - >, - Layered< - Box> + Send + Sync + 'static>, - Layered, - >, +// type Subscriber = Layered< +// OpenTelemetryLayer< +// Layered< +// Box> + Send + Sync + 'static>, +// Layered, +// >, +// Telemetry, +// >, +// Layered< +// Box> + Send + Sync + 'static>, +// Layered, +// >, +// >; + +type SubscriberWithNoTelemetry = Layered< + Box> + Send + Sync + 'static>, + Layered, >; -struct TracingSubscriber { - subscriber: Subscriber, - tracer: Tracer, +// #[derive(Clone)] +struct TracingSubscriber { + subscriber_without_telem: SubscriberWithNoTelemetry, + tracer: Telemetry, _fmt: PhantomData, } -impl TracingSubscriber { +impl TracingSubscriber { pub fn init(self) { - use tracing_subscriber::registry; - self.tracer.init().unwrap(); - registry().with(self.filter).with(self.fmt_layer).with(self.tracer).init(); + + let telem = tracing_opentelemetry::layer().with_tracer(self.tracer); + self.subscriber_without_telem.with(telem).init(); + } +} + +impl Debug for TracingSubscriber { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TracingSubscriber") + .field("subscriber", &"..") + .field("tracer", &self.tracer) + .finish() } } @@ -115,16 +131,41 @@ impl TracingBuilder { EnvFilter::try_new(DEFAULT_LOG_FILTER).expect("default filter should be valid") }); - let fmt_layer = fmt::layer().with_timer(LocalTime::new()); + let base_layer = fmt::layer().with_timer(LocalTime::new()); + + // Use an enum to preserve type information instead of Box + enum FmtLayer { + Full(F), + Json(J), + } + + impl Layer for FmtLayer + where + S: tracing::Subscriber, + F: Layer, + J: Layer, + { + fn on_layer(&mut self, subscriber: &mut S) { + match self { + FmtLayer::Full(layer) => layer.on_layer(subscriber), + FmtLayer::Json(layer) => layer.on_layer(subscriber), + } + } + } + let fmt_layer = match self.log_format { - LogFormat::Full => fmt_layer.boxed(), - LogFormat::Json => fmt_layer.json().boxed(), + LogFormat::Full => FmtLayer::Full(base_layer), + LogFormat::Json => FmtLayer::Json(base_layer.json()), }; - let telem = tracing_opentelemetry::layer().with_tracer(self.tracer); - let subscriber = tracing_subscriber::registry().with(filter).with(fmt_layer).with(telem); + // let telem = tracing_opentelemetry::layer().with_tracer(self.tracer); + let subscriber = tracing_subscriber::registry().with(filter).with(fmt_layer); - Ok(TracingSubscriber { subscriber, _fmt: PhantomData }) + Ok(TracingSubscriber { + tracer: self.tracer, + subscriber_without_telem: subscriber, + _fmt: PhantomData, + }) } } From d4e9479eae7a2ac6d65d0c4f797dfa23054c969c Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Mon, 15 Sep 2025 20:13:23 -0400 Subject: [PATCH 8/9] wip --- crates/cli/src/args.rs | 13 +-- crates/tracing/src/builder.rs | 163 +++++++++------------------------- crates/tracing/src/fmt.rs | 21 +++++ crates/tracing/src/gcloud.rs | 47 +--------- crates/tracing/src/lib.rs | 46 +++++----- crates/tracing/src/otlp.rs | 31 +------ 6 files changed, 89 insertions(+), 232 deletions(-) diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 32aed8e1e..9b459f653 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -27,7 +27,6 @@ use katana_node::config::Config; use katana_node::Node; use katana_primitives::genesis::allocation::DevAllocationsGenerator; use katana_primitives::genesis::constant::DEFAULT_PREFUNDED_ACCOUNT_BALANCE; -use katana_tracing::LogFormat; use serde::{Deserialize, Serialize}; use tracing::info; use url::Url; @@ -130,17 +129,7 @@ impl NodeArgs { pub async fn execute(&self) -> Result<()> { // Initialize logging with tracer let tracer_config = self.tracer_config(); - - match self.logging.log_format { - LogFormat::Full => { - katana_tracing::TracingBuilder::new().build()?; - } - LogFormat::Json => { - katana_tracing::TracingBuilder::new().json().init(tracer_config)?; - } - } - - // katana_tracing::init(self.logging.log_format, tracer_config).await?; + katana_tracing::init(self.logging.log_format, tracer_config).await?; self.start_node().await } diff --git a/crates/tracing/src/builder.rs b/crates/tracing/src/builder.rs index 3cb588f81..a63fb5d68 100644 --- a/crates/tracing/src/builder.rs +++ b/crates/tracing/src/builder.rs @@ -1,13 +1,11 @@ use std::fmt::Debug; -use std::marker::PhantomData; -// use tracing_opentelemetry::OpenTelemetryLayer; -use tracing_subscriber::fmt::format::{self}; +use tracing_subscriber::fmt::format::{DefaultFields, Format, Full, Json, JsonFields}; use tracing_subscriber::layer::{Layered, SubscriberExt}; use tracing_subscriber::util::SubscriberInitExt; -use tracing_subscriber::{fmt, EnvFilter, Layer, Registry}; +use tracing_subscriber::{fmt, EnvFilter, Registry}; -use crate::fmt::LocalTime; +use crate::fmt::{FmtLayer, LocalTime}; use crate::{Error, LogFormat, TelemetryTracer}; const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=off,pipeline=debug,\ @@ -17,87 +15,32 @@ const DEFAULT_LOG_FILTER: &str = "katana_db::mdbx=trace,cairo_native::compiler=o pub type NoopTracer = opentelemetry::trace::noop::NoopTracer; -// Format trait markers -pub type DefaultFormat = format::Full; -pub type Full = format::Full; -pub type Json = format::Json; - -// type Subscriber = Layered< -// OpenTelemetryLayer< -// Layered< -// Box> + Send + Sync + 'static>, -// Layered, -// >, -// Telemetry, -// >, -// Layered< -// Box> + Send + Sync + 'static>, -// Layered, -// >, -// >; - -type SubscriberWithNoTelemetry = Layered< - Box> + Send + Sync + 'static>, - Layered, ->; - -// #[derive(Clone)] -struct TracingSubscriber { - subscriber_without_telem: SubscriberWithNoTelemetry, - tracer: Telemetry, - _fmt: PhantomData, -} - -impl TracingSubscriber { - pub fn init(self) { - self.tracer.init().unwrap(); - - let telem = tracing_opentelemetry::layer().with_tracer(self.tracer); - self.subscriber_without_telem.with(telem).init(); - } -} - -impl Debug for TracingSubscriber { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("TracingSubscriber") - .field("subscriber", &"..") - .field("tracer", &self.tracer) - .finish() - } -} - #[derive(Debug)] -pub struct TracingBuilder { - log_format: LogFormat, +pub struct TracingBuilder { filter: Option, + log_format: LogFormat, tracer: Telemetry, - _format: PhantomData, } impl TracingBuilder { /// Create a new tracing builder pub fn new() -> Self { - Self { - filter: None, - log_format: LogFormat::Full, - tracer: NoopTracer::new(), - _format: PhantomData, - } + Self { filter: None, log_format: LogFormat::Full, tracer: NoopTracer::new() } } } -impl TracingBuilder { - pub fn with_telemetry(self, tracer: T) -> TracingBuilder { - TracingBuilder { - filter: self.filter, - log_format: self.log_format, - tracer, - _format: PhantomData, - } +impl TracingBuilder { + pub fn with_telemetry(self, tracer: T) -> TracingBuilder { + TracingBuilder { filter: self.filter, log_format: self.log_format, tracer } } } -impl TracingBuilder { +impl TracingBuilder { + /// Set the log format to JSON + pub fn json(self) -> TracingBuilder { + TracingBuilder { log_format: LogFormat::Json, tracer: self.tracer, filter: self.filter } + } + /// Set a custom filter from a string pub fn with_filter(mut self, filter: &str) -> Result { self.filter = Some(EnvFilter::try_new(filter)?); @@ -124,79 +67,59 @@ impl TracingBuilder { } } -impl TracingBuilder { +impl TracingBuilder { /// Try to initialize the tracing subscriber without telemetry - pub fn build(self) -> Result, Error> { + pub fn build(self) -> Result, Error> { let filter = self.filter.unwrap_or_else(|| { EnvFilter::try_new(DEFAULT_LOG_FILTER).expect("default filter should be valid") }); let base_layer = fmt::layer().with_timer(LocalTime::new()); - // Use an enum to preserve type information instead of Box - enum FmtLayer { - Full(F), - Json(J), - } - - impl Layer for FmtLayer - where - S: tracing::Subscriber, - F: Layer, - J: Layer, - { - fn on_layer(&mut self, subscriber: &mut S) { - match self { - FmtLayer::Full(layer) => layer.on_layer(subscriber), - FmtLayer::Json(layer) => layer.on_layer(subscriber), - } - } - } - let fmt_layer = match self.log_format { LogFormat::Full => FmtLayer::Full(base_layer), LogFormat::Json => FmtLayer::Json(base_layer.json()), }; - // let telem = tracing_opentelemetry::layer().with_tracer(self.tracer); - let subscriber = tracing_subscriber::registry().with(filter).with(fmt_layer); - Ok(TracingSubscriber { tracer: self.tracer, - subscriber_without_telem: subscriber, - _fmt: PhantomData, + subscriber: tracing_subscriber::registry().with(filter).with(fmt_layer), }) } } -impl TracingBuilder { - /// Set the log format to JSON - pub fn json(self) -> TracingBuilder { - TracingBuilder { - log_format: LogFormat::Json, - tracer: self.tracer, - filter: self.filter, - _format: PhantomData, - } - } -} - impl Default for TracingBuilder { fn default() -> Self { Self::new() } } -#[cfg(test)] -#[tokio::test] -async fn foo() { - use crate::{GCloudTracerBuilder, OtlpTracerBuilder}; +/// The base subscribe type created by [`TracingBuilder`] and used by [`TracingSubscriber`]. +type BaseSubscriber = Layered< + FmtLayer< + fmt::Layer, DefaultFields, Format>, + fmt::Layer, JsonFields, Format>, + >, + Layered, +>; - let builder = TracingBuilder::new().build().unwrap(); +pub struct TracingSubscriber { + subscriber: BaseSubscriber, + tracer: Telemetry, +} - let oltp = OtlpTracerBuilder::new().build().unwrap(); - let gcloud = GCloudTracerBuilder::new().build().await.unwrap(); +impl TracingSubscriber { + pub fn init(self) { + self.tracer.init().unwrap(); + self.subscriber.with(tracing_opentelemetry::layer().with_tracer(self.tracer)).init(); + } +} - let builder_w_otlp = TracingBuilder::new().json().with_telemetry(oltp).build().unwrap(); - let builder_w_gcloud = TracingBuilder::new().json().with_telemetry(gcloud).build().unwrap(); +impl Debug for TracingSubscriber { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TracingSubscriber") + .field("subscriber", &"..") + .field("tracer", &self.tracer) + .finish() + } } diff --git a/crates/tracing/src/fmt.rs b/crates/tracing/src/fmt.rs index da1a3a7f8..fe00c17cd 100644 --- a/crates/tracing/src/fmt.rs +++ b/crates/tracing/src/fmt.rs @@ -3,6 +3,7 @@ use std::fmt::Display; use serde::{Deserialize, Serialize}; use tracing_subscriber::fmt::format::Writer; use tracing_subscriber::fmt::time::{self}; +use tracing_subscriber::Layer; /// Format for logging output. #[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize, Default)] @@ -66,3 +67,23 @@ impl time::FormatTime for LocalTime { write!(w, "{}", time.format(DEFAULT_TIMESTAMP_FORMAT)) } } + +// Use an enum to preserve type information instead of Box +pub enum FmtLayer { + Full(F), + Json(J), +} + +impl Layer for FmtLayer +where + S: tracing::Subscriber, + F: Layer, + J: Layer, +{ + fn on_layer(&mut self, subscriber: &mut S) { + match self { + FmtLayer::Full(layer) => layer.on_layer(subscriber), + FmtLayer::Json(layer) => layer.on_layer(subscriber), + } + } +} diff --git a/crates/tracing/src/gcloud.rs b/crates/tracing/src/gcloud.rs index d37dcbe43..85cfb106b 100644 --- a/crates/tracing/src/gcloud.rs +++ b/crates/tracing/src/gcloud.rs @@ -2,7 +2,8 @@ use http::Request; use opentelemetry::trace::Tracer; use opentelemetry_gcloud_trace::{GcpCloudTraceExporterBuilder, SdkTracer}; use opentelemetry_http::HeaderExtractor; -use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; +use opentelemetry_sdk::trace::SdkTracerProvider; +use opentelemetry_sdk::Resource; use opentelemetry_stackdriver::google_trace_context_propagator::GoogleTraceContextPropagator; use tower_http::trace::MakeSpan; use tracing_opentelemetry::{OpenTelemetrySpanExt, PreSampledTracer}; @@ -178,46 +179,4 @@ impl TelemetryTracer for GCloudTracer { } } -// /// Initialize Google Cloud Trace exporter with custom service name (backward compatibility) -// pub(crate) async fn init_tracer_with_service( -// gcloud_config: &GcloudConfig, -// service_name: &str, -// ) -> Result { -// rustls::crypto::ring::default_provider() -// .install_default() -// .map_err(|_| Error::InstallCryptoFailed)?; - -// let resource = Resource::builder().with_service_name(service_name.to_string()).build(); - -// let mut trace_exporter = if let Some(project_id) = &gcloud_config.project_id { -// GcpCloudTraceExporterBuilder::new(project_id.clone()) -// } else { -// // Default will attempt to find project ID from environment variables in the following -// // order: -// // - GCP_PROJECT -// // - PROJECT_ID -// // - GCP_PROJECT_ID -// GcpCloudTraceExporterBuilder::for_default_project_id().await? -// }; - -// trace_exporter = trace_exporter.with_resource(resource); - -// let tracer_provider = trace_exporter.create_provider().await?; -// let tracer = trace_exporter.install(&tracer_provider).await?; - -// // Set the Google Cloud trace context propagator globally -// // This will handle both extraction and injection of X-Cloud-Trace-Context headers -// opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); -// opentelemetry::global::set_tracer_provider(tracer_provider.clone()); - -// Ok(GCloudTracer::new(tracer)) -// } - -// /// Initialize Google Cloud Trace exporter and OpenTelemetry propagators for Google Cloud trace -// /// context support (backward compatibility). -// /// -// /// Make sure to set `GOOGLE_APPLICATION_CREDENTIALS` env var to authenticate to gcloud -// #[allow(dead_code)] -// pub(crate) async fn init_tracer(gcloud_config: &GcloudConfig) -> Result { -// init_tracer_with_service(gcloud_config, "katana").await -// } +impl crate::__private::Sealed for GCloudTracer {} diff --git a/crates/tracing/src/lib.rs b/crates/tracing/src/lib.rs index fa01bad6f..7929e8c18 100644 --- a/crates/tracing/src/lib.rs +++ b/crates/tracing/src/lib.rs @@ -17,10 +17,18 @@ pub use otlp::{OtlpConfig, OtlpTracerBuilder}; use crate::builder::NoopTracer; -trait TelemetryTracer: Tracer + PreSampledTracer + Send + Sync + 'static { +mod __private { + pub trait Sealed {} +} + +pub trait TelemetryTracer: + Tracer + PreSampledTracer + Send + Sync + __private::Sealed + 'static +{ fn init(&self) -> Result<(), Error>; } +impl __private::Sealed for NoopTracer {} + impl TelemetryTracer for NoopTracer { fn init(&self) -> Result<(), Error> { Ok(()) @@ -60,29 +68,13 @@ pub enum Error { OtelSdk(#[from] opentelemetry_sdk::error::OTelSdkError), } -/// Initialize tracing with the given configuration. -/// -/// This function is maintained for backward compatibility. -/// For new code, consider using [`TracingBuilder`] with the new telemetry builders. -/// -/// # Example -/// ```rust,ignore -/// use katana_tracing::{OtlpTracingBuilder, TracingBuilder}; -/// -/// // New API (recommended): -/// let otlp_layer = OtlpTracingBuilder::new() -/// .service_name("my-service") -/// .endpoint("http://localhost:4317") -/// .build()?; -/// -/// TracingBuilder::new() -/// .json() -/// .with_default_filter()? -/// .init_with_otlp_telemetry(otlp_layer)?; -/// ``` pub async fn init(format: LogFormat, telemetry_config: Option) -> Result<(), Error> { - // Build the base tracing builder with format and filter - let builder = TracingBuilder::with_format(format).with_env_filter_or_default()?; + let builder = match format { + LogFormat::Full => TracingBuilder::new(), + LogFormat::Json => TracingBuilder::new().json(), + }; + + let builder = builder.with_env_filter_or_default()?; // Build telemetry layer and initialize based on config type match telemetry_config { @@ -93,8 +85,9 @@ pub async fn init(format: LogFormat, telemetry_config: Option) -> otlp_builder = otlp_builder.endpoint(endpoint); } let layer = otlp_builder.build()?; - builder.init_with_otlp_telemetry(layer)?; + builder.with_telemetry(layer).build()?.init(); } + Some(TracerConfig::GCloud(cfg)) => { // GCloud is async let mut gcloud_builder = GCloudTracerBuilder::new().service_name("katana"); @@ -102,10 +95,11 @@ pub async fn init(format: LogFormat, telemetry_config: Option) -> gcloud_builder = gcloud_builder.project_id(project_id); } let layer = gcloud_builder.build().await?; - builder.init_with_gcloud_telemetry(layer)?; + builder.with_telemetry(layer).build()?.init(); } + None => { - builder.try_init()?; + builder.build()?.init(); } } diff --git a/crates/tracing/src/otlp.rs b/crates/tracing/src/otlp.rs index 8620268ca..53ca31299 100644 --- a/crates/tracing/src/otlp.rs +++ b/crates/tracing/src/otlp.rs @@ -134,33 +134,4 @@ impl TelemetryTracer for OtlpTracer { } } -// /// Initialize OTLP tracer with custom service name (backward compatibility) -// pub fn init_tracer_with_service( -// otlp_config: &OtlpConfig, -// service_name: &str, -// ) -> Result { -// let resource = Resource::builder().with_service_name(service_name.to_string()).build(); - -// let mut exporter_builder = SpanExporterBuilder::new().with_tonic(); - -// if let Some(endpoint) = &otlp_config.endpoint { -// exporter_builder = exporter_builder.with_endpoint(endpoint); -// } - -// let exporter = exporter_builder.build()?; - -// let provider = SdkTracerProvider::builder() -// .with_id_generator(RandomIdGenerator::default()) -// .with_batch_exporter(exporter) -// .with_resource(resource) -// .build(); - -// opentelemetry::global::set_tracer_provider(provider.clone()); - -// Ok(provider.tracer(service_name.to_string())) -// } - -// /// Initialize OTLP tracer (backward compatibility) -// pub fn init_tracer(otlp_config: &OtlpConfig) -> Result { -// init_tracer_with_service(otlp_config, "katana") -// } +impl crate::__private::Sealed for OtlpTracer {} From d20781a83e6991729517a460ed41c8a7b030ea60 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 16 Sep 2025 12:20:40 -0400 Subject: [PATCH 9/9] wip --- crates/tracing/src/gcloud.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/tracing/src/gcloud.rs b/crates/tracing/src/gcloud.rs index 85cfb106b..5281d9424 100644 --- a/crates/tracing/src/gcloud.rs +++ b/crates/tracing/src/gcloud.rs @@ -103,12 +103,6 @@ impl GCloudTracerBuilder { let tracer_provider = trace_exporter.create_provider().await?; let tracer = trace_exporter.install(&tracer_provider).await?; - // // Set the Google Cloud trace context propagator globally - // // This will handle both extraction and injection of X-Cloud-Trace-Context headers - // opentelemetry::global::set_text_map_propagator(GoogleTraceContextPropagator::default()); - // opentelemetry::global::set_tracer_provider(tracer_provider.clone()); - - // Return the layer Ok(GCloudTracer { tracer, tracer_provider }) } }