diff --git a/editoast/Cargo.lock b/editoast/Cargo.lock index 86e93ff2c30..8a42b97a11d 100644 --- a/editoast/Cargo.lock +++ b/editoast/Cargo.lock @@ -1417,10 +1417,18 @@ dependencies = [ name = "editoast_common" version = "0.1.0" dependencies = [ + "derivative", + "opentelemetry", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", "rangemap", "serde", "serde_json", "thiserror 2.0.7", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "url", "utoipa", ] diff --git a/editoast/Cargo.toml b/editoast/Cargo.toml index b70a36242b0..92de1d3f1a4 100644 --- a/editoast/Cargo.toml +++ b/editoast/Cargo.toml @@ -57,6 +57,7 @@ openssl = "0.10.68" opentelemetry-semantic-conventions = { version = "0.26", features = [ "semconv_experimental", ] } +opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio", "trace"] } paste = "1.0.15" postgis_diesel = { version = "2.4.1", features = ["serde"] } postgres-openssl = "0.5.0" @@ -65,6 +66,9 @@ rand = "0.8.5" rangemap = "1.5.1" # 0.12.0 to 0.12.4 have weird timeout issues https://github.com/seanmonstar/reqwest/issues/2283 # This bug was introduced between 0.12.0 and 0.12.3. +opentelemetry = { version = "0.27.1", default-features = false, features = [ + "trace", +] } reqwest = { version = "0.11.27", features = ["json"] } rstest = { version = "0.19.0", default-features = false } serde = { version = "1.0.216", features = ["derive"] } @@ -78,6 +82,10 @@ tracing = { version = "0.1.41", default-features = false, features = [ "attributes", "log", ] } +tracing-opentelemetry = { version = "0.28.0", default-features = false, features = [ + "tracing-log", +] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } url = { version = "2.5.4", features = ["serde"] } urlencoding = "2.1.3" utoipa = { version = "4.2.3", features = ["chrono", "uuid"] } @@ -138,15 +146,13 @@ json-patch = { version = "3.0.1", default-features = false, features = [ lapin = "2.5.0" mime = "0.3.17" mvt.workspace = true -opentelemetry = { version = "0.27.1", default-features = false, features = [ - "trace", -] } +opentelemetry.workspace = true opentelemetry-otlp = { version = "0.27.0", default-features = false, features = [ "grpc-tonic", "trace", ] } opentelemetry-semantic-conventions.workspace = true -opentelemetry_sdk = { version = "0.27.1", features = ["rt-tokio", "trace"] } +opentelemetry_sdk.workspace = true ordered-float = { version = "4.5.0", features = ["serde"] } osm_to_railjson = { path = "./osm_to_railjson" } paste.workspace = true @@ -180,10 +186,8 @@ tower-http = { version = "0.6.2", features = [ "trace", ] } tracing.workspace = true -tracing-opentelemetry = { version = "0.28.0", default-features = false, features = [ - "tracing-log", -] } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } +tracing-opentelemetry.workspace = true +tracing-subscriber.workspace = true url.workspace = true utoipa.workspace = true uuid.workspace = true diff --git a/editoast/editoast_common/Cargo.toml b/editoast/editoast_common/Cargo.toml index 0b71df445b6..dda87103f91 100644 --- a/editoast/editoast_common/Cargo.toml +++ b/editoast/editoast_common/Cargo.toml @@ -7,10 +7,18 @@ edition.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +derivative.workspace = true +opentelemetry.workspace = true +opentelemetry-semantic-conventions.workspace = true +opentelemetry_sdk.workspace = true rangemap.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true thiserror.workspace = true +tracing.workspace = true +tracing-opentelemetry.workspace = true +tracing-subscriber.workspace = true +url.workspace = true utoipa.workspace = true [lints] diff --git a/editoast/editoast_common/src/lib.rs b/editoast/editoast_common/src/lib.rs index c7e38d3b550..a3aa86cc55e 100644 --- a/editoast/editoast_common/src/lib.rs +++ b/editoast/editoast_common/src/lib.rs @@ -2,6 +2,7 @@ pub mod geometry; mod hash_rounded_float; pub mod rangemap_utils; pub mod schemas; +pub mod tracing; pub use hash_rounded_float::hash_float; pub use hash_rounded_float::hash_float_slice; diff --git a/editoast/editoast_common/src/tracing.rs b/editoast/editoast_common/src/tracing.rs new file mode 100644 index 00000000000..c576710c75c --- /dev/null +++ b/editoast/editoast_common/src/tracing.rs @@ -0,0 +1,93 @@ +use std::time::Duration; + +use derivative::Derivative; +use opentelemetry::trace::TracerProvider; +use opentelemetry::KeyValue; +use opentelemetry_sdk::export::trace::SpanExporter; +use opentelemetry_sdk::propagation::TraceContextPropagator; +use opentelemetry_sdk::resource::EnvResourceDetector; +use opentelemetry_sdk::resource::SdkProvidedResourceDetector; +use opentelemetry_sdk::resource::TelemetryResourceDetector; +use opentelemetry_sdk::Resource; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::Layer; +use url::Url; + +#[derive(Debug, PartialEq)] +pub enum Stream { + Stderr, + Stdout, +} + +#[derive(Debug, Derivative)] +#[derivative(Default)] +pub struct Telemetry { + #[derivative(Default(value = r#""osrd-editoast".into()"#))] + pub service_name: String, + #[derivative(Default(value = r#"Url::parse("http://localhost:4317").unwrap()"#))] + pub endpoint: Url, +} + +pub struct TracingConfig { + pub stream: Stream, + pub telemetry: Option, +} + +impl Default for TracingConfig { + fn default() -> Self { + TracingConfig { + stream: Stream::Stdout, + telemetry: Some(Telemetry::default()), + } + } +} + +pub fn create_tracing_subscriber( + tracing_config: TracingConfig, + exporter: T, +) -> impl tracing::Subscriber { + let env_filter_layer = tracing_subscriber::EnvFilter::builder() + // Set the default log level to 'info' + .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) + .from_env_lossy(); + let fmt_layer = tracing_subscriber::fmt::layer() + .pretty() + .with_file(true) + .with_line_number(false); + let fmt_layer = if tracing_config.stream == Stream::Stderr { + fmt_layer.with_writer(std::io::stderr).boxed() + } else { + fmt_layer.boxed() + }; + // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#runtime-configuration-with-layers + let telemetry_layer = match tracing_config.telemetry { + None => None, + Some(telemetry) => { + let resource = Resource::new(vec![KeyValue::new( + opentelemetry_semantic_conventions::resource::SERVICE_NAME, + telemetry.service_name.clone(), + )]) + .merge(&Resource::from_detectors( + Duration::from_secs(10), + vec![ + Box::new(SdkProvidedResourceDetector), + Box::new(TelemetryResourceDetector), + Box::new(EnvResourceDetector::new()), + ], + )); + let otlp_tracer = opentelemetry_sdk::trace::TracerProvider::builder() + .with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio) + .with_resource(resource) + .build() + .tracer("osrd-editoast"); + let layer = tracing_opentelemetry::OpenTelemetryLayer::new(otlp_tracer); + opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new()); + Some(layer) + } + }; + + tracing_subscriber::registry() + .with(telemetry_layer) + .with(env_filter_layer) + .with(fmt_layer) +} diff --git a/editoast/src/client/telemetry_config.rs b/editoast/src/client/telemetry_config.rs index 738fc2c0ec6..b475c8bad24 100644 --- a/editoast/src/client/telemetry_config.rs +++ b/editoast/src/client/telemetry_config.rs @@ -17,6 +17,15 @@ pub struct TelemetryConfig { pub telemetry_endpoint: Url, } +impl From for editoast_common::tracing::Telemetry { + fn from(telemetry_config: TelemetryConfig) -> Self { + Self { + service_name: telemetry_config.service_name, + endpoint: telemetry_config.telemetry_endpoint, + } + } +} + #[derive(Default, ValueEnum, Debug, Derivative, Clone, strum::Display)] #[strum(serialize_all = "lowercase")] pub enum TelemetryKind { diff --git a/editoast/src/main.rs b/editoast/src/main.rs index 7ca4b46553f..7cc95f7b5c6 100644 --- a/editoast/src/main.rs +++ b/editoast/src/main.rs @@ -30,29 +30,21 @@ use client::user::UserCommand; use client::Client; use client::Color; use client::Commands; +use editoast_common::tracing::create_tracing_subscriber; +use editoast_common::tracing::TracingConfig; use editoast_models::DbConnectionPoolV2; use models::RollingStockModel; -use opentelemetry::trace::TracerProvider as _; -use opentelemetry_sdk::propagation::TraceContextPropagator; -use opentelemetry_sdk::resource::EnvResourceDetector; -use opentelemetry_sdk::resource::SdkProvidedResourceDetector; -use opentelemetry_sdk::resource::TelemetryResourceDetector; +use tracing_subscriber::util::SubscriberInitExt; pub use views::AppState; use models::prelude::*; -use opentelemetry::KeyValue; use opentelemetry_otlp::WithExportConfig as _; -use opentelemetry_sdk::Resource; use std::error::Error; use std::io::IsTerminal; use std::process::exit; use std::sync::Arc; -use std::time::Duration; use thiserror::Error; use tracing::error; -use tracing_subscriber::layer::SubscriberExt as _; -use tracing_subscriber::util::SubscriberInitExt as _; -use tracing_subscriber::Layer as _; pub use valkey_utils::ValkeyClient; pub use valkey_utils::ValkeyConnection; @@ -66,61 +58,18 @@ pub use valkey_utils::ValkeyConnection; /// - we *expect* a webserver to output logging information, so since it's an expected /// output (and not extra information), it should be on stdout #[derive(Debug, PartialEq)] -enum EditoastMode { +pub enum EditoastMode { Webservice, Cli, } -fn init_tracing(mode: EditoastMode, telemetry_config: &client::TelemetryConfig) { - let env_filter_layer = tracing_subscriber::EnvFilter::builder() - // Set the default log level to 'info' - .with_default_directive(tracing_subscriber::filter::LevelFilter::INFO.into()) - .from_env_lossy(); - let fmt_layer = tracing_subscriber::fmt::layer() - .pretty() - .with_file(true) - .with_line_number(false); - let fmt_layer = if mode == EditoastMode::Cli { - fmt_layer.with_writer(std::io::stderr).boxed() - } else { - fmt_layer.boxed() - }; - // https://docs.rs/tracing-subscriber/latest/tracing_subscriber/layer/index.html#runtime-configuration-with-layers - let telemetry_layer = match telemetry_config.telemetry_kind { - client::TelemetryKind::None => None, - client::TelemetryKind::Opentelemetry => { - let exporter = opentelemetry_otlp::SpanExporter::builder() - .with_tonic() - .with_endpoint(telemetry_config.telemetry_endpoint.as_str()) - .build() - .expect("failed to build a span exporter"); - let resource = Resource::new(vec![KeyValue::new( - opentelemetry_semantic_conventions::resource::SERVICE_NAME, - telemetry_config.service_name.clone(), - )]) - .merge(&Resource::from_detectors( - Duration::from_secs(10), - vec![ - Box::new(SdkProvidedResourceDetector), - Box::new(TelemetryResourceDetector), - Box::new(EnvResourceDetector::new()), - ], - )); - let otlp_tracer = opentelemetry_sdk::trace::TracerProvider::builder() - .with_batch_exporter(exporter, opentelemetry_sdk::runtime::Tokio) - .with_resource(resource) - .build() - .tracer("osrd-editoast"); - let layer = tracing_opentelemetry::OpenTelemetryLayer::new(otlp_tracer); - opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new()); - Some(layer) +impl From for editoast_common::tracing::Stream { + fn from(mode: EditoastMode) -> Self { + match mode { + EditoastMode::Webservice => Self::Stdout, + EditoastMode::Cli => Self::Stderr, } - }; - tracing_subscriber::registry() - .with(telemetry_layer) - .with(env_filter_layer) - .with(fmt_layer) - .init(); + } } impl EditoastMode { @@ -151,7 +100,22 @@ async fn main() { async fn run() -> Result<(), Box> { let client = Client::parse(); - init_tracing(EditoastMode::from_client(&client), &client.telemetry_config); + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .with_endpoint(client.telemetry_config.telemetry_endpoint.as_str()) + .build() + .expect("failed to build a span exporter"); + + let telemetry = match client.telemetry_config.telemetry_kind { + client::TelemetryKind::None => None, + client::TelemetryKind::Opentelemetry => Some(client.telemetry_config.clone().into()), + }; + + let tracing_config = TracingConfig { + stream: EditoastMode::from_client(&client).into(), + telemetry, + }; + create_tracing_subscriber(tracing_config, exporter).init(); let pg_config = client.postgres_config; let db_pool = diff --git a/editoast/src/views/mod.rs b/editoast/src/views/mod.rs index 824fd3d6259..b418628ba38 100644 --- a/editoast/src/views/mod.rs +++ b/editoast/src/views/mod.rs @@ -22,7 +22,7 @@ pub mod train_schedule; pub mod work_schedules; #[cfg(test)] -mod test_app; +pub mod test_app; use ::core::str; use std::collections::HashSet; diff --git a/editoast/src/views/test_app.rs b/editoast/src/views/test_app.rs index 9ab47d93081..2cdaa841f9b 100644 --- a/editoast/src/views/test_app.rs +++ b/editoast/src/views/test_app.rs @@ -7,8 +7,14 @@ use std::sync::Arc; use axum::Router; use axum_tracing_opentelemetry::middleware::OtelAxumLayer; use dashmap::DashMap; +use editoast_common::tracing::create_tracing_subscriber; +use editoast_common::tracing::TracingConfig; use editoast_models::DbConnectionPoolV2; use editoast_osrdyne_client::OsrdyneClient; +use futures::future::BoxFuture; +use opentelemetry_sdk::export::trace::ExportResult; +use opentelemetry_sdk::export::trace::SpanData; +use opentelemetry_sdk::export::trace::SpanExporter; use serde::de::DeserializeOwned; use tower_http::trace::TraceLayer; use url::Url; @@ -26,6 +32,15 @@ use axum_test::TestServer; use super::{authentication_middleware, CoreConfig, OsrdyneConfig, PostgresConfig, ServerConfig}; +#[derive(Debug)] +pub struct NoopSpanExporter; + +impl SpanExporter for NoopSpanExporter { + fn export(&mut self, _: Vec) -> BoxFuture<'static, ExportResult> { + Box::pin(std::future::ready(Ok(()))) + } +} + /// A builder interface for [TestApp] /// /// It allows configuring some parameters for the app service. @@ -106,15 +121,7 @@ impl TestAppBuilder { }; // Setup tracing - let sub = tracing_subscriber::fmt() - .pretty() - .with_env_filter( - tracing_subscriber::EnvFilter::builder() - .with_default_directive(tracing_subscriber::filter::LevelFilter::DEBUG.into()) - .from_env_lossy(), - ) - .with_span_events(tracing_subscriber::fmt::format::FmtSpan::CLOSE) - .finish(); + let sub = create_tracing_subscriber(TracingConfig::default(), NoopSpanExporter); let tracing_guard = tracing::subscriber::set_default(sub); // Config valkey