diff --git a/Cargo.lock b/Cargo.lock index 7ca8fafd..2554d60b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -187,7 +187,7 @@ dependencies = [ "prover-config", "prover-engine", "prover-executor", - "prover-logger", + "prover-tracer", "sp1-sdk", "sp1-zkvm", "tokio", @@ -209,7 +209,7 @@ dependencies = [ "insta", "pretty_assertions", "prover-config", - "prover-logger", + "prover-tracer", "prover-utils", "serde", "thiserror 2.0.12", @@ -319,7 +319,7 @@ dependencies = [ "prost", "prover-engine", "prover-executor", - "prover-logger", + "prover-tracer", "sp1-sdk", "tokio", "tokio-util", @@ -337,7 +337,7 @@ dependencies = [ "insta", "pretty_assertions", "prover-config", - "prover-logger", + "prover-tracer", "prover-utils", "serde", "thiserror 2.0.12", @@ -369,10 +369,10 @@ dependencies = [ "buildstructor", "futures", "lazy_static", - "opentelemetry", - "opentelemetry-prometheus", - "opentelemetry_sdk", - "prometheus", + "opentelemetry 0.27.1", + "opentelemetry-prometheus 0.27.0", + "opentelemetry_sdk 0.27.1", + "prometheus 0.13.4", "thiserror 2.0.12", "tokio", "tokio-util", @@ -4920,6 +4920,54 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror 2.0.12", + "tracing", +] + +[[package]] +name = "opentelemetry-http" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46d7ab32b827b5b495bd90fa95a6cb65ccc293555dcc3199ae2937d2d237c8ed" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry 0.29.1", + "reqwest", + "tracing", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" +dependencies = [ + "futures-core", + "http", + "opentelemetry 0.29.1", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk 0.29.0", + "prost", + "reqwest", + "thiserror 2.0.12", + "tokio", + "tonic 0.12.3", + "tracing", +] + [[package]] name = "opentelemetry-prometheus" version = "0.27.0" @@ -4927,13 +4975,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b834e966ea5e2d03dfe5f2253f03d22cce21403ee940265070eeee96cee0bcc" dependencies = [ "once_cell", - "opentelemetry", - "opentelemetry_sdk", - "prometheus", - "protobuf", + "opentelemetry 0.27.1", + "opentelemetry_sdk 0.27.1", + "prometheus 0.13.4", + "protobuf 2.28.0", "tracing", ] +[[package]] +name = "opentelemetry-prometheus" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "098a71a4430bb712be6130ed777335d2e5b19bc8566de5f2edddfce906def6ab" +dependencies = [ + "once_cell", + "opentelemetry 0.29.1", + "opentelemetry_sdk 0.29.0", + "prometheus 0.14.0", + "tracing", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c40da242381435e18570d5b9d50aca2a4f4f4d8e146231adb4e7768023309b3" +dependencies = [ + "opentelemetry 0.29.1", + "opentelemetry_sdk 0.29.0", + "prost", + "tonic 0.12.3", +] + [[package]] name = "opentelemetry_sdk" version = "0.27.1" @@ -4945,7 +5018,7 @@ dependencies = [ "futures-executor", "futures-util", "glob", - "opentelemetry", + "opentelemetry 0.27.1", "percent-encoding", "rand 0.8.5", "serde_json", @@ -4953,6 +5026,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry_sdk" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "opentelemetry 0.29.1", + "percent-encoding", + "rand 0.9.1", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tracing", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -5701,10 +5794,25 @@ dependencies = [ "lazy_static", "memchr", "parking_lot", - "protobuf", + "protobuf 2.28.0", "thiserror 1.0.69", ] +[[package]] +name = "prometheus" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf 3.7.2", + "thiserror 2.0.12", +] + [[package]] name = "proposer-client" version = "0.1.0" @@ -5770,7 +5878,7 @@ dependencies = [ "proposer-client", "proposer-elfs", "prover-alloy", - "prover-logger", + "prover-tracer", "serde", "sp1-core-executor", "sp1-prover", @@ -5860,6 +5968,26 @@ version = "2.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" +[[package]] +name = "protobuf" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" +dependencies = [ + "once_cell", + "protobuf-support", + "thiserror 1.0.69", +] + +[[package]] +name = "protobuf-support" +version = "3.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" +dependencies = [ + "thiserror 1.0.69", +] + [[package]] name = "prover-alloy" version = "0.1.0" @@ -5927,7 +6055,7 @@ dependencies = [ "futures", "prover-config", "prover-engine", - "prover-logger", + "prover-tracer", "serde", "sp1-prover", "sp1-sdk", @@ -5944,12 +6072,18 @@ dependencies = [ ] [[package]] -name = "prover-logger" +name = "prover-tracer" version = "0.1.0" dependencies = [ + "anyhow", + "opentelemetry 0.29.1", + "opentelemetry-otlp", + "opentelemetry-prometheus 0.29.1", + "opentelemetry_sdk 0.29.0", "serde", "tracing", "tracing-appender", + "tracing-opentelemetry", "tracing-subscriber 0.3.19", ] @@ -6241,6 +6375,7 @@ checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "http", @@ -9077,6 +9212,24 @@ dependencies = [ "tracing-subscriber 0.3.19", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8e764bd6f5813fd8bebc3117875190c5b0415be8f7f8059bffb6ecd979c444" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry 0.29.1", + "opentelemetry_sdk 0.29.0", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber 0.3.19", + "web-time", +] + [[package]] name = "tracing-serde" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 3cf6e071..2b4a7dee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ prover-config = { path = "crates/prover-config" } prover-elf-utils = { path = "crates/prover-elf-utils" } prover-engine = { path = "crates/prover-engine" } prover-executor = { path = "crates/prover-executor" } -prover-logger = { path = "crates/prover-logger" } +prover-tracer = { path = "crates/prover-tracer" } prover-utils = { path = "crates/prover-utils" } # TODO: this should probably move to interop diff --git a/crates/aggkit-prover-config/Cargo.toml b/crates/aggkit-prover-config/Cargo.toml index b5b4dc7a..59ef01e0 100644 --- a/crates/aggkit-prover-config/Cargo.toml +++ b/crates/aggkit-prover-config/Cargo.toml @@ -9,7 +9,7 @@ thiserror.workspace = true toml.workspace = true aggchain-proof-service.workspace = true -prover-logger.workspace = true +prover-tracer.workspace = true prover-utils.workspace = true prover-config.workspace = true diff --git a/crates/aggkit-prover-config/src/lib.rs b/crates/aggkit-prover-config/src/lib.rs index a5282f92..11f62360 100644 --- a/crates/aggkit-prover-config/src/lib.rs +++ b/crates/aggkit-prover-config/src/lib.rs @@ -5,7 +5,7 @@ use std::{ use aggchain_proof_service::config::AggchainProofServiceConfig; use prover_config::{NetworkProverConfig, ProverType}; -use prover_logger::log::Log; +use prover_tracer::TracingConfig; use serde::{Deserialize, Serialize}; pub use crate::{shutdown::ShutdownConfig, telemetry::TelemetryConfig}; @@ -28,7 +28,7 @@ pub struct ProverConfig { /// The log configuration. #[serde(default)] - pub log: Log, + pub log: TracingConfig, /// Telemetry configuration. #[serde(default)] @@ -54,7 +54,7 @@ impl Default for ProverConfig { fn default() -> Self { Self { grpc_endpoint: default_socket_addr(), - log: Log::default(), + log: TracingConfig::default(), telemetry: TelemetryConfig::default(), shutdown: ShutdownConfig::default(), aggchain_proof_service: AggchainProofServiceConfig::default(), diff --git a/crates/aggkit-prover/Cargo.toml b/crates/aggkit-prover/Cargo.toml index b0d1c975..7701fb73 100644 --- a/crates/aggkit-prover/Cargo.toml +++ b/crates/aggkit-prover/Cargo.toml @@ -37,7 +37,7 @@ proposer-service.workspace = true prover-config.workspace = true prover-engine.workspace = true prover-executor.workspace = true -prover-logger.workspace = true +prover-tracer.workspace = true [dev-dependencies] hyper-util = "0.1.10" diff --git a/crates/aggkit-prover/src/lib.rs b/crates/aggkit-prover/src/lib.rs index cf72fda4..2ca88c22 100644 --- a/crates/aggkit-prover/src/lib.rs +++ b/crates/aggkit-prover/src/lib.rs @@ -15,13 +15,8 @@ mod tests; pub fn runtime(cfg: PathBuf, version: &str) -> anyhow::Result<()> { let config = Arc::new(aggkit_prover_config::ProverConfig::try_load(&cfg)?); - // Initialize the logger - prover_logger::tracing(&config.log); - let global_cancellation_token = CancellationToken::new(); - info!("Starting AggKit Prover version info: {}", version); - let prover_runtime = tokio::runtime::Builder::new_multi_thread() .thread_name("aggkit-prover-runtime") .enable_all() @@ -33,6 +28,11 @@ pub fn runtime(cfg: PathBuf, version: &str) -> anyhow::Result<()> { .enable_all() .build()?; + // Initialize the tracing + metrics_runtime.block_on(async { prover_tracer::setup_tracing(&config.log, version) })?; + + info!("Running aggkit prover version info: {}", version); + let aggchain_proof_service = prover_runtime.block_on(async { let grpc_service = GrpcService::new(&config.aggchain_proof_service).await?; Ok::, aggchain_proof_service::Error>( diff --git a/crates/agglayer-prover-config/Cargo.toml b/crates/agglayer-prover-config/Cargo.toml index 98c6a857..385a8fd0 100644 --- a/crates/agglayer-prover-config/Cargo.toml +++ b/crates/agglayer-prover-config/Cargo.toml @@ -8,7 +8,7 @@ serde = { workspace = true, features = ["derive"] } thiserror.workspace = true toml.workspace = true -prover-logger.workspace = true +prover-tracer.workspace = true prover-utils.workspace = true prover-config.workspace = true diff --git a/crates/agglayer-prover-config/src/lib.rs b/crates/agglayer-prover-config/src/lib.rs index 3ddb119d..eda6f7ac 100644 --- a/crates/agglayer-prover-config/src/lib.rs +++ b/crates/agglayer-prover-config/src/lib.rs @@ -5,7 +5,7 @@ use std::{ }; use prover_config::{default_max_concurrency_limit, NetworkProverConfig, ProverType}; -use prover_logger::log::Log; +use prover_tracer::TracingConfig; use prover_utils::with; use serde::{Deserialize, Serialize}; @@ -29,7 +29,7 @@ pub struct ProverConfig { /// The log configuration. #[serde(default, alias = "Log")] - pub log: Log, + pub log: TracingConfig, /// Telemetry configuration. #[serde(default, alias = "Telemetry")] @@ -65,7 +65,7 @@ impl Default for ProverConfig { fn default() -> Self { Self { grpc_endpoint: default_socket_addr(), - log: Log::default(), + log: TracingConfig::default(), telemetry: TelemetryConfig::default(), shutdown: ShutdownConfig::default(), max_concurrency_limit: default_max_concurrency_limit(), diff --git a/crates/agglayer-prover/Cargo.toml b/crates/agglayer-prover/Cargo.toml index 56853740..a1351f8f 100644 --- a/crates/agglayer-prover/Cargo.toml +++ b/crates/agglayer-prover/Cargo.toml @@ -26,7 +26,7 @@ agglayer-prover-types.workspace = true agglayer-telemetry.workspace = true prover-engine.workspace = true prover-executor.workspace = true -prover-logger.workspace = true +prover-tracer.workspace = true diff --git a/crates/agglayer-prover/src/lib.rs b/crates/agglayer-prover/src/lib.rs index 4e9957ba..132426f9 100644 --- a/crates/agglayer-prover/src/lib.rs +++ b/crates/agglayer-prover/src/lib.rs @@ -18,13 +18,8 @@ mod rpc; pub fn main(cfg: PathBuf, version: &str, program: &'static [u8]) -> anyhow::Result<()> { let config = Arc::new(agglayer_prover_config::ProverConfig::try_load(&cfg)?); - // Initialize the logger - prover_logger::tracing(&config.log); - let global_cancellation_token = CancellationToken::new(); - info!("Starting agglayer prover version info: {}", version); - let prover_runtime = tokio::runtime::Builder::new_multi_thread() .thread_name("agglayer-prover-runtime") .enable_all() @@ -36,6 +31,11 @@ pub fn main(cfg: PathBuf, version: &str, program: &'static [u8]) -> anyhow::Resu .enable_all() .build()?; + // Initialize the tracing + metrics_runtime.block_on(async { prover_tracer::setup_tracing(&config.log, version) })?; + + info!("Running agglayer prover version info: {}", version); + let pp_service = prover_runtime.block_on(async { crate::prover::Prover::create_service(&config, program) }); diff --git a/crates/proposer-service/Cargo.toml b/crates/proposer-service/Cargo.toml index 788ae337..0cff77c3 100644 --- a/crates/proposer-service/Cargo.toml +++ b/crates/proposer-service/Cargo.toml @@ -17,7 +17,7 @@ aggkit-prover-types = { workspace = true, features = ["sp1"] } agglayer-evm-client.workspace = true proposer-client.workspace = true prover-alloy.workspace = true -prover-logger.workspace = true +prover-tracer.workspace = true proposer-elfs.workspace = true alloy-primitives.workspace = true diff --git a/crates/proposer-service/src/tests/proposer_service_test_program.rs b/crates/proposer-service/src/tests/proposer_service_test_program.rs index 3336c9d9..94047224 100644 --- a/crates/proposer-service/src/tests/proposer_service_test_program.rs +++ b/crates/proposer-service/src/tests/proposer_service_test_program.rs @@ -6,7 +6,7 @@ use clap::Parser; use proposer_client::{config::ProposerClientConfig, FepProposerRequest, GrpcUri}; use proposer_service::{config::ProposerServiceConfig, ProposerService}; use prover_alloy::L1RpcEndpoint; -use prover_logger::log::Log; +use prover_tracer::TracingConfig; use tower::{Service, ServiceExt}; use tracing::info; use url::Url; @@ -49,7 +49,7 @@ pub async fn main() -> anyhow::Result<()> { println!("Starting Proposer service test..."); // Initialize the tracing - prover_logger::tracing(&Log::default()); + prover_tracer::setup_tracing(&TracingConfig::default(), "proposer-service-test")?; let cli = Cli::parse(); diff --git a/crates/prover-executor/Cargo.toml b/crates/prover-executor/Cargo.toml index e9b03e97..c6aeb2fe 100644 --- a/crates/prover-executor/Cargo.toml +++ b/crates/prover-executor/Cargo.toml @@ -23,7 +23,7 @@ tonic-health = "0.12.3" tonic-reflection = "0.12.3" prover-engine.workspace = true -prover-logger.workspace = true +prover-tracer.workspace = true prover-config.workspace = true sp1-sdk = { workspace = true, features = ["native-gnark"] } diff --git a/crates/prover-logger/Cargo.toml b/crates/prover-logger/Cargo.toml deleted file mode 100644 index 3e96aa6c..00000000 --- a/crates/prover-logger/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "prover-logger" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -serde.workspace = true -tracing.workspace = true -tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } -tracing-appender.workspace = true - -[lints] -workspace = true diff --git a/crates/prover-logger/src/lib.rs b/crates/prover-logger/src/lib.rs deleted file mode 100644 index 722f42c3..00000000 --- a/crates/prover-logger/src/lib.rs +++ /dev/null @@ -1,34 +0,0 @@ -use log::Log; -use serde::{Deserialize, Serialize}; -use tracing_subscriber::{prelude::*, EnvFilter}; - -pub mod log; - -/// The log format. -#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum LogFormat { - #[default] - Pretty, - Json, -} -pub fn tracing(config: &Log) { - // TODO: Support multiple outputs. - let writer = config.outputs.first().cloned().unwrap_or_default(); - - let layer = match config.format { - LogFormat::Pretty => tracing_subscriber::fmt::layer() - .pretty() - .with_writer(writer.as_make_writer()) - .with_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| config.level.into())) - .boxed(), - - LogFormat::Json => tracing_subscriber::fmt::layer() - .json() - .with_writer(writer.as_make_writer()) - .with_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| config.level.into())) - .boxed(), - }; - - tracing_subscriber::Registry::default().with(layer).init(); -} diff --git a/crates/prover-logger/src/log.rs b/crates/prover-logger/src/log.rs deleted file mode 100644 index 300ea9e1..00000000 --- a/crates/prover-logger/src/log.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{fmt::Display, path::PathBuf}; - -use serde::{Deserialize, Deserializer, Serialize}; -use tracing_subscriber::{fmt::writer::BoxMakeWriter, EnvFilter}; - -use crate::LogFormat; - -/// The log configuration. -#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub struct Log { - /// The `RUST_LOG` environment variable will take precedence over the - /// configuration log level. - #[serde(default)] - pub level: LogLevel, - #[serde(default)] - pub outputs: Vec, - #[serde(default)] - pub format: LogFormat, -} - -/// The log level. -#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)] -#[serde(rename_all = "lowercase")] -pub enum LogLevel { - Trace, - Debug, - #[default] - Info, - Warn, - Error, - Fatal, -} - -impl Display for LogLevel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let level = match self { - LogLevel::Trace => "trace", - LogLevel::Debug => "debug", - LogLevel::Info => "info", - LogLevel::Warn => "warn", - LogLevel::Error => "error", - LogLevel::Fatal => "fatal", - }; - - write!(f, "{level}") - } -} - -impl From for EnvFilter { - fn from(value: LogLevel) -> Self { - EnvFilter::new(format!( - "warn,prover={value},aggkit={value},agglayer={value},pessimistic_proof={value}" - )) - } -} - -/// The log output. -/// -/// This can be either `stdout`, `stderr`, or a file path. -/// -/// The [`Deserialize`] implementation allows for the configuration file to -/// specify the output location as a string, which is then parsed into the -/// appropriate enum variant. If the string is not recognized to be either -/// `stdout` or `stderr`, it is assumed to be a file path. -#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq)] -pub enum LogOutput { - #[default] - Stdout, - Stderr, - File(PathBuf), -} - -impl<'de> Deserialize<'de> for LogOutput { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - // If the string is not recognized to be either `stdout` or `stderr`, - // it is assumed to be a file path. - match s.as_str() { - "stdout" => Ok(LogOutput::Stdout), - "stderr" => Ok(LogOutput::Stderr), - _ => Ok(LogOutput::File(PathBuf::from(s))), - } - } -} - -impl LogOutput { - /// Get a [`BoxMakeWriter`] for the log output. - /// - /// This can be used to plug the log output into the tracing subscriber. - pub fn as_make_writer(&self) -> BoxMakeWriter { - match self { - LogOutput::Stdout => BoxMakeWriter::new(std::io::stdout), - LogOutput::Stderr => BoxMakeWriter::new(std::io::stderr), - LogOutput::File(path) => { - let appender = tracing_appender::rolling::never(".", path); - BoxMakeWriter::new(appender) - } - } - } -} diff --git a/crates/prover-tracer/Cargo.toml b/crates/prover-tracer/Cargo.toml new file mode 100644 index 00000000..43d05b97 --- /dev/null +++ b/crates/prover-tracer/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "prover-tracer" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +anyhow.workspace = true +opentelemetry = { version = "0.29.1", features = ["metrics"] } +opentelemetry-otlp = { version = "0.29.0", features = ["trace", "grpc-tonic"]} +opentelemetry-prometheus = "0.29.1" +opentelemetry_sdk = { version = "0.29.0", features = ["metrics", "rt-tokio", "trace"] } +serde.workspace = true +tracing.workspace = true +tracing-opentelemetry = "0.30.0" +tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } +tracing-appender.workspace = true + +[lints] +workspace = true diff --git a/crates/prover-tracer/src/lib.rs b/crates/prover-tracer/src/lib.rs new file mode 100644 index 00000000..9fc923b7 --- /dev/null +++ b/crates/prover-tracer/src/lib.rs @@ -0,0 +1,308 @@ +use std::{fmt::Display, path::PathBuf, time::Duration}; + +use opentelemetry::{global, trace::TracerProvider, KeyValue}; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::{ + propagation::TraceContextPropagator, + trace::{BatchConfigBuilder, BatchSpanProcessor, Sampler, SpanLimits}, + Resource, +}; +use serde::{Deserialize, Deserializer, Serialize}; +use tracing_subscriber::{ + fmt::writer::BoxMakeWriter, prelude::*, util::SubscriberInitExt, EnvFilter, +}; + +pub const OTLP_BATCH_SCHEDULED_DELAY: Duration = Duration::from_millis(5_000); +pub const OTLP_BATCH_MAX_QUEUE_SIZE: usize = 2048; +pub const OTLP_BATCH_MAX_EXPORTER_BATCH_SIZE: usize = 512; + +/// The tracing format. +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum TracingFormat { + #[default] + Pretty, + Json, +} + +/// The tracing configuration. +#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct TracingConfig { + /// The `RUST_LOG` environment variable will take precedence over the + /// configuration tracing level. + #[serde(default)] + pub level: TracingLevel, + #[serde(default)] + pub outputs: Vec, + #[serde(default)] + pub format: TracingFormat, + /// Socket of the open telemetry agent endpoint. + /// If not provided open telemetry will not be used. + #[serde(skip_serializing_if = "Option::is_none")] + pub otlp_agent: Option, + /// Otlp service name. + /// If not provided open telemetry will not be used. + #[serde(skip_serializing_if = "Option::is_none")] + pub otlp_service_name: Option, +} + +/// The tracing level. +#[derive(Serialize, Deserialize, Debug, Default, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +pub enum TracingLevel { + Trace, + Debug, + #[default] + Info, + Warn, + Error, + Fatal, +} + +impl Display for TracingLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let level = match self { + TracingLevel::Trace => "trace", + TracingLevel::Debug => "debug", + TracingLevel::Info => "info", + TracingLevel::Warn => "warn", + TracingLevel::Error => "error", + TracingLevel::Fatal => "fatal", + }; + + write!(f, "{level}") + } +} + +impl From for EnvFilter { + fn from(value: TracingLevel) -> Self { + EnvFilter::new(format!( + "warn,prover={value},aggkit={value},agglayer={value},pessimistic_proof={value}" + )) + } +} + +/// The tracing output. +/// +/// This can be either `stdout`, `stderr`, or a file path. +/// +/// The [`Deserialize`] implementation allows for the configuration file to +/// specify the output location as a string, which is then parsed into the +/// appropriate enum variant. If the string is not recognized to be either +/// `stdout` or `stderr`, it is assumed to be a file path. +#[derive(Serialize, Debug, Clone, Default, PartialEq, Eq)] +pub enum TracingOutput { + #[default] + Stdout, + Stderr, + File(PathBuf), + Otlp, +} + +impl<'de> Deserialize<'de> for TracingOutput { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + // If the string is not recognized to be either `stdout` or `stderr`, + // it is assumed to be a file path. + match s.as_str() { + "stdout" => Ok(TracingOutput::Stdout), + "stderr" => Ok(TracingOutput::Stderr), + "otlp" => Ok(TracingOutput::Otlp), + _ => Ok(TracingOutput::File(PathBuf::from(s))), + } + } +} + +impl TracingOutput { + /// Get a [`BoxMakeWriter`] for the trace output. + /// + /// This can be used to plug the trace output into the tracing subscriber. + pub fn as_make_writer(&self) -> BoxMakeWriter { + match self { + TracingOutput::Stdout => BoxMakeWriter::new(std::io::stdout), + TracingOutput::Stderr => BoxMakeWriter::new(std::io::stderr), + // For OTLP, output traces also to stdout + TracingOutput::Otlp => BoxMakeWriter::new(std::io::stdout), + TracingOutput::File(path) => { + let appender = tracing_appender::rolling::never(".", path); + BoxMakeWriter::new(appender) + } + } + } +} + +pub fn setup_tracing(config: &TracingConfig, version: &str) -> anyhow::Result<()> { + let mut layers = Vec::new(); + + for writer in &config.outputs { + // Setup instrumentation if both otlp agent url and + // otlp service name are provided as arguments + if writer == &TracingOutput::Otlp { + let (Some(otlp_agent), Some(otlp_service_name)) = + (&config.otlp_agent, &config.otlp_service_name) + else { + anyhow::bail!( + "Otlp tracing requires both otlp agent url and otlp service provided" + ); + }; + + let resources = build_resources(otlp_service_name, version); + let otlp_exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(otlp_agent) + .build()?; + + let batch_processor_config = BatchConfigBuilder::default() + .with_scheduled_delay(match std::env::var("OTLP_BATCH_SCHEDULED_DELAY") { + Ok(v) => { + if let Ok(millis) = v.parse::() { + Duration::from_millis(millis) + } else { + OTLP_BATCH_SCHEDULED_DELAY + } + } + _ => OTLP_BATCH_SCHEDULED_DELAY, + }) + .with_max_queue_size(match std::env::var("OTLP_BATCH_MAX_QUEUE_SIZE") { + Ok(v) => v.parse::().unwrap_or(OTLP_BATCH_MAX_QUEUE_SIZE), + _ => OTLP_BATCH_MAX_QUEUE_SIZE, + }) + .with_max_export_batch_size( + match std::env::var("OTLP_BATCH_MAX_EXPORTER_BATCH_SIZE") { + Ok(v) => v + .parse::() + .unwrap_or(OTLP_BATCH_MAX_EXPORTER_BATCH_SIZE), + _ => OTLP_BATCH_MAX_EXPORTER_BATCH_SIZE, + }, + ); + + let span_limits = { + let mut span_limits = SpanLimits::default(); + if let Ok(max_events) = std::env::var("OTLP_MAX_EVENTS_PER_SPAN") { + if let Ok(value) = max_events.parse::() { + span_limits.max_events_per_span = value; + } + } + + if let Ok(max_attributes) = std::env::var("OTLP_MAX_ATTRIBUTES_PER_SPAN") { + if let Ok(value) = max_attributes.parse::() { + span_limits.max_attributes_per_span = value; + } + } + + if let Ok(max_links_per_span) = std::env::var("OTLP_MAX_LINKS_PER_SPAN") { + if let Ok(value) = max_links_per_span.parse::() { + span_limits.max_links_per_span = value; + } + } + + if let Ok(max_attributes_per_event) = std::env::var("OTLP_MAX_ATTRIBUTES_PER_EVENT") + { + if let Ok(value) = max_attributes_per_event.parse::() { + span_limits.max_attributes_per_event = value; + } + } + + if let Ok(max_attributes_per_link) = std::env::var("OTLP_MAX_ATTRIBUTES_PER_LINK") { + if let Ok(value) = max_attributes_per_link.parse::() { + span_limits.max_attributes_per_link = value; + } + } + span_limits + }; + + // Ensure that the span limits are not too low + let trace_provider = opentelemetry_sdk::trace::SdkTracerProvider::builder() + .with_sampler(Sampler::AlwaysOn) + .with_span_limits(span_limits) + .with_resource(Resource::builder().with_attributes(resources).build()) + .with_span_processor( + BatchSpanProcessor::builder(otlp_exporter) + .with_batch_config(batch_processor_config.build()) + .build(), + ) + .build(); + + let tracer = trace_provider.tracer("agglayer-otlp"); + + let _ = global::set_tracer_provider(trace_provider); + + layers.push( + tracing_opentelemetry::layer() + .with_tracer(tracer) + .with_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| config.level.into()), + ) + .boxed(), + ); + + global::set_text_map_propagator(TraceContextPropagator::new()); + } else { + layers.push(match config.format { + TracingFormat::Pretty => tracing_subscriber::fmt::layer() + .pretty() + .with_writer(writer.as_make_writer()) + .with_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| config.level.into()), + ) + .boxed(), + + TracingFormat::Json => tracing_subscriber::fmt::layer() + .json() + .with_writer(writer.as_make_writer()) + .with_filter( + EnvFilter::try_from_default_env().unwrap_or_else(|_| config.level.into()), + ) + .boxed(), + }); + } + } + + // We are using try_init because integration test may try + // to initialize this multiple times. + tracing_subscriber::Registry::default() + .with(layers) + .try_init() + .map_err(|e| anyhow::anyhow!("Unable to initialize tracing subscriber: {e:?}"))?; + + tracing::info!("Tracing initialized with config: {config:?}"); + + Ok(()) +} + +fn build_resources(otlp_service_name: &str, version: &str) -> Vec { + let mut resources = Vec::new(); + + resources.push(KeyValue::new("service.name", otlp_service_name.to_string())); + resources.push(KeyValue::new("service.version", version.to_string())); + + let custom_resources: Vec<_> = std::env::var("AGGLAYER_OTLP_TAGS") + .unwrap_or_default() + .split(',') + .filter_map(|tag_raw| { + let mut v = tag_raw.splitn(2, '='); + match (v.next(), v.next()) { + (Some(key), Some(value)) if !key.trim().is_empty() && !value.trim().is_empty() => { + Some(KeyValue::new( + key.trim().to_string(), + value.trim().to_string(), + )) + } + _ => { + eprint!( + "Invalid AGGLAYER_OTLP_TAGS entry: {tag_raw}. Expected format: key=value" + ); + None + } + } + }) + .collect(); + + resources.extend(custom_resources); + + resources +}