diff --git a/.gitignore b/.gitignore index 8d45a54..bdd6f98 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ # For book prosa_book/book -prosa_book/src/ch01-03-puppet.md +prosa_book/src/ch01-04-puppet.md *.html diff --git a/Cargo.toml b/Cargo.toml index f3d1ace..bf9a94f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ include = [ ] [workspace.dependencies] -prosa-utils = { version = "0.4.0", path = "prosa_utils", default-features = false } +prosa-utils = { version = "0.4.1", path = "prosa_utils" } prosa-macros = { version = "0.4.1", path = "prosa_macros" } thiserror = "2" aquamarine = "0.6" @@ -35,9 +35,9 @@ toml = "0.9" # Config Observability log = "0.4" -opentelemetry = { version = "0.29", features = ["metrics", "trace", "logs"] } -opentelemetry_sdk = { version = "0.29", features = ["metrics", "trace", "logs", "rt-tokio"] } -prometheus = { version = "0.14" } +opentelemetry = { version = "0.31.0", features = ["metrics", "trace", "logs"] } +opentelemetry_sdk = { version = "0.31.0", features = ["metrics", "trace", "logs", "rt-tokio"] } +prometheus = { version = "0.14", default-features = false } # Config SSL openssl-sys = { version = "0.9" } diff --git a/prosa/Cargo.toml b/prosa/Cargo.toml index 5b93bdb..944230d 100644 --- a/prosa/Cargo.toml +++ b/prosa/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prosa" -version = "0.4.0" +version = "0.4.1" authors.workspace = true description = "ProSA core" homepage.workspace = true @@ -10,10 +10,12 @@ license.workspace = true include.workspace = true [features] -default = ["http-proxy", "openssl"] +default = ["system-metrics", "http-proxy", "openssl", "prometheus"] +system-metrics = ["dep:memory-stats"] http-proxy = ["dep:async-http-proxy"] openssl = ["dep:openssl", "dep:tokio-openssl", "prosa-utils/config-openssl"] openssl-vendored = ["openssl", "openssl/vendored", "prosa-utils/config-openssl-vendored"] +prometheus = ["dep:prometheus", "prosa-utils/config-observability-prometheus"] queue = ["prosa-utils/queue"] [[example]] @@ -38,12 +40,11 @@ settings = "stub::proc::StubSettings" adaptor = ["stub::adaptor::StubParotAdaptor"] [dependencies] -prosa-utils = { workspace = true, features = ["msg", "config", "config-observability", "config-observability-prometheus"] } +prosa-utils = { workspace = true, features = ["msg", "config", "config-observability"] } prosa-macros.workspace = true log.workspace = true tracing = "0.1" thiserror.workspace = true -base64 = "0.22" url = { version = "2", features = ["serde"] } rlimit = "0.10" @@ -62,8 +63,8 @@ serde_yaml = "0.9" opentelemetry.workspace = true opentelemetry_sdk.workspace = true -prometheus.workspace = true -memory-stats = "1" +prometheus = { workspace = true, optional = true } +memory-stats = { version = "1", optional = true } [dev-dependencies] futures-util = { version = "0.3", default-features = false } diff --git a/prosa/src/core/main.rs b/prosa/src/core/main.rs index 89aaa99..9cc6a53 100644 --- a/prosa/src/core/main.rs +++ b/prosa/src/core/main.rs @@ -19,11 +19,11 @@ use super::{ use opentelemetry::metrics::{Meter, MeterProvider as _}; use opentelemetry::trace::TracerProvider as _; use opentelemetry::{InstrumentationScope, KeyValue}; -use std::borrow::Cow; use std::sync::{ Arc, atomic::{AtomicBool, Ordering}, }; +use std::{borrow::Cow, collections::HashSet}; use std::{collections::HashMap, fmt::Debug}; use tokio::{signal, sync::mpsc}; use tracing::{debug, info, warn}; @@ -76,6 +76,8 @@ where { internal_tx_queue: mpsc::Sender>, name: String, + scope_attributes: Vec, + #[cfg(feature = "prometheus")] prometheus_registry: prometheus::Registry, meter_provider: opentelemetry_sdk::metrics::SdkMeterProvider, tracer_provider: opentelemetry_sdk::trace::SdkTracerProvider, @@ -105,18 +107,36 @@ where internal_tx_queue: mpsc::Sender>, settings: &S, ) -> Main { - let prometheus_registry = prometheus::Registry::new(); - let meter_provider = settings - .get_observability() - .build_meter_provider(&prometheus_registry); - - Main { - internal_tx_queue, - name: settings.get_prosa_name(), - prometheus_registry, - meter_provider, - tracer_provider: settings.get_observability().build_tracer_provider(), - stop: Arc::new(AtomicBool::new(false)), + #[cfg(feature = "prometheus")] + { + let prometheus_registry = prometheus::Registry::new(); + let meter_provider = settings + .get_observability() + .build_meter_provider(&prometheus_registry); + + Main { + internal_tx_queue, + name: settings.get_prosa_name(), + scope_attributes: settings.get_observability().get_scope_attributes(), + prometheus_registry, + meter_provider, + tracer_provider: settings.get_observability().build_tracer_provider(), + stop: Arc::new(AtomicBool::new(false)), + } + } + + #[cfg(not(feature = "prometheus"))] + { + let meter_provider = settings.get_observability().build_meter_provider(); + + Main { + internal_tx_queue, + name: settings.get_prosa_name(), + scope_attributes: settings.get_observability().get_scope_attributes(), + meter_provider, + tracer_provider: settings.get_observability().build_tracer_provider(), + stop: Arc::new(AtomicBool::new(false)), + } } } @@ -126,6 +146,7 @@ where } /// Getter of the Prometheus registry + #[cfg(feature = "prometheus")] pub fn get_prometheus_registry(&self) -> &prometheus::Registry { &self.prometheus_registry } @@ -236,16 +257,16 @@ where /// Provide the opentelemetry Meter based on ProSA settings pub fn meter(&self, name: &'static str) -> opentelemetry::metrics::Meter { - self.meter_provider.meter(name) + let scope = InstrumentationScope::builder(name) + .with_version(env!("CARGO_PKG_VERSION")) + .with_attributes(self.scope_attributes.clone()) + .build(); + self.meter_provider.meter_with_scope(scope) } /// Provide the opentelemetry Tracer based on ProSA settings pub fn tracer(&self, name: impl Into>) -> opentelemetry_sdk::trace::Tracer { - let scope = InstrumentationScope::builder(name) - .with_version(env!("CARGO_PKG_VERSION")) - .with_attributes([KeyValue::new("prosa_name", self.name.clone())]) - .build(); - self.tracer_provider.tracer_with_scope(scope) + self.tracer_provider.tracer(name) } } @@ -279,16 +300,6 @@ impl MainProc where M: Sized + Clone + Debug + Tvf + Default + 'static + std::marker::Send + std::marker::Sync, { - /// Getter of the number of processors' queues - fn get_proc_queue_len(&self) -> usize { - let mut proc_queue_len = 0; - for proc_queue in self.processors.values() { - proc_queue_len += proc_queue.len(); - } - - proc_queue_len - } - async fn remove_proc(&mut self, proc_id: u32) -> Option>> { if let Some(proc) = self.processors.remove(&proc_id) { let mut new_services = (*self.services).clone(); @@ -423,49 +434,49 @@ where } async fn run(mut self) { - // Monitor RAM usage - let prosa_name = self.name.clone(); + #[cfg(feature = "system-metrics")] + { + // Monitor RAM usage + self.meter + .u64_observable_gauge("prosa_main_ram") + .with_description("RAM consumed by ProSA") + .with_unit("bytes") + .with_callback(move |observer| { + if let Some(usage) = memory_stats::memory_stats() { + observer.observe( + usage.physical_mem as u64, + &[KeyValue::new("type", "physical")], + ); + observer.observe( + usage.virtual_mem as u64, + &[KeyValue::new("type", "virtual")], + ); + } + }) + .build(); + } + + // Monitor services + let (service_update, new_service) = tokio::sync::watch::channel(self.services.clone()); self.meter - .u64_observable_gauge("prosa_main_ram") - .with_description("RAM consumed by ProSA") - .with_unit("bytes") + .u64_observable_gauge("prosa_services") + .with_description("Services declared to the main task") .with_callback(move |observer| { - if let Some(usage) = memory_stats::memory_stats() { - observer.observe( - usage.physical_mem as u64, - &[ - KeyValue::new("prosa_name", prosa_name.clone()), - KeyValue::new("type", "physical"), - ], - ); - observer.observe( - usage.virtual_mem as u64, - &[ - KeyValue::new("prosa_name", prosa_name.clone()), - KeyValue::new("type", "virtual"), - ], - ); - } + new_service.borrow().observe_metrics(observer); }) .build(); - // Monitor services - let services_meter = self - .meter - .u64_gauge("prosa_main_services") - .with_description("Services declared to the main task") - .build(); + let mut proc_names = HashMap::new(); + // Monitor processors objects - let mut crashed_proc = 0; - let mut restarted_proc = 0; + let mut crashed_proc: HashSet = HashSet::new(); + let mut restarted_proc = HashMap::new(); let processors_meter = self .meter - .u64_gauge("prosa_processors") + .i64_gauge("prosa_processors") .with_description("Processors declared to the main task") .build(); - let prosa_name = self.name.clone(); - /// Macro to notify processors for a change about service list macro_rules! prosa_main_update_srv { ( ) => { @@ -475,47 +486,51 @@ where }; } - /// Macro to record a change to the services - macro_rules! prosa_main_record_services { - ( ) => { - services_meter.record( - self.services.len() as u64, - &[KeyValue::new("prosa_name", prosa_name.clone())], - ); - }; - } - /// Macro to record a change to the processors macro_rules! prosa_main_record_proc { ( ) => { - processors_meter.record( - self.processors.len() as u64, - &[ - KeyValue::new("prosa_name", prosa_name.clone()), - KeyValue::new("type", "tasks"), - ], - ); - processors_meter.record( - self.get_proc_queue_len() as u64, - &[ - KeyValue::new("prosa_name", prosa_name.clone()), - KeyValue::new("type", "queues"), - ], - ); - processors_meter.record( - crashed_proc, - &[ - KeyValue::new("prosa_name", prosa_name.clone()), - KeyValue::new("type", "crashed"), - ], - ); - processors_meter.record( - restarted_proc, - &[ - KeyValue::new("prosa_name", prosa_name.clone()), - KeyValue::new("type", "restarted"), - ], - ); + for (id, name) in proc_names.iter() { + if crashed_proc.contains(id) { + // The processor is crashed + processors_meter.record( + -2, + &[ + KeyValue::new("type", "node"), + KeyValue::new("id", *id as i64), + KeyValue::new("title", name.to_string()), + ], + ); + } else if let Some(proc_service) = self.processors.get(id) { + // The processor is running + let nb_restarted = *restarted_proc.get(id).unwrap_or(&0); + processors_meter.record( + proc_service.len() as i64, + &[ + KeyValue::new("type", "queues"), + KeyValue::new("id", *id as i64), + KeyValue::new("title", name.to_string()), + ], + ); + processors_meter.record( + nb_restarted as i64, + &[ + KeyValue::new("type", "node"), + KeyValue::new("id", *id as i64), + KeyValue::new("title", name.to_string()), + ], + ); + } else { + // The processor is not running + processors_meter.record( + -1, + &[ + KeyValue::new("type", "node"), + KeyValue::new("id", *id as i64), + KeyValue::new("title", name.to_string()), + ], + ); + } + } }; } @@ -530,6 +545,7 @@ where if let Some(proc_service) = self.processors.get_mut(&proc_id) { proc_service.insert(queue_id, proc); } else { + proc_names.insert(proc_id, proc.name().to_string()); self.processors.insert(proc_id, HashMap::from([ (queue_id, proc), ])); @@ -553,9 +569,13 @@ where if let Some(err) = proc_err { if err.recoverable() { - restarted_proc += 1; + if let Some(restarted) = restarted_proc.get_mut(&proc_id) { + *restarted += 1; + } else { + restarted_proc.insert(proc_id, 1); + } } else { - crashed_proc += 1; + crashed_proc.insert(proc_id); } } @@ -577,7 +597,7 @@ where } } self.services = Arc::new(new_services); - prosa_main_record_services!(); + let _ = service_update.send(self.services.clone()); prosa_main_update_srv!(); } }, @@ -588,7 +608,7 @@ where new_services.add_service(name, proc_queue.clone()); } self.services = Arc::new(new_services); - prosa_main_record_services!(); + let _ = service_update.send(self.services.clone()); prosa_main_update_srv!(); } }, @@ -598,7 +618,7 @@ where new_services.remove_service_proc(&name, proc_id); } self.services = Arc::new(new_services); - prosa_main_record_services!(); + let _ = service_update.send(self.services.clone()); prosa_main_update_srv!(); }, InternalMainMsg::DeleteService(names, proc_id, queue_id) => { @@ -607,7 +627,7 @@ where new_services.remove_service(&name, proc_id, queue_id); } self.services = Arc::new(new_services); - prosa_main_record_services!(); + let _ = service_update.send(self.services.clone()); prosa_main_update_srv!(); }, InternalMainMsg::Command(cmd)=> { diff --git a/prosa/src/core/proc.rs b/prosa/src/core/proc.rs index 78d2cb3..30cad38 100644 --- a/prosa/src/core/proc.rs +++ b/prosa/src/core/proc.rs @@ -417,6 +417,7 @@ where } /// Getter of the Prometheus registry + #[cfg(feature = "prometheus")] pub fn get_prometheus_registry(&self) -> &prometheus::Registry { self.main.get_prometheus_registry() } @@ -468,8 +469,9 @@ where macro_rules! proc_run { ( $self:ident ) => { info!( - target: $self.name(), - "Run processor on {} threads", + "Run processor[{}] {} on {} threads", + $self.get_proc_id(), + $self.name(), $self.get_proc_threads() ); @@ -489,8 +491,9 @@ macro_rules! proc_run { // Log and restart if needed if proc_err.recoverable() { warn!( - target: $self.name(), - "Processor encounter an error `{}`. Will restart after {}ms", + "Processor[{}] {} encounter an error `{}`. Will restart after {}ms", + $self.get_proc_id(), + $self.name(), proc_err, (wait_time + recovery_duration).as_millis() ); @@ -501,8 +504,9 @@ macro_rules! proc_run { } } else { error!( - target: $self.name(), - "Processor encounter a fatal error `{}`", + "Processor[{}] {} encounter a fatal error `{}`", + $self.get_proc_id(), + $self.name(), proc_err ); diff --git a/prosa/src/core/service.rs b/prosa/src/core/service.rs index d96ffdc..15a7e61 100644 --- a/prosa/src/core/service.rs +++ b/prosa/src/core/service.rs @@ -4,9 +4,10 @@ use super::{ msg::InternalMsg, proc::{ProcBusParam, ProcParam}, }; +use opentelemetry::{KeyValue, metrics::AsyncInstrument}; use prosa_utils::msg::tvf::{Tvf, TvfError}; use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, fmt::{self, Debug}, sync::atomic, }; @@ -20,7 +21,7 @@ where M: Sized + Clone + Tvf, { /// HashMap that contain the service name as key and a vector of processor services with a round robin information - table: HashMap>, atomic::AtomicU64)>, + table: HashMap, (Vec>, atomic::AtomicU64)>, } impl ServiceTable @@ -37,6 +38,33 @@ where self.table.len() } + /// Method to record metrics on the service table + pub(crate) fn observe_metrics(&self, services_meter: &dyn AsyncInstrument) { + for (name, (services, _)) in self.table.iter() { + services_meter.observe( + services.len() as u64, + &[ + KeyValue::new("type", "node"), + KeyValue::new("id", name.to_string()), + ], + ); + let mut processor_link = HashSet::new(); + for service in services { + if processor_link.insert(service.proc_id) { + services_meter.observe( + 1, + &[ + KeyValue::new("type", "link"), + KeyValue::new("id", format!("{name}/{}", service.proc_id)), + KeyValue::new("source", name.to_string()), + KeyValue::new("target", service.proc_id as i64), + ], + ); + } + } + } + } + /// Method to know if the service is available from a processor /// /// Call by the processor to know if a service is available (service test) @@ -71,10 +99,8 @@ where services.push(proc_service); } } else { - self.table.insert( - name.to_string(), - (vec![proc_service], atomic::AtomicU64::new(0)), - ); + self.table + .insert(name.into(), (vec![proc_service], atomic::AtomicU64::new(0))); } } @@ -163,7 +189,7 @@ where M: Sized + Clone + Tvf, { proc_id: u32, - proc_name: String, + proc_name: Box, queue_id: u32, /// Processor queue use to send transactionnal message to the processor pub proc_queue: mpsc::Sender>, @@ -181,7 +207,7 @@ where ) -> ProcService { ProcService { proc_id: proc.get_proc_id(), - proc_name: proc.name().to_string(), + proc_name: proc.name().into(), queue_id, proc_queue, } @@ -191,7 +217,7 @@ where pub fn new_proc(proc: &ProcParam, queue_id: u32) -> ProcService { ProcService { proc_id: proc.get_proc_id(), - proc_name: proc.name().to_string(), + proc_name: proc.name().into(), queue_id, proc_queue: proc.get_service_queue(), } @@ -217,7 +243,7 @@ where } fn name(&self) -> &str { - self.proc_name.as_str() + self.proc_name.as_ref() } } diff --git a/prosa/src/io/stream.rs b/prosa/src/io/stream.rs index 39b58b3..fb19ad2 100644 --- a/prosa/src/io/stream.rs +++ b/prosa/src/io/stream.rs @@ -9,11 +9,10 @@ use std::{ time::Duration, }; -use prosa_utils::config::ssl::SslConfig; #[cfg(feature = "openssl")] use prosa_utils::config::ssl::SslConfigContext; +use prosa_utils::config::{ssl::SslConfig, url_authentication}; -use base64::{Engine as _, engine::general_purpose::STANDARD}; use serde::{Deserialize, Serialize}; use tokio::{ io::{AsyncRead, AsyncWrite, ReadBuf}, @@ -816,18 +815,7 @@ impl TargetSetting { /// assert_eq!(Some(String::from("Bearer token")), bearer_auth_target.get_authentication()); /// ``` pub fn get_authentication(&self) -> Option { - if let Some(password) = self.url.password() { - if self.url.username().is_empty() { - Some(format!("Bearer {password}")) - } else { - Some(format!( - "Basic {}", - STANDARD.encode(format!("{}:{}", self.url.username(), password)) - )) - } - } else { - None - } + url_authentication(&self.url) } /// Method to init the ssl context out of the ssl target configuration. diff --git a/prosa_book/init.sh b/prosa_book/init.sh index 4847748..b8092a2 100755 --- a/prosa_book/init.sh +++ b/prosa_book/init.sh @@ -3,7 +3,7 @@ PROSA_DIR=`dirname $0` # Download puppet documentation to add it -if [ ! -f $PROSA_DIR/src/ch01-03-puppet.md ]; then curl -H 'Content-type:text/html' https://raw.githubusercontent.com/worldline/Puppet-ProSA/refs/heads/main/README.md -o $PROSA_DIR/src/ch01-03-puppet.md; fi +if [ ! -f $PROSA_DIR/src/ch01-04-puppet.md ]; then curl -H 'Content-type:text/html' https://raw.githubusercontent.com/worldline/Puppet-ProSA/refs/heads/main/README.md -o $PROSA_DIR/src/ch01-04-puppet.md; fi # Set ProSA version VERSION=`grep -oP '(?<=^version = ").*(?=")' $PROSA_DIR/../prosa/Cargo.toml` diff --git a/prosa_book/src/SUMMARY.md b/prosa_book/src/SUMMARY.md index 80cf4f6..f846bb9 100644 --- a/prosa_book/src/SUMMARY.md +++ b/prosa_book/src/SUMMARY.md @@ -11,10 +11,11 @@ - [SSL](ch01-02-02-ssl.md) - [Stream](ch01-02-03-stream.md) - [Run ProSA](ch01-02-04-run.md) - - [Puppet](ch01-03-puppet.md) - - [Cloud](ch01-04-cloud.md) - - [GCP - Cloud Run](ch01-04-01-gcp-cloud_run.md) - - [Clever Cloud](ch01-04-02-clever_cloud.md) + - [Monitoring](ch01-03-monitoring.md) + - [Puppet](ch01-04-puppet.md) + - [Cloud](ch01-05-cloud.md) + - [GCP - Cloud Run](ch01-05-01-gcp-cloud_run.md) + - [Clever Cloud](ch01-05-02-clever_cloud.md) - [Adaptor](ch02-00-adaptor.md) - [TVF](ch02-01-tvf.md) diff --git a/prosa_book/src/ch00-00-prosa.md b/prosa_book/src/ch00-00-prosa.md index f6512c6..365e968 100644 --- a/prosa_book/src/ch00-00-prosa.md +++ b/prosa_book/src/ch00-00-prosa.md @@ -22,4 +22,4 @@ In fact, ProSA contains no direct references to Worldline's private properties. ## Version -This book is intended for version 0.4.0 of ProSA. +This book is intended for version 0.4.1 of ProSA. diff --git a/prosa_book/src/ch01-02-01-observability.md b/prosa_book/src/ch01-02-01-observability.md index 463b2b1..cc149d8 100644 --- a/prosa_book/src/ch01-02-01-observability.md +++ b/prosa_book/src/ch01-02-01-observability.md @@ -12,6 +12,35 @@ You can also configure your processor to act as a server that exposes those metr Of course all configurations can be mixed. You can send your logs to an OpenTelemetry collector and to stdout simultaneously. +### Attributes + +For each of your observability data, you can configure attribute that will add labels on your data. + +These attribute should follow the [OpenTelemetry resource conventions](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/resource/README.md). + +Some of these attributtes are automaticcaly field from ProSA depending of your environment: +- `service.name` took from _prosa name_ +- `host.arch` if detected from the compilation +- `os.type` if the OS was detected +- `service.version` the package version + +For your logs and traces (but not metrics to avoid overloading metrics indexes), you'll find: +- `process.creation.time` +- `process.pid` + +In the configuration you'll have: +```yaml +observability: + attributes: + # Override the service.name from ProSA + service.name: "my_service" + # Overried the version + service.version: "1.0.0" + metric: # metrics params + traces: # traces params + logs: # logs params +``` + ### Stdout If you want to direct all logs to stdout, you can do something like this: @@ -117,3 +146,5 @@ observability: prometheus: endpoint: "0.0.0.0:9090" ``` + +> You also need to enable the feature `prometheus` for ProSA. diff --git a/prosa_book/src/ch01-03-monitoring-grafana_graph.png b/prosa_book/src/ch01-03-monitoring-grafana_graph.png new file mode 100755 index 0000000..ed8e082 Binary files /dev/null and b/prosa_book/src/ch01-03-monitoring-grafana_graph.png differ diff --git a/prosa_book/src/ch01-03-monitoring-grafana_trace.png b/prosa_book/src/ch01-03-monitoring-grafana_trace.png new file mode 100755 index 0000000..8ecce05 Binary files /dev/null and b/prosa_book/src/ch01-03-monitoring-grafana_trace.png differ diff --git a/prosa_book/src/ch01-03-monitoring.md b/prosa_book/src/ch01-03-monitoring.md new file mode 100644 index 0000000..4f505ee --- /dev/null +++ b/prosa_book/src/ch01-03-monitoring.md @@ -0,0 +1,57 @@ +# Monitoring + +If you configured [Observability](ch01-02-01-observability.md), ProSA offers observability data out of the box. + +## Metrics + +Metrics are useful in processors. +To know more, refer to the processor you use to know which metrics they expose. +They should export [metrics](ch02-04-observability.md#metrics). + +But ProSA has also its own metrics to have a global view of what's running. +This generic dashboard will be available soon from Grafana if you want to import it. + +### Graphical representation + +Having a view of every processor and service is important to know if your processors are healthy and if your services are up and running. +There is a node graph to view the entire state of ProSA: + +![Grafana Graph](ch01-03-monitoring-grafana_graph.png) + +From this graph, you'll see: +- Processors 🔄 + - green indicates that they are running + - orange if they experienced at least one restart + - grey if they are stopped + - red if they crashed +- Services ⠶ + - green if at least one processor exposes it + - grey if they are not available +- Links between processors and services representing processors that expose a service + +From that graph, you have a complete view of your ProSA state. + +### System metrics + +ProSA provides RAM metrics in order to keep track of process allocation. +This could be useful to know if on a VM ProSA is using the RAM, or it's another process on the host. + +It provides 2 `type` of metrics: +- `virtual` for virtual RAM used +- `physical` for physical RAM used + +> To have system metrics, you need to enable the feature `system-metrics` for ProSA. + +## Traces + +Since ProSA is a transactional framework, traces are a must-have to view transaction-relative information. + +![Grafana Trace](ch01-03-monitoring-grafana_trace.png) + +From this global trace, you can have [processor spans](ch02-04-observability.md#traces) attached to it. + +## Logs + +All logs generated by ProSA are available. +Normally processors should use the ProSA [logging](ch02-04-observability.md#logs) system. +If they do not, please refer to your processor to know how to handle them. diff --git a/prosa_book/src/ch01-04-01-gcp-cloud_run.md b/prosa_book/src/ch01-05-01-gcp-cloud_run.md similarity index 100% rename from prosa_book/src/ch01-04-01-gcp-cloud_run.md rename to prosa_book/src/ch01-05-01-gcp-cloud_run.md diff --git a/prosa_book/src/ch01-04-02-clever_cloud.md b/prosa_book/src/ch01-05-02-clever_cloud.md similarity index 100% rename from prosa_book/src/ch01-04-02-clever_cloud.md rename to prosa_book/src/ch01-05-02-clever_cloud.md diff --git a/prosa_book/src/ch01-04-cloud.md b/prosa_book/src/ch01-05-cloud.md similarity index 94% rename from prosa_book/src/ch01-04-cloud.md rename to prosa_book/src/ch01-05-cloud.md index 56ac6d6..9f82a0f 100644 --- a/prosa_book/src/ch01-04-cloud.md +++ b/prosa_book/src/ch01-05-cloud.md @@ -10,13 +10,13 @@ Most of the time, there are PaaS offerings that work with Docker containers and To build a Docker image for your ProSA, refer to the [Cargo-ProSA Container](ch01-01-cargo-prosa.md#container) Select a base image that suits your PaaS requirements and push the generated image to your cloud repository. -You'll find an example in the subsection for [GCP Cloud Run](ch01-04-01-gcp-cloud_run.md) +You'll find an example in the subsection for [GCP Cloud Run](ch01-05-01-gcp-cloud_run.md) ## Rust runtime If your PaaS allows you to use the Rust runtime to run ProSA, you need to use the project generated by [Cargo-ProSA](ch01-01-cargo-prosa.html#use). -For an example, refer to the subsection for [Clever Cloud](ch01-04-02-clever_cloud.md) +For an example, refer to the subsection for [Clever Cloud](ch01-05-02-clever_cloud.md) [^paas]: Platform as a service - Run ProSA as a software without worrying about hardware, system, or infrastructure. diff --git a/prosa_macros/src/settings.rs b/prosa_macros/src/settings.rs index db989fd..0f3c5f7 100644 --- a/prosa_macros/src/settings.rs +++ b/prosa_macros/src/settings.rs @@ -243,6 +243,7 @@ fn generate_struct_impl_settings( } fn set_prosa_name(&mut self, name: std::string::String) { + self.observability.set_prosa_name(name.as_ref()); self.name = Some(name); } diff --git a/prosa_utils/Cargo.toml b/prosa_utils/Cargo.toml index 4f0fdee..9f15fd5 100644 --- a/prosa_utils/Cargo.toml +++ b/prosa_utils/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "prosa-utils" -version = "0.4.0" +version = "0.4.1" authors.workspace = true description = "ProSA utils" homepage.workspace = true @@ -10,15 +10,14 @@ license.workspace = true include.workspace = true [features] -default = ["full"] +default = ["msg", "config", "config-openssl", "config-observability"] msg = [] -config = ["dep:glob","dep:serde","dep:serde_yaml"] +config = ["dep:glob","dep:serde","dep:serde_yaml", "dep:base64"] config-openssl = ["config", "dep:openssl"] config-openssl-vendored = ["config-openssl", "openssl/vendored"] -config-observability = ["dep:log", "dep:tracing-core", "dep:tracing-subscriber", "dep:tracing-opentelemetry", "dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetry-stdout", "dep:opentelemetry-otlp", "dep:opentelemetry-appender-tracing", "dep:prometheus"] -config-observability-prometheus = ["config-observability", "dep:opentelemetry-prometheus", "dep:tokio", "dep:hyper", "dep:http-body-util", "dep:hyper-util"] +config-observability = ["dep:log", "dep:tracing-core", "dep:tracing-subscriber", "dep:tracing-opentelemetry", "dep:opentelemetry", "dep:opentelemetry_sdk", "dep:opentelemetry-stdout", "dep:opentelemetry-otlp", "dep:opentelemetry-appender-tracing"] +config-observability-prometheus = ["config-observability", "dep:prometheus", "dep:opentelemetry-prometheus", "dep:tokio", "dep:hyper", "dep:http-body-util", "dep:hyper-util"] queue = [] -full = ["msg", "config", "config-openssl", "config-observability", "config-observability-prometheus"] [package.metadata.docs.rs] all-features = true @@ -38,6 +37,7 @@ hex = "0.4" glob = { version = "0.3", optional = true } serde = { workspace = true, optional = true } serde_yaml = { version = "0.9", optional = true } +base64 = { version = "0.22", optional = true } # Config OpenSSL openssl = { workspace = true, optional = true } @@ -46,14 +46,14 @@ openssl = { workspace = true, optional = true } log = { workspace = true, optional = true } tracing-core = { version = "0.1", optional = true } tracing-subscriber = { version = "0.3", features = ["std", "env-filter"], optional = true } -tracing-opentelemetry = { version = "0.30", optional = true } +tracing-opentelemetry = { version = "0.32.1", optional = true } opentelemetry = { workspace = true, optional = true } opentelemetry_sdk = { workspace = true, optional = true } -opentelemetry-stdout = { version = "0.29", optional = true, features = ["metrics", "trace", "logs"]} -opentelemetry-otlp = { version = "0.29", optional = true, features = ["metrics", "trace", "logs", "grpc-tonic"]} +opentelemetry-stdout = { version = "0.31.0", optional = true, features = ["metrics", "trace", "logs"]} +opentelemetry-otlp = { version = "0.31.0", optional = true, features = ["metrics", "trace", "logs", "grpc-tonic"]} prometheus = { workspace = true, optional = true } -opentelemetry-prometheus = { version = "0.29", optional = true } -opentelemetry-appender-tracing = { version = "0.29", optional = true } +opentelemetry-prometheus = { version = "0.31.0", optional = true } +opentelemetry-appender-tracing = { version = "0.31.0", optional = true } # Web Observability tokio = { workspace = true, optional = true } diff --git a/prosa_utils/src/config.rs b/prosa_utils/src/config.rs index 4518a9a..60658a7 100644 --- a/prosa_utils/src/config.rs +++ b/prosa_utils/src/config.rs @@ -4,9 +4,11 @@ #![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/doc_assets/settings.svg"))] //! -use std::path::PathBuf; +use std::{path::PathBuf, process::Command}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; use thiserror::Error; +use url::Url; // Feature openssl or rusttls,... pub mod ssl; @@ -40,7 +42,7 @@ pub enum ConfigError { OpenSsl(#[from] openssl::error::ErrorStack), } -/// Method to get the country name from the OS +/// Method to try get the country name from the OS pub fn os_country() -> Option { if let Some(lang) = option_env!("LANG") { let language = if let Some(pos) = lang.find('.') { @@ -57,6 +59,70 @@ pub fn os_country() -> Option { None } +/// Method to try get the hostname from the OS +pub fn hostname() -> Option { + #[cfg(target_family = "unix")] + if let Some(host) = option_env!("HOSTNAME").map(str::trim) + && !host.is_empty() + && !host.contains('\n') + { + return Some(String::from(host)); + } + + #[cfg(target_family = "unix")] + return Command::new("hostname") + .arg("-s") + .output() + .ok() + .and_then(|h| { + str::from_utf8(h.stdout.trim_ascii()) + .ok() + .filter(|h| !h.is_empty() && !h.contains('\n')) + .map(|h| h.to_string()) + }); + + #[cfg(target_family = "windows")] + return Command::new("hostname").output().ok().and_then(|h| { + str::from_utf8(h.stdout.trim_ascii()) + .ok() + .filter(|h| !h.is_empty() && !h.contains('\n')) + .map(|h| h.to_string()) + }); + + #[cfg(all(not(target_family = "unix"), not(target_family = "windows")))] + return None; +} + +/// Method to get authentication value out of URL username/password +/// +/// - If user password is provided, it return *Basic* authentication with base64 encoded username:password +/// - If only password is provided, it return *Bearer* authentication with the password as token +/// +/// ``` +/// use url::Url; +/// use prosa_utils::config::url_authentication; +/// +/// let basic_auth_target = Url::parse("http://user:pass@localhost:8080").unwrap(); +/// assert_eq!(Some(String::from("Basic dXNlcjpwYXNz")), url_authentication(&basic_auth_target)); +/// +/// let bearer_auth_target = Url::parse("http://:token@localhost:8080").unwrap(); +/// assert_eq!(Some(String::from("Bearer token")), url_authentication(&bearer_auth_target)); +/// ``` +pub fn url_authentication(url: &Url) -> Option { + if let Some(password) = url.password() { + if url.username().is_empty() { + Some(format!("Bearer {password}")) + } else { + Some(format!( + "Basic {}", + STANDARD.encode(format!("{}:{}", url.username(), password)) + )) + } + } else { + None + } +} + #[cfg(test)] mod tests { use super::*; @@ -68,4 +134,30 @@ mod tests { assert_eq!(2, cn.len()); } } + + #[test] + fn test_hostname() { + let host = hostname(); + if let Some(hn) = host { + assert!(!hn.is_empty()); + } + } + + #[test] + fn test_url_authentication_basic() { + let basic_auth_target = Url::parse("http://user:pass@localhost:8080").unwrap(); + assert_eq!( + Some(String::from("Basic dXNlcjpwYXNz")), + url_authentication(&basic_auth_target) + ); + } + + #[test] + fn test_url_authentication_bearer() { + let bearer_auth_target = Url::parse("http://:token@localhost:8080").unwrap(); + assert_eq!( + Some(String::from("Bearer token")), + url_authentication(&bearer_auth_target) + ); + } } diff --git a/prosa_utils/src/config/observability.rs b/prosa_utils/src/config/observability.rs index fbb3236..f3e2c15 100644 --- a/prosa_utils/src/config/observability.rs +++ b/prosa_utils/src/config/observability.rs @@ -1,53 +1,83 @@ //! Definition of Opentelemetry configuration -use opentelemetry::trace::TracerProvider as _; -use opentelemetry_otlp::{ExportConfig, ExporterBuildError, Protocol, WithExportConfig}; +use opentelemetry::{KeyValue, trace::TracerProvider as _}; +use opentelemetry_otlp::{ + ExportConfig, ExporterBuildError, Protocol, WithExportConfig, WithHttpConfig, +}; use opentelemetry_sdk::{ logs::SdkLoggerProvider, - metrics::{PeriodicReader, SdkMeterProvider}, + metrics::SdkMeterProvider, trace::{SdkTracerProvider, Tracer}, }; use serde::{Deserialize, Serialize}; -use std::time::Duration; +use std::{collections::HashMap, time::Duration}; use tracing_subscriber::{filter, prelude::*}; use tracing_subscriber::{layer::SubscriberExt, util::TryInitError}; use url::Url; +use crate::config::url_authentication; + use super::tracing::{TelemetryFilter, TelemetryLevel}; /// Configuration struct of an **O**pen **T**e**l**emetry **P**rotocol Exporter #[derive(Debug, Deserialize, Serialize, Clone)] pub(crate) struct OTLPExporterCfg { pub(crate) level: Option, - #[serde(default = "OTLPExporterCfg::get_default_name")] - name: String, endpoint: Url, #[serde(skip_serializing)] timeout_sec: Option, } impl OTLPExporterCfg { - pub(crate) const DEFAULT_TRACER_NAME: &'static str = "prosa"; - - fn get_default_name() -> String { - Self::DEFAULT_TRACER_NAME.into() + pub(crate) fn get_protocol(&self) -> Protocol { + match self.endpoint.scheme().to_lowercase().as_str() { + "grpc" => Protocol::Grpc, + "http/json" => Protocol::HttpJson, + _ => Protocol::HttpBinary, + } } - pub(crate) fn get_protocol(&self) -> Protocol { - if self.endpoint.scheme().to_lowercase() == "grpc" { - Protocol::Grpc - } else { - Protocol::HttpBinary + pub(crate) fn get_header(&self) -> HashMap { + let mut headers = HashMap::with_capacity(1); + if let Some(authorization) = url_authentication(&self.endpoint) { + headers.insert("Authorization".to_string(), authorization); } + headers + } + + pub(crate) fn get_resource( + &self, + attr: Vec, + ) -> opentelemetry_sdk::resource::Resource { + opentelemetry_sdk::resource::Resource::builder() + .with_attributes(attr) + .with_attribute(opentelemetry::KeyValue::new( + "process.creation.time", + chrono::Utc::now().to_rfc3339(), + )) + .with_attribute(opentelemetry::KeyValue::new( + "process.pid", + opentelemetry::Value::I64(std::process::id() as i64), + )) + .build() } } impl From for ExportConfig { fn from(value: OTLPExporterCfg) -> Self { + let protocol = value.get_protocol(); + let mut endpoint = value.endpoint; + if !endpoint.username().is_empty() { + let _ = endpoint.set_username(""); + } + if endpoint.password().is_some() { + let _ = endpoint.set_password(None); + } + ExportConfig { - endpoint: Some(value.endpoint.to_string()), + endpoint: Some(endpoint.to_string()), timeout: value.timeout_sec.map(Duration::from_secs), - protocol: value.get_protocol(), + protocol, } } } @@ -56,7 +86,6 @@ impl Default for OTLPExporterCfg { fn default() -> Self { Self { level: None, - name: Self::get_default_name(), endpoint: Url::parse("grpc://localhost:4317").unwrap(), timeout_sec: None, } @@ -124,6 +153,15 @@ impl PrometheusExporterCfg { Ok(()) } + + pub(crate) fn get_resource( + &self, + attr: Vec, + ) -> opentelemetry_sdk::resource::Resource { + opentelemetry_sdk::resource::Resource::builder() + .with_attributes(attr) + .build() + } } /// Configuration struct of an stdout exporter @@ -146,7 +184,8 @@ impl TelemetryMetrics { /// Build a meter provider based on the self configuration fn build_provider( &self, - registry: &prometheus::Registry, + #[cfg(feature = "config-observability-prometheus")] resource_attr: Vec, + #[cfg(feature = "config-observability-prometheus")] registry: &prometheus::Registry, ) -> Result { let mut meter_provider = SdkMeterProvider::builder(); if let Some(s) = &self.otlp { @@ -158,11 +197,11 @@ impl TelemetryMetrics { } else { opentelemetry_otlp::MetricExporter::builder() .with_http() + .with_headers(s.get_header()) .with_export_config(s.clone().into()) .build() }?; - let reader = PeriodicReader::builder(exporter).build(); - meter_provider = meter_provider.with_reader(reader); + meter_provider = meter_provider.with_periodic_exporter(exporter); } #[cfg(feature = "config-observability-prometheus")] @@ -170,11 +209,13 @@ impl TelemetryMetrics { // configure OpenTelemetry to use this registry let exporter = opentelemetry_prometheus::exporter() .with_registry(registry.clone()) + .with_resource_selector(opentelemetry_prometheus::ResourceSelector::All) .without_target_info() - .without_scope_info() .build() .map_err(|e| ExporterBuildError::InternalFailure(e.to_string()))?; - meter_provider = meter_provider.with_reader(exporter); + meter_provider = meter_provider + .with_resource(prom.get_resource(resource_attr)) + .with_reader(exporter); // Initialize the Prometheus server if needed prom.init_prometheus_server(registry)?; @@ -182,8 +223,7 @@ impl TelemetryMetrics { if self.stdout.is_some() { let exporter = opentelemetry_stdout::MetricExporter::default(); - let reader = PeriodicReader::builder(exporter).build(); - meter_provider = meter_provider.with_reader(reader); + meter_provider = meter_provider.with_periodic_exporter(exporter); } Ok(meter_provider.build()) @@ -220,6 +260,7 @@ impl TelemetryData { /// Build a logger provider based on the self configuration fn build_logger_provider( &self, + resource_attr: Vec, ) -> Result<(SdkLoggerProvider, TelemetryLevel), ExporterBuildError> { let logs_provider = SdkLoggerProvider::builder(); if let Some(s) = &self.otlp { @@ -231,11 +272,15 @@ impl TelemetryData { } else { opentelemetry_otlp::LogExporter::builder() .with_http() + .with_headers(s.get_header()) .with_export_config(s.clone().into()) .build() }?; Ok(( - logs_provider.with_batch_exporter(exporter).build(), + logs_provider + .with_resource(s.get_resource(resource_attr)) + .with_batch_exporter(exporter) + .build(), s.level.unwrap_or_default(), )) } else if let Some(stdout) = &self.stdout { @@ -251,7 +296,10 @@ impl TelemetryData { } /// Build a tracer provider based on the self configuration - fn build_tracer_provider(&self) -> Result { + fn build_tracer_provider( + &self, + resource_attr: Vec, + ) -> Result { let mut trace_provider = SdkTracerProvider::builder(); if let Some(s) = &self.otlp { let exporter = if s.get_protocol() == Protocol::Grpc { @@ -262,37 +310,27 @@ impl TelemetryData { } else { opentelemetry_otlp::SpanExporter::builder() .with_http() + .with_headers(s.get_header()) .with_export_config(s.clone().into()) .build() }?; - trace_provider = trace_provider.with_batch_exporter(exporter); + + trace_provider = trace_provider + .with_resource(s.get_resource(resource_attr)) + .with_batch_exporter(exporter); } Ok(trace_provider.build()) } /// Build a tracer provider based on the self configuration - fn build_tracer(&self) -> Result { - let mut trace_provider = SdkTracerProvider::builder(); - if let Some(s) = &self.otlp { - let exporter = if s.get_protocol() == Protocol::Grpc { - opentelemetry_otlp::SpanExporter::builder() - .with_tonic() - .with_export_config(s.clone().into()) - .build() - } else { - opentelemetry_otlp::SpanExporter::builder() - .with_http() - .with_export_config(s.clone().into()) - .build() - }?; - trace_provider = trace_provider.with_batch_exporter(exporter); - Ok(trace_provider.build().tracer(s.name.clone())) - } else { - Ok(trace_provider - .build() - .tracer(OTLPExporterCfg::DEFAULT_TRACER_NAME)) - } + fn build_tracer( + &self, + name: &str, + resource_attr: Vec, + ) -> Result { + self.build_tracer_provider(resource_attr) + .map(|p| p.tracer(name.to_string())) } } @@ -325,6 +363,11 @@ impl Default for TelemetryData { /// ``` #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Observability { + /// Name of ProSA from observability perspective + service_name: Option, + /// Additional attributes for all telemetry data + #[serde(default)] + attributes: HashMap, /// Global level for observability #[serde(default)] level: TelemetryLevel, @@ -337,9 +380,40 @@ pub struct Observability { } impl Observability { + pub(crate) fn common_scope_attributes(service_name: String, capacity: usize) -> Vec { + let mut scope_attributes = Vec::with_capacity(capacity + 3); + scope_attributes.push(KeyValue::new("service.name", service_name)); + + if cfg!(target_arch = "x86_64") { + scope_attributes.push(KeyValue::new("host.arch", "amd64")); + } else if cfg!(target_arch = "aarch64") { + scope_attributes.push(KeyValue::new("host.arch", "arm64")); + } else if cfg!(target_arch = "arm") { + scope_attributes.push(KeyValue::new("host.arch", "arm32")); + } + + if cfg!(target_os = "linux") { + scope_attributes.push(KeyValue::new("os.type", "linux")); + } else if cfg!(target_os = "macos") { + scope_attributes.push(KeyValue::new("os.type", "darwin")); + } else if cfg!(target_os = "freebsd") { + scope_attributes.push(KeyValue::new("os.type", "freebsd")); + } else if cfg!(target_os = "openbsd") { + scope_attributes.push(KeyValue::new("os.type", "openbsd")); + } else if cfg!(target_os = "netbsd") { + scope_attributes.push(KeyValue::new("os.type", "netbsd")); + } else if cfg!(target_os = "windows") { + scope_attributes.push(KeyValue::new("os.type", "windows")); + } + + scope_attributes + } + /// Create an observability object with inline parameter instead of getting it from an external configuration pub fn new(level: TelemetryLevel) -> Observability { Observability { + service_name: None, + attributes: HashMap::new(), level, metrics: Some(TelemetryMetrics::default()), logs: Some(TelemetryData::default()), @@ -347,6 +421,49 @@ impl Observability { } } + /// Setter of the ProSA name for all observability `service.name` attributes + pub fn set_prosa_name(&mut self, name: &str) { + if self.service_name.is_none() { + self.service_name = Some(name.to_string()); + } + } + + /// Getter of the common scope attributes + pub fn get_scope_attributes(&self) -> Vec { + // start with common attributes + let mut scope_attr = if let Some(service_name) = self.attributes.get("service.name") { + Self::common_scope_attributes(service_name.clone(), self.attributes.len() + 2) + } else { + Self::common_scope_attributes( + self.service_name.clone().unwrap_or("prosa".to_string()), + self.attributes.len() + 2, + ) + }; + + if !self.attributes.contains_key("host.name") + && let Some(hostname) = super::hostname() + { + scope_attr.push(KeyValue::new("host.name", hostname)); + } + + if !self.attributes.contains_key("service.version") { + scope_attr.push(KeyValue::new("service.version", env!("CARGO_PKG_VERSION"))); + } + + // append custom attributes from configuration + scope_attr.append( + self.attributes + .iter() + .map(|(k, v)| { + KeyValue::new(k.clone(), opentelemetry::Value::String(v.clone().into())) + }) + .collect::>() + .as_mut(), + ); + + scope_attr + } + /// Getter of the log level (max value) pub fn get_logger_level(&self) -> TelemetryLevel { if let Some(logs) = &self.logs { @@ -362,9 +479,22 @@ impl Observability { } /// Meter provider builder + #[cfg(feature = "config-observability-prometheus")] pub fn build_meter_provider(&self, registry: &prometheus::Registry) -> SdkMeterProvider { if let Some(settings) = &self.metrics { - settings.build_provider(registry).unwrap_or_default() + settings + .build_provider(self.get_scope_attributes(), registry) + .unwrap_or_default() + } else { + SdkMeterProvider::default() + } + } + + /// Meter provider builder + #[cfg(not(feature = "config-observability-prometheus"))] + pub fn build_meter_provider(&self) -> SdkMeterProvider { + if let Some(settings) = &self.metrics { + settings.build_provider().unwrap_or_default() } else { SdkMeterProvider::default() } @@ -373,7 +503,7 @@ impl Observability { /// Logger provider builder pub fn build_logger_provider(&self) -> (SdkLoggerProvider, TelemetryLevel) { if let Some(settings) = &self.logs { - match settings.build_logger_provider() { + match settings.build_logger_provider(self.get_scope_attributes()) { Ok(m) => m, Err(_) => ( SdkLoggerProvider::builder().build(), @@ -401,7 +531,9 @@ impl Observability { /// ``` pub fn build_tracer_provider(&self) -> SdkTracerProvider { if let Some(settings) = &self.traces { - settings.build_tracer_provider().unwrap_or_default() + settings + .build_tracer_provider(self.get_scope_attributes()) + .unwrap_or_default() } else { SdkTracerProvider::default() } @@ -419,12 +551,17 @@ impl Observability { /// ``` pub fn build_tracer(&self) -> Tracer { if let Some(settings) = &self.traces { - match settings.build_tracer() { + match settings.build_tracer( + self.service_name.as_deref().unwrap_or("prosa"), + self.get_scope_attributes(), + ) { Ok(m) => m, - Err(_) => SdkTracerProvider::default().tracer(OTLPExporterCfg::DEFAULT_TRACER_NAME), + Err(_) => SdkTracerProvider::default() + .tracer(self.service_name.clone().unwrap_or("prosa".to_string())), } } else { - SdkTracerProvider::default().tracer(OTLPExporterCfg::DEFAULT_TRACER_NAME) + SdkTracerProvider::default() + .tracer(self.service_name.clone().unwrap_or("prosa".to_string())) } } @@ -461,7 +598,8 @@ impl Observability { subscriber.try_init() } } else if let Some(logs) = &self.logs - && let Ok((logger_provider, level)) = logs.build_logger_provider() + && let Ok((logger_provider, level)) = + logs.build_logger_provider(self.get_scope_attributes()) && level > TelemetryLevel::OFF { let logger_filter = filter.clone_with_level(level); @@ -482,6 +620,8 @@ impl Observability { impl Default for Observability { fn default() -> Self { Self { + service_name: None, + attributes: HashMap::new(), level: TelemetryLevel::default(), metrics: Some(TelemetryMetrics::default()), logs: Some(TelemetryData {