1#![allow(
7 unsafe_code,
8 missing_docs,
9 clippy::missing_errors_doc,
10 clippy::new_without_default
11)]
12
13#[cfg(not(target_arch = "wasm32"))]
15pub mod api_key_decorator;
16#[cfg(not(target_arch = "wasm32"))]
17pub mod composite_event_sink;
18#[cfg(not(target_arch = "wasm32"))]
19mod env_config;
20#[cfg(not(target_arch = "wasm32"))]
21pub mod http_event_sink;
22#[cfg(not(target_arch = "wasm32"))]
23pub mod local_event_sink;
24#[cfg(not(target_arch = "wasm32"))]
25pub mod log_interop;
26#[cfg(not(target_arch = "wasm32"))]
27pub mod oidc_client_credentials_decorator;
28#[cfg(not(target_arch = "wasm32"))]
29pub mod request_decorator;
30#[cfg(not(target_arch = "wasm32"))]
31pub mod stream_block;
32#[cfg(not(target_arch = "wasm32"))]
33pub mod stream_info;
34#[cfg(not(target_arch = "wasm32"))]
35pub mod system_monitor;
36#[cfg(not(target_arch = "wasm32"))]
37pub mod tracing_interop;
38
39#[cfg(feature = "jemalloc")]
44pub mod jemalloc_conf;
45
46#[cfg(target_arch = "wasm32")]
48mod console_event_sink;
49#[cfg(target_arch = "wasm32")]
50pub use console_event_sink::*;
51
52#[cfg(not(target_arch = "wasm32"))]
54mod native {
55 use std::any::TypeId;
56 use std::collections::HashMap;
57 use std::str::FromStr;
58 use std::sync::{Arc, Mutex, Weak};
59
60 use crate::env_config::{env_override, resolve};
61 use crate::log_interop::install_log_interop;
62 use crate::request_decorator::RequestDecorator;
63 use crate::tracing_interop::install_tracing_interop;
64 use micromegas_telemetry::property_names::RESERVED_PROPERTY_PREFIX;
65 use micromegas_tracing::event::BoxedEventSink;
66 use micromegas_tracing::info;
67 use micromegas_tracing::{
68 event::EventSink,
69 guards::{TracingSystemGuard, TracingThreadGuard},
70 prelude::*,
71 };
72
73 use crate::composite_event_sink::CompositeSink;
74 use crate::http_event_sink::{HttpEventSink, HttpSinkConfig};
75 use crate::local_event_sink::LocalEventSink;
76 use crate::system_monitor::spawn_system_monitor;
77
78 pub mod tokio_retry {
79 pub use tokio_retry2::*;
80 }
81
82 pub mod reqwest {
83 pub use reqwest::*;
84 }
85
86 pub const PROCESS_PROPERTIES_ENV_VAR: &str = "MICROMEGAS_PROCESS_PROPERTIES";
90
91 pub const LOCAL_SINK_MAX_LEVEL_ENV_VAR: &str = "MICROMEGAS_LOCAL_SINK_MAX_LEVEL";
99
100 pub const TELEMETRY_SINK_MAX_LEVEL_ENV_VAR: &str = "MICROMEGAS_TELEMETRY_SINK_MAX_LEVEL";
107
108 pub const DEFAULT_LOCAL_SINK_MAX_LEVEL: LevelFilter = LevelFilter::Info;
111
112 pub const DEFAULT_TELEMETRY_SINK_MAX_LEVEL: LevelFilter = LevelFilter::Debug;
115
116 const MAX_QUEUE_BYTES_ENV_VAR: &str = "MICROMEGAS_TELEMETRY_MAX_QUEUE_BYTES";
117 const HARD_QUEUE_BYTES_ENV_VAR: &str = "MICROMEGAS_TELEMETRY_HARD_QUEUE_BYTES";
118 const MAX_IN_FLIGHT_REQUESTS_ENV_VAR: &str = "MICROMEGAS_TELEMETRY_MAX_IN_FLIGHT_REQUESTS";
119 const REQUEST_TIMEOUT_SECS_ENV_VAR: &str = "MICROMEGAS_TELEMETRY_REQUEST_TIMEOUT_SECS";
120
121 pub fn merge_process_properties(
141 properties: &mut HashMap<String, String>,
142 raw: &str,
143 ) -> anyhow::Result<()> {
144 use anyhow::Context;
145
146 let mut parsed = Vec::new();
147 for entry in raw.split(',').map(str::trim).filter(|e| !e.is_empty()) {
148 let (key, value) = entry.split_once('=').with_context(|| {
149 format!("invalid {PROCESS_PROPERTIES_ENV_VAR} entry {entry:?}: expected key=value")
150 })?;
151 let (key, value) = (key.trim(), value.trim());
152 if key.is_empty() {
153 anyhow::bail!("invalid {PROCESS_PROPERTIES_ENV_VAR} entry {entry:?}: empty key");
154 }
155 if key.starts_with(RESERVED_PROPERTY_PREFIX) {
156 anyhow::bail!(
157 "invalid {PROCESS_PROPERTIES_ENV_VAR} entry {entry:?}: the \
158 {RESERVED_PROPERTY_PREFIX} namespace is server-written only and \
159 would be stripped on ingestion"
160 );
161 }
162 parsed.push((key.to_string(), value.to_string()));
163 }
164 for (key, value) in parsed {
165 properties.entry(key).or_insert(value);
166 }
167 Ok(())
168 }
169
170 pub struct TelemetryGuardBuilder {
171 logs_buffer_size: usize,
172 metrics_buffer_size: usize,
173 threads_buffer_size: usize,
174 target_max_levels: HashMap<String, String>,
175 max_level_override: Option<LevelFilter>,
176 interop_max_level_override: Option<LevelFilter>,
177 install_log_capture: bool,
178 install_tracing_capture: bool,
179 local_sink_enabled: bool,
180 local_sink_max_level: Option<LevelFilter>,
181 telemetry_sink_url: Option<String>,
182 telemetry_sink_max_level: Option<LevelFilter>,
183 telemetry_max_queue_bytes: Option<usize>,
184 telemetry_hard_queue_bytes: Option<usize>,
185 telemetry_max_in_flight_requests: Option<usize>,
186 telemetry_request_timeout: Option<std::time::Duration>,
187 telemetry_retry_by_priority:
188 Option<[core::iter::Take<tokio_retry::strategy::ExponentialBackoff>; 4]>,
189 telemetry_make_request_decorator: Box<dyn FnOnce() -> Arc<dyn RequestDecorator> + Send>,
190 extra_sinks: HashMap<TypeId, (LevelFilter, BoxedEventSink)>,
191 system_metrics_enabled: bool,
192 default_system_properties_enabled: bool,
193 process_properties: HashMap<String, String>,
194 }
195
196 impl Default for TelemetryGuardBuilder {
197 fn default() -> Self {
198 Self {
199 logs_buffer_size: 10 * 1024 * 1024,
200 metrics_buffer_size: 1024 * 1024,
201 threads_buffer_size: 10 * 1024 * 1024,
202 local_sink_enabled: true,
203 local_sink_max_level: None,
204 telemetry_sink_url: None,
205 telemetry_sink_max_level: None,
206 telemetry_max_queue_bytes: None,
207 telemetry_hard_queue_bytes: None,
208 telemetry_max_in_flight_requests: None,
209 telemetry_request_timeout: None,
210 telemetry_retry_by_priority: None,
211 telemetry_make_request_decorator: Box::new(|| {
212 Arc::new(crate::request_decorator::TrivialRequestDecorator {})
213 }),
214 target_max_levels: HashMap::default(),
215 max_level_override: None,
216 interop_max_level_override: None,
217 install_log_capture: false,
218 install_tracing_capture: true,
219 extra_sinks: HashMap::default(),
220 system_metrics_enabled: true,
221 default_system_properties_enabled: true,
222 process_properties: HashMap::default(),
223 }
224 }
225 }
226
227 impl TelemetryGuardBuilder {
228 #[must_use]
230 pub fn add_sink<Sink>(mut self, max_level: LevelFilter, sink: Sink) -> Self
231 where
232 Sink: EventSink + 'static,
233 {
234 let type_id = TypeId::of::<Sink>();
235
236 self.extra_sinks
237 .entry(type_id)
238 .or_insert_with(|| (max_level, Box::new(sink)));
239
240 self
241 }
242
243 #[must_use]
245 pub fn with_max_level_override(mut self, level_filter: LevelFilter) -> Self {
246 self.max_level_override = Some(level_filter);
247 self
248 }
249
250 #[must_use]
251 pub fn with_local_sink_enabled(mut self, enabled: bool) -> Self {
252 self.local_sink_enabled = enabled;
253 self
254 }
255
256 #[must_use]
257 pub fn with_interop_max_level_override(mut self, level_filter: LevelFilter) -> Self {
258 self.interop_max_level_override = Some(level_filter);
259 self
260 }
261
262 #[must_use]
263 pub fn with_install_log_capture(mut self, enabled: bool) -> Self {
264 self.install_log_capture = enabled;
265 self
266 }
267
268 #[must_use]
269 pub fn with_install_tracing_capture(mut self, enabled: bool) -> Self {
270 self.install_tracing_capture = enabled;
271 self
272 }
273
274 #[must_use]
279 pub fn with_local_sink_max_level(mut self, level_filter: LevelFilter) -> Self {
280 self.local_sink_max_level = Some(level_filter);
281 self
282 }
283
284 #[must_use]
289 pub fn with_telemetry_sink_max_level(mut self, level_filter: LevelFilter) -> Self {
290 self.telemetry_sink_max_level = Some(level_filter);
291 self
292 }
293
294 #[must_use]
295 pub fn with_ctrlc_handling(self) -> Self {
296 ctrlc::set_handler(move || {
297 info!("Ctrl+C was hit!");
298 micromegas_tracing::guards::shutdown_telemetry();
299 std::process::exit(1);
300 })
301 .expect("Error setting Ctrl+C handler");
302 self
303 }
304
305 #[must_use]
311 pub fn with_max_queue_bytes(mut self, bytes: usize) -> Self {
312 self.telemetry_max_queue_bytes = Some(bytes);
313 self
314 }
315
316 #[must_use]
322 pub fn with_hard_queue_bytes(mut self, bytes: usize) -> Self {
323 self.telemetry_hard_queue_bytes = Some(bytes);
324 self
325 }
326
327 #[must_use]
332 pub fn with_max_in_flight_requests(mut self, max_in_flight_requests: usize) -> Self {
333 self.telemetry_max_in_flight_requests = Some(max_in_flight_requests);
334 self
335 }
336
337 #[must_use]
346 pub fn with_request_timeout(mut self, timeout: std::time::Duration) -> Self {
347 self.telemetry_request_timeout = Some(timeout);
348 self
349 }
350
351 #[must_use]
355 pub fn with_retry_by_priority(
356 mut self,
357 retry_by_priority: [core::iter::Take<tokio_retry::strategy::ExponentialBackoff>; 4],
358 ) -> Self {
359 self.telemetry_retry_by_priority = Some(retry_by_priority);
360 self
361 }
362
363 #[must_use]
364 pub fn with_request_decorator(
365 mut self,
366 make_decorator: Box<dyn FnOnce() -> Arc<dyn RequestDecorator> + Send>,
367 ) -> Self {
368 self.telemetry_make_request_decorator = make_decorator;
369 self
370 }
371
372 #[must_use]
397 pub fn with_auth_from_env(mut self) -> Self {
398 use crate::api_key_decorator::ApiKeyRequestDecorator;
399 use crate::oidc_client_credentials_decorator::OidcClientCredentialsDecorator;
400
401 if let Ok(decorator) = ApiKeyRequestDecorator::from_env() {
403 info!("Configured telemetry sink with API key authentication");
404 self.telemetry_make_request_decorator = Box::new(move || Arc::new(decorator));
405 return self;
406 }
407
408 if let Ok(decorator) = OidcClientCredentialsDecorator::from_env() {
410 info!("Configured telemetry sink with OIDC client credentials authentication");
411 self.telemetry_make_request_decorator = Box::new(move || Arc::new(decorator));
412 return self;
413 }
414
415 info!(
417 "Telemetry sink authentication not configured - sending unauthenticated requests"
418 );
419 self
420 }
421
422 #[must_use]
423 pub fn with_system_metrics_enabled(mut self, enabled: bool) -> Self {
424 self.system_metrics_enabled = enabled;
425 self
426 }
427
428 #[must_use]
429 pub fn with_default_system_properties_enabled(mut self, enabled: bool) -> Self {
430 self.default_system_properties_enabled = enabled;
431 self
432 }
433
434 #[must_use]
439 pub fn with_telemetry_sink_url(mut self, url: String) -> Self {
440 self.telemetry_sink_url = Some(url);
441 self
442 }
443
444 #[must_use]
450 pub fn with_process_property(mut self, key: String, value: String) -> Self {
451 self.process_properties.insert(key, value);
452 self
453 }
454
455 #[must_use]
461 pub fn with_process_properties(
462 mut self,
463 process_properties: HashMap<String, String>,
464 ) -> Self {
465 self.process_properties.extend(process_properties);
466 self
467 }
468
469 fn populate_default_system_properties(&mut self) {
470 let props = &mut self.process_properties;
471 type DefaultProperties = Vec<(&'static str, Box<dyn FnOnce() -> String>)>;
473 let defaults: DefaultProperties = vec![
474 (
475 "exe",
476 Box::new(|| {
477 std::env::current_exe()
478 .unwrap_or_default()
479 .to_string_lossy()
480 .into_owned()
481 }),
482 ),
483 ("username", Box::new(whoami::username)),
484 ("realname", Box::new(whoami::realname)),
485 ("computer", Box::new(whoami::devicename)),
486 ("distro", Box::new(whoami::distro)),
487 (
488 "cpu_brand",
489 Box::new(|| {
490 #[cfg(target_arch = "x86_64")]
491 {
492 raw_cpuid::CpuId::new()
493 .get_processor_brand_string()
494 .map_or_else(|| "unknown".to_owned(), |b| b.as_str().to_owned())
495 }
496 #[cfg(not(target_arch = "x86_64"))]
497 {
498 String::from(std::env::consts::ARCH)
499 }
500 }),
501 ),
502 (
503 "physical_core_count",
504 Box::new(|| {
505 sysinfo::System::physical_core_count()
506 .map(|c: usize| c.to_string())
507 .unwrap_or_default()
508 }),
509 ),
510 (
511 "logical_cpu_count",
512 Box::new(|| {
513 use sysinfo::{CpuRefreshKind, RefreshKind};
514 let system = sysinfo::System::new_with_specifics(
515 RefreshKind::nothing().with_cpu(CpuRefreshKind::nothing()),
516 );
517 system.cpus().len().to_string()
518 }),
519 ),
520 (
521 "total_memory",
522 Box::new(|| {
523 use sysinfo::{MemoryRefreshKind, RefreshKind};
524 let system = sysinfo::System::new_with_specifics(
525 RefreshKind::nothing()
526 .with_memory(MemoryRefreshKind::nothing().with_ram()),
527 );
528 system.total_memory().to_string()
529 }),
530 ),
531 ];
532 for (key, make_value) in defaults {
533 props.entry(key.to_string()).or_insert_with(make_value);
534 }
535 }
536
537 pub fn build(mut self) -> anyhow::Result<TelemetryGuard> {
538 if let Some(raw) = std::env::var_os(PROCESS_PROPERTIES_ENV_VAR) {
544 merge_process_properties(&mut self.process_properties, &raw.to_string_lossy())?;
545 }
546 if self.default_system_properties_enabled {
547 self.populate_default_system_properties();
548 }
549 const LEVEL_EXPECTED: &str = "one of off, fatal, error, warn, info, debug, trace";
554 let local_sink_max_level = resolve(
555 self.local_sink_max_level,
556 LOCAL_SINK_MAX_LEVEL_ENV_VAR,
557 DEFAULT_LOCAL_SINK_MAX_LEVEL,
558 LEVEL_EXPECTED,
559 )?;
560 let telemetry_sink_max_level = resolve(
561 self.telemetry_sink_max_level,
562 TELEMETRY_SINK_MAX_LEVEL_ENV_VAR,
563 DEFAULT_TELEMETRY_SINK_MAX_LEVEL,
564 LEVEL_EXPECTED,
565 )?;
566 const NON_NEGATIVE_INTEGER: &str = "a non-negative integer";
567 let max_queue_bytes = resolve(
568 self.telemetry_max_queue_bytes,
569 MAX_QUEUE_BYTES_ENV_VAR,
570 HttpSinkConfig::DEFAULT_MAX_QUEUE_BYTES,
571 NON_NEGATIVE_INTEGER,
572 )?;
573 let hard_queue_bytes = resolve(
574 self.telemetry_hard_queue_bytes,
575 HARD_QUEUE_BYTES_ENV_VAR,
576 HttpSinkConfig::DEFAULT_HARD_QUEUE_BYTES,
577 NON_NEGATIVE_INTEGER,
578 )?;
579 let max_in_flight_requests = resolve(
580 self.telemetry_max_in_flight_requests,
581 MAX_IN_FLIGHT_REQUESTS_ENV_VAR,
582 HttpSinkConfig::DEFAULT_MAX_IN_FLIGHT_REQUESTS,
583 NON_NEGATIVE_INTEGER,
584 )?;
585 let request_timeout = match self.telemetry_request_timeout {
587 Some(timeout) => timeout,
588 None => env_override::<u64>(REQUEST_TIMEOUT_SECS_ENV_VAR, NON_NEGATIVE_INTEGER)?
589 .map(std::time::Duration::from_secs)
590 .unwrap_or(HttpSinkConfig::DEFAULT_REQUEST_TIMEOUT),
591 };
592 let target_max_level: Vec<_> = self
593 .target_max_levels
594 .into_iter()
595 .filter(|(key, _val)| key != "MAX_LEVEL")
596 .map(|(key, val)| {
597 (
598 key,
599 LevelFilter::from_str(val.as_str()).unwrap_or(LevelFilter::Off),
600 )
601 })
602 .collect();
603
604 let guard = {
605 lazy_static::lazy_static! {
606 static ref GLOBAL_WEAK_GUARD: Mutex<Weak<TracingSystemGuard>> = Mutex::new(Weak::new());
607 }
608 let mut weak_guard = GLOBAL_WEAK_GUARD.lock().unwrap();
609 let weak = &mut *weak_guard;
610
611 if let Some(arc) = weak.upgrade() {
612 arc
613 } else {
614 let mut sinks: Vec<(LevelFilter, BoxedEventSink)> = vec![];
615 let telemetry_sink_url = self
616 .telemetry_sink_url
617 .or_else(|| std::env::var("MICROMEGAS_TELEMETRY_URL").ok())
618 .filter(|url| !url.trim().is_empty());
619
620 if let Some(url) = telemetry_sink_url {
621 let retry_by_priority = self
622 .telemetry_retry_by_priority
623 .unwrap_or_else(HttpSinkConfig::default_retry_by_priority);
624 let config = HttpSinkConfig {
625 max_queue_bytes,
626 hard_queue_bytes,
627 max_in_flight_requests,
628 request_timeout,
629 retry_by_priority,
630 };
631 sinks.push((
632 telemetry_sink_max_level,
633 Box::new(HttpEventSink::new(
634 &url,
635 config,
636 self.telemetry_make_request_decorator,
637 )),
638 ));
639 }
640 if self.local_sink_enabled {
641 sinks.push((local_sink_max_level, Box::new(LocalEventSink::new())));
642 }
643 let mut extra_sinks = self.extra_sinks.into_values().collect();
644 sinks.append(&mut extra_sinks);
645
646 let sink: BoxedEventSink = Box::new(CompositeSink::new(
647 sinks,
648 target_max_level,
649 self.max_level_override,
650 ));
651
652 if self.install_log_capture {
654 install_log_interop(self.interop_max_level_override);
655 }
656 if self.install_tracing_capture {
657 install_tracing_interop(self.interop_max_level_override);
658 }
659
660 let arc = Arc::<TracingSystemGuard>::new(TracingSystemGuard::new(
661 self.logs_buffer_size,
662 self.metrics_buffer_size,
663 self.threads_buffer_size,
664 sink.into(),
665 self.process_properties,
666 std::env::var("MICROMEGAS_ENABLE_CPU_TRACING")
667 .map(|v| v == "true")
668 .unwrap_or(false), )?);
670
671 if self.system_metrics_enabled {
672 spawn_system_monitor();
673 }
674
675 *weak = Arc::<TracingSystemGuard>::downgrade(&arc);
676 arc
677 }
678 };
679 Ok(TelemetryGuard {
681 _guard: guard,
682 _thread_guard: TracingThreadGuard::new(),
683 })
684 }
685 }
686
687 pub struct TelemetryGuard {
688 _thread_guard: TracingThreadGuard,
690 _guard: Arc<TracingSystemGuard>,
691 }
692
693 impl TelemetryGuard {
694 pub fn new() -> anyhow::Result<Self> {
695 TelemetryGuardBuilder::default().build()
696 }
697 }
698}
699
700#[cfg(not(target_arch = "wasm32"))]
701pub use native::*;
702
703#[cfg(target_arch = "wasm32")]
705mod wasm {
706 use std::collections::HashMap;
707 use std::sync::Arc;
708
709 use micromegas_tracing::guards::TracingSystemGuard;
710
711 use crate::ConsoleEventSink;
712
713 pub struct TelemetryGuard {
714 _guard: Arc<TracingSystemGuard>,
715 }
716
717 pub fn init_telemetry() -> anyhow::Result<TelemetryGuard> {
718 let guard = Arc::new(TracingSystemGuard::new(
719 0,
720 0,
721 0,
722 Arc::new(ConsoleEventSink),
723 HashMap::new(),
724 false,
725 )?);
726 Ok(TelemetryGuard { _guard: guard })
727 }
728}
729
730#[cfg(target_arch = "wasm32")]
731pub use wasm::*;