diff --git a/Cargo.lock b/Cargo.lock index 9952666b..f2a5c058 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,6 +414,53 @@ dependencies = [ "thiserror", ] +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 1.0.1", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -993,6 +1040,10 @@ dependencies = [ "mail-send", "md5", "nlp", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", "parking_lot", "pem", "privdrop", @@ -2305,7 +2356,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -2324,7 +2375,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap", + "indexmap 2.2.6", "slab", "tokio", "tokio-util", @@ -2667,6 +2718,19 @@ dependencies = [ "webpki-roots 0.26.3", ] +[[package]] +name = "hyper-timeout" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3203a961e5c83b6f5498933e78b6b263e208c197b63e9c6c53cc82ffd3f63793" +dependencies = [ + "hyper 1.4.0", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.6" @@ -2920,6 +2984,16 @@ dependencies = [ "trc", ] +[[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.2.6" @@ -3193,7 +3267,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ee7893dab2e44ae5f9d0173f26ff4aa327c10b01b06a72b52dd9405b628640d" dependencies = [ - "indexmap", + "indexmap 2.2.6", ] [[package]] @@ -3577,6 +3651,12 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "maybe-async" version = "0.2.10" @@ -4012,6 +4092,90 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "opentelemetry" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c365a63eec4f55b7efeceb724f1336f26a9cf3427b70e59e2cd2a5b947fba96" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", +] + +[[package]] +name = "opentelemetry-http" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad31e9de44ee3538fb9d64fe3376c1362f406162434609e79aea2a41a0af78ab" +dependencies = [ + "async-trait", + "bytes", + "http 1.1.0", + "opentelemetry", + "reqwest 0.12.5", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b925a602ffb916fb7421276b86756027b37ee708f9dce2dbdcc51739f07e727" +dependencies = [ + "async-trait", + "futures-core", + "http 1.1.0", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest 0.12.5", + "thiserror", + "tokio", + "tonic", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ee9f20bff9c984511a02f082dc8ede839e4a9bf15cc2487c8d6fea5ad850d9" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cefe0543875379e47eb5f1e68ff83f45cc41366a92dfd0d073d513bf68e9a05" + +[[package]] +name = "opentelemetry_sdk" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692eac490ec80f24a17828d49b40b60f5aeaccdfe6a503f939713afd22bc28df" +dependencies = [ + "async-trait", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "percent-encoding", + "rand", + "serde_json", + "thiserror", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -4150,7 +4314,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" dependencies = [ "fixedbitset", - "indexmap", + "indexmap 2.2.6", ] [[package]] @@ -4441,6 +4605,29 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "prost" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13db3d3fde688c61e2446b4d843bc27a7e8af269a69440c0308021dc92333cc" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18bec9b0adc4eba778b33684b7ba3e7137789434769ee3ce3930463ef904cfca" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.68", +] + [[package]] name = "proxy-header" version = "0.1.0" @@ -6483,11 +6670,41 @@ version = "0.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1" dependencies = [ - "indexmap", + "indexmap 2.2.6", "toml_datetime", "winnow", ] +[[package]] +name = "tonic" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38659f4a91aba8598d27821589f5db7dddd94601e7a01b1e485a50e5484c7401" +dependencies = [ + "async-stream", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "h2 0.4.5", + "http 1.1.0", + "http-body 1.0.0", + "http-body-util", + "hyper 1.4.0", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "prost", + "socket2", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "totp-rs" version = "5.5.1" @@ -6511,11 +6728,16 @@ checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" dependencies = [ "futures-core", "futures-util", + "indexmap 1.9.3", "pin-project", "pin-project-lite", + "rand", + "slab", "tokio", + "tokio-util", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -7527,7 +7749,7 @@ dependencies = [ "displaydoc", "flate2", "hmac 0.12.1", - "indexmap", + "indexmap 2.2.6", "lzma-rs", "memchr", "pbkdf2", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index ff779199..329a28be 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -38,10 +38,10 @@ x509-parser = "0.16.0" pem = "3.0" chrono = { version = "0.4", features = ["serde"] } hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } -#opentelemetry = { version = "0.22.0" } -#opentelemetry_sdk = { version = "0.22.1", features = ["rt-tokio"] } -#opentelemetry-otlp = { version = "0.15.0", features = ["http-proto", "reqwest-client"] } -#opentelemetry-semantic-conventions = { version = "0.14.0" } +opentelemetry = { version = "0.24" } +opentelemetry_sdk = { version = "0.24" } +opentelemetry-otlp = { version = "0.17", features = ["http-proto", "reqwest-client"] } +opentelemetry-semantic-conventions = { version = "0.16.0" } imagesize = "0.13" sha1 = "0.10" sha2 = "0.10.6" diff --git a/crates/common/src/config/tracers.rs b/crates/common/src/config/tracers.rs index 6c378fd3..97227648 100644 --- a/crates/common/src/config/tracers.rs +++ b/crates/common/src/config/tracers.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{str::FromStr, time::Duration}; +use std::{collections::HashMap, str::FromStr, time::Duration}; use ahash::{AHashMap, AHashSet}; use base64::{engine::general_purpose::STANDARD, Engine}; @@ -12,6 +12,8 @@ use hyper::{ header::{HeaderName, HeaderValue, AUTHORIZATION, CONTENT_TYPE}, HeaderMap, }; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::export::{logs::LogExporter, trace::SpanExporter}; use trc::{subscriber::Interests, EventType, Level, TracingEvent}; use utils::config::{utils::ParseValue, Config}; @@ -32,6 +34,15 @@ pub enum TracerType { Journal, } +#[derive(Debug)] +pub struct OtelTracer { + pub span_exporter: Box, + pub span_exporter_enable: bool, + pub log_exporter: Box, + pub log_exporter_enable: bool, + pub throttle: Duration, +} + #[derive(Debug)] pub struct ConsoleTracer { pub ansi: bool, @@ -48,13 +59,6 @@ pub struct LogTracer { pub multiline: bool, } -#[derive(Debug)] -pub struct OtelTracer { - pub endpoint: String, - pub headers: AHashMap, - pub is_http: bool, -} - #[derive(Debug)] pub struct WebhookTracer { pub url: String, @@ -101,6 +105,17 @@ impl Tracers { } } + let event_names = EventType::variants() + .into_iter() + .filter_map(|e| { + if e != EventType::Tracing(TracingEvent::WebhookError) { + Some((e, e.name())) + } else { + None + } + }) + .collect::>(); + // Parse tracers let mut tracers: Vec = Vec::new(); let mut global_interests = Interests::default(); @@ -172,24 +187,78 @@ impl Tracers { .unwrap_or(true), }), "otel" | "open-telemetry" => { + let timeout = config + .property::(("tracer", id, "timeout")) + .unwrap_or(Duration::from_secs( + opentelemetry_otlp::OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT, + )); + let throttle = config + .property_or_default(("tracer", id, "throttle"), "1s") + .unwrap_or_else(|| Duration::from_secs(1)); + let log_exporter_enable = config + .property_or_default(("tracer", id, "enable.log-exporter"), "true") + .unwrap_or(true); + let span_exporter_enable = config + .property_or_default(("tracer", id, "enable.span-exporter"), "true") + .unwrap_or(true); + match config .value_require(("tracer", id, "transport")) .unwrap_or_default() { - "gprc" => TracerType::Otel(OtelTracer { - endpoint: config - .value(("tracer", id, "endpoint")) - .unwrap_or_default() - .to_string(), - headers: Default::default(), - is_http: false, - }), + "grpc" => { + let mut span_exporter = opentelemetry_otlp::new_exporter() + .tonic() + .with_protocol(opentelemetry_otlp::Protocol::Grpc) + .with_timeout(timeout); + let mut log_exporter = opentelemetry_otlp::new_exporter() + .tonic() + .with_protocol(opentelemetry_otlp::Protocol::Grpc) + .with_timeout(timeout); + if let Some(endpoint) = config.value(("tracer", id, "endpoint")) { + span_exporter = span_exporter.with_endpoint(endpoint); + log_exporter = log_exporter.with_endpoint(endpoint); + } + + match ( + span_exporter.build_span_exporter(), + log_exporter.build_log_exporter(), + ) { + (Ok(span_exporter), Ok(log_exporter)) => { + TracerType::Otel(OtelTracer { + span_exporter: Box::new(span_exporter), + log_exporter: Box::new(log_exporter), + throttle, + span_exporter_enable, + log_exporter_enable, + }) + } + (Err(err), _) => { + config.new_build_error( + ("tracer", id), + format!( + "Failed to build OpenTelemetry span exporter: {err}" + ), + ); + continue; + } + (_, Err(err)) => { + config.new_build_error( + ("tracer", id), + format!( + "Failed to build OpenTelemetry log exporter: {err}" + ), + ); + continue; + } + } + } "http" => { if let Some(endpoint) = config .value_require(("tracer", id, "endpoint")) .map(|s| s.to_string()) { - let mut headers = AHashMap::new(); + let mut headers = HashMap::new(); let mut err = None; for (_, value) in config.values(("tracer", id, "headers")) { if let Some((key, value)) = value.split_once(':') { @@ -207,11 +276,51 @@ impl Tracers { config.new_parse_error(("tracer", id, "headers"), err); } - TracerType::Otel(OtelTracer { - endpoint, - headers, - is_http: true, - }) + let mut span_exporter = opentelemetry_otlp::new_exporter() + .http() + .with_endpoint(&endpoint) + .with_timeout(timeout); + let mut log_exporter = opentelemetry_otlp::new_exporter() + .http() + .with_endpoint(&endpoint) + .with_timeout(timeout); + if !headers.is_empty() { + span_exporter = span_exporter.with_headers(headers.clone()); + log_exporter = log_exporter.with_headers(headers); + } + + match ( + span_exporter.build_span_exporter(), + log_exporter.build_log_exporter(), + ) { + (Ok(span_exporter), Ok(log_exporter)) => { + TracerType::Otel(OtelTracer { + span_exporter: Box::new(span_exporter), + log_exporter: Box::new(log_exporter), + throttle, + span_exporter_enable, + log_exporter_enable, + }) + } + (Err(err), _) => { + config.new_build_error( + ("tracer", id), + format!( + "Failed to build OpenTelemetry span exporter: {err}" + ), + ); + continue; + } + (_, Err(err)) => { + config.new_build_error( + ("tracer", id), + format!( + "Failed to build OpenTelemetry log exporter: {err}" + ), + ); + continue; + } + } } else { continue; } @@ -280,9 +389,34 @@ impl Tracers { disabled_events.insert(EventType::Tracing(TracingEvent::JournalError)); } } - for (_, event_type) in config.properties::(("tracer", id, "disabled-events")) + for (_, event_type) in + config.properties::(("tracer", id, "disabled-events")) { - disabled_events.insert(event_type); + match event_type { + EventOrMany::Event(event_type) => { + disabled_events.insert(event_type); + } + EventOrMany::StartsWith(value) => { + for (event_type, name) in event_names.iter() { + if name.starts_with(&value) { + disabled_events.insert(*event_type); + } + } + } + EventOrMany::EndsWith(value) => { + for (event_type, name) in event_names.iter() { + if name.ends_with(&value) { + disabled_events.insert(*event_type); + } + } + } + EventOrMany::All => { + for (event_type, _) in event_names.iter() { + disabled_events.insert(*event_type); + } + break; + } + } } // Build interests lists @@ -292,7 +426,7 @@ impl Tracers { .get(&event_type) .copied() .unwrap_or(event_type.level()); - if event_level <= level { + if level.is_contained(event_level) { tracer.interests.set(event_type); global_interests.set(event_type); } @@ -323,7 +457,7 @@ impl Tracers { .get(&event_type) .copied() .unwrap_or(event_type.level()); - if event_level <= Level::Info { + if Level::Info.is_contained(event_level) { global_interests.set(event_type); } } diff --git a/crates/common/src/tracing/mod.rs b/crates/common/src/tracing/mod.rs index 6652806f..5bfd3e23 100644 --- a/crates/common/src/tracing/mod.rs +++ b/crates/common/src/tracing/mod.rs @@ -5,15 +5,21 @@ */ pub mod log; +pub mod otel; pub mod stdout; pub mod webhook; +use std::time::Duration; + use log::spawn_log_tracer; +use otel::spawn_otel_tracer; use stdout::spawn_console_tracer; use trc::{collector::Collector, subscriber::SubscriberBuilder}; use webhook::spawn_webhook_tracer; -use crate::config::tracers::{ConsoleTracer, TracerType, Tracers}; +use crate::config::tracers::{TracerType, Tracers}; + +pub const LONG_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24 * 365); impl Tracers { pub fn enable(self) { @@ -68,16 +74,16 @@ impl Tracers { pub fn test_tracer(level: trc::Level) { let mut interests = trc::subscriber::Interests::default(); for event in trc::EventType::variants() { - if event.level() <= level { + if level.is_contained(event.level()) { interests.set(event); } } spawn_console_tracer( - SubscriberBuilder::new("stdout".to_string()) + SubscriberBuilder::new("stderr".to_string()) .with_interests(interests.clone()) .with_lossy(false), - ConsoleTracer { + crate::config::tracers::ConsoleTracer { ansi: true, multiline: false, buffered: true, @@ -91,129 +97,13 @@ impl Tracers { impl TracerType { pub fn spawn(self, builder: SubscriberBuilder) { + let todo = "journal"; match self { TracerType::Console(settings) => spawn_console_tracer(builder, settings), TracerType::Log(settings) => spawn_log_tracer(builder, settings), TracerType::Webhook(settings) => spawn_webhook_tracer(builder, settings), - TracerType::Otel(_) => todo!(), + TracerType::Otel(settings) => spawn_otel_tracer(builder, settings), TracerType::Journal => todo!(), } } } - -/* -impl Tracers { - pub fn enable(self, config: &mut Config) -> Option> { - let mut layers: Option + Sync + Send>> = None; - let mut guards = Vec::new(); - - for tracer in self.tracers { - let (Tracer::Stdout { level, .. } - | Tracer::Log { level, .. } - | Tracer::Journal { level, .. } - | Tracer::Otel { level, .. }) = tracer; - - let filter = match EnvFilter::builder().parse(format!( - "smtp={level},imap={level},jmap={level},pop3={level},store={level},common={level},utils={level},directory={level},se_common={level}" - )) { - Ok(filter) => { - filter - } - Err(err) => { - config.new_build_error("tracer", format!("Failed to set env filter: {err}")); - continue; - } - }; - - let layer = match tracer { - Tracer::Stdout { ansi, .. } => tracing_subscriber::fmt::layer() - .with_ansi(ansi) - .with_filter(filter) - .boxed(), - Tracer::Log { appender, ansi, .. } => { - let (non_blocking, guard) = tracing_appender::non_blocking(appender); - guards.push(guard); - tracing_subscriber::fmt::layer() - .with_writer(non_blocking) - .with_ansi(ansi) - .with_filter(filter) - .boxed() - } - Tracer::Otel { tracer, .. } => { - let tracer = match tracer { - OtelTracer::Gprc(exporter) => opentelemetry_otlp::new_pipeline() - .tracing() - .with_exporter(exporter), - OtelTracer::Http(exporter) => opentelemetry_otlp::new_pipeline() - .tracing() - .with_exporter(exporter), - } - .with_trace_config( - trace::config() - .with_resource(Resource::new(vec![ - KeyValue::new(SERVICE_NAME, "stalwart-mail".to_string()), - KeyValue::new( - SERVICE_VERSION, - env!("CARGO_PKG_VERSION").to_string(), - ), - ])) - .with_sampler(Sampler::AlwaysOn), - ) - .install_batch(opentelemetry_sdk::runtime::Tokio); - - match tracer { - Ok(tracer) => tracing_opentelemetry::layer() - .with_tracer(tracer) - .with_filter(filter) - .boxed(), - Err(err) => { - config.new_build_error( - "tracer", - format!("Failed to start OpenTelemetry: {err}"), - ); - continue; - } - } - } - Tracer::Journal { .. } => { - #[cfg(unix)] - { - match tracing_journald::layer() { - Ok(layer) => layer.with_filter(filter).boxed(), - Err(err) => { - config.new_build_error( - "tracer", - format!("Failed to start Journald: {err}"), - ); - continue; - } - } - } - - #[cfg(not(unix))] - { - config.new_build_error( - "tracer", - "Journald is only available on Unix systems.", - ); - continue; - } - } - }; - - layers = Some(match layers { - Some(layers) => layers.and_then(layer).boxed(), - None => layer, - }); - } - - match tracing_subscriber::registry().with(layers?).try_init() { - Ok(_) => Some(guards), - Err(err) => { - config.new_build_error("tracer", format!("Failed to start tracing: {err}")); - None - } - } - } -} -*/ diff --git a/crates/common/src/tracing/otel.rs b/crates/common/src/tracing/otel.rs new file mode 100644 index 00000000..897c3cb7 --- /dev/null +++ b/crates/common/src/tracing/otel.rs @@ -0,0 +1,273 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::{ + borrow::Cow, + time::{Duration, Instant, SystemTime, UNIX_EPOCH}, +}; + +use mail_parser::DateTime; +use opentelemetry::{ + logs::{AnyValue, Severity}, + trace::{SpanContext, SpanKind, Status, TraceFlags, TraceState}, + InstrumentationLibrary, Key, KeyValue, Value, +}; +use opentelemetry_sdk::{ + export::{logs::LogData, trace::SpanData}, + logs::LogRecord, + trace::{SpanEvents, SpanLinks}, + Resource, +}; +use opentelemetry_semantic_conventions::resource::{SERVICE_NAME, SERVICE_VERSION}; +use trc::{subscriber::SubscriberBuilder, Event, EventDetails, Level, TracingEvent}; + +use crate::config::tracers::OtelTracer; + +use super::LONG_SLUMBER; + +pub(crate) fn spawn_otel_tracer(builder: SubscriberBuilder, mut otel: OtelTracer) { + let (_, mut rx) = builder.register(); + tokio::spawn(async move { + let resource = Cow::Owned(Resource::new([ + KeyValue::new(SERVICE_NAME, "stalwart-mail"), + KeyValue::new(SERVICE_VERSION, env!("CARGO_PKG_VERSION")), + ])); + let instrumentation = InstrumentationLibrary::builder("stalwart-mail") + .with_version(env!("CARGO_PKG_VERSION")) + .build(); + + otel.log_exporter.set_resource(&resource); + otel.span_exporter.set_resource(&resource); + + let mut wakeup_time = LONG_SLUMBER; + let mut next_delivery = Instant::now(); + + let mut pending_logs = Vec::new(); + let mut pending_spans = Vec::new(); + + loop { + // Wait for the next event or timeout + let event_or_timeout = tokio::time::timeout(wakeup_time, rx.recv()).await; + + match event_or_timeout { + Ok(Some(events)) => { + for event in events { + if otel.span_exporter_enable && event.inner.typ.is_span_end() { + if let Some(start_span) = event.inner.span.as_ref() { + pending_spans.push(build_span_data( + start_span, + &event, + [&event].into_iter(), + &instrumentation, + )); + } + } + + if otel.log_exporter_enable { + pending_logs.push(build_log_record(&event, &instrumentation)); + } + } + } + Ok(None) => { + break; + } + Err(_) => (), + } + + // Process events + let mut next_retry = None; + let now = Instant::now(); + if next_delivery <= now { + if !pending_spans.is_empty() || !pending_logs.is_empty() { + next_delivery = now + otel.throttle; + + if !pending_spans.is_empty() { + if let Err(err) = otel + .span_exporter + .export(std::mem::take(&mut pending_spans)) + .await + { + trc::event!( + Tracing(TracingEvent::OtelError), + Details = "Failed to export spans", + Reason = err.to_string() + ); + } + } + + if !pending_logs.is_empty() { + if let Err(err) = otel + .log_exporter + .export(std::mem::take(&mut pending_logs)) + .await + { + trc::event!( + Tracing(TracingEvent::OtelError), + Details = "Failed to export logs", + Reason = err.to_string() + ); + } + } + } + } else if !pending_logs.is_empty() || !pending_spans.is_empty() { + // Retry later + let this_retry = next_delivery - now; + match next_retry { + Some(next_retry) if this_retry >= next_retry => {} + _ => { + next_retry = Some(this_retry); + } + } + } + wakeup_time = next_retry.unwrap_or(LONG_SLUMBER); + } + }); +} + +fn build_span_data( + start_span: &Event, + end_span: &Event, + span_events: I, + instrumentation: &InstrumentationLibrary, +) -> SpanData +where + I: IntoIterator, + T: AsRef>, +{ + let span_id = start_span.span_id().unwrap(); + + let mut events = SpanEvents::default(); + events.events = span_events + .into_iter() + .map(|event| { + let event = event.as_ref(); + + opentelemetry::trace::Event::new( + event.inner.typ.name(), + UNIX_EPOCH + Duration::from_secs(event.inner.timestamp), + event.keys.iter().filter_map(build_key_value).collect(), + 0, + ) + }) + .collect(); + + SpanData { + span_context: SpanContext::new( + (span_id as u128).into(), + span_id.into(), + TraceFlags::default(), + false, + TraceState::default(), + ), + dropped_attributes_count: 0, + parent_span_id: 0.into(), + name: start_span.inner.typ.name().into(), + start_time: UNIX_EPOCH + Duration::from_secs(start_span.inner.timestamp), + end_time: UNIX_EPOCH + Duration::from_secs(end_span.inner.timestamp), + attributes: start_span.keys.iter().filter_map(build_key_value).collect(), + events, + links: SpanLinks::default(), + status: Status::default(), + span_kind: SpanKind::Server, + instrumentation_lib: instrumentation.clone(), + } +} + +fn build_log_record( + event: &Event, + instrumentation: &InstrumentationLibrary, +) -> Cow<'static, LogData> { + let mut record = LogRecord::default(); + record.event_name = Cow::Borrowed(event.inner.typ.name()).into(); + record.severity_number = match event.inner.level { + Level::Trace => Severity::Trace, + Level::Debug => Severity::Debug, + Level::Info => Severity::Info, + Level::Warn => Severity::Warn, + Level::Error => Severity::Error, + Level::Disable => Severity::Error, + } + .into(); + record.severity_text = Cow::Borrowed(event.inner.level.as_str()).into(); + record.body = AnyValue::String(event.inner.typ.description().into()).into(); + record.timestamp = (UNIX_EPOCH + Duration::from_secs(event.inner.timestamp)).into(); + record.observed_timestamp = SystemTime::now().into(); + record.attributes = (!event.keys.is_empty()).then(|| { + event + .keys + .iter() + .map(|(k, v)| (build_key(k), build_any_value(v))) + .collect() + }); + + Cow::Owned(LogData { + record, + instrumentation: instrumentation.clone(), + }) +} + +fn build_key_value(key_value: &(trc::Key, trc::Value)) -> Option { + (key_value.0 != trc::Key::SpanId).then(|| KeyValue { + key: build_key(&key_value.0), + value: match &key_value.1 { + trc::Value::Static(v) => Value::String((*v).into()), + trc::Value::String(v) => Value::String(v.clone().into()), + trc::Value::UInt(v) => Value::I64(*v as i64), + trc::Value::Int(v) => Value::I64(*v), + trc::Value::Float(v) => Value::F64(*v), + trc::Value::Timestamp(v) => { + Value::String(DateTime::from_timestamp(*v as i64).to_rfc3339().into()) + } + trc::Value::Duration(v) => Value::I64(*v as i64), + trc::Value::Bytes(_) => Value::String("[binary data]".into()), + trc::Value::Bool(v) => Value::Bool(*v), + trc::Value::Ipv4(v) => Value::String(v.to_string().into()), + trc::Value::Ipv6(v) => Value::String(v.to_string().into()), + trc::Value::Protocol(v) => Value::String(v.name().into()), + trc::Value::Event(_) => Value::String("[event data]".into()), + trc::Value::Array(_) => Value::String("[array]".into()), + trc::Value::None => Value::Bool(false), + }, + }) +} + +fn build_key(key: &trc::Key) -> Key { + Key::from_static_str(key.name()) +} + +fn build_any_value(value: &trc::Value) -> AnyValue { + match value { + trc::Value::Static(v) => AnyValue::String((*v).into()), + trc::Value::String(v) => AnyValue::String(v.clone().into()), + trc::Value::UInt(v) => AnyValue::Int(*v as i64), + trc::Value::Int(v) => AnyValue::Int(*v), + trc::Value::Float(v) => AnyValue::Double(*v), + trc::Value::Timestamp(v) => { + AnyValue::String(DateTime::from_timestamp(*v as i64).to_rfc3339().into()) + } + trc::Value::Duration(v) => AnyValue::Int(*v as i64), + trc::Value::Bytes(v) => AnyValue::Bytes(v.clone()), + trc::Value::Bool(v) => AnyValue::Boolean(*v), + trc::Value::Ipv4(v) => AnyValue::String(v.to_string().into()), + trc::Value::Ipv6(v) => AnyValue::String(v.to_string().into()), + trc::Value::Protocol(v) => AnyValue::String(v.name().into()), + trc::Value::Event(v) => AnyValue::Map( + [( + Key::from_static_str("eventName"), + AnyValue::String(v.inner.name().into()), + )] + .into_iter() + .chain( + v.keys + .iter() + .map(|(k, v)| (build_key(k), build_any_value(v))), + ) + .collect(), + ), + trc::Value::Array(v) => AnyValue::ListAny(v.iter().map(build_any_value).collect()), + trc::Value::None => AnyValue::Boolean(false), + } +} diff --git a/crates/common/src/tracing/webhook.rs b/crates/common/src/tracing/webhook.rs index e45024e7..26bc73e1 100644 --- a/crates/common/src/tracing/webhook.rs +++ b/crates/common/src/tracing/webhook.rs @@ -9,7 +9,7 @@ use std::{ atomic::{AtomicBool, Ordering}, Arc, }, - time::{Duration, Instant}, + time::Instant, }; use crate::config::tracers::WebhookTracer; @@ -23,7 +23,7 @@ use trc::{ ServerEvent, TracingEvent, }; -pub const LONG_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24 * 365); +use super::LONG_SLUMBER; pub(crate) fn spawn_webhook_tracer(builder: SubscriberBuilder, settings: WebhookTracer) { let (tx, mut rx) = builder.register(); diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs index 6515b09b..874719d2 100644 --- a/crates/jmap/src/api/management/mod.rs +++ b/crates/jmap/src/api/management/mod.rs @@ -39,6 +39,7 @@ pub enum ManagementApiError<'x> { } impl JMAP { + #[allow(unused_variables)] pub async fn handle_api_manage_request( &self, req: &HttpRequest, diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 3c60056b..f8d2a857 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -98,11 +98,7 @@ async fn main() -> std::io::Result<()> { } // Wait for shutdown signal - wait_for_shutdown(&format!( - "Shutting down Stalwart Mail Server v{}...", - env!("CARGO_PKG_VERSION") - )) - .await; + wait_for_shutdown().await; // Shutdown collector Collector::shutdown(); diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index dbfe3697..4a3daab1 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -200,7 +200,7 @@ impl DeliveryAttempt { } trc::event!( - Delivery(DeliveryEvent::AttemptCount), + Delivery(DeliveryEvent::DomainDeliveryStart), SpanId = message.span_id, Domain = domain.domain.clone(), Count = domain.retry.inner, diff --git a/crates/trc/event-macro/src/lib.rs b/crates/trc/event-macro/src/lib.rs index d5a834ce..bec4c26e 100644 --- a/crates/trc/event-macro/src/lib.rs +++ b/crates/trc/event-macro/src/lib.rs @@ -15,10 +15,7 @@ pub fn event_type(_attr: TokenStream, item: TokenStream) -> TokenStream { let input = parse_macro_input!(item as DeriveInput); let name = &input.ident; let name_str = name.to_string(); - let prefix = name_str - .strip_suffix("Event") - .unwrap_or(&name_str) - .to_ascii_lowercase(); + let prefix = to_snake_case(name_str.strip_suffix("Event").unwrap_or(&name_str)); let enum_variants = match &input.data { Data::Enum(data_enum) => &data_enum.variants, @@ -28,7 +25,6 @@ pub fn event_type(_attr: TokenStream, item: TokenStream) -> TokenStream { let mut variant_ids = Vec::new(); let mut variant_names = Vec::new(); let mut event_names = Vec::new(); - let mut event_names_lowercase = Vec::new(); for variant in enum_variants { unsafe { @@ -36,10 +32,11 @@ pub fn event_type(_attr: TokenStream, item: TokenStream) -> TokenStream { GLOBAL_ID_COUNTER += 1; } let variant_name = &variant.ident; - let event_name = format!("{prefix}{variant_name}"); + event_names.push(format!( + "{prefix}.{}", + to_snake_case(&variant_name.to_string()) + )); variant_names.push(variant_name); - event_names_lowercase.push(event_name.to_ascii_lowercase()); - event_names.push(event_name); } let id_fn = quote! { @@ -61,7 +58,7 @@ pub fn event_type(_attr: TokenStream, item: TokenStream) -> TokenStream { let parse_fn = quote! { pub fn try_parse(name: &str) -> Option { match name { - #(#event_names_lowercase => Some(Self::#variant_names),)* + #(#event_names => Some(Self::#variant_names),)* _ => None, } } @@ -117,12 +114,8 @@ pub fn event_family(_attr: TokenStream, item: TokenStream) -> TokenStream { let variant_names: Vec<_> = variant_idents .iter() .map(|ident| { - ident - .to_string() - .char_indices() - .take_while(|(i, c)| *i == 0 || c.is_lowercase()) - .map(|(_, c)| c.to_ascii_lowercase()) - .collect::() + let name_str = ident.to_string(); + to_snake_case(name_str.strip_suffix("Event").unwrap_or(&name_str)) }) .collect(); @@ -145,14 +138,12 @@ pub fn event_family(_attr: TokenStream, item: TokenStream) -> TokenStream { } pub fn try_parse(name: &str) -> Option { - let name = name.to_ascii_lowercase(); - + match name.trim().split_once('.')?.0 { #( - if name.starts_with(#variant_names) { - return <#event_types>::try_parse(&name).map(#name::#variant_idents); - } + #variant_names => <#event_types>::try_parse(&name).map(#name::#variant_idents), )* - None + _ => None, + } } pub fn variants() -> Vec<#name> { @@ -219,3 +210,18 @@ pub fn total_event_count(_item: TokenStream) -> TokenStream { }; TokenStream::from(expanded) } + +fn to_snake_case(name: &str) -> String { + let mut out = String::with_capacity(name.len()); + for (idx, ch) in name.char_indices() { + if ch.is_ascii_uppercase() { + if idx > 0 { + out.push('-'); + } + out.push(ch.to_ascii_lowercase()); + } else { + out.push(ch); + } + } + out +} diff --git a/crates/trc/src/collector.rs b/crates/trc/src/collector.rs index 85172cc7..c3bab9ec 100644 --- a/crates/trc/src/collector.rs +++ b/crates/trc/src/collector.rs @@ -114,11 +114,12 @@ impl Collector { event } EV_CONN_END | EV_ATTEMPT_END => { - if self + if let Some(span) = self .active_spans .remove(&event.span_id().expect("Missing span ID")) - .is_none() { + event.inner.span = Some(span.clone()); + } else { #[cfg(debug_assertions)] { if event.span_id().unwrap() != 0 { diff --git a/crates/trc/src/fmt.rs b/crates/trc/src/fmt.rs index f1025b5f..2e293fd4 100644 --- a/crates/trc/src/fmt.rs +++ b/crates/trc/src/fmt.rs @@ -96,13 +96,18 @@ impl FmtWriter { .await?; } self.writer - .write_all(event.inner.typ.name().as_bytes()) + .write_all(event.inner.typ.description().as_bytes()) .await?; if self.ansi { self.writer.write_all(Color::reset().as_bytes()).await?; } + self.writer.write_all(" (".as_bytes()).await?; self.writer - .write_all(if self.multiline { "\n" } else { " " }.as_bytes()) + .write_all(event.inner.typ.name().as_bytes()) + .await?; + + self.writer + .write_all(if self.multiline { ")\n" } else { ") " }.as_bytes()) .await?; // Write keys @@ -235,7 +240,12 @@ impl FmtWriter { self.writer.write_all(v.name().as_bytes()).await?; } Value::Event(e) => { + self.writer + .write_all(e.inner.description().as_bytes()) + .await?; + self.writer.write_all(" (".as_bytes()).await?; self.writer.write_all(e.inner.name().as_bytes()).await?; + self.writer.write_all(")".as_bytes()).await?; if !e.keys.is_empty() { self.writer .write_all(if self.multiline { "\n" } else { " { " }.as_bytes()) @@ -310,3 +320,22 @@ impl Color { "\x1b[0m" } } + +#[cfg(test)] +mod tests { + use crate::{EventType, Level}; + + #[test] + fn print_all_events() { + assert!(!Level::Disable.is_contained(Level::Warn)); + assert!(Level::Trace.is_contained(Level::Error)); + assert!(Level::Trace.is_contained(Level::Debug)); + assert!(!Level::Error.is_contained(Level::Trace)); + assert!(!Level::Debug.is_contained(Level::Trace)); + + for event in EventType::variants() { + println!("{}", event.name()); + assert_eq!(EventType::try_parse(event.name()).unwrap(), event); + } + } +} diff --git a/crates/trc/src/imple.rs b/crates/trc/src/imple.rs index 791e2bbf..82294b82 100644 --- a/crates/trc/src/imple.rs +++ b/crates/trc/src/imple.rs @@ -724,6 +724,10 @@ impl Level { Self::Error => "ERROR", } } + + pub fn is_contained(&self, other: Self) -> bool { + *self >= other && other != Level::Disable && *self != Level::Disable + } } impl Display for Level { @@ -735,6 +739,69 @@ impl Display for Level { impl Eq for Error {} impl EventType { + #[inline(always)] + pub fn is_span_start(&self) -> bool { + matches!( + self, + EventType::Network(NetworkEvent::ConnectionStart) + | EventType::Delivery(DeliveryEvent::AttemptStart) + ) + } + + #[inline(always)] + pub fn is_span_end(&self) -> bool { + matches!( + self, + EventType::Network(NetworkEvent::ConnectionEnd) + | EventType::Delivery(DeliveryEvent::AttemptEnd) + ) + } + + pub fn description(&self) -> &'static str { + match self { + EventType::Store(event) => event.description(), + EventType::Jmap(event) => event.description(), + EventType::Imap(event) => event.description(), + EventType::ManageSieve(event) => event.description(), + EventType::Pop3(event) => event.description(), + EventType::Smtp(event) => event.description(), + EventType::Network(event) => event.description(), + EventType::Limit(event) => event.description(), + EventType::Manage(event) => event.description(), + EventType::Auth(event) => event.description(), + EventType::Config(event) => event.description(), + EventType::Resource(event) => event.description(), + EventType::Sieve(event) => event.description(), + EventType::Spam(event) => event.description(), + EventType::Server(event) => event.description(), + EventType::Purge(event) => event.description(), + EventType::Eval(event) => event.description(), + EventType::Acme(event) => event.description(), + EventType::Http(event) => event.description(), + EventType::Arc(event) => event.description(), + EventType::Dkim(event) => event.description(), + EventType::Dmarc(event) => event.description(), + EventType::Iprev(event) => event.description(), + EventType::Dane(event) => event.description(), + EventType::Spf(event) => event.description(), + EventType::MailAuth(event) => event.description(), + EventType::Tls(event) => event.description(), + EventType::PushSubscription(event) => event.description(), + EventType::Cluster(event) => event.description(), + EventType::Housekeeper(event) => event.description(), + EventType::FtsIndex(event) => event.description(), + EventType::Milter(event) => event.description(), + EventType::MtaHook(event) => event.description(), + EventType::Delivery(event) => event.description(), + EventType::Queue(event) => event.description(), + EventType::TlsRpt(event) => event.description(), + EventType::MtaSts(event) => event.description(), + EventType::IncomingReport(event) => event.description(), + EventType::OutgoingReport(event) => event.description(), + EventType::Tracing(event) => event.description(), + } + } + pub fn level(&self) -> Level { match self { EventType::Store(event) => match event { @@ -1151,7 +1218,7 @@ impl EventType { | DeliveryEvent::AttemptEnd | DeliveryEvent::Completed | DeliveryEvent::Failed - | DeliveryEvent::AttemptCount + | DeliveryEvent::DomainDeliveryStart | DeliveryEvent::MxLookupFailed | DeliveryEvent::IpLookupFailed | DeliveryEvent::NullMX @@ -1244,7 +1311,7 @@ impl EventType { }, EventType::Tracing(event) => match event { TracingEvent::Update => Level::Disable, - _ => Level::Error, + _ => Level::Warn, }, } } @@ -1255,3 +1322,817 @@ impl From for usize { value.id() } } + +impl HttpEvent { + pub fn description(&self) -> &'static str { + match self { + HttpEvent::Error => "An HTTP error occurred", + HttpEvent::RequestUrl => "HTTP request URL", + HttpEvent::RequestBody => "HTTP request body", + HttpEvent::ResponseBody => "HTTP response body", + HttpEvent::XForwardedMissing => "X-Forwarded-For header is missing", + } + } +} + +impl ClusterEvent { + pub fn description(&self) -> &'static str { + match self { + ClusterEvent::PeerAlive => "A peer is alive", + ClusterEvent::PeerDiscovered => "A new peer was discovered", + ClusterEvent::PeerOffline => "A peer went offline", + ClusterEvent::PeerSuspected => "A peer is suspected to be offline", + ClusterEvent::PeerSuspectedIsAlive => "A suspected peer is actually alive", + ClusterEvent::PeerBackOnline => "A peer came back online", + ClusterEvent::PeerLeaving => "A peer is leaving the cluster", + ClusterEvent::PeerHasConfigChanges => "A peer has configuration changes", + ClusterEvent::PeerHasListChanges => "A peer has list changes", + ClusterEvent::OneOrMorePeersOffline => "One or more peers are offline", + ClusterEvent::EmptyPacket => "Received an empty gossip packet", + ClusterEvent::InvalidPacket => "Received an invalid gossip packet", + ClusterEvent::DecryptionError => "Failed to decrypt a gossip packet", + ClusterEvent::Error => "A cluster error occurred", + } + } +} + +impl HousekeeperEvent { + pub fn description(&self) -> &'static str { + match self { + HousekeeperEvent::Start => "Housekeeper process started", + HousekeeperEvent::Stop => "Housekeeper process stopped", + HousekeeperEvent::Schedule => "Housekeeper task scheduled", + HousekeeperEvent::PurgeAccounts => "Purging accounts", + HousekeeperEvent::PurgeSessions => "Purging sessions", + HousekeeperEvent::PurgeStore => "Purging store", + } + } +} + +impl FtsIndexEvent { + pub fn description(&self) -> &'static str { + match self { + FtsIndexEvent::Index => "Full-text search index done", + FtsIndexEvent::Locked => "Full-text search index is locked", + FtsIndexEvent::LockBusy => "Full-text search index lock is busy", + FtsIndexEvent::BlobNotFound => "Blob not found for full-text indexing", + FtsIndexEvent::MetadataNotFound => "Metadata not found for full-text indexing", + } + } +} + +impl ImapEvent { + pub fn description(&self) -> &'static str { + match self { + ImapEvent::GetAcl => "IMAP GET ACL command", + ImapEvent::SetAcl => "IMAP SET ACL command", + ImapEvent::MyRights => "IMAP MYRIGHTS command", + ImapEvent::ListRights => "IMAP LISTRIGHTS command", + ImapEvent::Append => "IMAP APPEND command", + ImapEvent::Capabilities => "IMAP CAPABILITIES command", + ImapEvent::Id => "IMAP ID command", + ImapEvent::Close => "IMAP CLOSE command", + ImapEvent::Copy => "IMAP COPY command", + ImapEvent::Move => "IMAP MOVE command", + ImapEvent::CreateMailbox => "IMAP CREATE mailbox command", + ImapEvent::DeleteMailbox => "IMAP DELETE mailbox command", + ImapEvent::RenameMailbox => "IMAP RENAME mailbox command", + ImapEvent::Enable => "IMAP ENABLE command", + ImapEvent::Expunge => "IMAP EXPUNGE command", + ImapEvent::Fetch => "IMAP FETCH command", + ImapEvent::IdleStart => "IMAP IDLE start", + ImapEvent::IdleStop => "IMAP IDLE stop", + ImapEvent::List => "IMAP LIST command", + ImapEvent::Lsub => "IMAP LSUB command", + ImapEvent::Logout => "IMAP LOGOUT command", + ImapEvent::Namespace => "IMAP NAMESPACE command", + ImapEvent::Noop => "IMAP NOOP command", + ImapEvent::Search => "IMAP SEARCH command", + ImapEvent::Sort => "IMAP SORT command", + ImapEvent::Select => "IMAP SELECT command", + ImapEvent::Status => "IMAP STATUS command", + ImapEvent::Store => "IMAP STORE command", + ImapEvent::Subscribe => "IMAP SUBSCRIBE command", + ImapEvent::Unsubscribe => "IMAP UNSUBSCRIBE command", + ImapEvent::Thread => "IMAP THREAD command", + ImapEvent::Error => "IMAP error occurred", + ImapEvent::RawInput => "Raw IMAP input received", + ImapEvent::RawOutput => "Raw IMAP output sent", + } + } +} + +impl Pop3Event { + pub fn description(&self) -> &'static str { + match self { + Pop3Event::Delete => "POP3 DELETE command", + Pop3Event::Reset => "POP3 RESET command", + Pop3Event::Quit => "POP3 QUIT command", + Pop3Event::Fetch => "POP3 FETCH command", + Pop3Event::List => "POP3 LIST command", + Pop3Event::ListMessage => "POP3 LIST specific message command", + Pop3Event::Uidl => "POP3 UIDL command", + Pop3Event::UidlMessage => "POP3 UIDL specific message command", + Pop3Event::Stat => "POP3 STAT command", + Pop3Event::Noop => "POP3 NOOP command", + Pop3Event::Capabilities => "POP3 CAPABILITIES command", + Pop3Event::StartTls => "POP3 STARTTLS command", + Pop3Event::Utf8 => "POP3 UTF8 command", + Pop3Event::Error => "POP3 error occurred", + Pop3Event::RawInput => "Raw POP3 input received", + Pop3Event::RawOutput => "Raw POP3 output sent", + } + } +} + +impl ManageSieveEvent { + pub fn description(&self) -> &'static str { + match self { + ManageSieveEvent::CreateScript => "ManageSieve CREATE script command", + ManageSieveEvent::UpdateScript => "ManageSieve UPDATE script command", + ManageSieveEvent::GetScript => "ManageSieve GET script command", + ManageSieveEvent::DeleteScript => "ManageSieve DELETE script command", + ManageSieveEvent::RenameScript => "ManageSieve RENAME script command", + ManageSieveEvent::CheckScript => "ManageSieve CHECK script command", + ManageSieveEvent::HaveSpace => "ManageSieve HAVESPACE command", + ManageSieveEvent::ListScripts => "ManageSieve LIST scripts command", + ManageSieveEvent::SetActive => "ManageSieve SET ACTIVE command", + ManageSieveEvent::Capabilities => "ManageSieve CAPABILITIES command", + ManageSieveEvent::StartTls => "ManageSieve STARTTLS command", + ManageSieveEvent::Unauthenticate => "ManageSieve UNAUTHENTICATE command", + ManageSieveEvent::Logout => "ManageSieve LOGOUT command", + ManageSieveEvent::Noop => "ManageSieve NOOP command", + ManageSieveEvent::Error => "ManageSieve error occurred", + ManageSieveEvent::RawInput => "Raw ManageSieve input received", + ManageSieveEvent::RawOutput => "Raw ManageSieve output sent", + } + } +} + +impl SmtpEvent { + pub fn description(&self) -> &'static str { + match self { + SmtpEvent::Error => "SMTP error occurred", + SmtpEvent::RemoteIdNotFound => "Remote host ID not found", + SmtpEvent::ConcurrencyLimitExceeded => "Concurrency limit exceeded", + SmtpEvent::TransferLimitExceeded => "Transfer limit exceeded", + SmtpEvent::RateLimitExceeded => "Rate limit exceeded", + SmtpEvent::TimeLimitExceeded => "Time limit exceeded", + SmtpEvent::MissingAuthDirectory => "Missing auth directory", + SmtpEvent::MessageParseFailed => "Message parsing failed", + SmtpEvent::MessageTooLarge => "Message too large", + SmtpEvent::LoopDetected => "Mail loop detected", + SmtpEvent::PipeSuccess => "Pipe command succeeded", + SmtpEvent::PipeError => "Pipe command failed", + SmtpEvent::DkimPass => "DKIM verification passed", + SmtpEvent::DkimFail => "DKIM verification failed", + SmtpEvent::ArcPass => "ARC verification passed", + SmtpEvent::ArcFail => "ARC verification failed", + SmtpEvent::SpfEhloPass => "SPF EHLO check passed", + SmtpEvent::SpfEhloFail => "SPF EHLO check failed", + SmtpEvent::SpfFromPass => "SPF From check passed", + SmtpEvent::SpfFromFail => "SPF From check failed", + SmtpEvent::DmarcPass => "DMARC check passed", + SmtpEvent::DmarcFail => "DMARC check failed", + SmtpEvent::IprevPass => "IPREV check passed", + SmtpEvent::IprevFail => "IPREV check failed", + SmtpEvent::TooManyMessages => "Too many messages", + SmtpEvent::Ehlo => "SMTP EHLO command", + SmtpEvent::InvalidEhlo => "Invalid EHLO command", + SmtpEvent::DidNotSayEhlo => "Client did not say EHLO", + SmtpEvent::EhloExpected => "EHLO command expected", + SmtpEvent::LhloExpected => "LHLO command expected", + SmtpEvent::MailFromUnauthenticated => "MAIL FROM unauthenticated", + SmtpEvent::MailFromUnauthorized => "MAIL FROM unauthorized", + SmtpEvent::MailFromRewritten => "MAIL FROM address rewritten", + SmtpEvent::MailFromMissing => "MAIL FROM address missing", + SmtpEvent::MailFrom => "SMTP MAIL FROM command", + SmtpEvent::MultipleMailFrom => "Multiple MAIL FROM commands", + SmtpEvent::MailboxDoesNotExist => "Mailbox does not exist", + SmtpEvent::RelayNotAllowed => "Relay not allowed", + SmtpEvent::RcptTo => "SMTP RCPT TO command", + SmtpEvent::RcptToDuplicate => "Duplicate RCPT TO", + SmtpEvent::RcptToRewritten => "RCPT TO address rewritten", + SmtpEvent::RcptToMissing => "RCPT TO address missing", + SmtpEvent::TooManyRecipients => "Too many recipients", + SmtpEvent::TooManyInvalidRcpt => "Too many invalid recipients", + SmtpEvent::RawInput => "Raw SMTP input received", + SmtpEvent::RawOutput => "Raw SMTP output sent", + SmtpEvent::MissingLocalHostname => "Missing local hostname", + SmtpEvent::Vrfy => "SMTP VRFY command", + SmtpEvent::VrfyNotFound => "VRFY address not found", + SmtpEvent::VrfyDisabled => "VRFY command disabled", + SmtpEvent::Expn => "SMTP EXPN command", + SmtpEvent::ExpnNotFound => "EXPN address not found", + SmtpEvent::ExpnDisabled => "EXPN command disabled", + SmtpEvent::RequireTlsDisabled => "REQUIRETLS extension disabled", + SmtpEvent::DeliverByDisabled => "DELIVERBY extension disabled", + SmtpEvent::DeliverByInvalid => "Invalid DELIVERBY parameter", + SmtpEvent::FutureReleaseDisabled => "FUTURE RELEASE extension disabled", + SmtpEvent::FutureReleaseInvalid => "Invalid FUTURE RELEASE parameter", + SmtpEvent::MtPriorityDisabled => "MT-PRIORITY extension disabled", + SmtpEvent::MtPriorityInvalid => "Invalid MT-PRIORITY parameter", + SmtpEvent::DsnDisabled => "DSN extension disabled", + SmtpEvent::AuthNotAllowed => "Authentication not allowed", + SmtpEvent::AuthMechanismNotSupported => "Auth mechanism not supported", + SmtpEvent::AuthExchangeTooLong => "Auth exchange too long", + SmtpEvent::AlreadyAuthenticated => "Already authenticated", + SmtpEvent::Noop => "SMTP NOOP command", + SmtpEvent::StartTls => "SMTP STARTTLS command", + SmtpEvent::StartTlsUnavailable => "STARTTLS unavailable", + SmtpEvent::StartTlsAlready => "TLS already active", + SmtpEvent::Rset => "SMTP RSET command", + SmtpEvent::Quit => "SMTP QUIT command", + SmtpEvent::Help => "SMTP HELP command", + SmtpEvent::CommandNotImplemented => "Command not implemented", + SmtpEvent::InvalidCommand => "Invalid command", + SmtpEvent::InvalidSenderAddress => "Invalid sender address", + SmtpEvent::InvalidRecipientAddress => "Invalid recipient address", + SmtpEvent::InvalidParameter => "Invalid parameter", + SmtpEvent::UnsupportedParameter => "Unsupported parameter", + SmtpEvent::SyntaxError => "Syntax error", + SmtpEvent::RequestTooLarge => "Request too large", + } + } +} + +impl DeliveryEvent { + pub fn description(&self) -> &'static str { + match self { + DeliveryEvent::AttemptStart => "Delivery attempt started", + DeliveryEvent::AttemptEnd => "Delivery attempt ended", + DeliveryEvent::Completed => "Delivery completed", + DeliveryEvent::Failed => "Delivery failed", + DeliveryEvent::DomainDeliveryStart => "New delivery attempt for domain", + DeliveryEvent::MxLookup => "MX record lookup", + DeliveryEvent::MxLookupFailed => "MX record lookup failed", + DeliveryEvent::IpLookup => "IP address lookup", + DeliveryEvent::IpLookupFailed => "IP address lookup failed", + DeliveryEvent::NullMX => "Null MX record found", + DeliveryEvent::Connect => "Connecting to remote server", + DeliveryEvent::ConnectError => "Connection error", + DeliveryEvent::MissingOutboundHostname => "Missing outbound hostname in configuration", + DeliveryEvent::GreetingFailed => "SMTP greeting failed", + DeliveryEvent::Ehlo => "SMTP EHLO command", + DeliveryEvent::EhloRejected => "SMTP EHLO rejected", + DeliveryEvent::Auth => "SMTP authentication", + DeliveryEvent::AuthFailed => "SMTP authentication failed", + DeliveryEvent::MailFrom => "SMTP MAIL FROM command", + DeliveryEvent::MailFromRejected => "SMTP MAIL FROM rejected", + DeliveryEvent::Delivered => "Message delivered", + DeliveryEvent::RcptTo => "SMTP RCPT TO command", + DeliveryEvent::RcptToRejected => "SMTP RCPT TO rejected", + DeliveryEvent::RcptToFailed => "SMTP RCPT TO failed", + DeliveryEvent::MessageRejected => "Message rejected by remote server", + DeliveryEvent::StartTls => "SMTP STARTTLS command", + DeliveryEvent::StartTlsUnavailable => "STARTTLS unavailable", + DeliveryEvent::StartTlsError => "STARTTLS error", + DeliveryEvent::StartTlsDisabled => "STARTTLS disabled", + DeliveryEvent::ImplicitTlsError => "Implicit TLS error", + DeliveryEvent::ConcurrencyLimitExceeded => "Concurrency limit exceeded", + DeliveryEvent::RateLimitExceeded => "Rate limit exceeded", + DeliveryEvent::DoubleBounce => "Discarding message after double bounce", + DeliveryEvent::DsnSuccess => "DSN success notification", + DeliveryEvent::DsnTempFail => "DSN temporary failure notification", + DeliveryEvent::DsnPermFail => "DSN permanent failure notification", + DeliveryEvent::RawInput => "Raw SMTP input received", + DeliveryEvent::RawOutput => "Raw SMTP output sent", + } + } +} + +impl QueueEvent { + pub fn description(&self) -> &'static str { + match self { + QueueEvent::Scheduled => "Message scheduled for delivery", + QueueEvent::Rescheduled => "Message rescheduled for delivery", + QueueEvent::LockBusy => "Queue lock is busy", + QueueEvent::Locked => "Queue is locked", + QueueEvent::BlobNotFound => "Message blob not found", + QueueEvent::RateLimitExceeded => "Rate limit exceeded", + QueueEvent::ConcurrencyLimitExceeded => "Concurrency limit exceeded", + QueueEvent::QuotaExceeded => "Quota exceeded", + } + } +} + +impl IncomingReportEvent { + pub fn description(&self) -> &'static str { + match self { + IncomingReportEvent::DmarcReport => "DMARC report received", + IncomingReportEvent::DmarcReportWithWarnings => "DMARC report received with warnings", + IncomingReportEvent::TlsReport => "TLS report received", + IncomingReportEvent::TlsReportWithWarnings => "TLS report received with warnings", + IncomingReportEvent::AbuseReport => "Abuse report received", + IncomingReportEvent::AuthFailureReport => "Authentication failure report received", + IncomingReportEvent::FraudReport => "Fraud report received", + IncomingReportEvent::NotSpamReport => "Not spam report received", + IncomingReportEvent::VirusReport => "Virus report received", + IncomingReportEvent::OtherReport => "Other type of report received", + IncomingReportEvent::MessageParseFailed => "Failed to parse incoming report message", + IncomingReportEvent::DmarcParseFailed => "Failed to parse DMARC report", + IncomingReportEvent::TlsRpcParseFailed => "Failed to parse TLS RPC report", + IncomingReportEvent::ArfParseFailed => "Failed to parse ARF report", + IncomingReportEvent::DecompressError => "Error decompressing report", + } + } +} + +impl OutgoingReportEvent { + pub fn description(&self) -> &'static str { + match self { + OutgoingReportEvent::SpfReport => "SPF report sent", + OutgoingReportEvent::SpfRateLimited => "SPF report rate limited", + OutgoingReportEvent::DkimReport => "DKIM report sent", + OutgoingReportEvent::DkimRateLimited => "DKIM report rate limited", + OutgoingReportEvent::DmarcReport => "DMARC report sent", + OutgoingReportEvent::DmarcRateLimited => "DMARC report rate limited", + OutgoingReportEvent::DmarcAggregateReport => "DMARC aggregate report sent", + OutgoingReportEvent::TlsAggregate => "TLS aggregate report sent", + OutgoingReportEvent::HttpSubmission => "Report submitted via HTTP", + OutgoingReportEvent::UnauthorizedReportingAddress => "Unauthorized reporting address", + OutgoingReportEvent::ReportingAddressValidationError => { + "Error validating reporting address" + } + OutgoingReportEvent::NotFound => "Report not found", + OutgoingReportEvent::SubmissionError => "Error submitting report", + OutgoingReportEvent::NoRecipientsFound => "No recipients found for report", + OutgoingReportEvent::LockBusy => "Report lock is busy", + OutgoingReportEvent::LockDeleted => "Report lock was deleted", + OutgoingReportEvent::Locked => "Report is locked", + } + } +} + +impl MtaStsEvent { + pub fn description(&self) -> &'static str { + match self { + MtaStsEvent::Authorized => "Host authorized by MTA-STS policy", + MtaStsEvent::NotAuthorized => "Host not authorized by MTA-STS policy", + MtaStsEvent::PolicyFetch => "Fetched MTA-STS policy", + MtaStsEvent::PolicyNotFound => "MTA-STS policy not found", + MtaStsEvent::PolicyFetchError => "Error fetching MTA-STS policy", + MtaStsEvent::InvalidPolicy => "Invalid MTA-STS policy", + } + } +} + +impl TlsRptEvent { + pub fn description(&self) -> &'static str { + match self { + TlsRptEvent::RecordFetch => "Fetched TLS-RPT record", + TlsRptEvent::RecordFetchError => "Error fetching TLS-RPT record", + } + } +} + +impl DaneEvent { + pub fn description(&self) -> &'static str { + match self { + DaneEvent::AuthenticationSuccess => "DANE authentication successful", + DaneEvent::AuthenticationFailure => "DANE authentication failed", + DaneEvent::NoCertificatesFound => "No certificates found for DANE", + DaneEvent::CertificateParseError => "Error parsing certificate for DANE", + DaneEvent::TlsaRecordMatch => "TLSA record match found", + DaneEvent::TlsaRecordFetch => "Fetching TLSA record", + DaneEvent::TlsaRecordFetchError => "Error fetching TLSA record", + DaneEvent::TlsaRecordNotFound => "TLSA record not found", + DaneEvent::TlsaRecordNotDnssecSigned => "TLSA record not DNSSEC signed", + DaneEvent::TlsaRecordInvalid => "Invalid TLSA record", + } + } +} + +impl MilterEvent { + pub fn description(&self) -> &'static str { + match self { + MilterEvent::Read => "Reading from Milter", + MilterEvent::Write => "Writing to Milter", + MilterEvent::ActionAccept => "Milter action: Accept", + MilterEvent::ActionDiscard => "Milter action: Discard", + MilterEvent::ActionReject => "Milter action: Reject", + MilterEvent::ActionTempFail => "Milter action: Temporary failure", + MilterEvent::ActionReplyCode => "Milter action: Reply code", + MilterEvent::ActionConnectionFailure => "Milter action: Connection failure", + MilterEvent::ActionShutdown => "Milter action: Shutdown", + MilterEvent::IoError => "Milter I/O error", + MilterEvent::FrameTooLarge => "Milter frame too large", + MilterEvent::FrameInvalid => "Invalid Milter frame", + MilterEvent::UnexpectedResponse => "Unexpected Milter response", + MilterEvent::Timeout => "Milter timeout", + MilterEvent::TlsInvalidName => "Invalid TLS name for Milter", + MilterEvent::Disconnected => "Milter disconnected", + MilterEvent::ParseError => "Milter parse error", + } + } +} + +impl MtaHookEvent { + pub fn description(&self) -> &'static str { + match self { + MtaHookEvent::ActionAccept => "MTA hook action: Accept", + MtaHookEvent::ActionDiscard => "MTA hook action: Discard", + MtaHookEvent::ActionReject => "MTA hook action: Reject", + MtaHookEvent::ActionQuarantine => "MTA hook action: Quarantine", + MtaHookEvent::Error => "MTA hook error", + } + } +} + +impl PushSubscriptionEvent { + pub fn description(&self) -> &'static str { + match self { + PushSubscriptionEvent::Success => "Push subscription successful", + PushSubscriptionEvent::Error => "Push subscription error", + PushSubscriptionEvent::NotFound => "Push subscription not found", + } + } +} + +impl SpamEvent { + pub fn description(&self) -> &'static str { + match self { + SpamEvent::PyzorError => "Pyzor error", + SpamEvent::ListUpdated => "Spam list updated", + SpamEvent::Train => "Training spam filter", + SpamEvent::TrainBalance => "Balancing spam filter training data", + SpamEvent::TrainError => "Error training spam filter", + SpamEvent::Classify => "Classifying message for spam", + SpamEvent::ClassifyError => "Error classifying message for spam", + SpamEvent::NotEnoughTrainingData => "Not enough training data for spam filter", + } + } +} + +impl SieveEvent { + pub fn description(&self) -> &'static str { + match self { + SieveEvent::ActionAccept => "Sieve action: Accept", + SieveEvent::ActionAcceptReplace => "Sieve action: Accept and replace", + SieveEvent::ActionDiscard => "Sieve action: Discard", + SieveEvent::ActionReject => "Sieve action: Reject", + SieveEvent::SendMessage => "Sieve sending message", + SieveEvent::MessageTooLarge => "Sieve message too large", + SieveEvent::ScriptNotFound => "Sieve script not found", + SieveEvent::ListNotFound => "Sieve list not found", + SieveEvent::RuntimeError => "Sieve runtime error", + SieveEvent::UnexpectedError => "Unexpected Sieve error", + SieveEvent::NotSupported => "Sieve action not supported", + SieveEvent::QuotaExceeded => "Sieve quota exceeded", + } + } +} + +impl TlsEvent { + pub fn description(&self) -> &'static str { + match self { + TlsEvent::Handshake => "TLS handshake", + TlsEvent::HandshakeError => "TLS handshake error", + TlsEvent::NotConfigured => "TLS not configured", + TlsEvent::CertificateNotFound => "TLS certificate not found", + TlsEvent::NoCertificatesAvailable => "No TLS certificates available", + TlsEvent::MultipleCertificatesAvailable => "Multiple TLS certificates available", + } + } +} + +impl NetworkEvent { + pub fn description(&self) -> &'static str { + match self { + NetworkEvent::ConnectionStart => "Network connection started", + NetworkEvent::ConnectionEnd => "Network connection ended", + NetworkEvent::ListenStart => "Network listening started", + NetworkEvent::ListenStop => "Network listening stopped", + NetworkEvent::ListenError => "Network listening error", + NetworkEvent::BindError => "Network bind error", + NetworkEvent::ReadError => "Network read error", + NetworkEvent::WriteError => "Network write error", + NetworkEvent::FlushError => "Network flush error", + NetworkEvent::AcceptError => "Network accept error", + NetworkEvent::SplitError => "Network split error", + NetworkEvent::Timeout => "Network timeout", + NetworkEvent::Closed => "Network connection closed", + NetworkEvent::ProxyError => "Proxy protocol error", + NetworkEvent::SetOptError => "Network set option error", + NetworkEvent::DropBlocked => "Dropped connection from blocked IP address", + } + } +} + +impl ServerEvent { + pub fn description(&self) -> &'static str { + match self { + ServerEvent::Startup => { + concat!("Starting Stalwart Mail Server v", env!("CARGO_PKG_VERSION")) + } + ServerEvent::Shutdown => concat!( + "Shutting down Stalwart Mail Server v", + env!("CARGO_PKG_VERSION") + ), + ServerEvent::StartupError => "Server startup error", + ServerEvent::ThreadError => "Server thread error", + ServerEvent::Licensing => "Server licensing event", + } + } +} + +impl TracingEvent { + pub fn description(&self) -> &'static str { + match self { + TracingEvent::Update => "Tracing update", + TracingEvent::LogError => "Log collector error", + TracingEvent::WebhookError => "Webhook collector error", + TracingEvent::OtelError => "OpenTelemetry collector error", + TracingEvent::JournalError => "Journal collector error", + } + } +} + +impl AcmeEvent { + pub fn description(&self) -> &'static str { + match self { + AcmeEvent::AuthStart => "ACME authentication started", + AcmeEvent::AuthPending => "ACME authentication pending", + AcmeEvent::AuthValid => "ACME authentication valid", + AcmeEvent::AuthCompleted => "ACME authentication completed", + AcmeEvent::AuthError => "ACME authentication error", + AcmeEvent::AuthTooManyAttempts => "Too many ACME authentication attempts", + AcmeEvent::ProcessCert => "Processing ACME certificate", + AcmeEvent::OrderStart => "ACME order started", + AcmeEvent::OrderProcessing => "ACME order processing", + AcmeEvent::OrderCompleted => "ACME order completed", + AcmeEvent::OrderReady => "ACME order ready", + AcmeEvent::OrderValid => "ACME order valid", + AcmeEvent::OrderInvalid => "ACME order invalid", + AcmeEvent::RenewBackoff => "ACME renew backoff", + AcmeEvent::DnsRecordCreated => "ACME DNS record created", + AcmeEvent::DnsRecordCreationFailed => "ACME DNS record creation failed", + AcmeEvent::DnsRecordDeletionFailed => "ACME DNS record deletion failed", + AcmeEvent::DnsRecordNotPropagated => "ACME DNS record not propagated", + AcmeEvent::DnsRecordLookupFailed => "ACME DNS record lookup failed", + AcmeEvent::DnsRecordPropagated => "ACME DNS record propagated", + AcmeEvent::DnsRecordPropagationTimeout => "ACME DNS record propagation timeout", + AcmeEvent::ClientSuppliedSNI => "ACME client supplied SNI", + AcmeEvent::ClientMissingSNI => "ACME client missing SNI", + AcmeEvent::TlsAlpnReceived => "ACME TLS ALPN received", + AcmeEvent::TlsAlpnError => "ACME TLS ALPN error", + AcmeEvent::TokenNotFound => "ACME token not found", + AcmeEvent::Error => "ACME error", + } + } +} + +impl PurgeEvent { + pub fn description(&self) -> &'static str { + match self { + PurgeEvent::Started => "Purge started", + PurgeEvent::Finished => "Purge finished", + PurgeEvent::Running => "Purge running", + PurgeEvent::Error => "Purge error", + PurgeEvent::PurgeActive => "Active purge in progress", + PurgeEvent::AutoExpunge => "Auto-expunge executed", + PurgeEvent::TombstoneCleanup => "Tombstone cleanup executed", + } + } +} + +impl EvalEvent { + pub fn description(&self) -> &'static str { + match self { + EvalEvent::Result => "Expression evaluation result", + EvalEvent::Error => "Expression evaluation error", + EvalEvent::DirectoryNotFound => "Directory not found while evaluating expression", + EvalEvent::StoreNotFound => "Store not found while evaluating expression", + } + } +} + +impl ConfigEvent { + pub fn description(&self) -> &'static str { + match self { + ConfigEvent::ParseError => "Configuration parse error", + ConfigEvent::BuildError => "Configuration build error", + ConfigEvent::MacroError => "Configuration macro error", + ConfigEvent::WriteError => "Configuration write error", + ConfigEvent::FetchError => "Configuration fetch error", + ConfigEvent::DefaultApplied => "Default configuration applied", + ConfigEvent::MissingSetting => "Missing configuration setting", + ConfigEvent::UnusedSetting => "Unused configuration setting", + ConfigEvent::ParseWarning => "Configuration parse warning", + ConfigEvent::BuildWarning => "Configuration build warning", + ConfigEvent::ImportExternal => "Importing external configuration", + ConfigEvent::ExternalKeyIgnored => "External configuration key ignored", + ConfigEvent::AlreadyUpToDate => "Configuration already up to date", + } + } +} + +impl ArcEvent { + pub fn description(&self) -> &'static str { + match self { + ArcEvent::ChainTooLong => "ARC chain too long", + ArcEvent::InvalidInstance => "Invalid ARC instance", + ArcEvent::InvalidCV => "Invalid ARC CV", + ArcEvent::HasHeaderTag => "ARC has header tag", + ArcEvent::BrokenChain => "Broken ARC chain", + ArcEvent::SealerNotFound => "ARC sealer not found", + } + } +} + +impl DkimEvent { + pub fn description(&self) -> &'static str { + match self { + DkimEvent::Pass => "DKIM verification passed", + DkimEvent::Neutral => "DKIM verification neutral", + DkimEvent::Fail => "DKIM verification failed", + DkimEvent::PermError => "DKIM permanent error", + DkimEvent::TempError => "DKIM temporary error", + DkimEvent::None => "No DKIM signature", + DkimEvent::UnsupportedVersion => "Unsupported DKIM version", + DkimEvent::UnsupportedAlgorithm => "Unsupported DKIM algorithm", + DkimEvent::UnsupportedCanonicalization => "Unsupported DKIM canonicalization", + DkimEvent::UnsupportedKeyType => "Unsupported DKIM key type", + DkimEvent::FailedBodyHashMatch => "DKIM body hash mismatch", + DkimEvent::FailedVerification => "DKIM verification failed", + DkimEvent::FailedAuidMatch => "DKIM AUID mismatch", + DkimEvent::RevokedPublicKey => "DKIM public key revoked", + DkimEvent::IncompatibleAlgorithms => "Incompatible DKIM algorithms", + DkimEvent::SignatureExpired => "DKIM signature expired", + DkimEvent::SignatureLength => "DKIM signature length issue", + DkimEvent::SignerNotFound => "DKIM signer not found", + } + } +} + +impl SpfEvent { + pub fn description(&self) -> &'static str { + match self { + SpfEvent::Pass => "SPF check passed", + SpfEvent::Fail => "SPF check failed", + SpfEvent::SoftFail => "SPF soft fail", + SpfEvent::Neutral => "SPF neutral result", + SpfEvent::TempError => "SPF temporary error", + SpfEvent::PermError => "SPF permanent error", + SpfEvent::None => "No SPF record", + } + } +} + +impl DmarcEvent { + pub fn description(&self) -> &'static str { + match self { + DmarcEvent::Pass => "DMARC check passed", + DmarcEvent::Fail => "DMARC check failed", + DmarcEvent::PermError => "DMARC permanent error", + DmarcEvent::TempError => "DMARC temporary error", + DmarcEvent::None => "No DMARC record", + } + } +} + +impl IprevEvent { + pub fn description(&self) -> &'static str { + match self { + IprevEvent::Pass => "IPREV check passed", + IprevEvent::Fail => "IPREV check failed", + IprevEvent::PermError => "IPREV permanent error", + IprevEvent::TempError => "IPREV temporary error", + IprevEvent::None => "No IPREV record", + } + } +} + +impl MailAuthEvent { + pub fn description(&self) -> &'static str { + match self { + MailAuthEvent::ParseError => "Mail authentication parse error", + MailAuthEvent::MissingParameters => "Missing mail authentication parameters", + MailAuthEvent::NoHeadersFound => "No headers found in message", + MailAuthEvent::Crypto => "Crypto error during mail authentication", + MailAuthEvent::Io => "I/O error during mail authentication", + MailAuthEvent::Base64 => "Base64 error during mail authentication", + MailAuthEvent::DnsError => "DNS error", + MailAuthEvent::DnsRecordNotFound => "DNS record not found", + MailAuthEvent::DnsInvalidRecordType => "Invalid DNS record type", + MailAuthEvent::PolicyNotAligned => "Policy not aligned", + } + } +} + +impl StoreEvent { + pub fn description(&self) -> &'static str { + match self { + StoreEvent::IngestError => "Message ingestion error", + StoreEvent::AssertValueFailed => "Another process modified the record", + StoreEvent::FoundationDBError => "FoundationDB error", + StoreEvent::MySQLError => "MySQL error", + StoreEvent::PostgreSQLError => "PostgreSQL error", + StoreEvent::RocksDBError => "RocksDB error", + StoreEvent::SQLiteError => "SQLite error", + StoreEvent::LdapError => "LDAP error", + StoreEvent::ElasticSearchError => "ElasticSearch error", + StoreEvent::RedisError => "Redis error", + StoreEvent::S3Error => "S3 error", + StoreEvent::FilesystemError => "Filesystem error", + StoreEvent::PoolError => "Connection pool error", + StoreEvent::DataCorruption => "Data corruption detected", + StoreEvent::DecompressError => "Decompression error", + StoreEvent::DeserializeError => "Deserialization error", + StoreEvent::NotFound => "Record not found in database", + StoreEvent::NotConfigured => "Store not configured", + StoreEvent::NotSupported => "Operation not supported by store", + StoreEvent::UnexpectedError => "Unexpected store error", + StoreEvent::CryptoError => "Store crypto error", + StoreEvent::BlobMissingMarker => "Blob missing marker", + StoreEvent::Ingest => "Message ingested", + StoreEvent::IngestDuplicate => "Skipping duplicate message", + StoreEvent::SqlQuery => "SQL query executed", + StoreEvent::LdapQuery => "LDAP query executed", + StoreEvent::LdapBind => "LDAP bind operation", + } + } +} + +impl JmapEvent { + pub fn description(&self) -> &'static str { + match self { + JmapEvent::MethodCall => "JMAP method call", + JmapEvent::InvalidArguments => "Invalid JMAP arguments", + JmapEvent::RequestTooLarge => "JMAP request too large", + JmapEvent::StateMismatch => "JMAP state mismatch", + JmapEvent::AnchorNotFound => "JMAP anchor not found", + JmapEvent::UnsupportedFilter => "Unsupported JMAP filter", + JmapEvent::UnsupportedSort => "Unsupported JMAP sort", + JmapEvent::UnknownMethod => "Unknown JMAP method", + JmapEvent::InvalidResultReference => "Invalid JMAP result reference", + JmapEvent::Forbidden => "JMAP operation forbidden", + JmapEvent::AccountNotFound => "JMAP account not found", + JmapEvent::AccountNotSupportedByMethod => "JMAP account not supported by method", + JmapEvent::AccountReadOnly => "JMAP account is read-only", + JmapEvent::NotFound => "JMAP resource not found", + JmapEvent::CannotCalculateChanges => "Cannot calculate JMAP changes", + JmapEvent::UnknownDataType => "Unknown JMAP data type", + JmapEvent::UnknownCapability => "Unknown JMAP capability", + JmapEvent::NotJSON => "JMAP request is not JSON", + JmapEvent::NotRequest => "JMAP input is not a request", + JmapEvent::WebsocketStart => "JMAP WebSocket connection started", + JmapEvent::WebsocketStop => "JMAP WebSocket connection stopped", + JmapEvent::WebsocketError => "JMAP WebSocket error", + } + } +} + +impl LimitEvent { + pub fn description(&self) -> &'static str { + match self { + LimitEvent::SizeRequest => "Request size limit reached", + LimitEvent::SizeUpload => "Upload size limit reached", + LimitEvent::CallsIn => "Incoming calls limit reached", + LimitEvent::ConcurrentRequest => "Concurrent request limit reached", + LimitEvent::ConcurrentUpload => "Concurrent upload limit reached", + LimitEvent::ConcurrentConnection => "Concurrent connection limit reached", + LimitEvent::Quota => "Quota limit reached", + LimitEvent::BlobQuota => "Blob quota limit reached", + LimitEvent::TooManyRequests => "Too many requests", + } + } +} + +impl ManageEvent { + pub fn description(&self) -> &'static str { + match self { + ManageEvent::MissingParameter => "Missing management parameter", + ManageEvent::AlreadyExists => "Managed resource already exists", + ManageEvent::AssertFailed => "Management assertion failed", + ManageEvent::NotFound => "Managed resource not found", + ManageEvent::NotSupported => "Management operation not supported", + ManageEvent::Error => "Management error", + } + } +} + +impl AuthEvent { + pub fn description(&self) -> &'static str { + match self { + AuthEvent::Success => "Authentication successful", + AuthEvent::Failed => "Authentication failed", + AuthEvent::MissingTotp => "Missing TOTP for authentication", + AuthEvent::TooManyAttempts => "Too many authentication attempts", + AuthEvent::Banned => "IP address banned after multiple authentication failures", + AuthEvent::Error => "Authentication error", + } + } +} + +impl ResourceEvent { + pub fn description(&self) -> &'static str { + match self { + ResourceEvent::NotFound => "Resource not found", + ResourceEvent::BadParameters => "Bad resource parameters", + ResourceEvent::Error => "Resource error", + ResourceEvent::DownloadExternal => "Downloading external resource", + ResourceEvent::WebadminUnpacked => "Webadmin resource unpacked", + } + } +} diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index e012bce1..2cc00b4d 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -27,7 +27,7 @@ pub type Error = Event; #[derive(Debug, Clone)] pub struct Event { pub inner: T, - keys: Vec<(Key, Value)>, + pub keys: Vec<(Key, Value)>, } #[derive(Debug, Clone)] @@ -41,12 +41,12 @@ pub struct EventDetails { #[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)] #[repr(usize)] pub enum Level { - Disable = 0, - Trace = 1, - Debug = 2, - Info = 3, - Warn = 4, - Error = 5, + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Disable = 5, } #[derive(Debug, Default, Clone)] @@ -439,7 +439,7 @@ pub enum DeliveryEvent { AttemptEnd, Completed, Failed, - AttemptCount, + DomainDeliveryStart, MxLookup, MxLookupFailed, IpLookup, diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 1a042447..d8fec81a 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -143,7 +143,7 @@ pub fn failed(message: &str) -> ! { std::process::exit(1); } -pub async fn wait_for_shutdown(message: &str) { +pub async fn wait_for_shutdown() { #[cfg(not(target_env = "msvc"))] let signal = { use tokio::signal::unix::{signal, SignalKind}; @@ -172,11 +172,7 @@ pub async fn wait_for_shutdown(message: &str) { } }; - trc::event!( - Server(trc::ServerEvent::Shutdown), - Details = message.to_string(), - CausedBy = signal - ); + trc::event!(Server(trc::ServerEvent::Shutdown), CausedBy = signal); } pub fn rustls_client_config(allow_invalid_certs: bool) -> ClientConfig { diff --git a/tests/resources/otel/docker-compose.yaml b/tests/resources/otel/docker-compose.yaml new file mode 100644 index 00000000..edeeb41d --- /dev/null +++ b/tests/resources/otel/docker-compose.yaml @@ -0,0 +1,45 @@ +# docker compose up -d + +version: "2" +services: + + # Jaeger + jaeger-all-in-one: + image: jaegertracing/all-in-one:latest + restart: always + network_mode: host + ports: + - "16686:16686" + - "14268" + - "14250" + + # Zipkin + zipkin-all-in-one: + image: openzipkin/zipkin:latest + restart: always + network_mode: host + ports: + - "9411:9411" + + # Collector + otel-collector: + image: otel/opentelemetry-collector:latest + restart: always + network_mode: host + command: + [ + "--config=/etc/otel-collector-config.yaml", + "${OTELCOL_ARGS}" + ] + volumes: + - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml + ports: + - "1888:1888" # pprof extension + - "8888:8888" # Prometheus metrics exposed by the collector + - "8889:8889" # Prometheus exporter metrics + - "13133:13133" # health_check extension + - "4317:4317" # OTLP gRPC receiver + - "55679:55679" # zpages extension + depends_on: + - jaeger-all-in-one + - zipkin-all-in-one diff --git a/tests/resources/otel/otel-collector-config.yaml b/tests/resources/otel/otel-collector-config.yaml new file mode 100644 index 00000000..5c6d4927 --- /dev/null +++ b/tests/resources/otel/otel-collector-config.yaml @@ -0,0 +1,40 @@ +# docker run -p 4317:4317 --network host --rm -v $(pwd)/otel-collector-config.yaml:/etc/otelcol/config.yaml otel/opentelemetry-collector + +receivers: + otlp: + protocols: + grpc: + +exporters: + zipkin: + endpoint: "http://zipkin-all-in-one:9411/api/v2/spans" + format: proto + + otlp: + endpoint: jaeger-all-in-one:4317 + tls: + insecure: true + debug: + verbosity: detailed + +processors: + batch: + +extensions: + health_check: + pprof: + endpoint: :1888 + zpages: + endpoint: :55679 + +service: + extensions: [pprof, zpages, health_check] + pipelines: + traces: + receivers: [otlp] + processors: [batch] + exporters: [zipkin, otlp] + logs: + receivers: [otlp] + processors: [batch] + exporters: [debug] diff --git a/tests/resources/otel/stalwart-config.toml b/tests/resources/otel/stalwart-config.toml new file mode 100644 index 00000000..4824833a --- /dev/null +++ b/tests/resources/otel/stalwart-config.toml @@ -0,0 +1,6 @@ +[tracer.otel] +type = "otel" +transport = "grpc" +endpoint = "http://127.0.0.1:4317" +level = "trace" + diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index 7f5246d9..a33c6539 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -269,6 +269,14 @@ user-code = "1s" token = "1s" refresh-token = "3s" refresh-token-renew = "2s" + +[tracer.console] +type = "console" +level = "{LEVEL}" +multiline = false +ansi = true +disabled-events = ["network.*"] + "#; #[allow(dead_code)] @@ -285,7 +293,11 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest { let mut config = Config::new( add_test_certs(SERVER) .replace("{STORE}", store_id) - .replace("{TMP}", &temp_dir.path.display().to_string()), + .replace("{TMP}", &temp_dir.path.display().to_string()) + .replace( + "{LEVEL}", + &std::env::var("LOG").unwrap_or_else(|_| "disable".to_string()), + ), ) .unwrap(); config.resolve_all_macros().await; @@ -300,6 +312,7 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest { let stores = Stores::parse_all(&mut config).await; // Parse core + let tracers = Tracers::parse(&mut config); let core = Core::parse(&mut config, stores, Default::default()).await; let store = core.storage.data.clone(); let shared_core = core.into_shared(); @@ -307,6 +320,9 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest { // Parse acceptors servers.parse_tcp_acceptors(&mut config, shared_core.clone()); + // Enable tracing + tracers.enable(); + // Setup IPC channels let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); let ipc = Ipc { delivery_tx }; @@ -415,10 +431,6 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest { #[tokio::test] pub async fn imap_tests() { - if let Ok(level) = std::env::var("LOG") { - Tracers::test_tracer(level.parse().unwrap()); - } - // Prepare settings let start_time = Instant::now(); let delete = true; diff --git a/tests/src/jmap/auth_limits.rs b/tests/src/jmap/auth_limits.rs index 1de19ae7..3cfeee38 100644 --- a/tests/src/jmap/auth_limits.rs +++ b/tests/src/jmap/auth_limits.rs @@ -266,11 +266,7 @@ pub async fn test(params: &mut JMAPTest) { assert_is_empty(server).await; // Check webhook events - params.webhook.assert_contains(&[ - "authFailure", - "authSuccess", - "authBanned", - "\"name\": \"jdoe@example.com\"", - "\"type\": \"individual\"", - ]); + params + .webhook + .assert_contains(&["auth.failed", "auth.success", "auth.banned"]); } diff --git a/tests/src/jmap/delivery.rs b/tests/src/jmap/delivery.rs index bcf90262..4124f758 100644 --- a/tests/src/jmap/delivery.rs +++ b/tests/src/jmap/delivery.rs @@ -307,13 +307,10 @@ pub async fn test(params: &mut JMAPTest) { // Check webhook events params.webhook.assert_contains(&[ - "message.accepted", - "message.appended", - "dsn", - "\"returnPath\": \"bill@example.com\"", - "\"sender\": \"bill@example.com\"", - "\"address\": \"john.doe@example.com\"", - "\"type\": \"success\"", + "store.ingest", + "delivery.dsn", + "\"from\": \"bill@example.com\"", + "\"to\": \"john.doe@example.com\"", ]); } diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 94e9fb65..401a60ee 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -274,9 +274,16 @@ refresh-token-renew = "2s" expn = true vrfy = true +[tracer.console] +type = "console" +level = "{LEVEL}" +multiline = false +ansi = true +disabled-events = ["network.*"] + [webhook."test"] url = "http://127.0.0.1:8821/hook" -events = ["*"] +events = ["auth.*", "delivery.dsn*", "store.ingest"] signature-key = "ovos-moles" throttle = "100ms" @@ -284,10 +291,6 @@ throttle = "100ms" #[tokio::test(flavor = "multi_thread")] pub async fn jmap_tests() { - if let Ok(level) = std::env::var("LOG") { - Tracers::test_tracer(level.parse().unwrap()); - } - let delete = true; let mut params = init_jmap_tests( &std::env::var("STORE") @@ -297,7 +300,7 @@ pub async fn jmap_tests() { .await; webhooks::test(&mut params).await; - email_query::test(&mut params, delete).await; + /*email_query::test(&mut params, delete).await; email_get::test(&mut params).await; email_set::test(&mut params).await; email_parse::test(&mut params).await; @@ -309,7 +312,7 @@ pub async fn jmap_tests() { thread_merge::test(&mut params).await; mailbox::test(&mut params).await; delivery::test(&mut params).await; - auth_acl::test(&mut params).await; + auth_acl::test(&mut params).await;*/ auth_limits::test(&mut params).await; auth_oauth::test(&mut params).await; event_source::test(&mut params).await; @@ -331,10 +334,6 @@ pub async fn jmap_tests() { #[tokio::test(flavor = "multi_thread")] #[ignore] pub async fn jmap_stress_tests() { - if let Ok(level) = std::env::var("LOG") { - Tracers::test_tracer(level.parse().unwrap()); - } - let params = init_jmap_tests( &std::env::var("STORE") .expect("Missing store type. Try running `STORE= cargo test`"), @@ -426,7 +425,11 @@ async fn init_jmap_tests(store_id: &str, delete_if_exists: bool) -> JMAPTest { let mut config = Config::new( add_test_certs(SERVER) .replace("{STORE}", store_id) - .replace("{TMP}", &temp_dir.path.display().to_string()), + .replace("{TMP}", &temp_dir.path.display().to_string()) + .replace( + "{LEVEL}", + &std::env::var("LOG").unwrap_or_else(|_| "disable".to_string()), + ), ) .unwrap(); config.resolve_all_macros().await; diff --git a/tests/src/jmap/webhooks.rs b/tests/src/jmap/webhooks.rs index 7dd9b595..8119bb29 100644 --- a/tests/src/jmap/webhooks.rs +++ b/tests/src/jmap/webhooks.rs @@ -42,7 +42,7 @@ pub async fn test(params: &mut JMAPTest) { tokio::time::sleep(Duration::from_millis(1000)).await; // Check for events - params.webhook.assert_contains(&["authSuccess"]); + params.webhook.assert_contains(&["auth.success"]); } impl MockWebhookEndpoint {