From 3b84c5611e8d723e2053ebeb62ff0b02b2c7db71 Mon Sep 17 00:00:00 2001 From: Manuthor Date: Wed, 1 Jul 2026 09:54:54 +0200 Subject: [PATCH 1/3] fix: ensure cosmian_logger build on wasm - even not used on wasm --- .github/workflows/ci.yml | 11 +++ .pre-commit-config.yaml | 7 ++ crate/logger/Cargo.toml | 27 +++--- crate/logger/src/error.rs | 3 + crate/logger/src/lib.rs | 194 ++++++++++++++++++++++---------------- crate/logger/src/types.rs | 41 +++++--- 6 files changed, 174 insertions(+), 109 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cf42a0..9ca7db4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,17 @@ jobs: - name: Tests all targets all features run: cargo test --lib --workspace --all-targets --all-features + wasm-build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.90.0 + targets: wasm32-unknown-unknown + - name: Build cosmian_logger for wasm32-unknown-unknown + run: cargo build --target wasm32-unknown-unknown -p cosmian_logger + cargo-lint: uses: Cosmian/reusable_workflows/.github/workflows/cargo-lint.yml@develop with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4cfeece..55e8340 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,6 +4,7 @@ # pre-commit install # pre-commit install --install-hooks -t commit-msg # pre-commit autoupdate +# rustup target add wasm32-unknown-unknown # # (optional) Creating a virtual environment # ``` @@ -148,3 +149,9 @@ repos: language: system pass_filenames: false always_run: true + - id: wasm-build-cosmian-logger + name: WASM build - cosmian_logger + entry: cargo build --target wasm32-unknown-unknown -p cosmian_logger + language: system + pass_filenames: false + files: ^crate/logger/ diff --git a/crate/logger/Cargo.toml b/crate/logger/Cargo.toml index cb2af14..3ed1620 100644 --- a/crate/logger/Cargo.toml +++ b/crate/logger/Cargo.toml @@ -8,32 +8,33 @@ license.workspace = true [dependencies] opentelemetry = "0.32" +opentelemetry-semantic-conventions = "0.32" +thiserror = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = [ + "ansi", + "env-filter", + "fmt", + "std" +] } + +# The following crates rely on tokio, tonic, or Unix-specific syscalls that do +# not compile on wasm32. +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] opentelemetry-appender-tracing = "0.32" opentelemetry-otlp = { version = "0.32", features = [ "metrics", "logs", "grpc-tonic" ] } -opentelemetry-semantic-conventions = "0.32" opentelemetry_sdk = { version = "0.32", features = [ "metrics", "logs", "rt-tokio" ] } -thiserror = { workspace = true } syslog-tracing = "0.3" -tracing = { workspace = true } -tracing-opentelemetry = { version = "0.33" } -tracing-subscriber = { workspace = true, features = [ - "ansi", - "env-filter", - "fmt", - "std" -] } - -# tracing-appender uses platform-specific file rotation that does not compile on wasm32. -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-appender = { workspace = true } +tracing-opentelemetry = { version = "0.33" } [dev-dependencies] tokio = { version = "1", features = ["rt-multi-thread", "macros"] } diff --git a/crate/logger/src/error.rs b/crate/logger/src/error.rs index be09938..6f2f15e 100644 --- a/crate/logger/src/error.rs +++ b/crate/logger/src/error.rs @@ -1,12 +1,15 @@ /// Errors that can occur while initialising or configuring the logging stack. #[derive(Debug, thiserror::Error)] pub enum LoggingError { + #[cfg(not(target_arch = "wasm32"))] #[error("failed to build OTLP span exporter: {0}")] SpanExporter(#[source] opentelemetry_otlp::ExporterBuildError), + #[cfg(not(target_arch = "wasm32"))] #[error("failed to build OTLP log exporter: {0}")] LogExporter(#[source] opentelemetry_otlp::ExporterBuildError), + #[cfg(not(target_arch = "wasm32"))] #[error("failed to build OTLP metric exporter: {0}")] MetricExporter(#[source] opentelemetry_otlp::ExporterBuildError), diff --git a/crate/logger/src/lib.rs b/crate/logger/src/lib.rs index b1b9bcc..f1db2bf 100644 --- a/crate/logger/src/lib.rs +++ b/crate/logger/src/lib.rs @@ -6,9 +6,13 @@ pub mod types; use std::sync::atomic::{AtomicBool, Ordering}; pub use error::{LoggingError, LoggingResult, ResultExt}; +#[cfg(not(target_arch = "wasm32"))] use opentelemetry::trace::TracerProvider; +#[cfg(not(target_arch = "wasm32"))] use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; +#[cfg(not(target_arch = "wasm32"))] use opentelemetry_otlp::WithExportConfig; +#[cfg(not(target_arch = "wasm32"))] use opentelemetry_sdk::{ logs::SdkLoggerProvider, metrics::{PeriodicReader, SdkMeterProvider}, @@ -28,6 +32,7 @@ pub mod reexport { static INITIALIZED: AtomicBool = AtomicBool::new(false); +#[cfg(not(target_arch = "wasm32"))] fn build_resource_simple(service_name: &str) -> Resource { Resource::builder_empty() .with_attributes([opentelemetry::KeyValue::new( @@ -37,6 +42,7 @@ fn build_resource_simple(service_name: &str) -> Resource { .build() } +#[cfg(not(target_arch = "wasm32"))] fn build_resource_with_config(service_name: &str, config: &TelemetryConfig) -> Resource { use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION}; let mut kvs = vec![opentelemetry::KeyValue::new( @@ -55,6 +61,7 @@ fn build_resource_with_config(service_name: &str, config: &TelemetryConfig) -> R Resource::builder_empty().with_attributes(kvs).build() } +#[cfg(not(target_arch = "wasm32"))] fn new_tracer_provider(endpoint: &str, resource: Resource) -> LoggingResult { let exporter = opentelemetry_otlp::SpanExporter::builder() .with_tonic() @@ -71,6 +78,7 @@ fn new_tracer_provider(endpoint: &str, resource: Resource) -> LoggingResult LoggingResult { let exporter = opentelemetry_otlp::LogExporter::builder() .with_tonic() @@ -85,6 +93,7 @@ fn new_logger_provider(endpoint: &str, resource: Resource) -> LoggingResult LoggingResult { let exporter = opentelemetry_otlp::MetricExporter::builder() .with_tonic() @@ -117,6 +126,9 @@ pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { // (std::env::set_var is `unsafe`). If `rust_log` is provided, apply it // directly when building the `EnvFilter` below. + // `mut` is required on non-wasm targets where guards fields are populated + // inside the #[cfg(not(target_arch = "wasm32"))] block below. + #[allow(unused_mut)] let mut guards = LoggingGuards::default(); // --- stdout layer --- @@ -135,101 +147,118 @@ pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { ) }; - // --- rolling file layer --- - // tracing-appender uses platform-specific file rotation support and does not - // compile on wasm32. - #[cfg(not(target_arch = "wasm32"))] - let file_layer = config.log_to_file.as_ref().map(|(dir, name)| { - if !dir.exists() { - if let Err(e) = std::fs::create_dir_all(dir) { - eprintln!("Failed to create log directory {}: {e}", dir.display()); - } - } - let appender = tracing_appender::rolling::daily(dir, name); - let (non_blocking, guard) = tracing_appender::non_blocking(appender); - guards._rolling_appender_guard = Some(guard); - tracing_subscriber::fmt::layer() - .with_writer(non_blocking) - .with_ansi(false) - .compact() - }); - - #[cfg(target_arch = "wasm32")] - let file_layer = None; - - // --- syslog layer (Unix only) --- - #[cfg(not(target_os = "windows"))] - let syslog_layer = if config.log_to_syslog { - std::ffi::CString::new(config.service_name.as_str()) - .ok() - .and_then(|id| { - syslog_tracing::Syslog::new(id, Default::default(), syslog_tracing::Facility::User) - }) - .map(|syslog| { - tracing_subscriber::fmt::layer() - .with_writer(syslog) - .with_ansi(false) - .compact() - }) - } else { - None - }; - - // --- OTLP layer --- - let otel_layer = config.otlp.as_ref().and_then(|telemetry| { - let resource = build_resource_with_config(&config.service_name, telemetry); + let filter = config + .rust_log + .as_deref() + .map(EnvFilter::new) + .unwrap_or_else(EnvFilter::from_default_env); - let tracer_provider = match new_tracer_provider(&telemetry.otlp_url, resource.clone()) { - Ok(tp) => tp, - Err(e) => { - eprintln!("Failed to initialise OTLP span exporter: {e}"); - return None; + // ── non-wasm subscriber: file + syslog + OTLP layers ──────────────────── + #[cfg(not(target_arch = "wasm32"))] + { + // --- rolling file layer --- + // tracing-appender uses platform-specific file rotation support and does + // not compile on wasm32. + let file_layer = config.log_to_file.as_ref().map(|(dir, name)| { + if !dir.exists() { + if let Err(e) = std::fs::create_dir_all(dir) { + eprintln!("Failed to create log directory {}: {e}", dir.display()); + } } + let appender = tracing_appender::rolling::daily(dir, name); + let (non_blocking, guard) = tracing_appender::non_blocking(appender); + guards._rolling_appender_guard = Some(guard); + tracing_subscriber::fmt::layer() + .with_writer(non_blocking) + .with_ansi(false) + .compact() + }); + + // --- syslog layer (Unix only) --- + #[cfg(not(target_os = "windows"))] + let syslog_layer = if config.log_to_syslog { + std::ffi::CString::new(config.service_name.as_str()) + .ok() + .and_then(|id| { + syslog_tracing::Syslog::new( + id, + Default::default(), + syslog_tracing::Facility::User, + ) + }) + .map(|syslog| { + tracing_subscriber::fmt::layer() + .with_writer(syslog) + .with_ansi(false) + .compact() + }) + } else { + None }; - let tracer = tracer_provider.tracer(config.service_name.clone()); - opentelemetry::global::set_tracer_provider(tracer_provider.clone()); - guards._tracer_provider = Some(tracer_provider); + // --- OTLP layer --- + let otel_layer = config.otlp.as_ref().and_then(|telemetry| { + let resource = build_resource_with_config(&config.service_name, telemetry); - if telemetry.enable_metering { - match new_meter_provider(&telemetry.otlp_url, resource) { - Ok(mp) => { - opentelemetry::global::set_meter_provider(mp.clone()); - guards._meter_provider = Some(mp); + let tracer_provider = match new_tracer_provider(&telemetry.otlp_url, resource.clone()) { + Ok(tp) => tp, + Err(e) => { + eprintln!("Failed to initialise OTLP span exporter: {e}"); + return None; + } + }; + + let tracer = tracer_provider.tracer(config.service_name.clone()); + opentelemetry::global::set_tracer_provider(tracer_provider.clone()); + guards._tracer_provider = Some(tracer_provider); + + if telemetry.enable_metering { + match new_meter_provider(&telemetry.otlp_url, resource) { + Ok(mp) => { + opentelemetry::global::set_meter_provider(mp.clone()); + guards._meter_provider = Some(mp); + } + Err(e) => eprintln!("Failed to initialise OTLP metric exporter: {e}"), } - Err(e) => eprintln!("Failed to initialise OTLP metric exporter: {e}"), } - } - Some(tracing_opentelemetry::layer().with_tracer(tracer)) - }); - - let filter = config - .rust_log - .as_deref() - .map(EnvFilter::new) - .unwrap_or_else(EnvFilter::from_default_env); + Some(tracing_opentelemetry::layer().with_tracer(tracer)) + }); - let ts = tracing_subscriber::registry() - .with(filter) - .with(stdout_layer) - .with(file_layer) - .with(otel_layer); + let ts = tracing_subscriber::registry() + .with(filter) + .with(stdout_layer) + .with(file_layer) + .with(otel_layer); - #[cfg(not(target_os = "windows"))] - let ts = ts.with(syslog_layer); + #[cfg(not(target_os = "windows"))] + let ts = ts.with(syslog_layer); - if let Err(e) = ts.try_init() { - // Best-effort cleanup: avoid leaving background exporters running when - // subscriber init fails. - if let Some(tp) = guards._tracer_provider.take() { - let _ = tp.shutdown(); + if let Err(e) = ts.try_init() { + // Best-effort cleanup: avoid leaving background exporters running when + // subscriber init fails. + if let Some(tp) = guards._tracer_provider.take() { + let _ = tp.shutdown(); + } + if let Some(mp) = guards._meter_provider.take() { + let _ = mp.shutdown(); + } + INITIALIZED.store(false, Ordering::SeqCst); + eprintln!("Failed to initialise tracing: {e}"); } - if let Some(mp) = guards._meter_provider.take() { - let _ = mp.shutdown(); + } + + // ── wasm32 subscriber: stdout only ─────────────────────────────────────── + #[cfg(target_arch = "wasm32")] + { + let ts = tracing_subscriber::registry() + .with(filter) + .with(stdout_layer); + + if let Err(e) = ts.try_init() { + INITIALIZED.store(false, Ordering::SeqCst); + eprintln!("Failed to initialise tracing: {e}"); } - INITIALIZED.store(false, Ordering::SeqCst); - eprintln!("Failed to initialise tracing: {e}"); } guards @@ -255,6 +284,7 @@ pub fn log_init(rust_log: Option<&str>) { /// /// Returns [`TelemetryGuards`] that must be kept alive for the entire process /// lifetime. Call [`TelemetryGuards::shutdown`] before exit to flush buffers. +#[cfg(not(target_arch = "wasm32"))] pub fn init_tracing(default_service_name: &str) -> LoggingResult { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); diff --git a/crate/logger/src/types.rs b/crate/logger/src/types.rs index c955cb7..dcfaaa0 100644 --- a/crate/logger/src/types.rs +++ b/crate/logger/src/types.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +#[cfg(not(target_arch = "wasm32"))] use opentelemetry_sdk::{ logs::SdkLoggerProvider, metrics::SdkMeterProvider, trace::SdkTracerProvider, }; @@ -28,7 +29,7 @@ pub struct TracingConfig { pub no_log_to_stdout: bool, /// Forward logs to syslog (Unix only). - #[cfg(not(target_os = "windows"))] + #[cfg(all(not(target_os = "windows"), not(target_arch = "wasm32")))] pub log_to_syslog: bool, /// Rolling daily log file: `(directory, filename_prefix)`. @@ -49,22 +50,28 @@ pub struct TracingConfig { /// telemetry and release resources cleanly. #[derive(Default)] pub struct TelemetryGuards { + #[cfg(not(target_arch = "wasm32"))] pub(crate) tracer_provider: Option, + #[cfg(not(target_arch = "wasm32"))] pub(crate) logger_provider: Option, + #[cfg(not(target_arch = "wasm32"))] pub(crate) meter_provider: Option, } impl TelemetryGuards { /// Flush and shut down all active OTLP exporters. pub fn shutdown(self) { - if let Some(tp) = self.tracer_provider { - let _ = tp.shutdown(); - } - if let Some(lp) = self.logger_provider { - let _ = lp.shutdown(); - } - if let Some(mp) = self.meter_provider { - let _ = mp.shutdown(); + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(tp) = self.tracer_provider { + let _ = tp.shutdown(); + } + if let Some(lp) = self.logger_provider { + let _ = lp.shutdown(); + } + if let Some(mp) = self.meter_provider { + let _ = mp.shutdown(); + } } } @@ -73,6 +80,7 @@ impl TelemetryGuards { /// `env!("CARGO_PKG_NAME")`. /// /// Returns a no-op meter when OTLP is not configured. + #[cfg(not(target_arch = "wasm32"))] pub fn meter(&self, scope: &'static str) -> opentelemetry::metrics::Meter { opentelemetry::global::meter(scope) } @@ -83,7 +91,9 @@ impl TelemetryGuards { /// The tracing pipeline shuts down cleanly when this value is dropped. #[derive(Default)] pub struct LoggingGuards { + #[cfg(not(target_arch = "wasm32"))] pub(crate) _tracer_provider: Option, + #[cfg(not(target_arch = "wasm32"))] pub(crate) _meter_provider: Option, #[cfg(not(target_arch = "wasm32"))] pub(crate) _rolling_appender_guard: Option, @@ -91,11 +101,14 @@ pub struct LoggingGuards { impl Drop for LoggingGuards { fn drop(&mut self) { - if let Some(tp) = self._tracer_provider.take() { - let _ = tp.shutdown(); - } - if let Some(mp) = self._meter_provider.take() { - let _ = mp.shutdown(); + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(tp) = self._tracer_provider.take() { + let _ = tp.shutdown(); + } + if let Some(mp) = self._meter_provider.take() { + let _ = mp.shutdown(); + } } } } From a5d763d065a4dcd294ab7ee9a2133748f956ca78 Mon Sep 17 00:00:00 2001 From: Manuthor Date: Wed, 1 Jul 2026 10:13:29 +0200 Subject: [PATCH 2/3] fix: put fn_name before log message --- crate/logger/src/lib.rs | 80 +++++++++++++++++++++++++++++++++++++- crate/logger/src/macros.rs | 23 +++++++---- crate/logger/src/tests.rs | 15 +++++++ 3 files changed, 109 insertions(+), 9 deletions(-) diff --git a/crate/logger/src/lib.rs b/crate/logger/src/lib.rs index f1db2bf..c0caf4b 100644 --- a/crate/logger/src/lib.rs +++ b/crate/logger/src/lib.rs @@ -111,6 +111,78 @@ fn new_meter_provider(endpoint: &str, resource: Resource) -> LoggingResult, +} + +impl tracing::field::Visit for FieldsBefore { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + use std::fmt::Write as _; + if field.name() == "message" { + // `format_args!` implements Debug via Display: {:?} gives the raw + // (unquoted) message text — identical to DefaultFields behaviour. + self.message = Some(format!("{value:?}")); + } else { + if !self.fields.is_empty() { + self.fields.push(' '); + } + let _ = write!(self.fields, "{}={value:?}", field.name()); + } + } + + fn record_str(&mut self, field: &tracing::field::Field, value: &str) { + if field.name() == "message" { + self.message = Some(value.to_owned()); + } else { + // Non-message strings are printed with quotes via the &str Debug impl. + self.record_debug(field, &value); + } + } +} + +impl<'writer> tracing_subscriber::fmt::format::FormatFields<'writer> for FieldsBeforeMessage { + fn format_fields( + &self, + mut writer: tracing_subscriber::fmt::format::Writer<'writer>, + fields: R, + ) -> std::fmt::Result { + let mut v = FieldsBefore { + fields: String::new(), + message: None, + }; + fields.record(&mut v); + if !v.fields.is_empty() { + write!(writer, "{}", v.fields)?; + } + if let Some(msg) = v.message { + if !v.fields.is_empty() { + write!(writer, " ")?; + } + write!(writer, "{msg}")?; + } + Ok(()) + } +} + // ── public API ─────────────────────────────────────────────────────────────── /// Initialise the global tracing subscriber from a [`TracingConfig`]. @@ -143,7 +215,8 @@ pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { .with_line_number(true) .with_file(true) .with_ansi(config.with_ansi_colors) - .compact(), + .compact() + .fmt_fields(FieldsBeforeMessage), ) }; @@ -172,6 +245,7 @@ pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { .with_writer(non_blocking) .with_ansi(false) .compact() + .fmt_fields(FieldsBeforeMessage) }); // --- syslog layer (Unix only) --- @@ -191,6 +265,7 @@ pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { .with_writer(syslog) .with_ansi(false) .compact() + .fmt_fields(FieldsBeforeMessage) }) } else { None @@ -291,7 +366,8 @@ pub fn init_tracing(default_service_name: &str) -> LoggingResult &str { - // Remove `::_f` suffix - let trimmed = match name.rfind("::") { + // Remove `::_f` suffix added by our inner-function trick. + let mut s = match name.rfind("::") { Some(pos) => &name[..pos], - None => name, + None => return name, }; - // Take last segment - match trimmed.rfind("::") { - Some(pos) => &trimmed[pos + 2..], - None => trimmed, + // Strip trailing anonymous segments: `{{closure}}`, `{{async_fn_body}}`, + // etc. These appear when the macro is called from inside an async fn + // body, which the compiler desugars to a closure/state-machine type. + while s.ends_with("}}") { + s = match s.rfind("::") { + Some(pos) => &s[..pos], + None => return s, + }; + } + // Return the last named segment. + match s.rfind("::") { + Some(pos) => &s[pos + 2..], + None => s, } } _strip(::std::any::type_name_of_val(&_f)) diff --git a/crate/logger/src/tests.rs b/crate/logger/src/tests.rs index 20fb439..a92cd65 100644 --- a/crate/logger/src/tests.rs +++ b/crate/logger/src/tests.rs @@ -72,6 +72,21 @@ mod macros { ); } + /// __fn_name! must resolve to the enclosing async fn name, not + /// `{{closure}}`. + #[tokio::test] + async fn fn_name_inside_async_fn() { + let name = crate::__fn_name!(); + assert!( + !name.contains("{{"), + "fn_name must not contain '{{{{' (got '{name}') — async closure stripping failed" + ); + assert!( + name.contains("fn_name_inside_async_fn"), + "expected 'fn_name_inside_async_fn', got '{name}'" + ); + } + /// Verify that our info!/debug!/warn!/error!/trace! macros expand without /// panicking (they rely on a subscriber being present; if none is installed /// the tracing no-op path is taken instead). From 12f00886d99c22cc11d2c5a92eb7d8f80c81e6ea Mon Sep 17 00:00:00 2001 From: Manuthor Date: Wed, 1 Jul 2026 10:29:08 +0200 Subject: [PATCH 3/3] fix: address PR review comments --- crate/logger/src/types.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crate/logger/src/types.rs b/crate/logger/src/types.rs index dcfaaa0..e8e9070 100644 --- a/crate/logger/src/types.rs +++ b/crate/logger/src/types.rs @@ -29,7 +29,7 @@ pub struct TracingConfig { pub no_log_to_stdout: bool, /// Forward logs to syslog (Unix only). - #[cfg(all(not(target_os = "windows"), not(target_arch = "wasm32")))] + #[cfg(not(target_os = "windows"))] pub log_to_syslog: bool, /// Rolling daily log file: `(directory, filename_prefix)`. @@ -80,7 +80,6 @@ impl TelemetryGuards { /// `env!("CARGO_PKG_NAME")`. /// /// Returns a no-op meter when OTLP is not configured. - #[cfg(not(target_arch = "wasm32"))] pub fn meter(&self, scope: &'static str) -> opentelemetry::metrics::Meter { opentelemetry::global::meter(scope) }