From c49df2dfa7c8fc73e5b3b6f813eba0230c441324 Mon Sep 17 00:00:00 2001 From: pauline ramon Date: Tue, 23 Jun 2026 12:02:24 +0200 Subject: [PATCH 01/14] feat(logger): add init_tracing() for Kubernetes OTLP deployments Add a convenience function init_tracing(default_service_name) that auto-configures tracing from standard OpenTelemetry environment variables: - OTEL_SERVICE_NAME: service name reported to the collector - OTEL_EXPORTER_OTLP_ENDPOINT: OTLP gRPC endpoint (enables OTLP when set) When OTEL_EXPORTER_OTLP_ENDPOINT is absent, only stdout logging is enabled. Gated behind the full feature flag. Bump version to 0.7.3. --- Cargo.lock | 6 ++--- Cargo.toml | 2 +- crate/logger/src/lib.rs | 2 ++ crate/logger/src/tracing.rs | 46 +++++++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b89d55a..34d1b38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,7 +495,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cosmian_config_utils" -version = "0.7.2" +version = "0.7.3" dependencies = [ "base64 0.21.7", "serde", @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "cosmian_http_client" -version = "0.7.2" +version = "0.7.3" dependencies = [ "actix-http", "actix-identity", @@ -530,7 +530,7 @@ dependencies = [ [[package]] name = "cosmian_logger" -version = "0.7.2" +version = "0.7.3" dependencies = [ "opentelemetry", "opentelemetry-otlp", diff --git a/Cargo.toml b/Cargo.toml index 759930a..6d5709b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crate/config_utils", "crate/logger", "crate/http_client"] resolver = "2" [workspace.package] -version = "0.7.2" +version = "0.7.3" edition = "2021" rust-version = "1.71.0" authors = [ diff --git a/crate/logger/src/lib.rs b/crate/logger/src/lib.rs index d5871a9..8811d2a 100644 --- a/crate/logger/src/lib.rs +++ b/crate/logger/src/lib.rs @@ -33,6 +33,8 @@ pub use error::LoggerError; pub use log_utils::log_init; #[cfg(feature = "full")] pub use tracing::TelemetryConfig; +#[cfg(feature = "full")] +pub use tracing::init_tracing; pub use tracing::{tracing_init, LoggingGuards, TracingConfig}; /// Re-exported dependencies for use with the logging macros diff --git a/crate/logger/src/tracing.rs b/crate/logger/src/tracing.rs index 7d3413b..dab8adb 100644 --- a/crate/logger/src/tracing.rs +++ b/crate/logger/src/tracing.rs @@ -409,3 +409,49 @@ fn tracing_init_(config: &TracingConfig) -> Result { Ok(otel_guard) } + +// ============================================================================ +// Kubernetes / Cloud-native convenience initializer +// ============================================================================ + +/// Initialize tracing for Kubernetes deployments using standard OpenTelemetry +/// environment variables. +/// +/// Reads the following environment variables: +/// - `OTEL_SERVICE_NAME`: the service name reported to the collector +/// (falls back to `default_service_name`). +/// - `OTEL_EXPORTER_OTLP_ENDPOINT`: OTLP gRPC endpoint (e.g. +/// `http://otel-collector:4317`). When set, OTLP traces and metrics are +/// exported. When absent, only stdout logging is enabled. +/// +/// Returns a [`LoggingGuards`] that **must be kept alive** for the duration of +/// the process (dropping it flushes and shuts down the OTLP pipeline). +/// +/// # Example +/// +/// ```rust,ignore +/// #[tokio::main] +/// async fn main() { +/// let _guards = cosmian_logger::init_tracing("my-service") +/// .expect("failed to initialize tracing"); +/// tracing::info!("service started"); +/// } +/// ``` +#[cfg(feature = "full")] +pub fn init_tracing(default_service_name: &str) -> LoggingGuards { + let service_name = std::env::var("OTEL_SERVICE_NAME") + .unwrap_or_else(|_| default_service_name.to_owned()); + + let otlp = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") + .ok() + .map(|url| TelemetryConfig { + otlp_url: url, + ..Default::default() + }); + + tracing_init(&TracingConfig { + service_name, + otlp, + ..Default::default() + }) +} From 0dedbe97bb45e3c2b712eeda13f4cf71c956ae4b Mon Sep 17 00:00:00 2001 From: Pauline <59414053+p0wline@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:49:28 +0200 Subject: [PATCH 02/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crate/logger/src/tracing.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crate/logger/src/tracing.rs b/crate/logger/src/tracing.rs index dab8adb..629e22c 100644 --- a/crate/logger/src/tracing.rs +++ b/crate/logger/src/tracing.rs @@ -432,8 +432,7 @@ fn tracing_init_(config: &TracingConfig) -> Result { /// ```rust,ignore /// #[tokio::main] /// async fn main() { -/// let _guards = cosmian_logger::init_tracing("my-service") -/// .expect("failed to initialize tracing"); +/// let _guards = cosmian_logger::init_tracing("my-service"); /// tracing::info!("service started"); /// } /// ``` From 5f827864203573af457b0a64f37f04d4919fea9f Mon Sep 17 00:00:00 2001 From: Pauline <59414053+p0wline@users.noreply.github.com> Date: Tue, 23 Jun 2026 13:50:01 +0200 Subject: [PATCH 03/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crate/logger/src/tracing.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crate/logger/src/tracing.rs b/crate/logger/src/tracing.rs index 629e22c..297c7ae 100644 --- a/crate/logger/src/tracing.rs +++ b/crate/logger/src/tracing.rs @@ -421,8 +421,9 @@ fn tracing_init_(config: &TracingConfig) -> Result { /// - `OTEL_SERVICE_NAME`: the service name reported to the collector /// (falls back to `default_service_name`). /// - `OTEL_EXPORTER_OTLP_ENDPOINT`: OTLP gRPC endpoint (e.g. -/// `http://otel-collector:4317`). When set, OTLP traces and metrics are -/// exported. When absent, only stdout logging is enabled. +/// `http://otel-collector:4317`). When set, OTLP traces are +/// exported. (Metrics export depends on `TelemetryConfig::enable_metering`, +/// which defaults to false.) When absent, only stdout logging is enabled. /// /// Returns a [`LoggingGuards`] that **must be kept alive** for the duration of /// the process (dropping it flushes and shuts down the OTLP pipeline). From c1c0f339bda9bdc4d021629ff27e31299fec4832 Mon Sep 17 00:00:00 2001 From: pauline ramon Date: Tue, 23 Jun 2026 14:03:12 +0200 Subject: [PATCH 04/14] fix(logger): rename init_tracing to tracing_init_from_env, fix doc --- crate/logger/src/lib.rs | 4 ++-- crate/logger/src/tracing.rs | 13 ++++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/crate/logger/src/lib.rs b/crate/logger/src/lib.rs index 8811d2a..e2fe983 100644 --- a/crate/logger/src/lib.rs +++ b/crate/logger/src/lib.rs @@ -32,9 +32,9 @@ mod tracing; pub use error::LoggerError; pub use log_utils::log_init; #[cfg(feature = "full")] -pub use tracing::TelemetryConfig; +pub use tracing::tracing_init_from_env; #[cfg(feature = "full")] -pub use tracing::init_tracing; +pub use tracing::TelemetryConfig; pub use tracing::{tracing_init, LoggingGuards, TracingConfig}; /// Re-exported dependencies for use with the logging macros diff --git a/crate/logger/src/tracing.rs b/crate/logger/src/tracing.rs index 297c7ae..b55b53e 100644 --- a/crate/logger/src/tracing.rs +++ b/crate/logger/src/tracing.rs @@ -421,9 +421,8 @@ fn tracing_init_(config: &TracingConfig) -> Result { /// - `OTEL_SERVICE_NAME`: the service name reported to the collector /// (falls back to `default_service_name`). /// - `OTEL_EXPORTER_OTLP_ENDPOINT`: OTLP gRPC endpoint (e.g. -/// `http://otel-collector:4317`). When set, OTLP traces are -/// exported. (Metrics export depends on `TelemetryConfig::enable_metering`, -/// which defaults to false.) When absent, only stdout logging is enabled. +/// `http://otel-collector:4317`). When set, OTLP traces are exported. +/// When absent, only stdout logging is enabled. /// /// Returns a [`LoggingGuards`] that **must be kept alive** for the duration of /// the process (dropping it flushes and shuts down the OTLP pipeline). @@ -433,14 +432,14 @@ fn tracing_init_(config: &TracingConfig) -> Result { /// ```rust,ignore /// #[tokio::main] /// async fn main() { -/// let _guards = cosmian_logger::init_tracing("my-service"); +/// let _guards = cosmian_logger::tracing_init_from_env("my-service"); /// tracing::info!("service started"); /// } /// ``` #[cfg(feature = "full")] -pub fn init_tracing(default_service_name: &str) -> LoggingGuards { - let service_name = std::env::var("OTEL_SERVICE_NAME") - .unwrap_or_else(|_| default_service_name.to_owned()); +pub fn tracing_init_from_env(default_service_name: &str) -> LoggingGuards { + let service_name = + std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| default_service_name.to_owned()); let otlp = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") .ok() From 9b51ef4dcf3c4f48fa3b551362a7ad10feb569a9 Mon Sep 17 00:00:00 2001 From: Pauline <59414053+p0wline@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:15:47 +0200 Subject: [PATCH 05/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crate/logger/src/tracing.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crate/logger/src/tracing.rs b/crate/logger/src/tracing.rs index b55b53e..425cd11 100644 --- a/crate/logger/src/tracing.rs +++ b/crate/logger/src/tracing.rs @@ -438,8 +438,11 @@ fn tracing_init_(config: &TracingConfig) -> Result { /// ``` #[cfg(feature = "full")] pub fn tracing_init_from_env(default_service_name: &str) -> LoggingGuards { - let service_name = - std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| default_service_name.to_owned()); + let service_name = std::env::var("OTEL_SERVICE_NAME") + .ok() + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) + .unwrap_or_else(|| default_service_name.to_owned()); let otlp = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") .ok() From 6259df15aa9fd1056c7546eb65a0c8935555f4b2 Mon Sep 17 00:00:00 2001 From: Pauline <59414053+p0wline@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:16:21 +0200 Subject: [PATCH 06/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crate/logger/src/tracing.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crate/logger/src/tracing.rs b/crate/logger/src/tracing.rs index 425cd11..853b14f 100644 --- a/crate/logger/src/tracing.rs +++ b/crate/logger/src/tracing.rs @@ -446,6 +446,8 @@ pub fn tracing_init_from_env(default_service_name: &str) -> LoggingGuards { let otlp = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") .ok() + .map(|s| s.trim().to_owned()) + .filter(|s| !s.is_empty()) .map(|url| TelemetryConfig { otlp_url: url, ..Default::default() From cea858e9d4804bba99a8e1168b10c12cecc6128d Mon Sep 17 00:00:00 2001 From: Pauline <59414053+p0wline@users.noreply.github.com> Date: Tue, 23 Jun 2026 15:16:44 +0200 Subject: [PATCH 07/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crate/logger/src/tracing.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crate/logger/src/tracing.rs b/crate/logger/src/tracing.rs index 853b14f..ec32a6c 100644 --- a/crate/logger/src/tracing.rs +++ b/crate/logger/src/tracing.rs @@ -437,6 +437,7 @@ fn tracing_init_(config: &TracingConfig) -> Result { /// } /// ``` #[cfg(feature = "full")] +#[must_use] pub fn tracing_init_from_env(default_service_name: &str) -> LoggingGuards { let service_name = std::env::var("OTEL_SERVICE_NAME") .ok() From b615a7d0dc9f87999e2998b90325fb26584cf2bd Mon Sep 17 00:00:00 2001 From: Manuthor Date: Tue, 23 Jun 2026 17:58:57 +0200 Subject: [PATCH 08/14] fix: adopt complete refactor of velo shared logging crate --- Cargo.lock | 233 +++++++++-------- Cargo.toml | 2 +- crate/logger/Cargo.toml | 73 ++---- crate/logger/README.md | 325 +++++++++++++----------- crate/logger/src/error.rs | 67 +++-- crate/logger/src/lib.rs | 343 +++++++++++++++++++++---- crate/logger/src/log_utils.rs | 46 ---- crate/logger/src/macros.rs | 246 ++---------------- crate/logger/src/otlp.rs | 101 -------- crate/logger/src/tests.rs | 86 +++++++ crate/logger/src/tracing.rs | 462 ---------------------------------- crate/logger/src/types.rs | 89 +++++++ 12 files changed, 860 insertions(+), 1213 deletions(-) delete mode 100644 crate/logger/src/log_utils.rs delete mode 100644 crate/logger/src/otlp.rs create mode 100644 crate/logger/src/tests.rs delete mode 100644 crate/logger/src/tracing.rs create mode 100644 crate/logger/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 34d1b38..50e3ed4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -495,7 +495,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cosmian_config_utils" -version = "0.7.3" +version = "0.8.0" dependencies = [ "base64 0.21.7", "serde", @@ -508,7 +508,7 @@ dependencies = [ [[package]] name = "cosmian_http_client" -version = "0.7.3" +version = "0.8.0" dependencies = [ "actix-http", "actix-identity", @@ -518,7 +518,7 @@ dependencies = [ "cosmian_logger", "derive_more", "oauth2", - "reqwest", + "reqwest 0.12.24", "serde", "serde_json", "thiserror 2.0.17", @@ -530,12 +530,12 @@ dependencies = [ [[package]] name = "cosmian_logger" -version = "0.7.3" +version = "0.8.0" dependencies = [ "opentelemetry", + "opentelemetry-appender-tracing", "opentelemetry-otlp", "opentelemetry-semantic-conventions", - "opentelemetry-stdout", "opentelemetry_sdk", "syslog-tracing", "thiserror 2.0.17", @@ -890,12 +890,6 @@ dependencies = [ "polyval", ] -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - [[package]] name = "h2" version = "0.3.27" @@ -908,7 +902,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.12.1", + "indexmap", "slab", "tokio", "tokio-util", @@ -927,19 +921,13 @@ dependencies = [ "futures-core", "futures-sink", "http 1.4.0", - "indexmap 2.12.1", + "indexmap", "slab", "tokio", "tokio-util", "tracing", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.16.1" @@ -1245,16 +1233,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8a5a9a0ff0086c7a148acb942baaabeadf9504d10400b5a05645853729b9cd2" -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.12.1" @@ -1262,7 +1240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown", ] [[package]] @@ -1280,16 +1258,6 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "itertools" version = "0.14.0" @@ -1453,7 +1421,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1482,7 +1450,7 @@ dependencies = [ "getrandom 0.2.16", "http 1.4.0", "rand 0.8.5", - "reqwest", + "reqwest 0.12.24", "serde", "serde_json", "serde_path_to_error", @@ -1549,9 +1517,9 @@ dependencies = [ [[package]] name = "opentelemetry" -version = "0.29.1" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e87237e2775f74896f9ad219d26a2081751187eb7c9f5c58dde20a23b95d16c" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" dependencies = [ "futures-core", "futures-sink", @@ -1561,86 +1529,85 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-appender-tracing" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0080f0dc1d7c786f467cd85a4e395fcab11ee852004f39a29a18ab7c25d837" +dependencies = [ + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", +] + [[package]] name = "opentelemetry-http" -version = "0.29.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46d7ab32b827b5b495bd90fa95a6cb65ccc293555dcc3199ae2937d2d237c8ed" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" dependencies = [ "async-trait", "bytes", "http 1.4.0", "opentelemetry", - "reqwest", - "tracing", + "reqwest 0.13.4", ] [[package]] name = "opentelemetry-otlp" -version = "0.29.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d899720fe06916ccba71c01d04ecd77312734e2de3467fd30d9d580c8ce85656" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ - "futures-core", "http 1.4.0", "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest", + "reqwest 0.13.4", "thiserror 2.0.17", "tokio", "tonic", - "tracing", + "tonic-types", ] [[package]] name = "opentelemetry-proto" -version = "0.29.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c40da242381435e18570d5b9d50aca2a4f4f4d8e146231adb4e7768023309b3" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ "opentelemetry", "opentelemetry_sdk", "prost", "tonic", + "tonic-prost", ] [[package]] name = "opentelemetry-semantic-conventions" -version = "0.29.0" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b29a9f89f1a954936d5aa92f19b2feec3c8f3971d3e96206640db7f9706ae3" - -[[package]] -name = "opentelemetry-stdout" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7e27d446dabd68610ef0b77d07b102ecde827a4596ea9c01a4d3811e945b286" -dependencies = [ - "chrono", - "futures-util", - "opentelemetry", - "opentelemetry_sdk", -] +checksum = "6ca2f98a0437b427b4b08f19f1caa3c44db885a202bc12cfea13d6c702243d68" [[package]] name = "opentelemetry_sdk" -version = "0.29.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afdefb21d1d47394abc1ba6c57363ab141be19e27cc70d0e422b7f303e4d290b" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" dependencies = [ "futures-channel", "futures-executor", "futures-util", - "glob", "opentelemetry", "percent-encoding", + "portable-atomic", "rand 0.9.2", - "serde_json", "thiserror 2.0.17", - "tracing", + "tokio", + "tokio-stream", ] [[package]] @@ -1731,6 +1698,12 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1766,9 +1739,9 @@ dependencies = [ [[package]] name = "prost" -version = "0.13.5" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" dependencies = [ "bytes", "prost-derive", @@ -1776,9 +1749,9 @@ dependencies = [ [[package]] name = "prost-derive" -version = "0.13.5" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" dependencies = [ "anyhow", "itertools", @@ -1787,6 +1760,15 @@ dependencies = [ "syn", ] +[[package]] +name = "prost-types" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + [[package]] name = "quote" version = "1.0.42" @@ -1913,9 +1895,7 @@ checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" dependencies = [ "base64 0.22.1", "bytes", - "futures-channel", "futures-core", - "futures-util", "h2 0.4.12", "http 1.4.0", "http-body", @@ -1936,7 +1916,38 @@ dependencies = [ "sync_wrapper", "tokio", "tokio-native-tls", - "tower 0.5.2", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower", "tower-http", "tower-service", "url", @@ -2537,7 +2548,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.12.1", + "indexmap", "serde", "serde_spanned", "toml_datetime", @@ -2553,9 +2564,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "tonic" -version = "0.12.3" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" dependencies = [ "async-trait", "base64 0.22.1", @@ -2568,33 +2579,35 @@ dependencies = [ "hyper-util", "percent-encoding", "pin-project", - "prost", + "sync_wrapper", "tokio", "tokio-stream", - "tower 0.4.13", + "tower", "tower-layer", "tower-service", "tracing", ] [[package]] -name = "tower" -version = "0.4.13" +name = "tonic-prost" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ - "futures-core", - "futures-util", - "indexmap 1.9.3", - "pin-project", - "pin-project-lite", - "rand 0.8.5", - "slab", - "tokio", - "tokio-util", - "tower-layer", - "tower-service", - "tracing", + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", ] [[package]] @@ -2605,29 +2618,33 @@ checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] name = "tower-http" -version = "0.6.7" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" dependencies = [ "bitflags", "bytes", "futures-util", "http 1.4.0", "http-body", - "iri-string", "pin-project-lite", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", + "url", ] [[package]] @@ -2700,14 +2717,12 @@ dependencies = [ [[package]] name = "tracing-opentelemetry" -version = "0.30.0" +version = "0.33.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd8e764bd6f5813fd8bebc3117875190c5b0415be8f7f8059bffb6ecd979c444" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", - "once_cell", "opentelemetry", - "opentelemetry_sdk", "smallvec", "tracing", "tracing-core", diff --git a/Cargo.toml b/Cargo.toml index 6d5709b..69de925 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ members = ["crate/config_utils", "crate/logger", "crate/http_client"] resolver = "2" [workspace.package] -version = "0.7.3" +version = "0.8.0" edition = "2021" rust-version = "1.71.0" authors = [ diff --git a/crate/logger/Cargo.toml b/crate/logger/Cargo.toml index 770f444..23aa68e 100644 --- a/crate/logger/Cargo.toml +++ b/crate/logger/Cargo.toml @@ -1,60 +1,37 @@ [package] name = "cosmian_logger" +description = "Shared tracing and OTLP initialization for Rust services" version.workspace = true -authors.workspace = true edition.workspace = true -license.workspace = true -repository.workspace = true rust-version.workspace = true -description = "Logger helper" - -[[example]] -name = "tracing_example" -required-features = ["full"] - -[features] -default = [] -full = [ - "opentelemetry-otlp", - "opentelemetry-semantic-conventions", - "opentelemetry-stdout", - "opentelemetry_sdk", - "tracing-opentelemetry", - "syslog-tracing", -] +license.workspace = true [dependencies] -opentelemetry = { version = "0.29", features = ["trace", "metrics"] } -opentelemetry-otlp = { version = "0.29", features = [ - "trace", - "metrics", - "integration-testing", - "grpc-tonic", -], optional = true } -opentelemetry-semantic-conventions = { version = "0.29", features = [ - "semconv_experimental", -], optional = true } -opentelemetry-stdout = { version = "0.29", features = [ - "trace", - "metrics", -], optional = true } -opentelemetry_sdk = { version = "0.29", default-features = false, features = [ - "trace", -], optional = true } -syslog-tracing = { version = "0.3", optional = true } +opentelemetry = "0.32" +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.30", optional = true } +tracing-appender = { workspace = true } +tracing-opentelemetry = { version = "0.33" } +tokio = { version = "1", optional = true, features = ["rt-multi-thread", "macros"] } tracing-subscriber = { workspace = true, features = [ - "env-filter", - "fmt", - "ansi", + "ansi", + "env-filter", + "fmt", + "std" ] } -# tracing-appender uses the `symlink` crate for file rotation which does not compile on -# wasm32-unknown-unknown. Gate it to native targets only. -[target.'cfg(not(target_arch = "wasm32"))'.dependencies] -tracing-appender = { workspace = true } - -[dev-dependencies] -tokio = { version = "1.47", features = ["rt-multi-thread", "macros"] } +[features] +full = ["dep:tokio"] diff --git a/crate/logger/README.md b/crate/logger/README.md index 4651de4..5044301 100644 --- a/crate/logger/README.md +++ b/crate/logger/README.md @@ -1,223 +1,252 @@ -# Cosmian Logger +# shared_logging -A flexible logging crate that supports both synchronous and asynchronous environments. +## Why -## Features +Velo platform services export three observability signals — **logs**, **traces**, and **metrics** — to +Grafana Alloy over OTLP/gRPC. Without a shared, consistent initialisation path, each service would +re-implement the same boilerplate and risk diverging from the platform's +Alloy → Loki / Tempo / Prometheus pipeline. -- `full`: Enables complete functionality including OpenTelemetry integration, syslog support, and advanced tracing features -- Without `full`: Provides basic tracing functionality for synchronous applications +## What -⚠️ **Important**: If you need `TelemetryConfig` or OpenTelemetry functionality, you must enable the `full` feature: +`shared_logging` provides two initialisation paths and a set of logging macros: -```toml -[dependencies] -cosmian_logger = { version = "X.Y.Z", features = ["full"] } -``` - -## Usage +| API | Use case | +|-----|----------| +| [`tracing_init`] | Full-featured init driven by a `TracingConfig` struct: stdout, syslog, rolling files, and OTLP. Used by long-running services with rich operator configuration. | +| [`init_tracing`] | Lightweight init driven by two environment variables (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`). Used by minimal daemons and jobs. | +| `info!` / `debug!` / `warn!` / `error!` / `trace!` | Drop-in replacements for the `tracing` macros that automatically prefix each message with the calling function name. | -### With Full Features +### Signal pipeline -For applications that need OpenTelemetry and advanced features: +| Signal | SDK pipeline | Destination | +|--------|-------------|-------------| +| **Logs** | `opentelemetry-appender-tracing` bridges `tracing` events → `SdkLoggerProvider` → OTLP/gRPC `LogExporter` | Alloy → Loki | +| **Traces** | `tracing-opentelemetry` bridges `tracing` spans → `SdkTracerProvider` → OTLP/gRPC `SpanExporter` | Alloy → Tempo | +| **Metrics** | `SdkMeterProvider` with a 30-second `PeriodicReader` → OTLP/gRPC `MetricExporter` | Alloy → Prometheus | -```toml -[dependencies] -cosmian_logger = { version = "X.Y.Z", features = ["full"] } -``` +When `OTEL_EXPORTER_OTLP_ENDPOINT` is **not** set all three pipelines are skipped and +output falls back to structured console logging with no network traffic. -```rust -use cosmian_logger::{tracing_init, TelemetryConfig, TracingConfig}; +## How -#[tokio::main] -async fn main() { - let config = TracingConfig { - service_name: "my-service".to_string(), - otlp: Some(TelemetryConfig { - version: Some("1.0.0".to_string()), - environment: Some("production".to_string()), - otlp_url: "http://localhost:4317".to_string(), - enable_metering: true, - }), - no_log_to_stdout: false, - with_ansi_colors: true, - ..Default::default() - }; +### `tracing_init` layer composition - let _guard = tracing_init(&config); +```text +TracingConfig +│ +├─ stdout_layer (unless no_log_to_stdout) +├─ file_layer (when log_to_file is set — daily rolling) +├─ syslog_layer (when log_to_syslog, Unix only) +└─ otel_layer (when otlp is set) + ├─ SdkTracerProvider → SpanExporter ──► Tempo + └─ SdkMeterProvider → MetricExporter ──► Prometheus (when enable_metering) +``` - tracing::info!("Application started"); -} +### `init_tracing` layer composition + +```text +OTEL_EXPORTER_OTLP_ENDPOINT set? +│ +├─ YES ──► stdout layer +│ tracing-opentelemetry layer → SdkTracerProvider → SpanExporter ──► Tempo +│ OpenTelemetryTracingBridge → SdkLoggerProvider → LogExporter ──► Loki +│ SdkMeterProvider (global) → MetricExporter (30 s) ──► Prometheus +│ +└─ NO ──► stdout layer only ``` -### Without Full Features (Basic Mode) +All OTLP exporters share the same gRPC endpoint and `service.name` resource attribute so +every signal from a service is correlated in Grafana. -For synchronous applications that only need basic logging: +## Using ```toml [dependencies] -cosmian_logger = "X.Y.Z" +shared_logging = { workspace = true } ``` +### Full-featured init (`tracing_init`) + ```rust -use cosmian_logger::{tracing_init, TracingConfig}; +use shared_logging::{TelemetryConfig, TracingConfig, info, tracing_init}; fn main() { - let config = TracingConfig { - service_name: "my-sync-service".to_string(), - no_log_to_stdout: false, + let _guards = tracing_init(&TracingConfig { + service_name: "my-service".into(), + otlp: Some(TelemetryConfig { + otlp_url: "http://alloy.observability.svc:4317".into(), + version: option_env!("CARGO_PKG_VERSION").map(String::from), + environment: Some("production".into()), + enable_metering: true, + }), + rust_log: Some("info,my_crate=debug".into()), with_ansi_colors: true, - // Note: otlp field is not available without full feature ..Default::default() - }; - - let _guard = tracing_init(&config); + }); - tracing::info!("Synchronous application started"); + info!("service started"); // prefixed with function name in message } ``` -## Logging Macros +`LoggingGuards` is dropped at the end of `main` — no explicit shutdown needed. -The crate provides logging macros that work with or without the full feature: +### ANSI colours -```rust -use cosmian_logger::{info, debug, warn, error, trace}; +Coloured output is controlled by `with_ansi_colors` in `TracingConfig`. It applies only to the +**stdout layer** — the file and syslog layers always disable ANSI codes. -// Function name is automatically prefixed to log messages -info!("Application initialized"); -debug!(user_id = 123, "Processing user request"); -warn!("Low memory warning"); -error!(error = %err, "Operation failed"); +```rust +tracing_init(&TracingConfig { + service_name: "my-service".into(), + with_ansi_colors: true, // enable colours in stdout (default: false) + ..Default::default() +}); ``` -## Features Summary - -- **Basic logging**: stdout, file, and structured logging support -- **OpenTelemetry** (requires full feature): OTLP tracing and metrics -- **Syslog support** (requires full feature): System log integration -- **Structured logging**: Multiple message patterns supported -- **ANSI colors**: Configurable for interactive vs persistent outputs +Tip: set `with_ansi_colors: true` in development and `false` in production / when stdout is +piped to a log collector that does not interpret ANSI sequences. -A versatile logging and tracing utility for Rust applications that provides: +### Syslog (Unix only) -- Structured logging to stdout -- Syslog integration -- OpenTelemetry support for distributed tracing -- Runtime configuration options +Setting `log_to_syslog: true` adds a syslog layer alongside any other layers that are active. +Logs are forwarded to the system syslog daemon using `Facility::User` and the +`service_name` as the syslog identity. The layer is compiled out on Windows. -## Installation +```rust +tracing_init(&TracingConfig { + service_name: "my-service".into(), + #[cfg(not(target_os = "windows"))] + log_to_syslog: true, // writes to syslog(3) via syslog-tracing + no_log_to_stdout: false, // stdout and syslog are independent; both can be on + ..Default::default() +}); +``` -Add the dependency to your `Cargo.toml`: +You can verify syslog output on Linux with: -```toml -[dependencies] -cosmian_logger = { path = "../path/to/crate/logger" } +```sh +journalctl -t my-service -f +# or +tail -f /var/log/syslog | grep my-service ``` -## Basic Usage +On macOS: + +```sh +log stream --predicate 'senderImagePath contains "my-service"' +``` -For simple applications, use the `log_init` function to set up logging: +### Lightweight env-var init (`init_tracing`) ```rust -use cosmian_logger::log_init; -use tracing::{debug, info}; +use shared_logging::init_tracing; +use tracing::{info, warn}; -fn main() { - // Initialize with custom log level - log_init(Some("debug")); +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let telemetry = init_tracing("my-service")?; + + info!(version = "1.0", "service started"); + warn!(code = 42, "something noteworthy"); - info!("This is an info message"); - debug!("This is a debug message"); + telemetry.shutdown(); // flush all OTLP exporters before exit + Ok(()) } ``` -The `log_init` function accepts an optional log level string parameter: - -- When `None` is provided, it falls back to the `RUST_LOG` environment variable -- Log levels follow Rust's standard: trace, debug, info, warn, error +### Logging macros -## Advanced Configuration with OpenTelemetry +`shared_logging` exports `info!`, `debug!`, `warn!`, `error!`, and `trace!` macros that +wrap the standard `tracing` macros and automatically prepend `[function_name]` to every +message, making it easy to locate the call site in log aggregators: -For more advanced use cases with OpenTelemetry integration, enable the `full` feature: +```rust +use shared_logging::info; -```toml -[dependencies] -cosmian_logger = { version = "X.Y.Z", features = ["full"] } +fn process_request(id: u64) { + info!("processing request {id}"); + // emits: INFO [process_request] processing request 42 +} ``` +### Recording metrics + ```rust -use cosmian_logger::{tracing_init, TelemetryConfig, TracingConfig}; -use tracing::span; -use tracing_core::Level; +use opentelemetry::metrics::Counter; +use shared_logging::init_tracing; #[tokio::main] -async fn main() { - let config = TracingConfig { - service_name: "my_service".to_string(), - otlp: Some(TelemetryConfig { - version: Some("1.0.0".to_string()), - environment: Some("development".to_string()), - otlp_url: "http://localhost:4317".to_string(), - enable_metering: true, - }), - no_log_to_stdout: false, - #[cfg(not(target_os = "windows"))] - log_to_syslog: true, - rust_log: Some("debug".to_string()), - ..Default::default() - }; +async fn main() -> anyhow::Result<()> { + let telemetry = init_tracing("my-service")?; + let meter = telemetry.meter(env!("CARGO_PKG_NAME")); + + let requests: Counter = meter + .u64_counter("requests_total") + .with_description("Total number of requests processed") + .build(); + + requests.add(1, &[]); + telemetry.shutdown(); + Ok(()) +} +``` - let _otel_guard = tracing_init(&config); +### Distributed tracing with `#[instrument]` - // Create and enter a span for better tracing context - let span = span!(Level::TRACE, "application"); - let _span_guard = span.enter(); +```rust +use tracing::instrument; - // Your application code here - tracing::info!("Application started"); +#[instrument(fields(user_id = %user_id))] +async fn handle_request(user_id: u64) -> anyhow::Result<()> { + tracing::info!("handling request"); + Ok(()) } ``` -## OpenTelemetry Setup - -To use OpenTelemetry, start a collector like Jaeger: +### Environment variables -```bash -docker run -p16686:16686 -p4317:4317 -p4318:4318 \ --e COLLECTOR_OTLP_ENABLED=true -e LOG_LEVEL=debug \ -jaegertracing/jaeger:2.5.0 -``` +| Variable | Default | Description | +|---|---|---| +| `OTEL_EXPORTER_OTLP_ENDPOINT` | *(absent — console only)* | gRPC endpoint for all three OTLP signals, e.g. `http://obs-stack-alloy.observability.svc.cluster.local:4317` | +| `OTEL_SERVICE_NAME` | value passed to `init_tracing` | `service.name` resource attribute stamped on every signal | +| `RUST_LOG` | `info` | `tracing_subscriber` log filter, e.g. `debug,hyper=warn` | -Then access the Jaeger UI at `http://localhost:16686` +### Kubernetes manifest -## Configuration Options +```yaml +env: + - name: OTEL_EXPORTER_OTLP_ENDPOINT + value: "http://obs-stack-alloy.observability.svc.cluster.local:4317" + - name: OTEL_SERVICE_NAME + value: "my-service" + - name: RUST_LOG + value: "info" +``` -The `TracingConfig` struct supports: +## Building -- `service_name`: Name of your service for tracing -- `otlp`: OpenTelemetry configuration (only available with `full` feature) -- `no_log_to_stdout`: Disable logging to stdout -- `log_to_syslog`: Enable logging to system log (only available with `full` feature) -- `rust_log`: Log level configuration -- `with_ansi_colors`: Enable ANSI colors in output -- `log_to_file`: Optional file logging configuration +```sh +# From workspace root +cargo build -p shared_logging +cargo build -p shared_logging --release +``` -## In Tests +## Testing -The `log_init` function is safe to use in tests: +```sh +# Unit tests (no network required) +cargo test -p shared_logging -```rust -#[test] -fn test_something() { - cosmian_logger::log_init(Some("debug")); - // Your test code -} +# Smoke test against a live collector +OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \ +OTEL_SERVICE_NAME=test-service \ +RUST_LOG=debug \ + cargo test -p shared_logging -- --nocapture ``` -## Re-exports - -The logger crate re-exports common tracing utilities: +To port-forward the Velo platform's Alloy instance: -```rust -use cosmian_logger::reexport::{tracing, tracing_subscriber}; +```sh +kubectl port-forward -n observability svc/obs-stack-alloy 4317:4317 ``` diff --git a/crate/logger/src/error.rs b/crate/logger/src/error.rs index 8e8a314..be09938 100644 --- a/crate/logger/src/error.rs +++ b/crate/logger/src/error.rs @@ -1,41 +1,40 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum LoggerError { - #[error("OTLP error: {0}")] - Otlp(String), - - #[error("Parsing error: {0}")] - Parsing(String), - - #[error("Tracing subscriber error: {0}")] - TracingSubscriber(String), - - #[error("IO error: {0}")] - IOError(String), -} - -#[cfg(feature = "full")] -impl From for LoggerError { - fn from(e: opentelemetry_otlp::ExporterBuildError) -> Self { - Self::Otlp(e.to_string()) - } +/// Errors that can occur while initialising or configuring the logging stack. +#[derive(Debug, thiserror::Error)] +pub enum LoggingError { + #[error("failed to build OTLP span exporter: {0}")] + SpanExporter(#[source] opentelemetry_otlp::ExporterBuildError), + + #[error("failed to build OTLP log exporter: {0}")] + LogExporter(#[source] opentelemetry_otlp::ExporterBuildError), + + #[error("failed to build OTLP metric exporter: {0}")] + MetricExporter(#[source] opentelemetry_otlp::ExporterBuildError), + + #[error("failed to initialise tracing subscriber: {0}")] + SubscriberInit(#[source] tracing_subscriber::util::TryInitError), + + #[error("{context}: {source}")] + WithContext { + context: &'static str, + #[source] + source: Box, + }, } -impl From for LoggerError { - fn from(e: tracing_subscriber::filter::ParseError) -> Self { - Self::Parsing(e.to_string()) - } -} +/// Result type for the logging stack initialisation. +pub type LoggingResult = Result; -impl From for LoggerError { - fn from(value: tracing_subscriber::util::TryInitError) -> Self { - Self::TracingSubscriber(value.to_string()) - } +/// Extension trait that adds `.context(msg)` to [`LoggingResult`], mirroring +/// the ergonomics of `anyhow::Context`. +pub trait ResultExt { + fn context(self, ctx: &'static str) -> LoggingResult; } -impl From for LoggerError { - fn from(e: std::ffi::NulError) -> Self { - Self::Parsing(e.to_string()) +impl ResultExt for LoggingResult { + fn context(self, ctx: &'static str) -> LoggingResult { + self.map_err(|source| LoggingError::WithContext { + context: ctx, + source: Box::new(source), + }) } } diff --git a/crate/logger/src/lib.rs b/crate/logger/src/lib.rs index e2fe983..167a118 100644 --- a/crate/logger/src/lib.rs +++ b/crate/logger/src/lib.rs @@ -1,48 +1,305 @@ -//! # Cosmian Logger -//! -//! A flexible logging crate that supports both synchronous and asynchronous -//! environments. -//! -//! ## Features -//! -//! - `full`: Enables complete functionality including tokio/async support, -//! OpenTelemetry integration, and syslog support -//! - Without `full`: Provides basic tracing functionality for synchronous -//! applications -//! -//! ## Important Note -//! -//! If you need `TelemetryConfig` or full OpenTelemetry functionality, you must -//! enable the `full` feature: -//! -//! ```toml -//! [dependencies] -//! cosmian_logger = { version = "X.Y.Z", features = ["full"] } -//! ``` -//! -//! If you get an error like "no `TelemetryConfig` in the root", it means you -//! need to enable the full feature in your Cargo.toml dependency declaration. -mod error; -mod log_utils; +pub mod error; mod macros; -#[cfg(feature = "full")] -mod otlp; -mod tracing; - -pub use error::LoggerError; -pub use log_utils::log_init; -#[cfg(feature = "full")] -pub use tracing::tracing_init_from_env; -#[cfg(feature = "full")] -pub use tracing::TelemetryConfig; -pub use tracing::{tracing_init, LoggingGuards, TracingConfig}; - -/// Re-exported dependencies for use with the logging macros -/// -/// The logging macros (info!, debug!, warn!, error!, trace!) use these -/// re-exported tracing modules internally, so external crates don't need to add -/// tracing as a direct dependency. +mod tests; +pub mod types; + +use std::sync::atomic::{AtomicBool, Ordering}; + +pub use error::{LoggingError, LoggingResult, ResultExt}; +use opentelemetry::trace::TracerProvider; +use opentelemetry_appender_tracing::layer::OpenTelemetryTracingBridge; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::{ + logs::SdkLoggerProvider, + metrics::{PeriodicReader, SdkMeterProvider}, + trace::{RandomIdGenerator, Sampler, SdkTracerProvider}, + Resource, +}; +use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; +pub use types::{LoggingGuards, TelemetryConfig, TelemetryGuards, TracingConfig}; + +/// Re-exports for downstream crates (used by the logging macros). pub mod reexport { pub use tracing; pub use tracing_subscriber; } + +// ── private helpers ────────────────────────────────────────────────────────── + +static INITIALIZED: AtomicBool = AtomicBool::new(false); + +fn build_resource_simple(service_name: &str) -> Resource { + Resource::builder_empty() + .with_attributes([opentelemetry::KeyValue::new( + opentelemetry_semantic_conventions::resource::SERVICE_NAME, + service_name.to_owned(), + )]) + .build() +} + +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( + SERVICE_NAME, + service_name.to_owned(), + )]; + if let Some(v) = &config.version { + kvs.push(opentelemetry::KeyValue::new(SERVICE_VERSION, v.clone())); + } + if let Some(env) = &config.environment { + kvs.push(opentelemetry::KeyValue::new( + "deployment.environment.name", + env.clone(), + )); + } + Resource::builder_empty().with_attributes(kvs).build() +} + +fn new_tracer_provider(endpoint: &str, resource: Resource) -> LoggingResult { + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(endpoint) + .with_timeout(std::time::Duration::from_secs(3)) + .build() + .map_err(LoggingError::SpanExporter)?; + + Ok(SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_sampler(Sampler::AlwaysOn) + .with_id_generator(RandomIdGenerator::default()) + .with_resource(resource) + .build()) +} + +fn new_logger_provider(endpoint: &str, resource: Resource) -> LoggingResult { + let exporter = opentelemetry_otlp::LogExporter::builder() + .with_tonic() + .with_endpoint(endpoint) + .with_timeout(std::time::Duration::from_secs(3)) + .build() + .map_err(LoggingError::LogExporter)?; + + Ok(SdkLoggerProvider::builder() + .with_batch_exporter(exporter) + .with_resource(resource) + .build()) +} + +fn new_meter_provider(endpoint: &str, resource: Resource) -> LoggingResult { + let exporter = opentelemetry_otlp::MetricExporter::builder() + .with_tonic() + .with_endpoint(endpoint) + .build() + .map_err(LoggingError::MetricExporter)?; + + let reader = PeriodicReader::builder(exporter) + .with_interval(std::time::Duration::from_secs(30)) + .build(); + + Ok(SdkMeterProvider::builder() + .with_reader(reader) + .with_resource(resource) + .build()) +} + +// ── public API ─────────────────────────────────────────────────────────────── + +/// Initialise the global tracing subscriber from a [`TracingConfig`]. +/// +/// Safe to call more than once — subsequent calls are no-ops and return empty +/// guards. +pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { + if INITIALIZED.swap(true, Ordering::SeqCst) { + return LoggingGuards::default(); + } + + // NOTE: Avoid mutating process-wide environment variables here + // (std::env::set_var is `unsafe`). If `rust_log` is provided, apply it + // directly when building the `EnvFilter` below. + + let mut guards = LoggingGuards::default(); + + // --- stdout layer --- + let stdout_layer = if config.no_log_to_stdout { + None + } else { + Some( + tracing_subscriber::fmt::layer() + .with_level(true) + .with_target(true) + .with_thread_ids(true) + .with_line_number(true) + .with_file(true) + .with_ansi(config.with_ansi_colors) + .compact(), + ) + }; + + // --- rolling file layer --- + let file_layer = config.log_to_file.as_ref().map(|(dir, name)| { + 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 + }; + + // --- OTLP layer --- + // --- OTLP layer --- + let otel_layer = config.otlp.as_ref().and_then(|telemetry| { + let resource = build_resource_with_config(&config.service_name, telemetry); + + 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}"), + } + } + + Some(tracing_opentelemetry::layer().with_tracer(tracer)) + }); + + let filter = config + .rust_log + .as_deref() + .map(EnvFilter::new) + .unwrap_or_else(EnvFilter::from_default_env); + + 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); + + 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}"); + } + + guards +} + +/// Convenience initialiser for tests and simple CLIs (stdout only). +pub fn log_init(rust_log: Option<&str>) { + let _guards = tracing_init(&TracingConfig { + rust_log: rust_log.map(String::from), + with_ansi_colors: true, + ..Default::default() + }); +} + +/// Initialise structured logging, OTLP tracing, OTLP logs, and OTLP metrics. +/// +/// Reads two environment variables at start-up: +/// +/// | Variable | Effect | +/// |---|---| +/// | `OTEL_EXPORTER_OTLP_ENDPOINT` | Enables all three OTLP signals (gRPC). Falls back to console-only when absent. | +/// | `OTEL_SERVICE_NAME` | Overrides `default_service_name`. | +/// +/// Returns [`TelemetryGuards`] that must be kept alive for the entire process +/// lifetime. Call [`TelemetryGuards::shutdown`] before exit to flush buffers. +pub fn init_tracing(default_service_name: &str) -> LoggingResult { + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + + let stdout_layer = tracing_subscriber::fmt::layer() + .with_level(true) + .with_target(true) + .compact(); + + if let Ok(endpoint) = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") { + let service_name = + std::env::var("OTEL_SERVICE_NAME").unwrap_or_else(|_| default_service_name.to_string()); + let resource = build_resource_simple(&service_name); + + // ── traces ────────────────────────────────────────────────────────── + let tracer_provider = new_tracer_provider(&endpoint, resource.clone())?; + opentelemetry::global::set_tracer_provider(tracer_provider.clone()); + let tracer = tracer_provider.tracer(service_name.clone()); + + // ── logs ───────────────────────────────────────────────────────────── + let logger_provider = new_logger_provider(&endpoint, resource.clone())?; + let otel_log_layer = OpenTelemetryTracingBridge::new(&logger_provider); + + // ── metrics ────────────────────────────────────────────────────────── + let meter_provider = new_meter_provider(&endpoint, resource)?; + opentelemetry::global::set_meter_provider(meter_provider.clone()); + + tracing_subscriber::registry() + .with(filter) + .with(stdout_layer) + .with(tracing_opentelemetry::layer().with_tracer(tracer)) + .with(otel_log_layer) + .try_init() + .map_err(LoggingError::SubscriberInit) + .context("failed to initialize tracing subscriber with OTLP")?; + + tracing::info!( + endpoint = %endpoint, + service_name = %service_name, + "OTLP telemetry enabled (traces + logs + metrics)" + ); + + Ok(TelemetryGuards { + tracer_provider: Some(tracer_provider), + logger_provider: Some(logger_provider), + meter_provider: Some(meter_provider), + }) + } else { + tracing_subscriber::registry() + .with(filter) + .with(stdout_layer) + .try_init() + .map_err(LoggingError::SubscriberInit) + .context("failed to initialize tracing subscriber")?; + + tracing::info!("OTLP telemetry disabled; using console logs only"); + Ok(TelemetryGuards::default()) + } +} diff --git a/crate/logger/src/log_utils.rs b/crate/logger/src/log_utils.rs deleted file mode 100644 index 7f61a97..0000000 --- a/crate/logger/src/log_utils.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::{tracing_init, TracingConfig}; - -/// Initializing the stdout logger only. -/// (no open telemetry nor syslog) -/// -/// # Arguments -/// * `rust_log` - The log string to set for `RUST_LOG` -/// -/// # Notes -/// - calling `log_init(None`) is equivalent to calling -/// `log_init(option_env!("RUST_LOG"))` -/// - this function can be called from a `[tokio::test]` function, in contrast -/// to `tracing_init` -pub fn log_init(rust_log: Option<&str>) { - let config = TracingConfig { - #[cfg(feature = "full")] - otlp: None, - service_name: String::new(), - no_log_to_stdout: false, - log_to_file: None, - #[cfg(not(target_os = "windows"))] - log_to_syslog: false, - rust_log: rust_log - .or(option_env!("RUST_LOG")) - .map(std::borrow::ToOwned::to_owned), - with_ansi_colors: false, - }; - tracing_init(&config); -} - -#[cfg(test)] -mod tests { - use tracing::{debug, info, trace}; - - use super::*; - - #[test] - fn test_log_init() { - log_init(Some("debug")); - info!("This is an INFO test log message"); - debug!("This is a DEBUG test log message"); - debug!("RUST_LOG: {:?}", std::env::var("RUST_LOG")); - // The next message is a TRACING level and should be ignored - trace!("This is a TRACE test log message"); - } -} diff --git a/crate/logger/src/macros.rs b/crate/logger/src/macros.rs index 0fff3bf..4756ef5 100644 --- a/crate/logger/src/macros.rs +++ b/crate/logger/src/macros.rs @@ -1,256 +1,60 @@ -/// Helper macro to extract function name from the call site +//! Logging macros that prefix each message with the calling function name. + +/// Helper: extract the current function name at compile time. +#[doc(hidden)] #[macro_export] -macro_rules! __get_fn_name { +macro_rules! __fn_name { () => {{ - let type_name = std::any::type_name_of_val(&|| {}); - let parts: Vec<&str> = type_name.split("::").collect(); - // Find the last element that is not "{{closure}}" - let raw = parts - .iter() - .rev() - .find(|&&part| part != "{{closure}}") - .unwrap_or(&"unknown"); - // Strip lifetime/generic parameters (e.g. `foo<'_, '_, '_>` → `foo`) - match raw.find('<') { - Some(idx) => raw[..idx].to_string(), - None => raw.to_string(), + // `type_name_of_val` on a nested closure yields a path ending in + // `::{{closure}}`. We strip that suffix and extract the last segment. + fn _f() {} + fn _strip(name: &str) -> &str { + // Remove `::_f` suffix + let trimmed = match name.rfind("::") { + Some(pos) => &name[..pos], + None => name, + }; + // Take last segment + match trimmed.rfind("::") { + Some(pos) => &trimmed[pos + 2..], + None => trimmed, + } } + _strip(std::any::type_name_of_val(&_f)) }}; } -/// Macro to automatically add function name as prefix to info logs -/// Supports both simple format strings and structured logging with key-value -/// pairs #[macro_export] macro_rules! info { - (target: $target:expr, $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::info!(target: $target, "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - (target: $target:expr, $($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::info!(target: $target, $($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - ($($field:ident = $value:expr),+ $(,)?; $($rest:tt)*) => { - $crate::reexport::tracing::info!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($($rest)*)) - }; - ($($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::info!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; ($($arg:tt)*) => { - $crate::reexport::tracing::info!("[{}] {}", $crate::__get_fn_name!(), format!($($arg)*)) + $crate::reexport::tracing::info!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) }; } -/// Macro to automatically add function name as prefix to debug logs -/// Supports both simple format strings and structured logging with key-value -/// pairs #[macro_export] macro_rules! debug { - (target: $target:expr, $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::debug!(target: $target, "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - (target: $target:expr, $($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::debug!(target: $target, $($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - ($($field:ident = $value:expr),+ $(,)?; $($rest:tt)*) => { - $crate::reexport::tracing::debug!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($($rest)*)) - }; - ($($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::debug!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; ($($arg:tt)*) => { - $crate::reexport::tracing::debug!("[{}] {}", $crate::__get_fn_name!(), format!($($arg)*)) + $crate::reexport::tracing::debug!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) }; } -/// Macro to automatically add function name as prefix to warn logs -/// Supports both simple format strings and structured logging with key-value -/// pairs #[macro_export] macro_rules! warn { - (target: $target:expr, $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::warn!(target: $target, "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - (target: $target:expr, $($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::warn!(target: $target, $($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - ($($field:ident = $value:expr),+ $(,)?; $($rest:tt)*) => { - $crate::reexport::tracing::warn!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($($rest)*)) - }; - ($($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::warn!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; ($($arg:tt)*) => { - $crate::reexport::tracing::warn!("[{}] {}", $crate::__get_fn_name!(), format!($($arg)*)) + $crate::reexport::tracing::warn!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) }; } -/// Macro to automatically add function name as prefix to error logs -/// Supports both simple format strings and structured logging with key-value -/// pairs #[macro_export] macro_rules! error { - (target: $target:expr, $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::error!(target: $target, "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - (target: $target:expr, $($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::error!(target: $target, $($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - ($($field:ident = $value:expr),+ $(,)?; $($rest:tt)*) => { - $crate::reexport::tracing::error!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($($rest)*)) - }; - ($($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::error!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; ($($arg:tt)*) => { - $crate::reexport::tracing::error!("[{}] {}", $crate::__get_fn_name!(), format!($($arg)*)) + $crate::reexport::tracing::error!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) }; } -/// Macro to automatically add function name as prefix to trace logs -/// Supports both simple format strings and structured logging with key-value -/// pairs #[macro_export] macro_rules! trace { - (target: $target:expr, $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::trace!(target: $target, "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - (target: $target:expr, $($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::trace!(target: $target, $($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; - ($($field:ident = $value:expr),+ $(,)?; $($rest:tt)*) => { - $crate::reexport::tracing::trace!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($($rest)*)) - }; - ($($field:ident = $value:expr,)+ $fmt:literal $(, $($args:tt)*)?) => { - $crate::reexport::tracing::trace!($($field = $value,)+ "[{}] {}", $crate::__get_fn_name!(), format!($fmt $(, $($args)*)?)) - }; ($($arg:tt)*) => { - $crate::reexport::tracing::trace!("[{}] {}", $crate::__get_fn_name!(), format!($($arg)*)) + $crate::reexport::tracing::trace!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) }; } - -#[cfg(test)] -mod macro_tests { - use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt}; - - #[derive(Debug)] - enum ObjectType { - Document, - } - - struct TestObject { - obj_type: ObjectType, - } - - impl TestObject { - fn object_type(&self) -> &ObjectType { - &self.obj_type - } - } - - fn init_test_logging() { - let _ = tracing_subscriber::registry().with(fmt::layer()).try_init(); - } - - #[test] - fn test_structured_logging_macros() { - init_test_logging(); - - let uid = "test_uid"; - let owner = "test_owner"; - let object = TestObject { - obj_type: ObjectType::Document, - }; - - // Test the exact pattern from the user's request - info!( - uid = uid, - user = owner, - "Created Object of type {:?}", - &object.object_type() - ); - - // Test variations - debug!( - uid = uid, - user = owner, - "Debug message for object type {:?}", - &object.object_type() - ); - warn!( - user = owner, - "Warning about object type {:?}", - &object.object_type() - ); - error!( - uid = uid, - user = owner, - "Error related to object type {:?}", - &object.object_type() - ); - } - - #[test] - fn test_simple_logging_macros() { - init_test_logging(); - - info!("Simple info message"); - debug!("Simple debug message with arg: {}", 42); - warn!("Simple warning: {}", "test"); - } - - #[test] - fn test_target_with_structured_logging() { - init_test_logging(); - - let user = "test_user"; - - // Mock TTLV structure for testing - struct TestTag { - tag: String, - } - - impl TestTag { - fn as_str(&self) -> &str { - &self.tag - } - } - - struct TestTtlv { - tag: TestTag, - } - - let ttlv = TestTtlv { - tag: TestTag { - tag: "Create".to_string(), - }, - }; - - // Test the exact pattern from the user's request - info!(target: "kmip", user = user, tag = ttlv.tag.as_str(), "POST /kmip/2_1. Request: {:?} {}", ttlv.tag.as_str(), user); - } - - #[test] - fn test_all_macros_with_target() { - init_test_logging(); - - let user = "admin"; - let action = "test_action"; - - // Test all macros with target support - info!(target: "auth", user = user, action = action, "Info: User {} performed {}", user, action); - debug!(target: "auth", user = user, action = action, "Debug: User {} performed {}", user, action); - warn!(target: "auth", user = user, action = action, "Warning: User {} performed {}", user, action); - error!(target: "auth", user = user, action = action, "Error: User {} performed {}", user, action); - trace!(target: "auth", user = user, action = action, "Trace: User {} performed {}", user, action); - } - - #[test] - fn test_target_with_simple_message() { - init_test_logging(); - - let e = "parse error: invalid format"; - - // Test the exact pattern from the user's request - error!(target: "kmip", "Failed to parse RequestMessage: {}", e); - } -} diff --git a/crate/logger/src/otlp.rs b/crate/logger/src/otlp.rs deleted file mode 100644 index fd2ab4c..0000000 --- a/crate/logger/src/otlp.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::time::Duration; - -use opentelemetry::{global, KeyValue}; -use opentelemetry_otlp::WithExportConfig; -use opentelemetry_sdk::{ - metrics::{MeterProviderBuilder, PeriodicReader, SdkMeterProvider}, - trace::{RandomIdGenerator, Sampler, SdkTracerProvider}, - Resource, -}; -use opentelemetry_semantic_conventions::{ - attribute::{DEPLOYMENT_ENVIRONMENT_NAME, SERVICE_NAME, SERVICE_VERSION}, - SCHEMA_URL, -}; - -use crate::LoggerError; - -fn resource(service_name: &str, version: Option, environment: Option) -> Resource { - let mut attributes = vec![KeyValue::new(SERVICE_NAME, service_name.to_owned())]; - if let Some(version) = version { - attributes.push(KeyValue::new(SERVICE_VERSION, version)); - } - if let Some(environment) = environment { - attributes.push(KeyValue::new(DEPLOYMENT_ENVIRONMENT_NAME, environment)); - } - Resource::builder() - .with_service_name(service_name.to_owned()) - .with_schema_url(attributes, SCHEMA_URL) - .build() -} - -/// Internal function to initialize the OTLP tracer -/// that returns a Result with the `SdkTracerProvider` -pub(crate) fn init_tracer_provider( - service_name: &str, - url: &str, - version: Option, - environment: Option, -) -> Result { - let otlp_exporter = opentelemetry_otlp::SpanExporter::builder() - .with_tonic() - .with_endpoint(url.to_owned()) - .with_timeout(Duration::from_secs(3)) - .build() - .map_err(|e| { - LoggerError::Otlp(format!( - "Failed to create OTLP provider exporter. Make sure the endpoint is correct and \ - the server is running: {e}" - )) - })?; - - let tracer_provider = SdkTracerProvider::builder() - .with_batch_exporter(otlp_exporter) - .with_id_generator(RandomIdGenerator::default()) - .with_sampler(Sampler::AlwaysOn) - .with_resource(resource(service_name, version, environment)) - .with_max_events_per_span(64) - .with_max_attributes_per_span(16) - .build(); - - global::set_tracer_provider(tracer_provider.clone()); - - Ok(tracer_provider) -} - -// Construct MeterProvider for MetricsLayer -pub(crate) fn init_meter_provider( - service_name: &str, - url: &str, - version: Option, - environment: Option, -) -> Result { - let exporter = opentelemetry_otlp::MetricExporter::builder() - .with_tonic() - .with_temporality(opentelemetry_sdk::metrics::Temporality::default()) - .with_endpoint(url.to_owned()) - .build() - .map_err(|e| { - LoggerError::Otlp(format!( - "Failed to create OTLP meter exporter. Make sure the endpoint is correct and the \ - server is running: {e}" - )) - })?; - - let reader = PeriodicReader::builder(exporter) - .with_interval(Duration::from_secs(30)) - .build(); - - // For debugging in development - let stdout_reader = - PeriodicReader::builder(opentelemetry_stdout::MetricExporter::default()).build(); - - let meter_provider = MeterProviderBuilder::default() - .with_resource(resource(service_name, version, environment)) - .with_reader(reader) - .with_reader(stdout_reader) - .build(); - - global::set_meter_provider(meter_provider.clone()); - - Ok(meter_provider) -} diff --git a/crate/logger/src/tests.rs b/crate/logger/src/tests.rs new file mode 100644 index 0000000..20fb439 --- /dev/null +++ b/crate/logger/src/tests.rs @@ -0,0 +1,86 @@ +#![cfg(test)] + +use crate::{log_init, tracing_init, TelemetryConfig, TelemetryGuards, TracingConfig}; + +/// tracing_init is idempotent: the second call must not panic or reinitialise. +#[test] +fn init_twice_is_safe() { + let _g1 = tracing_init(&TracingConfig { + service_name: "test".into(), + rust_log: Some("info".into()), + with_ansi_colors: false, + ..Default::default() + }); + // second call must return empty guards without panicking + let _g2 = tracing_init(&TracingConfig::default()); +} + +/// log_init convenience wrapper must not panic when called after tracing_init. +#[test] +fn log_init_is_safe_after_init() { + // May already be initialised by another test — must not panic. + log_init(Some("warn")); +} + +/// TracingConfig defaults: all flags off, no otlp, no file, no syslog. +#[test] +fn tracing_config_default() { + let c = TracingConfig::default(); + assert!(c.service_name.is_empty()); + assert!(c.otlp.is_none()); + assert!(!c.no_log_to_stdout); + assert!(c.log_to_file.is_none()); + assert!(c.rust_log.is_none()); + assert!(!c.with_ansi_colors); + #[cfg(not(target_os = "windows"))] + assert!(!c.log_to_syslog); +} + +/// TelemetryConfig defaults. +#[test] +fn telemetry_config_default() { + let c = TelemetryConfig::default(); + assert!(c.version.is_none()); + assert!(c.environment.is_none()); + assert!(c.otlp_url.is_empty()); + assert!(!c.enable_metering); +} + +/// TelemetryGuards::shutdown on an empty guard must not panic. +#[test] +fn telemetry_guards_shutdown_empty() { + TelemetryGuards::default().shutdown(); +} + +/// TelemetryGuards::meter returns a valid (no-op) meter when OTLP is absent. +#[test] +fn telemetry_guards_meter_noop() { + let g = TelemetryGuards::default(); + // Should not panic, even with no provider installed. + let _m = g.meter("test-scope"); +} + +mod macros { + /// __fn_name! must return a non-empty string containing the function name. + #[test] + fn fn_name_not_empty() { + let name = crate::__fn_name!(); + assert!(!name.is_empty(), "function name must not be empty"); + assert!( + name.contains("fn_name_not_empty"), + "expected 'fn_name_not_empty', 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). + #[test] + fn macros_do_not_panic() { + crate::info!("info message {}", 1); + crate::debug!("debug message {}", 2); + crate::warn!("warn message {}", 3); + crate::error!("error message {}", 4); + crate::trace!("trace message {}", 5); + } +} diff --git a/crate/logger/src/tracing.rs b/crate/logger/src/tracing.rs deleted file mode 100644 index ec32a6c..0000000 --- a/crate/logger/src/tracing.rs +++ /dev/null @@ -1,462 +0,0 @@ -use std::{ - env::set_var, - path::PathBuf, - sync::atomic::{AtomicBool, Ordering}, -}; - -#[cfg(feature = "full")] -use opentelemetry::trace::TracerProvider; -#[cfg(feature = "full")] -use opentelemetry_sdk::{metrics::SdkMeterProvider, trace::SdkTracerProvider}; -#[cfg(feature = "full")] -use tracing::debug; -use tracing::{info, span, warn}; -#[cfg(feature = "full")] -use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer}; -use tracing_subscriber::{layer::SubscriberExt, reload, util::SubscriberInitExt, EnvFilter, Layer}; - -#[cfg(feature = "full")] -use crate::otlp; -use crate::LoggerError; - -static TRACING_SET: AtomicBool = AtomicBool::new(false); - -// ============================================================================ -// Configuration Types -// ============================================================================ - -#[derive(Debug, Default, Clone)] -pub struct TracingConfig { - /// The Name of the service using this config - /// Only used by the OTLP collector and syslog - pub service_name: String, - - /// Use the OpenTelemetry provider - #[cfg(feature = "full")] - pub otlp: Option, - - /// Do not log to stdout - pub no_log_to_stdout: bool, - - #[cfg(not(target_os = "windows"))] - /// log to syslog - pub log_to_syslog: bool, - - /// If set, logs will be written to the specified directory (first argument) - /// using the specified file name (second argument): .YYYY-MM-DD. - /// It is a rolling file appender that creates a new log file every day. - pub log_to_file: Option<(PathBuf, String)>, - - /// Default `RUST_LOG` configuration. - /// If it is not set, the value of the environment variable `RUST_LOG` will - /// be used. - pub rust_log: Option, - - /// If true, the logs to stdout will be written with ANSI colors. - pub with_ansi_colors: bool, -} - -#[cfg(feature = "full")] -#[derive(Debug, Default, Clone)] -pub struct TelemetryConfig { - /// The version of the service using this config - pub version: Option, - - /// The name of the environment - /// (for instance, "production", "staging", "development") - pub environment: Option, - - /// The OTLP collector URL - /// (for instance, ) - pub otlp_url: String, - - /// Tracing is enabled by default. - /// This controls whether metering should also be enabled. - pub enable_metering: bool, -} - -// ============================================================================ -// Logging Guards and Cleanup -// ============================================================================ - -#[derive(Default)] -pub struct LoggingGuards { - #[cfg(feature = "full")] - tracer_provider: Option, - #[cfg(feature = "full")] - meter_provider: Option, - #[cfg(not(target_arch = "wasm32"))] - rolling_appender_guard: Option, -} - -impl Drop for LoggingGuards { - fn drop(&mut self) { - #[cfg(feature = "full")] - { - if let Some(tracer_provider) = &mut self.tracer_provider { - debug!("dropping OTLP tracer"); - if let Err(err) = tracer_provider.shutdown() { - eprintln!("Trace provider shutdown error: {err:?}"); - } - } - if let Some(meter_provider) = &mut self.meter_provider { - debug!("dropping OTLP meter"); - if let Err(_err) = meter_provider.shutdown() { - // ignore the error - } - } - } - } -} - -// ============================================================================ -// Public Interface -// ============================================================================ - -/// Initialize the telemetry system -/// -/// # Usage -/// -/// ```rust-ignore -/// use cosmian_logger::{telemetry_init, TelemetryConfig}; -/// use tracing::span; -/// use tracing_core::Level; -/// -/// #[tokio::main] -/// async fn main() { -/// -/// let tracing = TracingConfig { -/// service_name: "test".to_string(), -/// otlp: Some(TelemetryConfig { -/// version: Some( -/// option_env!("CARGO_PKG_VERSION") -/// .unwrap_or("1.0.0") -/// .to_string(), -/// ), -/// environment: Some("test".to_string()), -/// otlp_url: "http://localhost:4317".to_string(), -/// enable_metering: true, -/// }), -/// no_log_to_stdout: false, -/// #[cfg(not(target_os = "windows"))] -/// log_to_syslog: true, -/// rust_log: Some("trace".to_string()), -/// with_ansi_colors: false, -/// }; -/// let _otel_guard = tracing_init(&tracing); -/// -/// let span = span!(Level::TRACE, "application"); -/// let _span_guard = span.enter(); -/// -/// tracing::info!( -/// monotonic_counter.foo = 1_u64, -/// key_1 = "bar", -/// key_2 = 10, -/// "handle foo", -/// ); -/// -/// tracing::info!(histogram.baz = 10, "histogram example",); -/// -/// } -/// ``` -/// -/// # Note -/// The OTLP gRPC provider fails when the telemetry is initialized from a test -/// started with `#[tokio::test]`. The reason is currently unknown. Use -/// `log_init()` instead. -/// -/// # Arguments -/// * `telemetry` - The `TelemetryConfig` object containing the telemetry -/// configuration -/// -/// # Errors -/// Returns an error if there is an issue initializing the telemetry system. -pub fn tracing_init(tracing_config: &TracingConfig) -> LoggingGuards { - // Set the RUST_LOG environment variable if a config value is provided - if let Some(rust_log) = &tracing_config.rust_log { - set_var("RUST_LOG", rust_log); - } - - // Enable backtraces for all errors - set_var("RUST_BACKTRACE", "full"); - - if TRACING_SET.swap(true, Ordering::Acquire) { - let span = span!(tracing::Level::INFO, "tracing_init"); - let _guard = span.enter(); - warn!("Tracing already initialized or crashed"); - return LoggingGuards::default(); - } - - match tracing_init_(tracing_config) { - Ok(otel_guard) => { - let span = span!(tracing::Level::INFO, "tracing_init"); - let _guard = span.enter(); - info!("Tracing initialized with config {tracing_config:#?}",); - otel_guard - } - Err(err) => { - TRACING_SET.store(false, Ordering::Release); - // If we cannot initialize the tracing system, we should not panic - eprintln!("Failed to initialize tracing: {err:?}"); - LoggingGuards::default() - } - } -} - -// ============================================================================ -// Internal Implementation -// ============================================================================ - -/// Configuration for fmt layer formatting options -#[derive(Clone, Copy)] -struct FmtConfig { - with_level: bool, - with_target: bool, - with_thread_ids: bool, - with_line_number: bool, - with_file: bool, - with_ansi: bool, -} - -impl FmtConfig { - /// Standard fmt layer configuration with customizable ANSI colors - const fn standard(with_ansi: bool) -> Self { - Self { - with_level: true, - with_target: true, - with_thread_ids: true, - with_line_number: true, - with_file: true, - with_ansi, - } - } -} - -/// Macro to apply standard fmt layer configuration -macro_rules! configure_fmt_layer { - ($layer:expr, $config:expr) => {{ - $layer - .with_level($config.with_level) - .with_target($config.with_target) - .with_thread_ids($config.with_thread_ids) - .with_line_number($config.with_line_number) - .with_file($config.with_file) - .with_ansi($config.with_ansi) - }}; -} - -fn tracing_init_(config: &TracingConfig) -> Result { - let mut otel_guard = LoggingGuards::default(); - let mut layers = vec![]; - - // ======================================== - // Filter Configuration - // ======================================== - let filter = { - #[cfg(feature = "full")] - { - if config.otlp.is_some() { - // ======================================== - // OTLP Filter Configuration - // ======================================== - // To prevent a telemetry-induced-telemetry loop, OpenTelemetry's own internal - // logging is properly suppressed. However, logs emitted by external components - // (such as reqwest, tonic, etc.) are not suppressed as they do not propagate - // OpenTelemetry context. Until this issue is addressed - // (https://github.com/open-telemetry/opentelemetry-rust/issues/2877), - // filtering like this is the best way to suppress such logs. - // - // The filter levels are set as follows: - // - Allow `info` level and above by default. - // - Completely restrict logs from `hyper`, `tonic`, `h2`, and `reqwest`. - // - // Note: This filtering will also drop logs from these components even when - // they are used outside of the OTLP Exporter. - let (filter, _reload_handle) = reload::Layer::new( - EnvFilter::from_default_env() - .add_directive("hyper=error".parse()?) - .add_directive("tonic=error".parse()?) - .add_directive("tower::buffer=off".parse()?) - .add_directive("opentelemetry-otlp=off".parse()?) - .add_directive("opentelemetry_sdk=error".parse()?) - // .add_directive("reqwest=off".parse()?) - .add_directive("h2=off".parse()?), - ); - filter - } else { - // If no OTLP URL is provided, we can use the default filter - let (filter, _reload_handle) = reload::Layer::new(EnvFilter::from_default_env()); - filter - } - } - - #[cfg(not(feature = "full"))] - { - // ======================================== - // Standard Filter Configuration - // ======================================== - // If no OTLP URL is provided, we can use the default filter - let (filter, _reload_handle) = reload::Layer::new(EnvFilter::from_default_env()); - filter - } - }; - - // ======================================== - // Stdout Logging Layer - // ======================================== - // Logging to stdout - if !config.no_log_to_stdout { - let fmt_layer = configure_fmt_layer!( - tracing_subscriber::fmt::layer(), - FmtConfig::standard(config.with_ansi_colors) - ) - .compact(); - layers.push(fmt_layer.boxed()); - } - - // ======================================== - // File Logging Layer - // ======================================== - // Logging the rolling file appender - // tracing-appender uses the `symlink` crate which is not available on wasm32. - #[cfg(not(target_arch = "wasm32"))] - if let Some((dir, name)) = &config.log_to_file { - // create the logs directory if it does not exist - if !dir.exists() { - std::fs::create_dir_all(dir).map_err(|err| { - LoggerError::IOError(format!("Failed to create logs directory: {dir:?}: {err:?}")) - })?; - } - - // Configure a daily rolling file appender - // Log files will be created in the "logs" directory - // with names like ".YYYY-MM-DD" - let file_appender = tracing_appender::rolling::daily(dir, name); - let (non_blocking_writer, guard) = tracing_appender::non_blocking(file_appender); - otel_guard.rolling_appender_guard = Some(guard); - - let fmt_layer = configure_fmt_layer!( - tracing_subscriber::fmt::layer().with_writer(non_blocking_writer), - FmtConfig::standard(false) // No ANSI colors for file logs - ) - .compact(); - layers.push(fmt_layer.boxed()); - } - - // ======================================== - // Syslog Logging Layer (Unix only) - // ======================================== - // Logging to syslog - #[cfg(all(not(target_os = "windows"), feature = "full"))] - if config.log_to_syslog { - let identity = - std::borrow::Cow::Owned(std::ffi::CString::new(config.service_name.clone())?); - let (options, facility) = Default::default(); - if let Some(syslog) = syslog_tracing::Syslog::new(identity, options, facility) { - let syslog_layer = configure_fmt_layer!( - tracing_subscriber::fmt::layer().with_writer(syslog), - FmtConfig::standard(false) // No ANSI colors for syslog - ); - layers.push(syslog_layer.boxed()); - } - } - - // ======================================== - // OpenTelemetry Logging Layer - // ======================================== - // Logging to the OpenTelemetry collector - #[cfg(feature = "full")] - if let Some(otlp_config) = &config.otlp { - // The OpenTelemetry tracing provider - let otlp_provider = otlp::init_tracer_provider( - &config.service_name, - &otlp_config.otlp_url, - otlp_config.version.clone(), - otlp_config.environment.clone(), - )?; - layers.push( - OpenTelemetryLayer::new(otlp_provider.tracer(config.service_name.clone())).boxed(), - ); - - let meter_provider = otlp_config - .enable_metering - .then(|| { - otlp::init_meter_provider( - &config.service_name, - &otlp_config.otlp_url, - otlp_config.version.clone(), - otlp_config.environment.clone(), - ) - .map(|meter_provider| { - layers.push(MetricsLayer::new(meter_provider.clone()).boxed()); - meter_provider - }) - }) - .transpose()?; - - otel_guard.tracer_provider = Some(otlp_provider); - otel_guard.meter_provider = meter_provider; - } - - // ======================================== - // Initialize Tracing Subscriber - // ======================================== - // Initialize the global tracing subscriber - tracing_subscriber::registry() - .with(filter) - .with(layers) - .try_init()?; - - Ok(otel_guard) -} - -// ============================================================================ -// Kubernetes / Cloud-native convenience initializer -// ============================================================================ - -/// Initialize tracing for Kubernetes deployments using standard OpenTelemetry -/// environment variables. -/// -/// Reads the following environment variables: -/// - `OTEL_SERVICE_NAME`: the service name reported to the collector -/// (falls back to `default_service_name`). -/// - `OTEL_EXPORTER_OTLP_ENDPOINT`: OTLP gRPC endpoint (e.g. -/// `http://otel-collector:4317`). When set, OTLP traces are exported. -/// When absent, only stdout logging is enabled. -/// -/// Returns a [`LoggingGuards`] that **must be kept alive** for the duration of -/// the process (dropping it flushes and shuts down the OTLP pipeline). -/// -/// # Example -/// -/// ```rust,ignore -/// #[tokio::main] -/// async fn main() { -/// let _guards = cosmian_logger::tracing_init_from_env("my-service"); -/// tracing::info!("service started"); -/// } -/// ``` -#[cfg(feature = "full")] -#[must_use] -pub fn tracing_init_from_env(default_service_name: &str) -> LoggingGuards { - let service_name = std::env::var("OTEL_SERVICE_NAME") - .ok() - .map(|s| s.trim().to_owned()) - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| default_service_name.to_owned()); - - let otlp = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT") - .ok() - .map(|s| s.trim().to_owned()) - .filter(|s| !s.is_empty()) - .map(|url| TelemetryConfig { - otlp_url: url, - ..Default::default() - }); - - tracing_init(&TracingConfig { - service_name, - otlp, - ..Default::default() - }) -} diff --git a/crate/logger/src/types.rs b/crate/logger/src/types.rs new file mode 100644 index 0000000..d510ba0 --- /dev/null +++ b/crate/logger/src/types.rs @@ -0,0 +1,89 @@ +use std::path::PathBuf; + +use opentelemetry_sdk::{ + logs::SdkLoggerProvider, metrics::SdkMeterProvider, trace::SdkTracerProvider, +}; + +// ── config ─────────────────────────────────────────────────────────────────── + +/// OTLP exporter configuration (used with [`TracingConfig`]). +#[derive(Debug, Default, Clone)] +pub struct TelemetryConfig { + pub version: Option, + pub environment: Option, + pub otlp_url: String, + pub enable_metering: bool, +} + +/// Configuration for the tracing / logging stack (used with [`tracing_init`]). +#[derive(Debug, Default, Clone)] +pub struct TracingConfig { + /// Logical service name (appears in spans and OTLP resource). + pub service_name: String, + + /// OpenTelemetry OTLP configuration. + pub otlp: Option, + + /// Suppress stdout logging. + pub no_log_to_stdout: bool, + + /// Forward logs to syslog (Unix only). + #[cfg(not(target_os = "windows"))] + pub log_to_syslog: bool, + + /// Rolling daily log file: `(directory, filename_prefix)`. + pub log_to_file: Option<(PathBuf, String)>, + + /// Override for `RUST_LOG`. Applied before subscriber init. + pub rust_log: Option, + + /// Enable ANSI colour codes in stdout output. + pub with_ansi_colors: bool, +} + +// ── guards ─────────────────────────────────────────────────────────────────── + +/// Guards that keep OTLP exporters alive for the lifetime of the process. +/// +/// Call [`TelemetryGuards::shutdown`] before process exit to flush pending +/// telemetry and release resources cleanly. +#[derive(Default)] +pub struct TelemetryGuards { + pub(crate) tracer_provider: Option, + pub(crate) logger_provider: Option, + 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(); + } + } + + /// Returns a [`Meter`] from the global `MeterProvider` installed during + /// [`init_tracing`]. Use a `'static` scope name, typically + /// `env!("CARGO_PKG_NAME")`. + /// + /// Returns a no-op meter when OTLP is not configured. + pub fn meter(&self, scope: &'static str) -> opentelemetry::metrics::Meter { + opentelemetry::global::meter(scope) + } +} + +/// Guards that keep background exporters alive (for [`tracing_init`]). +/// +/// The tracing pipeline shuts down cleanly when this value is dropped. +#[derive(Default)] +pub struct LoggingGuards { + pub(crate) _tracer_provider: Option, + pub(crate) _meter_provider: Option, + pub(crate) _rolling_appender_guard: Option, +} From c2fba2bcc6746a1c36106552e72fd4c177a2e80b Mon Sep 17 00:00:00 2001 From: pauline ramon Date: Wed, 24 Jun 2026 14:56:09 +0200 Subject: [PATCH 09/14] fix(logger): make macros fully compatible with tracing structured field syntax Replace single-arm format_args!() wrapper with two-arm macros: - arm 1: target: $target, inject fn_name field right after - arm 2: all other syntax, inject fn_name as first field fn_name is now a structured field (visible in OTLP/JSON and console output) instead of a text prefix. This preserves the full tracing API surface: target:, named fields (key = %val), shorthand (?err, %val), and plain format strings all work without modification. Fixes compilation of codebases that use structured tracing syntax (KMS). --- crate/logger/src/macros.rs | 84 ++++++++++++++++++++++++++++++++------ 1 file changed, 72 insertions(+), 12 deletions(-) diff --git a/crate/logger/src/macros.rs b/crate/logger/src/macros.rs index 4756ef5..54addf7 100644 --- a/crate/logger/src/macros.rs +++ b/crate/logger/src/macros.rs @@ -1,6 +1,15 @@ -//! Logging macros that prefix each message with the calling function name. +//! Logging macros that annotate each event with a `fn_name` structured field. +//! +//! The full tracing field syntax is preserved: `target:`, `?field`, `%field`, +//! named fields (`key = value`), and positional format strings all work +//! exactly as they do with the bare `tracing::` macros. +//! A `fn_name` structured field is automatically injected so that every log +//! event carries the name of the emitting function, which is visible in +//! structured back-ends (OTLP, JSON) and standard `tracing-subscriber` output. /// Helper: extract the current function name at compile time. +/// +/// Returns `&'static str` — the last path segment of the calling function. #[doc(hidden)] #[macro_export] macro_rules! __fn_name { @@ -20,41 +29,92 @@ macro_rules! __fn_name { None => trimmed, } } - _strip(std::any::type_name_of_val(&_f)) + _strip(::std::any::type_name_of_val(&_f)) }}; } +/// Emit a `tracing::info!` event, automatically adding a `fn_name` field. +/// +/// All standard tracing field syntax is supported: +/// ```rust,ignore +/// info!("plain message"); +/// info!("format {}", value); +/// info!(target: "my_target", "message"); +/// info!(key = %value, "message"); +/// info!(?err, "import failed"); +/// ``` #[macro_export] macro_rules! info { - ($($arg:tt)*) => { - $crate::reexport::tracing::info!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) + // `target:` must remain the first token — inject fn_name right after it. + (target: $target:expr, $($rest:tt)*) => { + $crate::reexport::tracing::info!( + target: $target, + fn_name = $crate::__fn_name!(), + $($rest)* + ) + }; + // All other syntax (plain message, structured fields, ?field, %field …). + ($($rest:tt)*) => { + $crate::reexport::tracing::info!(fn_name = $crate::__fn_name!(), $($rest)*) }; } +/// Emit a `tracing::debug!` event, automatically adding a `fn_name` field. #[macro_export] macro_rules! debug { - ($($arg:tt)*) => { - $crate::reexport::tracing::debug!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) + (target: $target:expr, $($rest:tt)*) => { + $crate::reexport::tracing::debug!( + target: $target, + fn_name = $crate::__fn_name!(), + $($rest)* + ) + }; + ($($rest:tt)*) => { + $crate::reexport::tracing::debug!(fn_name = $crate::__fn_name!(), $($rest)*) }; } +/// Emit a `tracing::warn!` event, automatically adding a `fn_name` field. #[macro_export] macro_rules! warn { - ($($arg:tt)*) => { - $crate::reexport::tracing::warn!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) + (target: $target:expr, $($rest:tt)*) => { + $crate::reexport::tracing::warn!( + target: $target, + fn_name = $crate::__fn_name!(), + $($rest)* + ) + }; + ($($rest:tt)*) => { + $crate::reexport::tracing::warn!(fn_name = $crate::__fn_name!(), $($rest)*) }; } +/// Emit a `tracing::error!` event, automatically adding a `fn_name` field. #[macro_export] macro_rules! error { - ($($arg:tt)*) => { - $crate::reexport::tracing::error!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) + (target: $target:expr, $($rest:tt)*) => { + $crate::reexport::tracing::error!( + target: $target, + fn_name = $crate::__fn_name!(), + $($rest)* + ) + }; + ($($rest:tt)*) => { + $crate::reexport::tracing::error!(fn_name = $crate::__fn_name!(), $($rest)*) }; } +/// Emit a `tracing::trace!` event, automatically adding a `fn_name` field. #[macro_export] macro_rules! trace { - ($($arg:tt)*) => { - $crate::reexport::tracing::trace!("[{}] {}", $crate::__fn_name!(), format_args!($($arg)*)) + (target: $target:expr, $($rest:tt)*) => { + $crate::reexport::tracing::trace!( + target: $target, + fn_name = $crate::__fn_name!(), + $($rest)* + ) + }; + ($($rest:tt)*) => { + $crate::reexport::tracing::trace!(fn_name = $crate::__fn_name!(), $($rest)*) }; } From a67308999682b4b18934d0e06842b4132e6742da Mon Sep 17 00:00:00 2001 From: Pauline <59414053+p0wline@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:41:39 +0200 Subject: [PATCH 10/14] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crate/logger/src/lib.rs | 9 ++++++++- crate/logger/src/types.rs | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/crate/logger/src/lib.rs b/crate/logger/src/lib.rs index 167a118..f5f496a 100644 --- a/crate/logger/src/lib.rs +++ b/crate/logger/src/lib.rs @@ -136,7 +136,12 @@ 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() { + let _ = std::fs::create_dir_all(dir); + } let appender = tracing_appender::rolling::daily(dir, name); let (non_blocking, guard) = tracing_appender::non_blocking(appender); guards._rolling_appender_guard = Some(guard); @@ -146,6 +151,9 @@ pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { .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 { @@ -164,7 +172,6 @@ pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { None }; - // --- OTLP layer --- // --- OTLP layer --- let otel_layer = config.otlp.as_ref().and_then(|telemetry| { let resource = build_resource_with_config(&config.service_name, telemetry); diff --git a/crate/logger/src/types.rs b/crate/logger/src/types.rs index d510ba0..c955cb7 100644 --- a/crate/logger/src/types.rs +++ b/crate/logger/src/types.rs @@ -85,5 +85,17 @@ impl TelemetryGuards { pub struct LoggingGuards { pub(crate) _tracer_provider: Option, pub(crate) _meter_provider: Option, + #[cfg(not(target_arch = "wasm32"))] pub(crate) _rolling_appender_guard: Option, } + +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(); + } + } +} From 74a2a14af133ea519f6b0ea694ada1c7da4b0716 Mon Sep 17 00:00:00 2001 From: Pauline <59414053+p0wline@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:50:27 +0200 Subject: [PATCH 11/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crate/logger/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crate/logger/README.md b/crate/logger/README.md index 5044301..bef5931 100644 --- a/crate/logger/README.md +++ b/crate/logger/README.md @@ -15,7 +15,7 @@ Alloy → Loki / Tempo / Prometheus pipeline. |-----|----------| | [`tracing_init`] | Full-featured init driven by a `TracingConfig` struct: stdout, syslog, rolling files, and OTLP. Used by long-running services with rich operator configuration. | | [`init_tracing`] | Lightweight init driven by two environment variables (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_SERVICE_NAME`). Used by minimal daemons and jobs. | -| `info!` / `debug!` / `warn!` / `error!` / `trace!` | Drop-in replacements for the `tracing` macros that automatically prefix each message with the calling function name. | +| `info!` / `debug!` / `warn!` / `error!` / `trace!` | Drop-in replacements for the `tracing` macros that automatically add a `fn_name` structured field to each event. | ### Signal pipeline From f54be6f6ca603ce5d0cbef5d8b00083da879d677 Mon Sep 17 00:00:00 2001 From: Pauline <59414053+p0wline@users.noreply.github.com> Date: Wed, 24 Jun 2026 16:50:45 +0200 Subject: [PATCH 12/14] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- crate/logger/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crate/logger/README.md b/crate/logger/README.md index bef5931..fae45f4 100644 --- a/crate/logger/README.md +++ b/crate/logger/README.md @@ -166,7 +166,7 @@ use shared_logging::info; fn process_request(id: u64) { info!("processing request {id}"); - // emits: INFO [process_request] processing request 42 + // emits a `fn_name=process_request` structured field alongside the message (formatting depends on subscriber) } ``` From 1676de6e9cb487df01960fb4effdd66e44878b26 Mon Sep 17 00:00:00 2001 From: pauline ramon Date: Wed, 24 Jun 2026 17:12:57 +0200 Subject: [PATCH 13/14] fix(logger): address Copilot review comments --- crate/logger/Cargo.toml | 7 ++- crate/logger/README.md | 30 ++++++------ crate/logger/src/macros.rs | 93 ++++++++++++++++++-------------------- 3 files changed, 61 insertions(+), 69 deletions(-) diff --git a/crate/logger/Cargo.toml b/crate/logger/Cargo.toml index 23aa68e..9b61901 100644 --- a/crate/logger/Cargo.toml +++ b/crate/logger/Cargo.toml @@ -23,9 +23,7 @@ opentelemetry_sdk = { version = "0.32", features = [ thiserror = { workspace = true } syslog-tracing = "0.3" tracing = { workspace = true } -tracing-appender = { workspace = true } tracing-opentelemetry = { version = "0.33" } -tokio = { version = "1", optional = true, features = ["rt-multi-thread", "macros"] } tracing-subscriber = { workspace = true, features = [ "ansi", "env-filter", @@ -33,5 +31,6 @@ tracing-subscriber = { workspace = true, features = [ "std" ] } -[features] -full = ["dep:tokio"] +# tracing-appender uses platform-specific file rotation that does not compile on wasm32. +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +tracing-appender = { workspace = true } diff --git a/crate/logger/README.md b/crate/logger/README.md index fae45f4..83e37a9 100644 --- a/crate/logger/README.md +++ b/crate/logger/README.md @@ -1,4 +1,4 @@ -# shared_logging +# cosmian_logger ## Why @@ -9,7 +9,7 @@ Alloy → Loki / Tempo / Prometheus pipeline. ## What -`shared_logging` provides two initialisation paths and a set of logging macros: +`cosmian_logger` provides two initialisation paths and a set of logging macros: | API | Use case | |-----|----------| @@ -63,13 +63,13 @@ every signal from a service is correlated in Grafana. ```toml [dependencies] -shared_logging = { workspace = true } +cosmian_logger = { workspace = true } ``` ### Full-featured init (`tracing_init`) ```rust -use shared_logging::{TelemetryConfig, TracingConfig, info, tracing_init}; +use cosmian_logger::{TelemetryConfig, TracingConfig, info, tracing_init}; fn main() { let _guards = tracing_init(&TracingConfig { @@ -85,7 +85,7 @@ fn main() { ..Default::default() }); - info!("service started"); // prefixed with function name in message + info!("service started"); // emits a fn_name= structured field } ``` @@ -140,7 +140,7 @@ log stream --predicate 'senderImagePath contains "my-service"' ### Lightweight env-var init (`init_tracing`) ```rust -use shared_logging::init_tracing; +use cosmian_logger::init_tracing; use tracing::{info, warn}; #[tokio::main] @@ -157,12 +157,12 @@ async fn main() -> anyhow::Result<()> { ### Logging macros -`shared_logging` exports `info!`, `debug!`, `warn!`, `error!`, and `trace!` macros that -wrap the standard `tracing` macros and automatically prepend `[function_name]` to every -message, making it easy to locate the call site in log aggregators: +`cosmian_logger` exports `info!`, `debug!`, `warn!`, `error!`, and `trace!` macros that +wrap the standard `tracing` macros and automatically inject a `fn_name` structured field, +making it easy to locate the call site in structured log aggregators: ```rust -use shared_logging::info; +use cosmian_logger::info; fn process_request(id: u64) { info!("processing request {id}"); @@ -174,7 +174,7 @@ fn process_request(id: u64) { ```rust use opentelemetry::metrics::Counter; -use shared_logging::init_tracing; +use cosmian_logger::init_tracing; #[tokio::main] async fn main() -> anyhow::Result<()> { @@ -228,21 +228,21 @@ env: ```sh # From workspace root -cargo build -p shared_logging -cargo build -p shared_logging --release +cargo build -p cosmian_logger +cargo build -p cosmian_logger --release ``` ## Testing ```sh # Unit tests (no network required) -cargo test -p shared_logging +cargo test -p cosmian_logger # Smoke test against a live collector OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \ OTEL_SERVICE_NAME=test-service \ RUST_LOG=debug \ - cargo test -p shared_logging -- --nocapture + cargo test -p cosmian_logger -- --nocapture ``` To port-forward the Velo platform's Alloy instance: diff --git a/crate/logger/src/macros.rs b/crate/logger/src/macros.rs index 54addf7..e3ed22c 100644 --- a/crate/logger/src/macros.rs +++ b/crate/logger/src/macros.rs @@ -46,75 +46,68 @@ macro_rules! __fn_name { #[macro_export] macro_rules! info { // `target:` must remain the first token — inject fn_name right after it. - (target: $target:expr, $($rest:tt)*) => { - $crate::reexport::tracing::info!( - target: $target, - fn_name = $crate::__fn_name!(), - $($rest)* - ) - }; + // The fn_name is bound to a local to avoid passing a block-expression + // directly as a macro argument (which confuses token-tree parsers such as + // `tokio::select!`). + (target: $target:expr, $($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::info!(target: $target, fn_name = __kms_fn_name, $($rest)*) + }}; // All other syntax (plain message, structured fields, ?field, %field …). - ($($rest:tt)*) => { - $crate::reexport::tracing::info!(fn_name = $crate::__fn_name!(), $($rest)*) - }; + ($($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::info!(fn_name = __kms_fn_name, $($rest)*) + }}; } /// Emit a `tracing::debug!` event, automatically adding a `fn_name` field. #[macro_export] macro_rules! debug { - (target: $target:expr, $($rest:tt)*) => { - $crate::reexport::tracing::debug!( - target: $target, - fn_name = $crate::__fn_name!(), - $($rest)* - ) - }; - ($($rest:tt)*) => { - $crate::reexport::tracing::debug!(fn_name = $crate::__fn_name!(), $($rest)*) - }; + (target: $target:expr, $($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::debug!(target: $target, fn_name = __kms_fn_name, $($rest)*) + }}; + ($($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::debug!(fn_name = __kms_fn_name, $($rest)*) + }}; } /// Emit a `tracing::warn!` event, automatically adding a `fn_name` field. #[macro_export] macro_rules! warn { - (target: $target:expr, $($rest:tt)*) => { - $crate::reexport::tracing::warn!( - target: $target, - fn_name = $crate::__fn_name!(), - $($rest)* - ) - }; - ($($rest:tt)*) => { - $crate::reexport::tracing::warn!(fn_name = $crate::__fn_name!(), $($rest)*) - }; + (target: $target:expr, $($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::warn!(target: $target, fn_name = __kms_fn_name, $($rest)*) + }}; + ($($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::warn!(fn_name = __kms_fn_name, $($rest)*) + }}; } /// Emit a `tracing::error!` event, automatically adding a `fn_name` field. #[macro_export] macro_rules! error { - (target: $target:expr, $($rest:tt)*) => { - $crate::reexport::tracing::error!( - target: $target, - fn_name = $crate::__fn_name!(), - $($rest)* - ) - }; - ($($rest:tt)*) => { - $crate::reexport::tracing::error!(fn_name = $crate::__fn_name!(), $($rest)*) - }; + (target: $target:expr, $($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::error!(target: $target, fn_name = __kms_fn_name, $($rest)*) + }}; + ($($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::error!(fn_name = __kms_fn_name, $($rest)*) + }}; } /// Emit a `tracing::trace!` event, automatically adding a `fn_name` field. #[macro_export] macro_rules! trace { - (target: $target:expr, $($rest:tt)*) => { - $crate::reexport::tracing::trace!( - target: $target, - fn_name = $crate::__fn_name!(), - $($rest)* - ) - }; - ($($rest:tt)*) => { - $crate::reexport::tracing::trace!(fn_name = $crate::__fn_name!(), $($rest)*) - }; + (target: $target:expr, $($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::trace!(target: $target, fn_name = __kms_fn_name, $($rest)*) + }}; + ($($rest:tt)*) => {{ + let __kms_fn_name = $crate::__fn_name!(); + $crate::reexport::tracing::trace!(fn_name = __kms_fn_name, $($rest)*) + }}; } From ad0cb731690539e9902ee3a1ec8de69751a5b169 Mon Sep 17 00:00:00 2001 From: Manuthor Date: Wed, 24 Jun 2026 19:17:35 +0200 Subject: [PATCH 14/14] fix: PR review --- crate/logger/Cargo.toml | 7 +++++++ crate/logger/examples/tracing_example.rs | 10 ---------- crate/logger/src/lib.rs | 7 +++++-- crate/logger/src/macros.rs | 7 +++++-- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/crate/logger/Cargo.toml b/crate/logger/Cargo.toml index 9b61901..cb2af14 100644 --- a/crate/logger/Cargo.toml +++ b/crate/logger/Cargo.toml @@ -34,3 +34,10 @@ tracing-subscriber = { workspace = true, features = [ # tracing-appender uses platform-specific file rotation that does not compile on wasm32. [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tracing-appender = { workspace = true } + +[dev-dependencies] +tokio = { version = "1", features = ["rt-multi-thread", "macros"] } + +[[example]] +name = "tracing_example" +required-features = ["full"] diff --git a/crate/logger/examples/tracing_example.rs b/crate/logger/examples/tracing_example.rs index 2bdffa0..251abf3 100644 --- a/crate/logger/examples/tracing_example.rs +++ b/crate/logger/examples/tracing_example.rs @@ -1,12 +1,9 @@ //! Example of how to use the tracing system //! cargo run --example tracing_example -p cosmian_logger --features full -#[cfg(feature = "full")] use std::path::PathBuf; -#[cfg(feature = "full")] use cosmian_logger::{tracing_init, TelemetryConfig, TracingConfig}; -#[cfg(feature = "full")] use tracing::span; /// Example of how to use the tracing system @@ -18,7 +15,6 @@ use tracing::span; /// -e COLLECTOR_OTLP_ENABLED=true -e LOG_LEVEL=debug \ /// jaegertracing/jaeger:2.5.0 /// ``` -#[cfg(feature = "full")] #[tokio::main] async fn main() { println!( @@ -64,12 +60,6 @@ Make sure that Jaeger is started and running on localhost:4317: tracing::debug!("Tracing after second initialization attempt"); } -#[cfg(not(feature = "full"))] -fn main() { - println!("Run with `--features full` to enable this example"); -} - -#[cfg(feature = "full")] async fn foo() { tracing::info!( monotonic_counter.foo = 1_u64, diff --git a/crate/logger/src/lib.rs b/crate/logger/src/lib.rs index f5f496a..b1b9bcc 100644 --- a/crate/logger/src/lib.rs +++ b/crate/logger/src/lib.rs @@ -136,11 +136,14 @@ pub fn tracing_init(config: &TracingConfig) -> LoggingGuards { }; // --- rolling file layer --- - // tracing-appender uses platform-specific file rotation support and does not compile on wasm32. + // 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() { - let _ = std::fs::create_dir_all(dir); + 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); diff --git a/crate/logger/src/macros.rs b/crate/logger/src/macros.rs index e3ed22c..d50a869 100644 --- a/crate/logger/src/macros.rs +++ b/crate/logger/src/macros.rs @@ -14,8 +14,11 @@ #[macro_export] macro_rules! __fn_name { () => {{ - // `type_name_of_val` on a nested closure yields a path ending in - // `::{{closure}}`. We strip that suffix and extract the last segment. + // Define a nested function `_f` inside the calling function. + // `std::any::type_name_of_val(&_f)` yields the full path of `_f`, + // e.g. `my_crate::module::calling_fn::_f`. + // We strip the trailing `::_f` segment and then take the last `::` + // segment to obtain the enclosing function name. fn _f() {} fn _strip(name: &str) -> &str { // Remove `::_f` suffix