Import upstream v0.16.22, stripped
Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f Enterprise-only files removed or emptied: 63 Enterprise-only snippets removed: 117 in 50 files Dangling module declarations removed: 5 Cargo edits turning enterprise off: 14 Verification: clean Enterprise feature gates left for rebuilt features: 19 in 18 files Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "trc"
|
||||
version = "0.16.22"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
event_macro = { path = "./event-macro" }
|
||||
mail-auth = { version = "0.13", features = ["arc"] }
|
||||
mail-parser = { version = "0.11", features = ["full_encoding"] }
|
||||
base64 = "0.23.1"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0.151"
|
||||
reqwest = { version = "0.13", default-features = false, features = ["rustls", "http2"]}
|
||||
rtrb = "0.4.0"
|
||||
parking_lot = "0.12.5"
|
||||
tokio = { version = "1.53", features = ["net", "macros"] }
|
||||
ahash = "0.8.12"
|
||||
rkyv = { version = "0.8.18", features = ["little_endian"] }
|
||||
compact_str = "0.10.0"
|
||||
hashify = "0.2.9"
|
||||
|
||||
[features]
|
||||
test_mode = []
|
||||
dev_mode = []
|
||||
enterprise = []
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "event_macro"
|
||||
version = "0.16.22"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "3.0", features = ["full"] }
|
||||
quote = "1.0"
|
||||
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use syn::{Data, DeriveInput, Expr, ExprPath, Ident, Token, parse::Parse, parse_macro_input};
|
||||
|
||||
static mut GLOBAL_ID_COUNTER: usize = 0;
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn key_names(_attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let input = parse_macro_input!(item as DeriveInput);
|
||||
let name = &input.ident;
|
||||
|
||||
let enum_variants = match &input.data {
|
||||
Data::Enum(data_enum) => &data_enum.variants,
|
||||
_ => panic!("This macro only works with enums"),
|
||||
};
|
||||
|
||||
let mut variant_names = Vec::new();
|
||||
let mut camel_case_names = Vec::new();
|
||||
let mut snake_case_names = Vec::new();
|
||||
|
||||
for variant in enum_variants.iter() {
|
||||
let variant_name = &variant.ident;
|
||||
variant_names.push(variant_name);
|
||||
snake_case_names.push(to_snake_case(&variant_name.to_string()));
|
||||
camel_case_names.push(
|
||||
variant_name
|
||||
.to_string()
|
||||
.char_indices()
|
||||
.map(|(i, c)| if i == 0 { c.to_ascii_lowercase() } else { c })
|
||||
.collect::<String>(),
|
||||
);
|
||||
}
|
||||
|
||||
let id_fn = quote! {
|
||||
pub fn id(&self) -> &'static str {
|
||||
match self {
|
||||
#(Self::#variant_names => #snake_case_names,)*
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let name_fn = quote! {
|
||||
pub fn name(&self) -> &'static str {
|
||||
match self {
|
||||
#(Self::#variant_names => #camel_case_names,)*
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let parse_fn = quote! {
|
||||
pub fn try_parse(name: &str) -> Option<Self> {
|
||||
match name {
|
||||
#(#camel_case_names => Some(Self::#variant_names),)*
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let expanded = quote! {
|
||||
#input
|
||||
|
||||
impl #name {
|
||||
#name_fn
|
||||
#id_fn
|
||||
#parse_fn
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn total_event_count(_item: TokenStream) -> TokenStream {
|
||||
let count = unsafe { GLOBAL_ID_COUNTER };
|
||||
let expanded = quote! {
|
||||
#count
|
||||
};
|
||||
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
|
||||
}
|
||||
|
||||
struct EventMacroInput {
|
||||
event: Ident,
|
||||
param: Expr,
|
||||
key_values: Vec<(Ident, Expr)>,
|
||||
}
|
||||
|
||||
impl Parse for EventMacroInput {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let event: Ident = input.parse()?;
|
||||
let content;
|
||||
syn::parenthesized!(content in input);
|
||||
let param: Expr = content.parse()?;
|
||||
|
||||
let mut key_values = Vec::new();
|
||||
while !input.is_empty() {
|
||||
input.parse::<Token![,]>()?;
|
||||
if input.is_empty() {
|
||||
break;
|
||||
}
|
||||
let key: Ident = input.parse()?;
|
||||
input.parse::<Token![=]>()?;
|
||||
let value: Expr = input.parse()?;
|
||||
key_values.push((key, value));
|
||||
}
|
||||
|
||||
Ok(EventMacroInput {
|
||||
event,
|
||||
param,
|
||||
key_values,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn event(input: TokenStream) -> TokenStream {
|
||||
let EventMacroInput {
|
||||
event,
|
||||
param,
|
||||
key_values,
|
||||
} = parse_macro_input!(input as EventMacroInput);
|
||||
|
||||
let key_value_tokens = key_values.iter().map(|(key, value)| {
|
||||
quote! {
|
||||
(trc::Key::#key, trc::Value::from(#value))
|
||||
}
|
||||
});
|
||||
// This avoids having to evaluate expensive values when we know we are not interested in the event
|
||||
let key_value_metric_tokens = key_values.iter().filter_map(|(key, value)| {
|
||||
if key.is_metric_key() {
|
||||
Some(quote! {
|
||||
(trc::Key::#key, trc::Value::from(#value))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let expanded = if matches!(¶m, Expr::Path(ExprPath { path, .. }) if path.segments.len() > 1 && path.segments.last().unwrap().arguments.is_empty() )
|
||||
{
|
||||
quote! {{
|
||||
const ET: trc::EventType = trc::EventType::#event(#param);
|
||||
const ET_ID: usize = ET.to_id() as usize;
|
||||
if trc::Collector::has_interest(ET_ID) {
|
||||
let keys = vec![#(#key_value_tokens),*];
|
||||
if trc::Collector::is_metric(ET_ID) {
|
||||
trc::Collector::record_metric(ET, ET_ID, &keys);
|
||||
}
|
||||
trc::Event::with_keys(ET, keys).send();
|
||||
} else if trc::Collector::is_metric(ET_ID) {
|
||||
trc::Collector::record_metric(ET, ET_ID, &[#(#key_value_metric_tokens),*]);
|
||||
}
|
||||
}}
|
||||
} else {
|
||||
quote! {{
|
||||
let et = trc::EventType::#event(#param);
|
||||
let et_id = et.to_id() as usize;
|
||||
if trc::Collector::has_interest(et_id) {
|
||||
let keys = vec![#(#key_value_tokens),*];
|
||||
if trc::Collector::is_metric(et_id) {
|
||||
trc::Collector::record_metric(et, et_id, &keys);
|
||||
}
|
||||
trc::Event::with_keys(et, keys).send();
|
||||
} else if trc::Collector::is_metric(et_id) {
|
||||
trc::Collector::record_metric(et, et_id, &[#(#key_value_metric_tokens),*]);
|
||||
}
|
||||
}}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
trait IsMetricKey {
|
||||
fn is_metric_key(&self) -> bool;
|
||||
}
|
||||
|
||||
impl IsMetricKey for Ident {
|
||||
fn is_metric_key(&self) -> bool {
|
||||
matches!(
|
||||
self.to_string().as_ref(),
|
||||
"Total"
|
||||
| "Elapsed"
|
||||
| "Size"
|
||||
| "TotalSuccesses"
|
||||
| "TotalFailures"
|
||||
| "DmarcPass"
|
||||
| "DmarcQuarantine"
|
||||
| "DmarcReject"
|
||||
| "DmarcNone"
|
||||
| "DkimPass"
|
||||
| "DkimFail"
|
||||
| "DkimNone"
|
||||
| "SpfPass"
|
||||
| "SpfFail"
|
||||
| "SpfNone"
|
||||
| "Protocol"
|
||||
| "Code"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::atomic::{AtomicU32, AtomicU64, Ordering};
|
||||
|
||||
pub struct AtomicU32Array<const N: usize>([AtomicU32; N]);
|
||||
pub struct AtomicU64Array<const N: usize>([AtomicU64; N]);
|
||||
|
||||
impl<const N: usize> AtomicU32Array<N> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
pub const fn new() -> Self {
|
||||
Self({
|
||||
const INIT: AtomicU32 = AtomicU32::new(0);
|
||||
let mut array = [INIT; N];
|
||||
let mut i = 0;
|
||||
while i < N {
|
||||
array[i] = AtomicU32::new(0);
|
||||
i += 1;
|
||||
}
|
||||
array
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self, index: usize) -> u32 {
|
||||
self.0[index].load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set(&self, index: usize, value: u32) {
|
||||
self.0[index].store(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add(&self, index: usize, value: u32) {
|
||||
self.0[index].fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &[AtomicU32; N] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> AtomicU64Array<N> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
pub const fn new() -> Self {
|
||||
Self({
|
||||
const INIT: AtomicU64 = AtomicU64::new(0);
|
||||
let mut array = [INIT; N];
|
||||
let mut i = 0;
|
||||
while i < N {
|
||||
array[i] = AtomicU64::new(0);
|
||||
i += 1;
|
||||
}
|
||||
array
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self, index: usize) -> u64 {
|
||||
self.0[index].load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set(&self, index: usize, value: u64) {
|
||||
self.0[index].store(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add(&self, index: usize, value: u64) {
|
||||
self.0[index].fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &[AtomicU64; N] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
use crate::ipc::{USIZE_BITS, USIZE_BITS_MASK, bitset::Bitset};
|
||||
|
||||
pub struct AtomicBitset<const N: usize>([AtomicUsize; N]);
|
||||
|
||||
impl<const N: usize> AtomicBitset<N> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
pub const fn new() -> Self {
|
||||
Self({
|
||||
const INIT: AtomicUsize = AtomicUsize::new(0);
|
||||
let mut array = [INIT; N];
|
||||
let mut i = 0;
|
||||
while i < N {
|
||||
array[i] = AtomicUsize::new(0);
|
||||
i += 1;
|
||||
}
|
||||
array
|
||||
})
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set(&self, index: impl Into<usize>) {
|
||||
let index = index.into();
|
||||
self.0[index / USIZE_BITS].fetch_or(1 << (index & USIZE_BITS_MASK), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&self, index: impl Into<usize>) {
|
||||
let index = index.into();
|
||||
self.0[index / USIZE_BITS].fetch_and(!(1 << (index & USIZE_BITS_MASK)), Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self, index: impl Into<usize>) -> bool {
|
||||
let index = index.into();
|
||||
self.0[index / USIZE_BITS].load(Ordering::Relaxed) & (1 << (index & USIZE_BITS_MASK)) != 0
|
||||
}
|
||||
|
||||
pub fn update(&self, bitset: impl AsRef<Bitset<N>>) {
|
||||
let bitset = bitset.as_ref();
|
||||
for i in 0..N {
|
||||
self.0[i].store(bitset.0[i], Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn union(&self, bitset: impl AsRef<Bitset<N>>) {
|
||||
let bitset = bitset.as_ref();
|
||||
for i in 0..N {
|
||||
self.0[i].fetch_or(bitset.0[i], Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_all(&self) {
|
||||
for i in 0..N {
|
||||
self.0[i].store(0, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
for i in 0..N {
|
||||
if self.0[i].load(Ordering::Relaxed) != 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const TEST_SIZE: usize = 1000;
|
||||
type TestBitset = AtomicBitset<{ TEST_SIZE.div_ceil(USIZE_BITS) }>;
|
||||
static BITSET: TestBitset = TestBitset::new();
|
||||
|
||||
#[test]
|
||||
fn test_atomic_bitset() {
|
||||
for i in 0..TEST_SIZE {
|
||||
assert!(!BITSET.get(i), "Bit {} should be unset in new BITSET", i);
|
||||
}
|
||||
|
||||
for i in 0..TEST_SIZE {
|
||||
assert!(!BITSET.get(i), "Bit {} should be initially unset", i);
|
||||
BITSET.set(i);
|
||||
assert!(BITSET.get(i), "Bit {} should be set after setting", i);
|
||||
}
|
||||
|
||||
BITSET.clear_all();
|
||||
|
||||
for i in 0..TEST_SIZE {
|
||||
BITSET.set(i);
|
||||
assert!(BITSET.get(i), "Bit {} should be set before clearing", i);
|
||||
BITSET.clear(i);
|
||||
assert!(!BITSET.get(i), "Bit {} should be unset after clearing", i);
|
||||
}
|
||||
|
||||
BITSET.clear_all();
|
||||
|
||||
// Set even bits
|
||||
for i in (0..TEST_SIZE).step_by(2) {
|
||||
BITSET.set(i);
|
||||
}
|
||||
|
||||
// Check all bits
|
||||
for i in 0..TEST_SIZE {
|
||||
if i % 2 == 0 {
|
||||
assert!(BITSET.get(i), "Even bit {} should be set", i);
|
||||
} else {
|
||||
assert!(!BITSET.get(i), "Odd bit {} should be unset", i);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear even bits and set odd bits
|
||||
for i in 0..TEST_SIZE {
|
||||
if i % 2 == 0 {
|
||||
BITSET.clear(i);
|
||||
} else {
|
||||
BITSET.set(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Check all bits again
|
||||
for i in 0..TEST_SIZE {
|
||||
if i % 2 == 0 {
|
||||
assert!(!BITSET.get(i), "Even bit {} should now be unset", i);
|
||||
} else {
|
||||
assert!(BITSET.get(i), "Odd bit {} should now be set", i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
pub struct AtomicCounter {
|
||||
id: &'static str,
|
||||
description: &'static str,
|
||||
unit: &'static str,
|
||||
value: AtomicU64,
|
||||
}
|
||||
|
||||
impl AtomicCounter {
|
||||
pub const fn new(id: &'static str, description: &'static str, unit: &'static str) -> Self {
|
||||
Self {
|
||||
id,
|
||||
description,
|
||||
unit,
|
||||
value: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn increment(&self) {
|
||||
self.value.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn increment_by(&self, value: u64) {
|
||||
self.value.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn decrement(&self) {
|
||||
self.value.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn decrement_by(&self, value: u64) {
|
||||
self.value.fetch_sub(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> u64 {
|
||||
self.value.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn id(&self) -> &'static str {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn description(&self) -> &'static str {
|
||||
self.description
|
||||
}
|
||||
|
||||
pub fn unit(&self) -> &'static str {
|
||||
self.unit
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.value.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use crate::MetricType;
|
||||
|
||||
pub struct AtomicGauge {
|
||||
id: MetricType,
|
||||
value: AtomicU64,
|
||||
}
|
||||
|
||||
impl AtomicGauge {
|
||||
pub const fn new(id: MetricType) -> Self {
|
||||
Self {
|
||||
id,
|
||||
value: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn increment(&self) {
|
||||
self.value.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set(&self, value: u64) {
|
||||
self.value.store(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn decrement(&self) {
|
||||
self.value.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self) -> u64 {
|
||||
self.value.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add(&self, value: u64) {
|
||||
self.value.fetch_add(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn subtract(&self, value: u64) {
|
||||
self.value.fetch_sub(value, Ordering::Relaxed);
|
||||
}
|
||||
|
||||
pub fn id(&self) -> MetricType {
|
||||
self.id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
use crate::MetricType;
|
||||
|
||||
use super::array::AtomicU32Array;
|
||||
|
||||
pub struct AtomicHistogram<const N: usize> {
|
||||
id: MetricType,
|
||||
buckets: AtomicU32Array<N>,
|
||||
upper_bounds: [u64; N],
|
||||
sum: AtomicU64,
|
||||
count: AtomicU64,
|
||||
min: AtomicU64,
|
||||
max: AtomicU64,
|
||||
}
|
||||
|
||||
impl<const N: usize> AtomicHistogram<N> {
|
||||
pub const fn new(id: MetricType, upper_bounds: [u64; N]) -> Self {
|
||||
Self {
|
||||
buckets: AtomicU32Array::new(),
|
||||
upper_bounds,
|
||||
sum: AtomicU64::new(0),
|
||||
count: AtomicU64::new(0),
|
||||
min: AtomicU64::new(u64::MAX),
|
||||
max: AtomicU64::new(0),
|
||||
id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn observe(&self, value: u64) {
|
||||
self.sum.fetch_add(value, Ordering::Relaxed);
|
||||
self.count.fetch_add(1, Ordering::Relaxed);
|
||||
self.min.fetch_min(value, Ordering::Relaxed);
|
||||
self.max.fetch_max(value, Ordering::Relaxed);
|
||||
|
||||
for (idx, upper_bound) in self.upper_bounds.iter().enumerate() {
|
||||
if value < *upper_bound {
|
||||
self.buckets.add(idx, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
pub fn id(&self) -> MetricType {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn sum(&self) -> u64 {
|
||||
self.sum.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn count(&self) -> u64 {
|
||||
self.count.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn average(&self) -> f64 {
|
||||
let sum = self.sum();
|
||||
let count = self.count();
|
||||
if count > 0 {
|
||||
sum as f64 / count as f64
|
||||
} else {
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
pub fn min(&self) -> Option<u64> {
|
||||
let min = self.min.load(Ordering::Relaxed);
|
||||
if min != u64::MAX { Some(min) } else { None }
|
||||
}
|
||||
|
||||
pub fn max(&self) -> Option<u64> {
|
||||
let max = self.max.load(Ordering::Relaxed);
|
||||
if max != 0 { Some(max) } else { None }
|
||||
}
|
||||
|
||||
pub fn buckets_iter(&self) -> impl IntoIterator<Item = u64> + '_ {
|
||||
self.buckets
|
||||
.inner()
|
||||
.iter()
|
||||
.map(|bucket| bucket.load(Ordering::Relaxed) as u64)
|
||||
}
|
||||
|
||||
pub fn buckets_vec(&self) -> Vec<u64> {
|
||||
let mut vec = Vec::with_capacity(N);
|
||||
for bucket in self.buckets.inner().iter() {
|
||||
vec.push(bucket.load(Ordering::Relaxed) as u64);
|
||||
}
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn buckets_len(&self) -> usize {
|
||||
N
|
||||
}
|
||||
|
||||
pub fn upper_bounds_iter(&self) -> impl IntoIterator<Item = u64> + '_ {
|
||||
self.upper_bounds.iter().copied()
|
||||
}
|
||||
|
||||
pub fn upper_bounds_vec(&self) -> Vec<f64> {
|
||||
let mut vec = Vec::with_capacity(N - 1);
|
||||
for upper_bound in self.upper_bounds.iter().take(N - 1) {
|
||||
vec.push(*upper_bound as f64);
|
||||
}
|
||||
vec
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.count.load(Ordering::Relaxed) > 0
|
||||
}
|
||||
|
||||
pub const fn new_message_sizes(id: MetricType) -> AtomicHistogram<12> {
|
||||
AtomicHistogram::new(
|
||||
id,
|
||||
[
|
||||
500, // 500 bytes
|
||||
1_000, // 1 KB
|
||||
10_000, // 10 KB
|
||||
100_000, // 100 KB
|
||||
1_000_000, // 1 MB
|
||||
5_000_000, // 5 MB
|
||||
10_000_000, // 10 MB
|
||||
25_000_000, // 25 MB
|
||||
50_000_000, // 50 MB
|
||||
100_000_000, // 100 MB
|
||||
500_000_000, // 500 MB
|
||||
u64::MAX, // Catch-all for any larger sizes
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn new_short_durations(id: MetricType) -> AtomicHistogram<12> {
|
||||
AtomicHistogram::new(
|
||||
id,
|
||||
[
|
||||
5, // 5 milliseconds
|
||||
10, // 10 milliseconds
|
||||
50, // 50 milliseconds
|
||||
100, // 100 milliseconds
|
||||
500, // 0.5 seconds
|
||||
1_000, // 1 second
|
||||
2_000, // 2 seconds
|
||||
5_000, // 5 seconds
|
||||
10_000, // 10 seconds
|
||||
30_000, // 30 seconds
|
||||
60_000, // 1 minute
|
||||
u64::MAX, // Catch-all for any longer durations
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn new_medium_durations(id: MetricType) -> AtomicHistogram<12> {
|
||||
AtomicHistogram::new(
|
||||
id,
|
||||
[
|
||||
250,
|
||||
500,
|
||||
1_000,
|
||||
5_000,
|
||||
10_000, // For quick connections (seconds)
|
||||
60_000,
|
||||
(60 * 5) * 1_000,
|
||||
(60 * 10) * 1_000,
|
||||
(60 * 30) * 1_000, // For medium-length connections (minutes)
|
||||
(60 * 60) * 1_000,
|
||||
(60 * 60 * 5) * 1_000,
|
||||
u64::MAX, // For extreme cases (8 hours and 1 day)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn new_long_durations(id: MetricType) -> AtomicHistogram<12> {
|
||||
AtomicHistogram::new(
|
||||
id,
|
||||
[
|
||||
1_000, // 1 second
|
||||
30_000, // 30 seconds
|
||||
300_000, // 5 minutes
|
||||
600_000, // 10 minutes
|
||||
1_800_000, // 30 minutes
|
||||
3_600_000, // 1 hour
|
||||
14_400_000, // 5 hours
|
||||
28_800_000, // 8 hours
|
||||
43_200_000, // 12 hours
|
||||
86_400_000, // 1 day
|
||||
604_800_000, // 1 week
|
||||
u64::MAX, // Catch-all for any longer durations
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod array;
|
||||
pub mod bitset;
|
||||
pub mod counter;
|
||||
pub mod gauge;
|
||||
pub mod histogram;
|
||||
@@ -0,0 +1,598 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{borrow::Cow, fmt::Debug, str::FromStr, time::Duration};
|
||||
|
||||
use compact_str::{CompactString, ToCompactString, format_compact};
|
||||
use mail_auth::common::verify::VerifySignature;
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl AsRef<EventType> for Error {
|
||||
fn as_ref(&self) -> &EventType {
|
||||
&self.0.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Value {
|
||||
fn from(value: &'static str) -> Self {
|
||||
Self::String(CompactString::const_new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Value {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(CompactString::from_string_buffer(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CompactString> for Value {
|
||||
fn from(value: CompactString) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Box<str>> for Value {
|
||||
fn from(value: Box<str>) -> Self {
|
||||
Self::String(CompactString::from(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for Value {
|
||||
fn from(value: u64) -> Self {
|
||||
Self::UInt(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i64> for Value {
|
||||
fn from(value: i64) -> Self {
|
||||
Self::Int(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f64> for Value {
|
||||
fn from(value: f64) -> Self {
|
||||
Self::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<f32> for Value {
|
||||
fn from(value: f32) -> Self {
|
||||
Self::Float(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u16> for Value {
|
||||
fn from(value: u16) -> Self {
|
||||
Self::UInt(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<i32> for Value {
|
||||
fn from(value: i32) -> Self {
|
||||
Self::Int(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for Value {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::UInt(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<usize> for Value {
|
||||
fn from(value: usize) -> Self {
|
||||
Self::UInt(value as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bool> for Value {
|
||||
fn from(value: bool) -> Self {
|
||||
Self::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IpAddr> for Value {
|
||||
fn from(value: IpAddr) -> Self {
|
||||
match value {
|
||||
IpAddr::V4(ip) => Value::Ipv4(ip),
|
||||
IpAddr::V6(ip) => Value::Ipv6(ip),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Into<Value>> From<Option<T>> for Value {
|
||||
fn from(value: Option<T>) -> Self {
|
||||
match value {
|
||||
Some(value) => value.into(),
|
||||
None => Self::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Duration> for Value {
|
||||
fn from(value: Duration) -> Self {
|
||||
Self::Duration(value.as_millis() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for Value {
|
||||
fn from(value: Error) -> Self {
|
||||
Self::Event(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EventType> for Error {
|
||||
fn from(value: EventType) -> Self {
|
||||
Error::new(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StoreEvent> for Error {
|
||||
fn from(value: StoreEvent) -> Self {
|
||||
Error::new(EventType::Store(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AuthEvent> for Error {
|
||||
fn from(value: AuthEvent) -> Self {
|
||||
Error::new(EventType::Auth(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for Value {
|
||||
fn from(value: Vec<u8>) -> Self {
|
||||
Self::Bytes(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[u8]> for Value {
|
||||
fn from(value: &[u8]) -> Self {
|
||||
Self::Bytes(value.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'static, str>> for Value {
|
||||
fn from(value: Cow<'static, str>) -> Self {
|
||||
match value {
|
||||
Cow::Borrowed(value) => Self::String(CompactString::const_new(value)),
|
||||
Cow::Owned(value) => Self::String(value.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<&crate::Result<T>> for Value
|
||||
where
|
||||
T: Debug,
|
||||
{
|
||||
fn from(value: &crate::Result<T>) -> Self {
|
||||
match value {
|
||||
Ok(value) => format_compact!("{:?}", value).into(),
|
||||
Err(err) => Value::Event(err.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Vec<T>> for Value
|
||||
where
|
||||
T: Into<Value>,
|
||||
{
|
||||
fn from(value: Vec<T>) -> Self {
|
||||
Self::Array(value.into_iter().map(Into::into).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<&[T]> for Value
|
||||
where
|
||||
T: Into<Value> + Clone,
|
||||
{
|
||||
fn from(value: &[T]) -> Self {
|
||||
Self::Array(value.iter().map(|v| v.clone().into()).collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventType {
|
||||
pub fn from_io_error(self, err: std::io::Error) -> Error {
|
||||
self.reason(err).details("I/O error")
|
||||
}
|
||||
|
||||
pub fn from_json_error(self, err: serde_json::Error) -> Error {
|
||||
self.reason(err).details("JSON deserialization failed")
|
||||
}
|
||||
|
||||
pub fn from_base64_error(self, err: base64::DecodeError) -> Error {
|
||||
self.reason(err).details("Base64 decoding failed")
|
||||
}
|
||||
|
||||
pub fn from_http_error(self, err: reqwest::Error) -> Error {
|
||||
self.into_err()
|
||||
.ctx_opt(
|
||||
Key::Url,
|
||||
err.url().map(|url| url.as_ref().to_compact_string()),
|
||||
)
|
||||
.ctx_opt(Key::Code, err.status().map(|status| status.as_u16()))
|
||||
.reason(err)
|
||||
}
|
||||
|
||||
pub fn from_http_str_error(self, err: reqwest::header::ToStrError) -> Error {
|
||||
self.reason(err)
|
||||
.details("Failed to convert header to string")
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mail_auth::Error> for Error {
|
||||
fn from(err: mail_auth::Error) -> Self {
|
||||
match err {
|
||||
mail_auth::Error::ParseError => {
|
||||
EventType::MailAuth(MailAuthEvent::ParseError).into_err()
|
||||
}
|
||||
mail_auth::Error::MissingParameters => {
|
||||
EventType::MailAuth(MailAuthEvent::MissingParameters).into_err()
|
||||
}
|
||||
mail_auth::Error::NoHeadersFound => {
|
||||
EventType::MailAuth(MailAuthEvent::NoHeadersFound).into_err()
|
||||
}
|
||||
mail_auth::Error::Io(details) => EventType::MailAuth(MailAuthEvent::Io)
|
||||
.into_err()
|
||||
.details(CompactString::from(details)),
|
||||
mail_auth::Error::Base64 => EventType::MailAuth(MailAuthEvent::Base64).into_err(),
|
||||
mail_auth::Error::NotAligned => {
|
||||
EventType::MailAuth(MailAuthEvent::PolicyNotAligned).into_err()
|
||||
}
|
||||
mail_auth::Error::Crypto(err) => match err {
|
||||
mail_auth::common::crypto::CryptoError::Library(details) => {
|
||||
EventType::MailAuth(MailAuthEvent::Crypto)
|
||||
.into_err()
|
||||
.details(CompactString::from(details))
|
||||
}
|
||||
mail_auth::common::crypto::CryptoError::FailedVerification => {
|
||||
EventType::Dkim(DkimEvent::FailedVerification).into_err()
|
||||
}
|
||||
mail_auth::common::crypto::CryptoError::IncompatibleAlgorithms => {
|
||||
EventType::Dkim(DkimEvent::IncompatibleAlgorithms).into_err()
|
||||
}
|
||||
},
|
||||
mail_auth::Error::Dns(err) => match err {
|
||||
mail_auth::DnsError::Resolver(details) => {
|
||||
EventType::MailAuth(MailAuthEvent::DnsError)
|
||||
.into_err()
|
||||
.details(CompactString::from(details))
|
||||
}
|
||||
mail_auth::DnsError::RecordNotFound(code) => {
|
||||
EventType::MailAuth(MailAuthEvent::DnsRecordNotFound)
|
||||
.into_err()
|
||||
.code(code.to_str())
|
||||
}
|
||||
mail_auth::DnsError::InvalidRecordType => {
|
||||
EventType::MailAuth(MailAuthEvent::DnsInvalidRecordType).into_err()
|
||||
}
|
||||
},
|
||||
mail_auth::Error::Dkim(err) => match err {
|
||||
mail_auth::dkim::DkimError::UnsupportedVersion => {
|
||||
EventType::Dkim(DkimEvent::UnsupportedVersion).into_err()
|
||||
}
|
||||
mail_auth::dkim::DkimError::UnsupportedAlgorithm => {
|
||||
EventType::Dkim(DkimEvent::UnsupportedAlgorithm).into_err()
|
||||
}
|
||||
mail_auth::dkim::DkimError::UnsupportedCanonicalization => {
|
||||
EventType::Dkim(DkimEvent::UnsupportedCanonicalization).into_err()
|
||||
}
|
||||
mail_auth::dkim::DkimError::UnsupportedKeyType => {
|
||||
EventType::Dkim(DkimEvent::UnsupportedKeyType).into_err()
|
||||
}
|
||||
mail_auth::dkim::DkimError::FailedBodyHashMatch => {
|
||||
EventType::Dkim(DkimEvent::FailedBodyHashMatch).into_err()
|
||||
}
|
||||
mail_auth::dkim::DkimError::FailedAuidMatch => {
|
||||
EventType::Dkim(DkimEvent::FailedAuidMatch).into_err()
|
||||
}
|
||||
mail_auth::dkim::DkimError::RevokedPublicKey => {
|
||||
EventType::Dkim(DkimEvent::RevokedPublicKey).into_err()
|
||||
}
|
||||
mail_auth::dkim::DkimError::SignatureExpired => {
|
||||
EventType::Dkim(DkimEvent::SignatureExpired).into_err()
|
||||
}
|
||||
mail_auth::dkim::DkimError::SignatureLength => {
|
||||
EventType::Dkim(DkimEvent::SignatureLength).into_err()
|
||||
}
|
||||
},
|
||||
mail_auth::Error::Arc(err) => match err {
|
||||
mail_auth::arc::ArcError::ChainTooLong => {
|
||||
EventType::Arc(ArcEvent::ChainTooLong).into_err()
|
||||
}
|
||||
mail_auth::arc::ArcError::InvalidInstance(instance) => {
|
||||
EventType::Arc(ArcEvent::InvalidInstance).ctx(Key::Id, instance)
|
||||
}
|
||||
mail_auth::arc::ArcError::InvalidCV => {
|
||||
EventType::Arc(ArcEvent::InvalidCv).into_err()
|
||||
}
|
||||
mail_auth::arc::ArcError::HasHeaderTag => {
|
||||
EventType::Arc(ArcEvent::HasHeaderTag).into_err()
|
||||
}
|
||||
mail_auth::arc::ArcError::BrokenChain => {
|
||||
EventType::Arc(ArcEvent::BrokenChain).into_err()
|
||||
}
|
||||
mail_auth::arc::ArcError::FailedBodyHashMatch => {
|
||||
EventType::Dkim(DkimEvent::FailedBodyHashMatch).into_err()
|
||||
}
|
||||
mail_auth::arc::ArcError::SignatureExpired => {
|
||||
EventType::Dkim(DkimEvent::SignatureExpired).into_err()
|
||||
}
|
||||
mail_auth::arc::ArcError::SignatureLength => {
|
||||
EventType::Dkim(DkimEvent::SignatureLength).into_err()
|
||||
}
|
||||
},
|
||||
mail_auth::Error::Dkim2(err) => match err {
|
||||
mail_auth::dkim2::Dkim2Error::InstanceMissing(m) => {
|
||||
EventType::Dkim(DkimEvent::InstanceMissing).ctx(Key::Id, m)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::InstanceSyntax(m) => {
|
||||
EventType::Dkim(DkimEvent::InstanceSyntax).ctx(Key::Id, m)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::InstanceTagMissing { m, tag } => {
|
||||
EventType::Dkim(DkimEvent::InstanceTagMissing)
|
||||
.ctx(Key::Id, m)
|
||||
.details(tag)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::InstanceNotSigned(m) => {
|
||||
EventType::Dkim(DkimEvent::InstanceNotSigned).ctx(Key::Id, m)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::InstanceAboveSignature(m) => {
|
||||
EventType::Dkim(DkimEvent::InstanceAboveSignature).ctx(Key::Id, m)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::SignatureMissing(i) => {
|
||||
EventType::Dkim(DkimEvent::SignatureMissing).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::SignatureSyntax(i) => {
|
||||
EventType::Dkim(DkimEvent::SignatureSyntax).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::SignatureTagMissing { i, tag } => {
|
||||
EventType::Dkim(DkimEvent::SignatureTagMissing)
|
||||
.ctx(Key::Id, i)
|
||||
.details(tag)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::SignatureTagUnexpected { i, tag } => {
|
||||
EventType::Dkim(DkimEvent::SignatureTagUnexpected)
|
||||
.ctx(Key::Id, i)
|
||||
.details(tag)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::SequenceGap => {
|
||||
EventType::Dkim(DkimEvent::SequenceGap).into_err()
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::SequenceOverflow => {
|
||||
EventType::Dkim(DkimEvent::SequenceOverflow).into_err()
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::ChainTooLong => {
|
||||
EventType::Dkim(DkimEvent::ChainTooLong).into_err()
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::SignatureExpired(i) => {
|
||||
EventType::Dkim(DkimEvent::SignatureExpired).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::MailFromMismatch(i) => {
|
||||
EventType::Dkim(DkimEvent::MailFromMismatch).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::RcptToMismatch(i) => {
|
||||
EventType::Dkim(DkimEvent::RcptToMismatch).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::MailFromDomainMismatch(i) => {
|
||||
EventType::Dkim(DkimEvent::MailFromDomainMismatch).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::NextDomainMismatch(i) => {
|
||||
EventType::Dkim(DkimEvent::NextDomainMismatch).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::CustodyBreak(i) => {
|
||||
EventType::Dkim(DkimEvent::CustodyBreak).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::PublicKeyFetch(i) => {
|
||||
EventType::Dkim(DkimEvent::PublicKeyFetch).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::PublicKeyMissing(i) => {
|
||||
EventType::Dkim(DkimEvent::PublicKeyMissing).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::PublicKeyMultiple(i) => {
|
||||
EventType::Dkim(DkimEvent::PublicKeyMultiple).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::PublicKeySyntax(i) => {
|
||||
EventType::Dkim(DkimEvent::PublicKeySyntax).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::PublicKeyAlgorithmMismatch(i) => {
|
||||
EventType::Dkim(DkimEvent::PublicKeyAlgorithmMismatch).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::PublicKeyRevoked(i) => {
|
||||
EventType::Dkim(DkimEvent::RevokedPublicKey).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::IncorrectSignature(i) => {
|
||||
EventType::Dkim(DkimEvent::FailedVerification).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::NoValidAlgorithm(i) => {
|
||||
EventType::Dkim(DkimEvent::NoValidAlgorithm).ctx(Key::Id, i)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::HeaderHashMismatch(m) => {
|
||||
EventType::Dkim(DkimEvent::HeaderHashMismatch).ctx(Key::Id, m)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::BodyHashMismatch(m) => {
|
||||
EventType::Dkim(DkimEvent::FailedBodyHashMatch).ctx(Key::Id, m)
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::Modified => {
|
||||
EventType::Dkim(DkimEvent::Modified).into_err()
|
||||
}
|
||||
mail_auth::dkim2::Dkim2Error::Exploded => {
|
||||
EventType::Dkim(DkimEvent::Exploded).into_err()
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mail_auth::DkimResult> for Error {
|
||||
fn from(value: &mail_auth::DkimResult) -> Self {
|
||||
match value.clone() {
|
||||
mail_auth::DkimResult::Pass => Error::new(EventType::Dkim(DkimEvent::Pass)),
|
||||
mail_auth::DkimResult::Neutral(err) => {
|
||||
Error::new(EventType::Dkim(DkimEvent::Neutral)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::DkimResult::Fail(err) => {
|
||||
Error::new(EventType::Dkim(DkimEvent::Fail)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::DkimResult::PermError(err) => {
|
||||
Error::new(EventType::Dkim(DkimEvent::PermError)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::DkimResult::TempError(err) => {
|
||||
Error::new(EventType::Dkim(DkimEvent::TempError)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::DkimResult::None => Error::new(EventType::Dkim(DkimEvent::None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mail_auth::Dkim2Result> for Error {
|
||||
fn from(value: &mail_auth::Dkim2Result) -> Self {
|
||||
match value.clone() {
|
||||
mail_auth::Dkim2Result::Pass => Error::new(EventType::Dkim(DkimEvent::Pass)),
|
||||
mail_auth::Dkim2Result::Fail(err) => {
|
||||
Error::new(EventType::Dkim(DkimEvent::Fail)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::Dkim2Result::PermError(err) => {
|
||||
Error::new(EventType::Dkim(DkimEvent::PermError)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::Dkim2Result::TempError(err) => {
|
||||
Error::new(EventType::Dkim(DkimEvent::TempError)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::Dkim2Result::None => Error::new(EventType::Dkim(DkimEvent::None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mail_auth::dkim2::Dkim2Output<'_>> for Error {
|
||||
fn from(value: &mail_auth::dkim2::Dkim2Output<'_>) -> Self {
|
||||
Error::from(value.result()).ctx_opt(
|
||||
Key::Domain,
|
||||
value
|
||||
.chain()
|
||||
.first()
|
||||
.map(|link| link.signature.d.to_compact_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mail_auth::DmarcResult> for Error {
|
||||
fn from(value: &mail_auth::DmarcResult) -> Self {
|
||||
match value.clone() {
|
||||
mail_auth::DmarcResult::Pass => Error::new(EventType::Dmarc(DmarcEvent::Pass)),
|
||||
mail_auth::DmarcResult::Fail(err) => {
|
||||
Error::new(EventType::Dmarc(DmarcEvent::Fail)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::DmarcResult::PermError(err) => {
|
||||
Error::new(EventType::Dmarc(DmarcEvent::PermError)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::DmarcResult::TempError(err) => {
|
||||
Error::new(EventType::Dmarc(DmarcEvent::TempError)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::DmarcResult::None => Error::new(EventType::Dmarc(DmarcEvent::None)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mail_auth::DkimOutput<'_>> for Error {
|
||||
fn from(value: &mail_auth::DkimOutput<'_>) -> Self {
|
||||
Error::from(value.result()).ctx_opt(
|
||||
Key::Domain,
|
||||
value.signature().map(|s| s.domain().to_compact_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mail_auth::IprevOutput> for Error {
|
||||
fn from(value: &mail_auth::IprevOutput) -> Self {
|
||||
match value.result().clone() {
|
||||
mail_auth::IprevResult::Pass => Error::new(EventType::Iprev(IprevEvent::Pass)),
|
||||
mail_auth::IprevResult::Fail(err) => {
|
||||
Error::new(EventType::Iprev(IprevEvent::Fail)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::IprevResult::PermError(err) => {
|
||||
Error::new(EventType::Iprev(IprevEvent::PermError)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::IprevResult::TempError(err) => {
|
||||
Error::new(EventType::Iprev(IprevEvent::TempError)).caused_by(Error::from(err))
|
||||
}
|
||||
mail_auth::IprevResult::None => Error::new(EventType::Iprev(IprevEvent::None)),
|
||||
}
|
||||
.ctx_opt(
|
||||
Key::Details,
|
||||
value.ptr.as_ref().map(|s| {
|
||||
s.iter()
|
||||
.map(|v| Value::String(v.as_ref().into()))
|
||||
.collect::<Vec<_>>()
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&mail_auth::SpfOutput> for Error {
|
||||
fn from(value: &mail_auth::SpfOutput) -> Self {
|
||||
Error::new(EventType::Spf(match value.result() {
|
||||
mail_auth::SpfResult::Pass => SpfEvent::Pass,
|
||||
mail_auth::SpfResult::Fail => SpfEvent::Fail,
|
||||
mail_auth::SpfResult::SoftFail => SpfEvent::SoftFail,
|
||||
mail_auth::SpfResult::Neutral => SpfEvent::Neutral,
|
||||
mail_auth::SpfResult::PermError => SpfEvent::PermError,
|
||||
mail_auth::SpfResult::TempError => SpfEvent::TempError,
|
||||
mail_auth::SpfResult::None => SpfEvent::None,
|
||||
}))
|
||||
.ctx_opt(
|
||||
Key::Details,
|
||||
value.explanation().map(|s| s.to_compact_string()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rkyv::rancor::Error> for Error {
|
||||
fn from(value: rkyv::rancor::Error) -> Self {
|
||||
Error::new(EventType::Store(StoreEvent::DeserializeError))
|
||||
.reason(value)
|
||||
.details("Rkyv de/serialization failed")
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AssertSuccess
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
fn assert_success(
|
||||
self,
|
||||
cause: EventType,
|
||||
) -> impl std::future::Future<Output = crate::Result<Self>> + Send;
|
||||
}
|
||||
|
||||
impl AssertSuccess for reqwest::Response {
|
||||
async fn assert_success(self, cause: EventType) -> crate::Result<Self> {
|
||||
let status = self.status();
|
||||
if status.is_success() {
|
||||
Ok(self)
|
||||
} else {
|
||||
Err(cause
|
||||
.ctx(Key::Code, status.as_u16())
|
||||
.details("HTTP request failed")
|
||||
.ctx_opt(Key::Reason, self.text().await.map(CompactString::from).ok()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for EventType {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
EventType::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Key {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
Key::parse(s).ok_or(())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::Level;
|
||||
use std::{cmp::Ordering, fmt::Display, str::FromStr};
|
||||
|
||||
impl PartialOrd for Level {
|
||||
#[inline(always)]
|
||||
fn partial_cmp(&self, other: &Level) -> Option<Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn lt(&self, other: &Level) -> bool {
|
||||
(*other as usize) < (*self as usize)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn le(&self, other: &Level) -> bool {
|
||||
(*other as usize) <= (*self as usize)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn gt(&self, other: &Level) -> bool {
|
||||
(*other as usize) > (*self as usize)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn ge(&self, other: &Level) -> bool {
|
||||
(*other as usize) >= (*self as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Level {
|
||||
#[inline(always)]
|
||||
fn cmp(&self, other: &Self) -> Ordering {
|
||||
(*other as usize).cmp(&(*self as usize))
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Level {
|
||||
type Err = String;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
match s.to_ascii_lowercase().as_str() {
|
||||
"disable" => Ok(Self::Disable),
|
||||
"trace" => Ok(Self::Trace),
|
||||
"debug" => Ok(Self::Debug),
|
||||
"info" => Ok(Self::Info),
|
||||
"warn" => Ok(Self::Warn),
|
||||
"error" => Ok(Self::Error),
|
||||
_ => Err(s.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Level {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Disable => "DISABLE",
|
||||
Self::Trace => "TRACE",
|
||||
Self::Debug => "DEBUG",
|
||||
Self::Info => "INFO",
|
||||
Self::Warn => "WARN",
|
||||
Self::Error => "ERROR",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_contained(&self, other: Self) -> bool {
|
||||
*self >= other && other != Level::Disable && *self != Level::Disable
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Level {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.as_str().fmt(f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,683 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod conv;
|
||||
pub mod level;
|
||||
|
||||
pub mod enums;
|
||||
#[allow(clippy::match_like_matches_macro)]
|
||||
pub mod enums_impl;
|
||||
|
||||
use compact_str::ToCompactString;
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::*;
|
||||
|
||||
impl<T> Event<T> {
|
||||
pub fn with_capacity(inner: T, capacity: usize) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
keys: Vec::with_capacity(capacity),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_keys(inner: T, keys: Vec<(Key, Value)>) -> Self {
|
||||
Self { inner, keys }
|
||||
}
|
||||
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
keys: Vec::with_capacity(5),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(&self, key: Key) -> Option<&Value> {
|
||||
self.keys
|
||||
.iter()
|
||||
.find_map(|(k, v)| if *k == key { Some(v) } else { None })
|
||||
}
|
||||
|
||||
pub fn value_as_str(&self, key: Key) -> Option<&str> {
|
||||
self.value(key).and_then(|v| v.as_str())
|
||||
}
|
||||
|
||||
pub fn value_as_uint(&self, key: Key) -> Option<u64> {
|
||||
self.value(key).and_then(|v| v.to_uint())
|
||||
}
|
||||
|
||||
pub fn take_value(&mut self, key: Key) -> Option<Value> {
|
||||
self.keys.iter_mut().find_map(|(k, v)| {
|
||||
if *k == key {
|
||||
Some(std::mem::take(v))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn into_boxed(self) -> Box<Self> {
|
||||
Box::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
#[inline(always)]
|
||||
pub fn new(inner: EventType) -> Self {
|
||||
Error(Box::new(Event::new(inner)))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set_ctx(&mut self, key: Key, value: impl Into<Value>) {
|
||||
self.0.keys.push((key, value.into()));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ctx(mut self, key: Key, value: impl Into<Value>) -> Self {
|
||||
self.0.keys.push((key, value.into()));
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ctx_unique(mut self, key: Key, value: impl Into<Value>) -> Self {
|
||||
if self.0.keys.iter().all(|(k, _)| *k != key) {
|
||||
self.0.keys.push((key, value.into()));
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ctx_opt(self, key: Key, value: Option<impl Into<Value>>) -> Self {
|
||||
match value {
|
||||
Some(value) => self.ctx(key, value),
|
||||
None => self,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn matches(&self, inner: EventType) -> bool {
|
||||
self.0.inner == inner
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn event_type(&self) -> EventType {
|
||||
self.0.inner
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn span_id(self, session_id: u64) -> Self {
|
||||
self.ctx(Key::SpanId, session_id)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Self {
|
||||
self.ctx(Key::CausedBy, error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn details(self, error: impl Into<Value>) -> Self {
|
||||
self.ctx(Key::Details, error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn code(self, error: impl Into<Value>) -> Self {
|
||||
self.ctx(Key::Code, error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn id(self, error: impl Into<Value>) -> Self {
|
||||
self.ctx(Key::Id, error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Self {
|
||||
self.ctx(Key::Reason, error.to_compact_string())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn document_id(self, id: u32) -> Self {
|
||||
self.ctx(Key::DocumentId, id)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn account_id(self, id: u32) -> Self {
|
||||
self.ctx(Key::AccountId, id)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn collection(self, id: impl Into<u8>) -> Self {
|
||||
self.ctx(Key::Collection, id.into() as u64)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn wrap(self, cause: EventType) -> Self {
|
||||
Error::new(cause).caused_by(self)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn keys(&self) -> &[(Key, Value)] {
|
||||
&self.0.keys
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn value(&self, key: Key) -> Option<&Value> {
|
||||
self.0.value(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn value_as_str(&self, key: Key) -> Option<&str> {
|
||||
self.0.value_as_str(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn value_as_uint(&self, key: Key) -> Option<u64> {
|
||||
self.0.value_as_uint(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn take_value(&mut self, key: Key) -> Option<Value> {
|
||||
self.0.take_value(key)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_assertion_failure(&self) -> bool {
|
||||
self.0.inner == EventType::Store(StoreEvent::AssertValueFailed)
|
||||
}
|
||||
|
||||
pub fn key(&self, key: Key) -> Option<&Value> {
|
||||
self.0
|
||||
.keys
|
||||
.iter()
|
||||
.find_map(|(k, v)| if *k == key { Some(v) } else { None })
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_jmap_method_error(&self) -> bool {
|
||||
!matches!(
|
||||
self.0.inner,
|
||||
EventType::Jmap(
|
||||
JmapEvent::UnknownCapability | JmapEvent::NotJson | JmapEvent::NotRequest
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn must_disconnect(&self) -> bool {
|
||||
matches!(
|
||||
self.0.inner,
|
||||
EventType::Network(_)
|
||||
| EventType::Auth(AuthEvent::TooManyAttempts)
|
||||
| EventType::Limit(LimitEvent::ConcurrentRequest | LimitEvent::TooManyRequests)
|
||||
| EventType::Security(_)
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn should_write_err(&self) -> bool {
|
||||
!matches!(self.0.inner, EventType::Network(_) | EventType::Security(_))
|
||||
}
|
||||
|
||||
pub fn corrupted_key(key: &[u8], value: Option<&[u8]>, caused_by: &'static str) -> Error {
|
||||
EventType::Store(StoreEvent::DataCorruption)
|
||||
.ctx(Key::Key, key)
|
||||
.ctx_opt(Key::Value, value)
|
||||
.ctx(Key::CausedBy, caused_by)
|
||||
}
|
||||
}
|
||||
|
||||
impl Event<EventDetails> {
|
||||
pub fn span_id(&self) -> Option<u64> {
|
||||
for (key, value) in &self.keys {
|
||||
match (key, value) {
|
||||
(Key::SpanId, Value::UInt(value)) => return Some(*value),
|
||||
(Key::SpanId, Value::Int(value)) => return Some(*value as u64),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl EventType {
|
||||
#[inline(always)]
|
||||
pub fn is_span_start(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
EventType::Smtp(SmtpEvent::ConnectionStart)
|
||||
| EventType::Imap(ImapEvent::ConnectionStart)
|
||||
| EventType::ManageSieve(ManageSieveEvent::ConnectionStart)
|
||||
| EventType::Pop3(Pop3Event::ConnectionStart)
|
||||
| EventType::Http(HttpEvent::ConnectionStart)
|
||||
| EventType::Delivery(DeliveryEvent::AttemptStart)
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_span_end(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
EventType::Smtp(SmtpEvent::ConnectionEnd)
|
||||
| EventType::Imap(ImapEvent::ConnectionEnd)
|
||||
| EventType::ManageSieve(ManageSieveEvent::ConnectionEnd)
|
||||
| EventType::Pop3(Pop3Event::ConnectionEnd)
|
||||
| EventType::Http(HttpEvent::ConnectionEnd)
|
||||
| EventType::Delivery(DeliveryEvent::AttemptEnd)
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_raw_io(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
EventType::Imap(ImapEvent::RawInput | ImapEvent::RawOutput)
|
||||
| EventType::Smtp(SmtpEvent::RawInput | SmtpEvent::RawOutput)
|
||||
| EventType::Pop3(Pop3Event::RawInput | Pop3Event::RawOutput)
|
||||
| EventType::ManageSieve(ManageSieveEvent::RawInput | ManageSieveEvent::RawOutput)
|
||||
| EventType::Delivery(DeliveryEvent::RawInput | DeliveryEvent::RawOutput)
|
||||
| EventType::Milter(MilterEvent::Read | MilterEvent::Write)
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl StoreEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Store(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl DnsEvent {
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Dns(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl AcmeEvent {
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Acme(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl DkimEvent {
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Dkim(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl SecurityEvent {
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Security(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl AuthEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Auth(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl JmapEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Jmap(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl LimitEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Limit(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ResourceEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Resource(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl SmtpEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Smtp(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl SieveEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Sieve(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl SpamEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Spam(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ImapEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Imap(self))
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn caused_by(self, error: impl Into<Value>) -> Error {
|
||||
self.into_err().caused_by(error)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn reason(self, error: impl Display) -> Error {
|
||||
self.into_err().reason(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl Pop3Event {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Pop3(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageSieveEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::ManageSieve(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl NetworkEvent {
|
||||
#[inline(always)]
|
||||
pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error {
|
||||
self.into_err().ctx(key, value)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn into_err(self) -> Error {
|
||||
Error::new(EventType::Network(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn from_maybe_string(value: &[u8]) -> Self {
|
||||
if let Ok(value) = std::str::from_utf8(value) {
|
||||
Self::String(value.into())
|
||||
} else {
|
||||
Self::Bytes(value.to_vec())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_uint(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::UInt(value) => Some(*value),
|
||||
Self::Int(value) => Some(*value as u64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> Option<&str> {
|
||||
match self {
|
||||
Self::String(value) => Some(value.as_str()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_string(self) -> Option<CompactString> {
|
||||
match self {
|
||||
Self::String(value) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AddContext<T> for Result<T> {
|
||||
#[inline(always)]
|
||||
fn caused_by(self, location: &'static str) -> Result<T> {
|
||||
match self {
|
||||
Ok(value) => Ok(value),
|
||||
Err(mut err) => {
|
||||
err.set_ctx(Key::CausedBy, location);
|
||||
Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
fn add_context<F>(self, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(Error) -> Error,
|
||||
{
|
||||
match self {
|
||||
Ok(value) => Ok(value),
|
||||
Err(err) => Err(f(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
impl Eq for Error {}
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.0.inner == other.0.inner && self.0.keys.len() == other.0.keys.len() {
|
||||
for kv in self.0.keys.iter() {
|
||||
if !other.0.keys.iter().any(|okv| kv == okv) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Self::String(l0), Self::String(r0)) => l0 == r0,
|
||||
(Self::UInt(l0), Self::UInt(r0)) => l0 == r0,
|
||||
(Self::Int(l0), Self::Int(r0)) => l0 == r0,
|
||||
(Self::Float(l0), Self::Float(r0)) => l0 == r0,
|
||||
(Self::Bytes(l0), Self::Bytes(r0)) => l0 == r0,
|
||||
(Self::Bool(l0), Self::Bool(r0)) => l0 == r0,
|
||||
(Self::Ipv4(l0), Self::Ipv4(r0)) => l0 == r0,
|
||||
(Self::Ipv6(l0), Self::Ipv6(r0)) => l0 == r0,
|
||||
(Self::Event(l0), Self::Event(r0)) => l0 == r0,
|
||||
(Self::Array(l0), Self::Array(r0)) => l0 == r0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Value {}
|
||||
|
||||
impl From<EventType> for usize {
|
||||
fn from(value: EventType) -> Self {
|
||||
value.to_id() as usize
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<Event<EventDetails>> for Event<EventDetails> {
|
||||
fn as_ref(&self) -> &Event<EventDetails> {
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use super::{USIZE_BITS, USIZE_BITS_MASK};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Bitset<const N: usize>(pub(crate) [usize; N]);
|
||||
|
||||
impl<const N: usize> Bitset<N> {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub const fn new() -> Self {
|
||||
Self([0; N])
|
||||
}
|
||||
|
||||
pub const fn all() -> Self {
|
||||
Self([usize::MAX; N])
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set(&mut self, index: impl Into<usize>) {
|
||||
let index = index.into();
|
||||
self.0[index / USIZE_BITS] |= 1 << (index & USIZE_BITS_MASK);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn clear(&mut self, index: impl Into<usize>) {
|
||||
let index = index.into();
|
||||
self.0[index / USIZE_BITS] &= !(1 << (index & USIZE_BITS_MASK));
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn get(&self, index: impl Into<usize>) -> bool {
|
||||
let index = index.into();
|
||||
self.0[index / USIZE_BITS] & (1 << (index & USIZE_BITS_MASK)) != 0
|
||||
}
|
||||
|
||||
pub fn union(&mut self, other: &Self) {
|
||||
for i in 0..N {
|
||||
self.0[i] |= other.0[i];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn intersection(&mut self, other: &Self) {
|
||||
for i in 0..N {
|
||||
self.0[i] &= other.0[i];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn difference(&mut self, other: &Self) {
|
||||
for i in 0..N {
|
||||
self.0[i] &= !other.0[i];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_many(&mut self, other: &Self) {
|
||||
for i in 0..N {
|
||||
self.0[i] &= !other.0[i];
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_all(&mut self) {
|
||||
for i in 0..N {
|
||||
self.0[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
for i in 0..N {
|
||||
if self.0[i] != 0 {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn inner(&self) -> &[usize; N] {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Default for Bitset<N> {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{
|
||||
cell::UnsafeCell,
|
||||
sync::{
|
||||
Arc,
|
||||
atomic::{AtomicU64, Ordering},
|
||||
},
|
||||
};
|
||||
|
||||
use rtrb::{Consumer, Producer, PushError, RingBuffer};
|
||||
|
||||
use crate::{
|
||||
Error, Event, EventType,
|
||||
ipc::collector::{COLLECTOR_THREAD, COLLECTOR_UPDATES, Update},
|
||||
};
|
||||
|
||||
use super::collector::{Collector, CollectorThread};
|
||||
|
||||
pub(crate) static CHANNEL_FLAGS: AtomicU64 = AtomicU64::new(0);
|
||||
pub(crate) const CHANNEL_SIZE: usize = 10240;
|
||||
pub(crate) const CHANNEL_UPDATE_MARKER: u64 = 1 << 63;
|
||||
|
||||
thread_local! {
|
||||
static EVENT_TX: UnsafeCell<Sender> = {
|
||||
// Create channel.
|
||||
let (tx, rx) = RingBuffer::new(CHANNEL_SIZE);
|
||||
|
||||
// Register receiver with collector.
|
||||
COLLECTOR_UPDATES.lock().push(Update::RegisterReceiver { receiver: Receiver { rx } });
|
||||
|
||||
// Spawn collector thread.
|
||||
let collector = COLLECTOR_THREAD.clone();
|
||||
CHANNEL_FLAGS.fetch_or(CHANNEL_UPDATE_MARKER, Ordering::Relaxed);
|
||||
collector.thread().unpark();
|
||||
|
||||
// Return sender.
|
||||
UnsafeCell::new(Sender {
|
||||
tx,
|
||||
collector,
|
||||
overflow: Vec::with_capacity(0),
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
pub struct Sender {
|
||||
tx: Producer<Event<EventType>>,
|
||||
collector: Arc<CollectorThread>,
|
||||
overflow: Vec<Event<EventType>>,
|
||||
}
|
||||
|
||||
pub struct Receiver {
|
||||
rx: Consumer<Event<EventType>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ChannelError;
|
||||
|
||||
impl Sender {
|
||||
pub fn send(&mut self, event: Event<EventType>) -> Result<(), ChannelError> {
|
||||
while let Some(event) = self.overflow.pop() {
|
||||
if let Err(PushError::Full(event)) = self.tx.push(event) {
|
||||
self.overflow.push(event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(PushError::Full(event)) = self.tx.push(event) {
|
||||
if self.overflow.len() <= CHANNEL_SIZE * 2 {
|
||||
self.overflow.push(event);
|
||||
} else {
|
||||
return Err(ChannelError);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Receiver {
|
||||
pub fn try_recv(&mut self) -> Result<Option<Event<EventType>>, ChannelError> {
|
||||
match self.rx.pop() {
|
||||
Ok(event) => Ok(Some(event)),
|
||||
Err(_) => {
|
||||
if !self.rx.is_abandoned() {
|
||||
Ok(None)
|
||||
} else {
|
||||
Err(ChannelError)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Event<EventType> {
|
||||
pub fn send(self) {
|
||||
// SAFETY: EVENT_TX is thread-local.
|
||||
let _ = EVENT_TX.try_with(|tx| unsafe {
|
||||
let tx = &mut *tx.get();
|
||||
if tx.send(self).is_ok() {
|
||||
CHANNEL_FLAGS.fetch_add(1, Ordering::Relaxed);
|
||||
tx.collector.thread().unpark();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn send_with_metrics(self) {
|
||||
Collector::record_metric(self.inner, self.inner.to_id() as usize, &self.keys);
|
||||
self.send();
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn send(self) {
|
||||
self.0.send();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::{
|
||||
sync::{Arc, LazyLock, atomic::Ordering},
|
||||
thread::{Builder, JoinHandle, park},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use atomics::bitset::AtomicBitset;
|
||||
use ipc::{
|
||||
USIZE_BITS,
|
||||
channel::{CHANNEL_FLAGS, CHANNEL_UPDATE_MARKER, Receiver},
|
||||
subscriber::{Interests, Subscriber},
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) type GlobalInterests = AtomicBitset<{ TOTAL_EVENT_COUNT.div_ceil(USIZE_BITS) }>;
|
||||
|
||||
pub(crate) static TRACE_INTERESTS: GlobalInterests = GlobalInterests::new();
|
||||
pub(crate) type CollectorThread = JoinHandle<()>;
|
||||
pub(crate) static ACTIVE_SUBSCRIBERS: Mutex<Vec<String>> = Mutex::new(Vec::new());
|
||||
pub(crate) static COLLECTOR_UPDATES: Mutex<Vec<Update>> = Mutex::new(Vec::new());
|
||||
|
||||
pub(crate) static EVENT_TYPES: &[EventType] = EventType::variants();
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub(crate) enum Update {
|
||||
RegisterReceiver {
|
||||
receiver: Receiver,
|
||||
},
|
||||
RegisterSubscriber {
|
||||
subscriber: Subscriber,
|
||||
},
|
||||
UnregisterSubscriber {
|
||||
id: String,
|
||||
},
|
||||
UpdateSubscriber {
|
||||
id: String,
|
||||
interests: Interests,
|
||||
lossy: bool,
|
||||
},
|
||||
UpdateLevels {
|
||||
levels: AHashMap<EventType, Level>,
|
||||
},
|
||||
Shutdown,
|
||||
}
|
||||
|
||||
pub struct Collector {
|
||||
receivers: Vec<Receiver>,
|
||||
subscribers: Vec<Subscriber>,
|
||||
levels: [Level; TOTAL_EVENT_COUNT],
|
||||
active_spans: AHashMap<u64, Arc<Event<EventDetails>>>,
|
||||
}
|
||||
|
||||
const HTTP_CONN_START: usize = EventType::Http(HttpEvent::ConnectionStart).to_id() as usize;
|
||||
const HTTP_CONN_END: usize = EventType::Http(HttpEvent::ConnectionEnd).to_id() as usize;
|
||||
const IMAP_CONN_START: usize = EventType::Imap(ImapEvent::ConnectionStart).to_id() as usize;
|
||||
const IMAP_CONN_END: usize = EventType::Imap(ImapEvent::ConnectionEnd).to_id() as usize;
|
||||
const POP3_CONN_START: usize = EventType::Pop3(Pop3Event::ConnectionStart).to_id() as usize;
|
||||
const POP3_CONN_END: usize = EventType::Pop3(Pop3Event::ConnectionEnd).to_id() as usize;
|
||||
const SMTP_CONN_START: usize = EventType::Smtp(SmtpEvent::ConnectionStart).to_id() as usize;
|
||||
const SMTP_CONN_END: usize = EventType::Smtp(SmtpEvent::ConnectionEnd).to_id() as usize;
|
||||
const MANAGE_SIEVE_CONN_START: usize =
|
||||
EventType::ManageSieve(ManageSieveEvent::ConnectionStart).to_id() as usize;
|
||||
const MANAGE_SIEVE_CONN_END: usize =
|
||||
EventType::ManageSieve(ManageSieveEvent::ConnectionEnd).to_id() as usize;
|
||||
const EV_ATTEMPT_START: usize = EventType::Delivery(DeliveryEvent::AttemptStart).to_id() as usize;
|
||||
const EV_ATTEMPT_END: usize = EventType::Delivery(DeliveryEvent::AttemptEnd).to_id() as usize;
|
||||
|
||||
const STALE_SPAN_CHECK_WATERMARK: usize = 8000;
|
||||
const SPAN_MAX_HOLD: u64 = 60 * 60 * 24; // 1 day
|
||||
|
||||
pub(crate) static COLLECTOR_THREAD: LazyLock<Arc<CollectorThread>> = LazyLock::new(|| {
|
||||
Arc::new(
|
||||
Builder::new()
|
||||
.name("stalwart-collector".to_string())
|
||||
.spawn(move || {
|
||||
Collector::default().collect();
|
||||
})
|
||||
.expect("Failed to start event collector"),
|
||||
)
|
||||
});
|
||||
|
||||
impl Collector {
|
||||
fn collect(&mut self) {
|
||||
let mut do_continue = true;
|
||||
|
||||
// Update
|
||||
self.update();
|
||||
|
||||
while do_continue {
|
||||
match CHANNEL_FLAGS.swap(0, Ordering::Relaxed) {
|
||||
0 => {
|
||||
park();
|
||||
}
|
||||
CHANNEL_UPDATE_MARKER..=u64::MAX => {
|
||||
do_continue = self.update();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Collect all events
|
||||
let mut closed_rxs = Vec::new();
|
||||
for (rx_idx, rx) in self.receivers.iter_mut().enumerate() {
|
||||
let timestamp = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.map_or(0, |d| d.as_secs());
|
||||
|
||||
loop {
|
||||
match rx.try_recv() {
|
||||
Ok(Some(event)) => {
|
||||
// Build event
|
||||
let event_id = event.inner.to_id() as usize;
|
||||
let mut event = Event {
|
||||
inner: EventDetails {
|
||||
level: self.levels[event_id],
|
||||
typ: event.inner,
|
||||
timestamp,
|
||||
span: None,
|
||||
},
|
||||
keys: event.keys,
|
||||
};
|
||||
|
||||
// Track spans
|
||||
let event = match event_id {
|
||||
HTTP_CONN_START
|
||||
| IMAP_CONN_START
|
||||
| POP3_CONN_START
|
||||
| SMTP_CONN_START
|
||||
| MANAGE_SIEVE_CONN_START
|
||||
| EV_ATTEMPT_START => {
|
||||
let event = Arc::new(event);
|
||||
self.active_spans.insert(
|
||||
event.span_id().unwrap_or_else(|| {
|
||||
panic!("Missing span ID: {event:?}")
|
||||
}),
|
||||
event.clone(),
|
||||
);
|
||||
|
||||
if self.active_spans.len() > STALE_SPAN_CHECK_WATERMARK {
|
||||
self.active_spans.retain(|_, span| {
|
||||
timestamp.saturating_sub(span.inner.timestamp)
|
||||
< SPAN_MAX_HOLD
|
||||
});
|
||||
}
|
||||
event
|
||||
}
|
||||
|
||||
HTTP_CONN_END
|
||||
| IMAP_CONN_END
|
||||
| POP3_CONN_END
|
||||
| SMTP_CONN_END
|
||||
| MANAGE_SIEVE_CONN_END
|
||||
| EV_ATTEMPT_END => {
|
||||
if let Some(span) = self
|
||||
.active_spans
|
||||
.remove(&event.span_id().expect("Missing span ID"))
|
||||
{
|
||||
event.inner.span = Some(span.clone());
|
||||
} else {
|
||||
#[cfg(any(feature = "dev_mode", feature = "test_mode"))]
|
||||
{
|
||||
if event.span_id().unwrap() != 0 {
|
||||
eprintln!("Unregistered span ID: {event:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Arc::new(event)
|
||||
}
|
||||
_ => {
|
||||
if let Some(span_id) = event.span_id() {
|
||||
if let Some(span) = self.active_spans.get(&span_id) {
|
||||
event.inner.span = Some(span.clone());
|
||||
} else {
|
||||
#[cfg(any(
|
||||
feature = "dev_mode",
|
||||
feature = "test_mode"
|
||||
))]
|
||||
{
|
||||
if span_id != 0 {
|
||||
eprintln!("Unregistered span ID: {event:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Arc::new(event)
|
||||
}
|
||||
};
|
||||
|
||||
// Send to subscribers
|
||||
for subscriber in self.subscribers.iter_mut() {
|
||||
subscriber.push_event(event_id, event.clone());
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
break;
|
||||
}
|
||||
Err(_) => {
|
||||
closed_rxs.push(rx_idx); // Channel is closed, remove.
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if do_continue {
|
||||
// Remove closed receivers (should be rare in Tokio)
|
||||
if !closed_rxs.is_empty() {
|
||||
let mut receivers = Vec::with_capacity(self.receivers.len() - closed_rxs.len());
|
||||
for (rx_idx, rx) in self.receivers.drain(..).enumerate() {
|
||||
if !closed_rxs.contains(&rx_idx) {
|
||||
receivers.push(rx);
|
||||
}
|
||||
}
|
||||
self.receivers = receivers;
|
||||
}
|
||||
|
||||
// Send batched events
|
||||
if !self.subscribers.is_empty() {
|
||||
self.subscribers
|
||||
.retain_mut(|subscriber| subscriber.send_batch().is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Send remaining events
|
||||
for mut subscriber in self.subscribers.drain(..) {
|
||||
let _ = subscriber.send_batch();
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) -> bool {
|
||||
for update in COLLECTOR_UPDATES.lock().drain(..) {
|
||||
match update {
|
||||
Update::RegisterReceiver { receiver } => {
|
||||
self.receivers.push(receiver);
|
||||
}
|
||||
Update::RegisterSubscriber { subscriber } => {
|
||||
ACTIVE_SUBSCRIBERS.lock().push(subscriber.id.clone());
|
||||
self.subscribers.push(subscriber);
|
||||
}
|
||||
Update::UnregisterSubscriber { id } => {
|
||||
ACTIVE_SUBSCRIBERS.lock().retain(|s| s != &id);
|
||||
self.subscribers.retain(|s| s.id != id);
|
||||
}
|
||||
Update::UpdateSubscriber {
|
||||
id,
|
||||
interests,
|
||||
lossy,
|
||||
} => {
|
||||
for subscriber in self.subscribers.iter_mut() {
|
||||
if subscriber.id == id {
|
||||
subscriber.interests = interests;
|
||||
subscriber.lossy = lossy;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Update::UpdateLevels { levels } => {
|
||||
for event in EVENT_TYPES.iter() {
|
||||
let event_id = event.to_id() as usize;
|
||||
if let Some(level) = levels.get(event) {
|
||||
self.levels[event_id] = *level;
|
||||
} else {
|
||||
self.levels[event_id] = event.level();
|
||||
}
|
||||
}
|
||||
}
|
||||
Update::Shutdown => return false,
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn set_interests(mut interests: Interests) {
|
||||
if !interests.is_empty() {
|
||||
for event_type in EVENT_TYPES.iter() {
|
||||
if event_type.is_span_start() || event_type.is_span_end() {
|
||||
interests.set(*event_type);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TRACE_INTERESTS.update(interests);
|
||||
}
|
||||
|
||||
pub fn union_interests(interests: Interests) {
|
||||
TRACE_INTERESTS.union(interests);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn has_interest(event: impl Into<usize>) -> bool {
|
||||
TRACE_INTERESTS.get(event)
|
||||
}
|
||||
|
||||
pub fn get_subscribers() -> Vec<String> {
|
||||
ACTIVE_SUBSCRIBERS.lock().clone()
|
||||
}
|
||||
|
||||
pub fn update_custom_levels(levels: AHashMap<EventType, Level>) {
|
||||
COLLECTOR_UPDATES
|
||||
.lock()
|
||||
.push(Update::UpdateLevels { levels });
|
||||
}
|
||||
|
||||
pub fn update_subscriber(id: String, interests: Interests, lossy: bool) {
|
||||
COLLECTOR_UPDATES.lock().push(Update::UpdateSubscriber {
|
||||
id,
|
||||
interests,
|
||||
lossy,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn remove_subscriber(id: String) {
|
||||
COLLECTOR_UPDATES
|
||||
.lock()
|
||||
.push(Update::UnregisterSubscriber { id });
|
||||
}
|
||||
|
||||
pub fn shutdown() {
|
||||
COLLECTOR_UPDATES.lock().push(Update::Shutdown);
|
||||
Collector::reload();
|
||||
}
|
||||
|
||||
pub fn is_enabled() -> bool {
|
||||
!TRACE_INTERESTS.is_empty()
|
||||
}
|
||||
|
||||
pub fn reload() {
|
||||
CHANNEL_FLAGS.fetch_or(CHANNEL_UPDATE_MARKER, Ordering::Relaxed);
|
||||
COLLECTOR_THREAD.thread().unpark();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Collector {
|
||||
fn default() -> Self {
|
||||
let mut c = Collector {
|
||||
subscribers: Vec::new(),
|
||||
levels: [Level::Disable; TOTAL_EVENT_COUNT],
|
||||
active_spans: AHashMap::new(),
|
||||
receivers: Vec::new(),
|
||||
};
|
||||
|
||||
for event in EVENT_TYPES.iter() {
|
||||
let event_id = event.to_id() as usize;
|
||||
c.levels[event_id] = event.level();
|
||||
}
|
||||
|
||||
c
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use atomics::{array::AtomicU32Array, gauge::AtomicGauge, histogram::AtomicHistogram};
|
||||
use ipc::{
|
||||
collector::{Collector, GlobalInterests},
|
||||
subscriber::Interests,
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
|
||||
pub(crate) static METRIC_INTERESTS: GlobalInterests = GlobalInterests::new();
|
||||
|
||||
static EVENT_COUNTERS: AtomicU32Array<TOTAL_EVENT_COUNT> = AtomicU32Array::new();
|
||||
static CONNECTION_METRICS: [ConnectionMetrics; TOTAL_CONN_TYPES] = init_conn_metrics();
|
||||
|
||||
static MESSAGE_INGESTION_TIME: AtomicHistogram<12> =
|
||||
AtomicHistogram::<10>::new_short_durations(MetricType::MessageIngestTime);
|
||||
static MESSAGE_INDEX_TIME: AtomicHistogram<12> =
|
||||
AtomicHistogram::<10>::new_short_durations(MetricType::MessageIngestIndexTime);
|
||||
static MESSAGE_DELIVERY_TIME: AtomicHistogram<12> =
|
||||
AtomicHistogram::<18>::new_long_durations(MetricType::DeliveryTotalTime);
|
||||
|
||||
static MESSAGE_INCOMING_SIZE: AtomicHistogram<12> =
|
||||
AtomicHistogram::<12>::new_message_sizes(MetricType::MessageSize);
|
||||
static MESSAGE_SUBMISSION_SIZE: AtomicHistogram<12> =
|
||||
AtomicHistogram::<12>::new_message_sizes(MetricType::MessageAuthenticatedSize);
|
||||
static MESSAGE_OUT_REPORT_SIZE: AtomicHistogram<12> =
|
||||
AtomicHistogram::<12>::new_message_sizes(MetricType::OutgoingReportSize);
|
||||
|
||||
static STORE_DATA_READ_TIME: AtomicHistogram<12> =
|
||||
AtomicHistogram::<10>::new_short_durations(MetricType::StoreDataReadTime);
|
||||
static STORE_DATA_WRITE_TIME: AtomicHistogram<12> =
|
||||
AtomicHistogram::<10>::new_short_durations(MetricType::StoreDataWriteTime);
|
||||
static STORE_BLOB_READ_TIME: AtomicHistogram<12> =
|
||||
AtomicHistogram::<10>::new_short_durations(MetricType::StoreBlobReadTime);
|
||||
static STORE_BLOB_WRITE_TIME: AtomicHistogram<12> =
|
||||
AtomicHistogram::<10>::new_short_durations(MetricType::StoreBlobWriteTime);
|
||||
|
||||
static DNS_LOOKUP_TIME: AtomicHistogram<12> =
|
||||
AtomicHistogram::<10>::new_short_durations(MetricType::DnsLookupTime);
|
||||
|
||||
static SERVER_MEMORY: AtomicGauge = AtomicGauge::new(MetricType::ServerMemory);
|
||||
static QUEUE_COUNT: AtomicGauge = AtomicGauge::new(MetricType::QueueCount);
|
||||
static USER_COUNT: AtomicGauge = AtomicGauge::new(MetricType::UserCount);
|
||||
static DOMAIN_COUNT: AtomicGauge = AtomicGauge::new(MetricType::DomainCount);
|
||||
|
||||
const CONN_SMTP_IN: usize = 0;
|
||||
const CONN_SMTP_OUT: usize = 1;
|
||||
const CONN_IMAP: usize = 2;
|
||||
const CONN_POP3: usize = 3;
|
||||
const CONN_HTTP: usize = 4;
|
||||
const CONN_SIEVE: usize = 5;
|
||||
const TOTAL_CONN_TYPES: usize = 6;
|
||||
|
||||
pub struct ConnectionMetrics {
|
||||
pub active_connections: AtomicGauge,
|
||||
pub elapsed: AtomicHistogram<12>,
|
||||
}
|
||||
|
||||
pub struct EventCounter {
|
||||
id: EventType,
|
||||
value: u32,
|
||||
}
|
||||
|
||||
impl Collector {
|
||||
pub fn record_metric(event: EventType, event_id: usize, keys: &[(Key, Value)]) {
|
||||
// Increment the event counter
|
||||
if !event.is_span_end() && !event.is_raw_io() {
|
||||
EVENT_COUNTERS.add(event_id, 1);
|
||||
}
|
||||
|
||||
// Extract variables
|
||||
let mut elapsed = 0;
|
||||
let mut size = 0;
|
||||
for (key, value) in keys {
|
||||
match (key, value) {
|
||||
(Key::Elapsed, Value::Duration(d)) => elapsed = *d,
|
||||
(Key::Size, Value::UInt(s)) => size = *s,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match event {
|
||||
EventType::Smtp(SmtpEvent::ConnectionStart) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_SMTP_IN];
|
||||
conn.active_connections.increment();
|
||||
}
|
||||
EventType::Smtp(SmtpEvent::ConnectionEnd) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_SMTP_IN];
|
||||
conn.active_connections.decrement();
|
||||
conn.elapsed.observe(elapsed);
|
||||
}
|
||||
EventType::Imap(ImapEvent::ConnectionStart) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_IMAP];
|
||||
conn.active_connections.increment();
|
||||
}
|
||||
EventType::Imap(ImapEvent::ConnectionEnd) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_IMAP];
|
||||
conn.active_connections.decrement();
|
||||
conn.elapsed.observe(elapsed);
|
||||
}
|
||||
EventType::Pop3(Pop3Event::ConnectionStart) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_POP3];
|
||||
conn.active_connections.increment();
|
||||
}
|
||||
EventType::Pop3(Pop3Event::ConnectionEnd) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_POP3];
|
||||
conn.active_connections.decrement();
|
||||
conn.elapsed.observe(elapsed);
|
||||
}
|
||||
EventType::Http(HttpEvent::ConnectionStart) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_HTTP];
|
||||
conn.active_connections.increment();
|
||||
}
|
||||
EventType::Http(HttpEvent::ConnectionEnd) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_HTTP];
|
||||
conn.active_connections.decrement();
|
||||
conn.elapsed.observe(elapsed);
|
||||
}
|
||||
EventType::ManageSieve(ManageSieveEvent::ConnectionStart) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_SIEVE];
|
||||
conn.active_connections.increment();
|
||||
}
|
||||
EventType::ManageSieve(ManageSieveEvent::ConnectionEnd) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_SIEVE];
|
||||
conn.active_connections.decrement();
|
||||
conn.elapsed.observe(elapsed);
|
||||
}
|
||||
EventType::Delivery(DeliveryEvent::AttemptStart) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_SMTP_OUT];
|
||||
conn.active_connections.increment();
|
||||
}
|
||||
EventType::Delivery(DeliveryEvent::AttemptEnd) => {
|
||||
let conn = &CONNECTION_METRICS[CONN_SMTP_OUT];
|
||||
conn.active_connections.decrement();
|
||||
conn.elapsed.observe(elapsed);
|
||||
}
|
||||
EventType::Delivery(DeliveryEvent::Completed) => {
|
||||
QUEUE_COUNT.decrement();
|
||||
MESSAGE_DELIVERY_TIME.observe(elapsed);
|
||||
}
|
||||
EventType::Delivery(
|
||||
DeliveryEvent::MxLookup | DeliveryEvent::IpLookup | DeliveryEvent::NullMx,
|
||||
)
|
||||
| EventType::TlsRpt(_)
|
||||
| EventType::MtaSts(_)
|
||||
| EventType::Dane(_)
|
||||
if elapsed > 0 =>
|
||||
{
|
||||
DNS_LOOKUP_TIME.observe(elapsed);
|
||||
}
|
||||
EventType::MessageIngest(
|
||||
MessageIngestEvent::Ham
|
||||
| MessageIngestEvent::Spam
|
||||
| MessageIngestEvent::ImapAppend
|
||||
| MessageIngestEvent::JmapAppend,
|
||||
) => {
|
||||
MESSAGE_INGESTION_TIME.observe(elapsed);
|
||||
}
|
||||
EventType::Queue(QueueEvent::MessageQueued) => {
|
||||
MESSAGE_INCOMING_SIZE.observe(size);
|
||||
QUEUE_COUNT.increment();
|
||||
}
|
||||
EventType::Queue(QueueEvent::AuthenticatedMessageQueued) => {
|
||||
MESSAGE_SUBMISSION_SIZE.observe(size);
|
||||
QUEUE_COUNT.increment();
|
||||
}
|
||||
EventType::Queue(QueueEvent::ReportQueued) => {
|
||||
MESSAGE_OUT_REPORT_SIZE.observe(size);
|
||||
QUEUE_COUNT.increment();
|
||||
}
|
||||
EventType::Queue(QueueEvent::AutogeneratedQueued | QueueEvent::DsnQueued) => {
|
||||
QUEUE_COUNT.increment();
|
||||
}
|
||||
EventType::MessageIngest(MessageIngestEvent::SearchIndex) => {
|
||||
MESSAGE_INDEX_TIME.observe(elapsed);
|
||||
}
|
||||
EventType::Store(StoreEvent::BlobWrite) => {
|
||||
STORE_BLOB_WRITE_TIME.observe(elapsed);
|
||||
}
|
||||
EventType::Store(StoreEvent::BlobRead) => {
|
||||
STORE_BLOB_READ_TIME.observe(elapsed);
|
||||
}
|
||||
EventType::Store(StoreEvent::DataWrite) => {
|
||||
STORE_DATA_WRITE_TIME.observe(elapsed);
|
||||
}
|
||||
EventType::Store(StoreEvent::DataIterate) => {
|
||||
STORE_DATA_READ_TIME.observe(elapsed);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn is_metric(event: impl Into<usize>) -> bool {
|
||||
METRIC_INTERESTS.get(event)
|
||||
}
|
||||
|
||||
pub fn set_metrics(interests: Interests) {
|
||||
METRIC_INTERESTS.update(interests);
|
||||
}
|
||||
|
||||
pub fn collect_counters(_is_enterprise: bool) -> impl Iterator<Item = EventCounter> {
|
||||
EVENT_COUNTERS
|
||||
.inner()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(event_id, value)| {
|
||||
let value = value.load(Ordering::Relaxed);
|
||||
if value > 0 {
|
||||
Some(EventCounter {
|
||||
id: EventType::from_id(event_id as u16)?,
|
||||
value,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn collect_gauges(is_enterprise: bool) -> impl Iterator<Item = &'static AtomicGauge> {
|
||||
static E_GAUGES: &[&AtomicGauge] =
|
||||
&[&SERVER_MEMORY, &QUEUE_COUNT, &USER_COUNT, &DOMAIN_COUNT];
|
||||
static C_GAUGES: &[&AtomicGauge] = &[&SERVER_MEMORY, &USER_COUNT, &DOMAIN_COUNT];
|
||||
|
||||
if is_enterprise { E_GAUGES } else { C_GAUGES }
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(CONNECTION_METRICS.iter().map(|m| &m.active_connections))
|
||||
}
|
||||
|
||||
pub fn collect_histograms(
|
||||
is_enterprise: bool,
|
||||
) -> impl Iterator<Item = &'static AtomicHistogram<12>> {
|
||||
static E_HISTOGRAMS: &[&AtomicHistogram<12>] = &[
|
||||
&MESSAGE_INGESTION_TIME,
|
||||
&MESSAGE_INDEX_TIME,
|
||||
&MESSAGE_DELIVERY_TIME,
|
||||
&MESSAGE_INCOMING_SIZE,
|
||||
&MESSAGE_SUBMISSION_SIZE,
|
||||
&MESSAGE_OUT_REPORT_SIZE,
|
||||
&STORE_DATA_READ_TIME,
|
||||
&STORE_DATA_WRITE_TIME,
|
||||
&STORE_BLOB_READ_TIME,
|
||||
&STORE_BLOB_WRITE_TIME,
|
||||
&DNS_LOOKUP_TIME,
|
||||
];
|
||||
static C_HISTOGRAMS: &[&AtomicHistogram<12>] = &[
|
||||
&MESSAGE_DELIVERY_TIME,
|
||||
&MESSAGE_INCOMING_SIZE,
|
||||
&MESSAGE_SUBMISSION_SIZE,
|
||||
];
|
||||
|
||||
if is_enterprise {
|
||||
E_HISTOGRAMS
|
||||
} else {
|
||||
C_HISTOGRAMS
|
||||
}
|
||||
.iter()
|
||||
.copied()
|
||||
.chain(CONNECTION_METRICS.iter().map(|m| &m.elapsed))
|
||||
.filter(|h| h.is_active())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn read_metric_counter(metric_id: usize) -> u32 {
|
||||
EVENT_COUNTERS.get(metric_id)
|
||||
}
|
||||
|
||||
pub fn read_metric(metric_type: MetricType) -> f64 {
|
||||
match metric_type {
|
||||
MetricType::ServerMemory => SERVER_MEMORY.get() as f64,
|
||||
MetricType::MessageIngestTime => MESSAGE_INGESTION_TIME.average(),
|
||||
MetricType::MessageIngestIndexTime => MESSAGE_INDEX_TIME.average(),
|
||||
MetricType::MessageSize => MESSAGE_INCOMING_SIZE.average(),
|
||||
MetricType::MessageAuthenticatedSize => MESSAGE_SUBMISSION_SIZE.average(),
|
||||
MetricType::DeliveryTotalTime => MESSAGE_DELIVERY_TIME.average(),
|
||||
MetricType::DeliveryAttemptTime => CONNECTION_METRICS[CONN_SMTP_OUT].elapsed.average(),
|
||||
MetricType::DeliveryActiveConnections => {
|
||||
CONNECTION_METRICS[CONN_SMTP_OUT].active_connections.get() as f64
|
||||
}
|
||||
MetricType::QueueCount => QUEUE_COUNT.get() as f64,
|
||||
MetricType::OutgoingReportSize => MESSAGE_OUT_REPORT_SIZE.average(),
|
||||
MetricType::StoreDataReadTime => STORE_DATA_READ_TIME.average(),
|
||||
MetricType::StoreDataWriteTime => STORE_DATA_WRITE_TIME.average(),
|
||||
MetricType::StoreBlobReadTime => STORE_BLOB_READ_TIME.average(),
|
||||
MetricType::StoreBlobWriteTime => STORE_BLOB_WRITE_TIME.average(),
|
||||
MetricType::DnsLookupTime => DNS_LOOKUP_TIME.average(),
|
||||
MetricType::HttpActiveConnections => {
|
||||
CONNECTION_METRICS[CONN_HTTP].active_connections.get() as f64
|
||||
}
|
||||
MetricType::HttpRequestTime => CONNECTION_METRICS[CONN_HTTP].elapsed.average(),
|
||||
MetricType::ImapActiveConnections => {
|
||||
CONNECTION_METRICS[CONN_IMAP].active_connections.get() as f64
|
||||
}
|
||||
MetricType::ImapRequestTime => CONNECTION_METRICS[CONN_IMAP].elapsed.average(),
|
||||
MetricType::Pop3ActiveConnections => {
|
||||
CONNECTION_METRICS[CONN_POP3].active_connections.get() as f64
|
||||
}
|
||||
MetricType::Pop3RequestTime => CONNECTION_METRICS[CONN_POP3].elapsed.average(),
|
||||
MetricType::SmtpActiveConnections => {
|
||||
CONNECTION_METRICS[CONN_SMTP_IN].active_connections.get() as f64
|
||||
}
|
||||
MetricType::SmtpRequestTime => CONNECTION_METRICS[CONN_SMTP_IN].elapsed.average(),
|
||||
MetricType::SieveActiveConnections => {
|
||||
CONNECTION_METRICS[CONN_SIEVE].active_connections.get() as f64
|
||||
}
|
||||
MetricType::SieveRequestTime => CONNECTION_METRICS[CONN_SIEVE].elapsed.average(),
|
||||
MetricType::UserCount => USER_COUNT.get() as f64,
|
||||
MetricType::DomainCount => DOMAIN_COUNT.get() as f64,
|
||||
_ => EVENT_COUNTERS.get(metric_type.event_id()) as f64,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_gauge(metric_type: MetricType, value: u64) {
|
||||
match metric_type {
|
||||
MetricType::ServerMemory => SERVER_MEMORY.set(value),
|
||||
MetricType::QueueCount => QUEUE_COUNT.set(value),
|
||||
MetricType::UserCount => USER_COUNT.set(value),
|
||||
MetricType::DomainCount => DOMAIN_COUNT.set(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_event_counter(event_type: EventType, value: u32) {
|
||||
EVENT_COUNTERS.add(event_type.into(), value);
|
||||
}
|
||||
|
||||
pub fn update_histogram(metric_type: MetricType, value: u64) {
|
||||
match metric_type {
|
||||
MetricType::MessageIngestTime => MESSAGE_INGESTION_TIME.observe(value),
|
||||
MetricType::MessageIngestIndexTime => MESSAGE_INDEX_TIME.observe(value),
|
||||
MetricType::DeliveryTotalTime => MESSAGE_DELIVERY_TIME.observe(value),
|
||||
MetricType::DeliveryAttemptTime => {
|
||||
CONNECTION_METRICS[CONN_SMTP_OUT].elapsed.observe(value)
|
||||
}
|
||||
MetricType::DnsLookupTime => DNS_LOOKUP_TIME.observe(value),
|
||||
MetricType::StoreDataReadTime => STORE_DATA_READ_TIME.observe(value),
|
||||
MetricType::StoreDataWriteTime => STORE_DATA_WRITE_TIME.observe(value),
|
||||
MetricType::StoreBlobReadTime => STORE_BLOB_READ_TIME.observe(value),
|
||||
MetricType::StoreBlobWriteTime => STORE_BLOB_WRITE_TIME.observe(value),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventCounter {
|
||||
pub fn id(&self) -> EventType {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn value(&self) -> u64 {
|
||||
self.value as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionMetrics {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
active_connections: AtomicGauge::new(MetricType::StoreBlobReadTime),
|
||||
elapsed: AtomicHistogram::<18>::new_medium_durations(MetricType::StoreBlobReadTime),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::declare_interior_mutable_const)]
|
||||
const fn init_conn_metrics() -> [ConnectionMetrics; TOTAL_CONN_TYPES] {
|
||||
const INIT: ConnectionMetrics = ConnectionMetrics::new();
|
||||
let mut array = [INIT; TOTAL_CONN_TYPES];
|
||||
let mut i = 0;
|
||||
while i < TOTAL_CONN_TYPES {
|
||||
let metric = match i {
|
||||
CONN_HTTP => &[
|
||||
MetricType::HttpRequestTime,
|
||||
MetricType::HttpActiveConnections,
|
||||
],
|
||||
CONN_IMAP => &[
|
||||
MetricType::ImapRequestTime,
|
||||
MetricType::ImapActiveConnections,
|
||||
],
|
||||
CONN_POP3 => &[
|
||||
MetricType::Pop3RequestTime,
|
||||
MetricType::Pop3ActiveConnections,
|
||||
],
|
||||
CONN_SMTP_IN => &[
|
||||
MetricType::SmtpRequestTime,
|
||||
MetricType::SmtpActiveConnections,
|
||||
],
|
||||
CONN_SMTP_OUT => &[
|
||||
MetricType::DeliveryAttemptTime,
|
||||
MetricType::DeliveryActiveConnections,
|
||||
],
|
||||
CONN_SIEVE => &[
|
||||
MetricType::SieveRequestTime,
|
||||
MetricType::SieveActiveConnections,
|
||||
],
|
||||
_ => &[MetricType::StoreBlobReadTime, MetricType::StoreBlobReadTime],
|
||||
};
|
||||
|
||||
array[i] = ConnectionMetrics {
|
||||
elapsed: AtomicHistogram::<18>::new_medium_durations(metric[0]),
|
||||
active_connections: AtomicGauge::new(metric[1]),
|
||||
};
|
||||
i += 1;
|
||||
}
|
||||
array
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod bitset;
|
||||
pub mod channel;
|
||||
pub mod collector;
|
||||
pub mod metrics;
|
||||
pub mod subscriber;
|
||||
|
||||
pub(crate) const USIZE_BITS: usize = std::mem::size_of::<usize>() * 8;
|
||||
pub(crate) const USIZE_BITS_MASK: usize = USIZE_BITS - 1;
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::sync::mpsc::{self, error::TrySendError};
|
||||
|
||||
use crate::{Event, EventDetails, EventType, Level, TOTAL_EVENT_COUNT};
|
||||
|
||||
use super::{
|
||||
USIZE_BITS,
|
||||
bitset::Bitset,
|
||||
channel::ChannelError,
|
||||
collector::{COLLECTOR_UPDATES, Collector, Update},
|
||||
};
|
||||
|
||||
const MAX_BATCH_SIZE: usize = 32768;
|
||||
|
||||
pub type Interests = Box<Bitset<{ TOTAL_EVENT_COUNT.div_ceil(USIZE_BITS) }>>;
|
||||
pub type EventBatch = Vec<Arc<Event<EventDetails>>>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Subscriber {
|
||||
pub id: String,
|
||||
pub interests: Interests,
|
||||
pub tx: mpsc::Sender<EventBatch>,
|
||||
pub lossy: bool,
|
||||
pub batch: EventBatch,
|
||||
}
|
||||
|
||||
pub struct SubscriberBuilder {
|
||||
pub id: String,
|
||||
pub interests: Interests,
|
||||
pub lossy: bool,
|
||||
}
|
||||
|
||||
impl Subscriber {
|
||||
#[inline(always)]
|
||||
pub fn push_event(&mut self, event_id: usize, trace: Arc<Event<EventDetails>>) {
|
||||
if self.interests.get(event_id) {
|
||||
self.batch.push(trace);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_batch(&mut self) -> Result<(), ChannelError> {
|
||||
if !self.batch.is_empty() {
|
||||
match self
|
||||
.tx
|
||||
.try_send(std::mem::replace(&mut self.batch, Vec::with_capacity(128)))
|
||||
{
|
||||
Ok(_) => Ok(()),
|
||||
Err(TrySendError::Full(mut events)) => {
|
||||
if self.lossy && events.len() > MAX_BATCH_SIZE {
|
||||
events.retain(|e| e.inner.level == Level::Error);
|
||||
if events.len() > MAX_BATCH_SIZE {
|
||||
events.truncate(MAX_BATCH_SIZE);
|
||||
}
|
||||
}
|
||||
self.batch = events;
|
||||
Ok(())
|
||||
}
|
||||
Err(TrySendError::Closed(_)) => Err(ChannelError),
|
||||
}
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SubscriberBuilder {
|
||||
pub fn new(id: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
interests: Default::default(),
|
||||
lossy: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_default_interests(mut self, level: Level) -> Self {
|
||||
for event in EventType::variants() {
|
||||
if event.level() >= level {
|
||||
self.interests.set(*event);
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_interests(mut self, interests: Interests) -> Self {
|
||||
self.interests = interests;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_interests(mut self, interest: impl IntoIterator<Item = impl Into<usize>>) -> Self {
|
||||
for level in interest {
|
||||
self.interests.set(level);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_lossy(mut self, lossy: bool) -> Self {
|
||||
self.lossy = lossy;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn register(self) -> (mpsc::Sender<EventBatch>, mpsc::Receiver<EventBatch>) {
|
||||
let (tx, rx) = mpsc::channel(8192);
|
||||
|
||||
COLLECTOR_UPDATES.lock().push(Update::RegisterSubscriber {
|
||||
subscriber: Subscriber {
|
||||
id: self.id,
|
||||
interests: self.interests,
|
||||
tx: tx.clone(),
|
||||
lossy: self.lossy,
|
||||
batch: Vec::new(),
|
||||
},
|
||||
});
|
||||
|
||||
// Notify collector
|
||||
Collector::reload();
|
||||
|
||||
(tx, rx)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#![warn(clippy::large_futures)]
|
||||
|
||||
pub mod atomics;
|
||||
pub mod event;
|
||||
pub mod ipc;
|
||||
pub mod macros;
|
||||
pub mod serializers;
|
||||
|
||||
pub use crate::event::enums::*;
|
||||
pub use crate::ipc::collector::Collector;
|
||||
use compact_str::CompactString;
|
||||
pub use event_macro::event;
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[repr(transparent)]
|
||||
pub struct Error(Box<Event<EventType>>);
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Event<T> {
|
||||
pub inner: T,
|
||||
pub keys: Vec<(Key, Value)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventDetails {
|
||||
pub typ: EventType,
|
||||
pub timestamp: u64,
|
||||
pub level: Level,
|
||||
pub span: Option<Arc<Event<EventDetails>>>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
|
||||
#[repr(usize)]
|
||||
pub enum Level {
|
||||
Trace = 0,
|
||||
Debug = 1,
|
||||
Info = 2,
|
||||
Warn = 3,
|
||||
Error = 4,
|
||||
Disable = 5,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum Value {
|
||||
String(CompactString),
|
||||
UInt(u64),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Timestamp(u64),
|
||||
Duration(u64),
|
||||
Bytes(Vec<u8>),
|
||||
Bool(bool),
|
||||
Ipv4(Ipv4Addr),
|
||||
Ipv6(Ipv6Addr),
|
||||
Event(Error),
|
||||
Array(Vec<Value>),
|
||||
#[default]
|
||||
None,
|
||||
}
|
||||
|
||||
pub trait AddContext<T> {
|
||||
fn caused_by(self, location: &'static str) -> Result<T>;
|
||||
fn add_context<F>(self, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(Error) -> Error;
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for MetricType {
|
||||
fn default() -> Self {
|
||||
MetricType::UserCount
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for EventType {
|
||||
fn default() -> Self {
|
||||
EventType::Store(StoreEvent::UnexpectedError)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! location {
|
||||
() => {{ concat!(file!(), ":", line!()) }};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! bail {
|
||||
($err:expr $(,)?) => {
|
||||
return Err($err);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! error {
|
||||
($err:expr $(,)?) => {
|
||||
let err = $err;
|
||||
let event_id = err.as_ref().to_id() as usize;
|
||||
|
||||
if $crate::Collector::is_metric(event_id) {
|
||||
$crate::Collector::record_metric(*err.as_ref(), event_id, err.keys());
|
||||
}
|
||||
if $crate::Collector::has_interest(event_id) {
|
||||
err.send();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use crate::{Error, Event, EventDetails, EventType, Key, MetricType, Value};
|
||||
use ahash::AHashSet;
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
use mail_parser::DateTime;
|
||||
use serde::{
|
||||
Serialize, Serializer,
|
||||
ser::{SerializeMap, SerializeSeq},
|
||||
};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static EVENT_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
struct Keys<'x> {
|
||||
keys: &'x [(Key, Value)],
|
||||
span_keys: &'x [(Key, Value)],
|
||||
}
|
||||
|
||||
pub struct JsonEventSerializer<T> {
|
||||
inner: T,
|
||||
with_id: bool,
|
||||
with_spans: bool,
|
||||
with_description: bool,
|
||||
}
|
||||
|
||||
impl<T> JsonEventSerializer<T> {
|
||||
pub fn new(inner: T) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
with_id: false,
|
||||
with_spans: false,
|
||||
with_description: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_id(mut self) -> Self {
|
||||
self.with_id = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_spans(mut self) -> Self {
|
||||
self.with_spans = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_description(mut self) -> Self {
|
||||
self.with_description = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn into_inner(self) -> T {
|
||||
self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<Event<EventDetails>>> Serialize for JsonEventSerializer<Vec<T>> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.inner.len()))?;
|
||||
for event in &self.inner {
|
||||
seq.serialize_element(&JsonEventSerializer {
|
||||
inner: event,
|
||||
with_id: self.with_id,
|
||||
with_spans: self.with_spans,
|
||||
with_description: self.with_description,
|
||||
})?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: AsRef<Event<EventDetails>>> Serialize for JsonEventSerializer<T> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let event = self.inner.as_ref();
|
||||
let mut map = serializer.serialize_map(None)?;
|
||||
if self.with_id {
|
||||
map.serialize_entry(
|
||||
"id",
|
||||
&format!(
|
||||
"{}{}{}",
|
||||
event.inner.timestamp,
|
||||
EVENT_ID_COUNTER.fetch_add(1, Ordering::Relaxed),
|
||||
event.inner.typ.to_id()
|
||||
),
|
||||
)?;
|
||||
}
|
||||
if self.with_description {
|
||||
map.serialize_entry("text", event.inner.typ.description())?;
|
||||
}
|
||||
map.serialize_entry(
|
||||
"createdAt",
|
||||
&DateTime::from_timestamp(event.inner.timestamp as i64).to_rfc3339(),
|
||||
)?;
|
||||
map.serialize_entry("type", event.inner.typ.as_str())?;
|
||||
map.serialize_entry(
|
||||
"data",
|
||||
&JsonEventSerializer {
|
||||
inner: Keys {
|
||||
keys: event.keys.as_slice(),
|
||||
span_keys: event
|
||||
.inner
|
||||
.span
|
||||
.as_ref()
|
||||
.map(|s| &s.keys[..])
|
||||
.unwrap_or(&[]),
|
||||
},
|
||||
with_spans: self.with_spans,
|
||||
with_description: self.with_description,
|
||||
with_id: self.with_id,
|
||||
},
|
||||
)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for JsonEventSerializer<Keys<'_>> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let keys_len = self.inner.keys.len() + self.inner.span_keys.len();
|
||||
let mut seen_keys = AHashSet::with_capacity(keys_len);
|
||||
let mut keys = serializer.serialize_map(Some(keys_len))?;
|
||||
for (key, value) in self.inner.keys.iter().chain(self.inner.span_keys.iter()) {
|
||||
if !matches!(value, Value::None)
|
||||
&& (self.with_spans || !matches!(key, Key::SpanId))
|
||||
&& seen_keys.insert(*key)
|
||||
{
|
||||
keys.serialize_entry(
|
||||
key.as_str(),
|
||||
&JsonEventSerializer {
|
||||
inner: value,
|
||||
with_spans: self.with_spans,
|
||||
with_description: self.with_description,
|
||||
with_id: self.with_id,
|
||||
},
|
||||
)?;
|
||||
}
|
||||
}
|
||||
keys.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for JsonEventSerializer<&Error> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut map = serializer.serialize_map(None)?;
|
||||
map.serialize_entry("type", self.inner.0.inner.as_str())?;
|
||||
if self.with_description {
|
||||
map.serialize_entry("text", self.inner.0.inner.description())?;
|
||||
}
|
||||
map.serialize_entry(
|
||||
"data",
|
||||
&JsonEventSerializer {
|
||||
inner: Keys {
|
||||
keys: self.inner.0.keys.as_slice(),
|
||||
span_keys: &[],
|
||||
},
|
||||
with_spans: self.with_spans,
|
||||
with_description: self.with_description,
|
||||
with_id: self.with_id,
|
||||
},
|
||||
)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for JsonEventSerializer<&Value> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match &self.inner {
|
||||
Value::String(value) => value.serialize(serializer),
|
||||
Value::UInt(value) => value.serialize(serializer),
|
||||
Value::Int(value) => value.serialize(serializer),
|
||||
Value::Float(value) => value.serialize(serializer),
|
||||
Value::Timestamp(value) => DateTime::from_timestamp(*value as i64)
|
||||
.to_rfc3339()
|
||||
.serialize(serializer),
|
||||
Value::Duration(value) => value.serialize(serializer),
|
||||
Value::Bytes(value) => STANDARD.encode(value).serialize(serializer),
|
||||
Value::Bool(value) => value.serialize(serializer),
|
||||
Value::Ipv4(value) => value.serialize(serializer),
|
||||
Value::Ipv6(value) => value.serialize(serializer),
|
||||
Value::Event(value) => JsonEventSerializer {
|
||||
inner: value,
|
||||
with_spans: self.with_spans,
|
||||
with_description: self.with_description,
|
||||
with_id: self.with_id,
|
||||
}
|
||||
.serialize(serializer),
|
||||
Value::Array(value) => JsonEventSerializer {
|
||||
inner: value,
|
||||
with_spans: self.with_spans,
|
||||
with_description: self.with_description,
|
||||
with_id: self.with_id,
|
||||
}
|
||||
.serialize(serializer),
|
||||
Value::None => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Serialize for JsonEventSerializer<&Vec<Value>> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let mut seq = serializer.serialize_seq(Some(self.inner.len()))?;
|
||||
for value in self.inner {
|
||||
seq.serialize_element(&JsonEventSerializer {
|
||||
inner: value,
|
||||
with_spans: self.with_spans,
|
||||
with_description: self.with_description,
|
||||
with_id: self.with_id,
|
||||
})?;
|
||||
}
|
||||
seq.end()
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for EventType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for EventType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = <&str>::deserialize(deserializer)?;
|
||||
Self::parse(s).ok_or_else(|| serde::de::Error::unknown_variant(s, &[]))
|
||||
}
|
||||
}
|
||||
|
||||
impl serde::Serialize for MetricType {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
serializer.serialize_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> serde::Deserialize<'de> for MetricType {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = <&str>::deserialize(deserializer)?;
|
||||
Self::parse(s).ok_or_else(|| serde::de::Error::unknown_variant(s, &[]))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
pub mod json;
|
||||
pub mod text;
|
||||
|
||||
@@ -0,0 +1,374 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use mail_parser::DateTime;
|
||||
use tokio::io::{AsyncWrite, AsyncWriteExt};
|
||||
|
||||
use crate::{Error, Event, EventDetails, Key, Level, Value};
|
||||
use base64::{Engine, engine::general_purpose::STANDARD};
|
||||
|
||||
pub struct FmtWriter<T: AsyncWrite + Unpin> {
|
||||
writer: T,
|
||||
ansi: bool,
|
||||
multiline: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
enum Color {
|
||||
Black,
|
||||
Red,
|
||||
Green,
|
||||
Yellow,
|
||||
Blue,
|
||||
Magenta,
|
||||
Cyan,
|
||||
White,
|
||||
}
|
||||
|
||||
impl<T: AsyncWrite + Unpin> FmtWriter<T> {
|
||||
pub fn new(writer: T) -> Self {
|
||||
Self {
|
||||
writer,
|
||||
ansi: false,
|
||||
multiline: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_ansi(self, ansi: bool) -> Self {
|
||||
Self { ansi, ..self }
|
||||
}
|
||||
|
||||
pub fn with_multiline(self, multiline: bool) -> Self {
|
||||
Self { multiline, ..self }
|
||||
}
|
||||
|
||||
pub async fn write(&mut self, event: &Event<EventDetails>) -> std::io::Result<()> {
|
||||
// Write timestamp
|
||||
if self.ansi {
|
||||
self.writer
|
||||
.write_all(Color::White.as_code().as_bytes())
|
||||
.await?;
|
||||
}
|
||||
self.writer
|
||||
.write_all(
|
||||
DateTime::from_timestamp(event.inner.timestamp as i64)
|
||||
.to_rfc3339()
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
if self.ansi {
|
||||
self.writer.write_all(Color::reset().as_bytes()).await?;
|
||||
}
|
||||
self.writer.write_all(" ".as_bytes()).await?;
|
||||
|
||||
// Write level
|
||||
if self.ansi {
|
||||
self.writer
|
||||
.write_all(
|
||||
match event.inner.level {
|
||||
Level::Error => Color::Red,
|
||||
Level::Warn => Color::Yellow,
|
||||
Level::Info => Color::Green,
|
||||
Level::Debug => Color::Blue,
|
||||
Level::Trace => Color::Magenta,
|
||||
Level::Disable => return Ok(()),
|
||||
}
|
||||
.as_code_bold()
|
||||
.as_bytes(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
self.writer
|
||||
.write_all(event.inner.level.as_str().as_bytes())
|
||||
.await?;
|
||||
if self.ansi {
|
||||
self.writer.write_all(Color::reset().as_bytes()).await?;
|
||||
}
|
||||
self.writer.write_all(" ".as_bytes()).await?;
|
||||
|
||||
// Write message
|
||||
if self.ansi {
|
||||
self.writer
|
||||
.write_all(Color::White.as_code_bold().as_bytes())
|
||||
.await?;
|
||||
}
|
||||
self.writer
|
||||
.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(event.inner.typ.as_str().as_bytes())
|
||||
.await?;
|
||||
|
||||
self.writer
|
||||
.write_all(if self.multiline { ")\n" } else { ") " }.as_bytes())
|
||||
.await?;
|
||||
|
||||
// Write keys
|
||||
if let Some(parent_event) = &event.inner.span {
|
||||
self.write_keys(&parent_event.keys, &event.keys, 1).await?;
|
||||
} else {
|
||||
self.write_keys(&[], &event.keys, 1).await?;
|
||||
}
|
||||
|
||||
if !self.multiline {
|
||||
self.writer.write_all("\n".as_bytes()).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn write_keys(
|
||||
&mut self,
|
||||
span_keys: &[(Key, Value)],
|
||||
keys: &[(Key, Value)],
|
||||
indent: usize,
|
||||
) -> std::io::Result<()> {
|
||||
Box::pin(async move {
|
||||
let mut is_first = true;
|
||||
for (key, value) in span_keys.iter().chain(keys.iter()) {
|
||||
if matches!(key, Key::SpanId) {
|
||||
continue;
|
||||
} else if is_first {
|
||||
is_first = false;
|
||||
} else if !self.multiline {
|
||||
self.writer.write_all(", ".as_bytes()).await?;
|
||||
}
|
||||
|
||||
// Write key
|
||||
if self.multiline {
|
||||
for _ in 0..indent {
|
||||
self.writer.write_all("\t".as_bytes()).await?;
|
||||
}
|
||||
}
|
||||
if self.ansi {
|
||||
self.writer
|
||||
.write_all(Color::Cyan.as_code().as_bytes())
|
||||
.await?;
|
||||
}
|
||||
self.writer.write_all(key.as_str().as_bytes()).await?;
|
||||
if self.ansi {
|
||||
self.writer.write_all(Color::reset().as_bytes()).await?;
|
||||
}
|
||||
|
||||
// Write value
|
||||
self.writer.write_all(" = ".as_bytes()).await?;
|
||||
self.write_value(value, indent).await?;
|
||||
|
||||
if self.multiline && !matches!(value, Value::Event(_)) {
|
||||
self.writer.write_all("\n".as_bytes()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
async fn write_value(&mut self, value: &Value, indent: usize) -> std::io::Result<()> {
|
||||
Box::pin(async move {
|
||||
match value {
|
||||
Value::String(v) => {
|
||||
self.writer.write_all("\"".as_bytes()).await?;
|
||||
for ch in v.as_bytes() {
|
||||
match ch {
|
||||
b'\r' => {
|
||||
self.writer.write_all("\\r".as_bytes()).await?;
|
||||
}
|
||||
b'\n' => {
|
||||
self.writer.write_all("\\n".as_bytes()).await?;
|
||||
}
|
||||
b'\t' => {
|
||||
self.writer.write_all("\\t".as_bytes()).await?;
|
||||
}
|
||||
b'\\' => {
|
||||
self.writer.write_all("\\\\".as_bytes()).await?;
|
||||
}
|
||||
_ => {
|
||||
self.writer.write_all(&[*ch]).await?;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.writer.write_all("\"".as_bytes()).await?;
|
||||
}
|
||||
Value::UInt(v) => {
|
||||
self.writer.write_all(v.to_string().as_bytes()).await?;
|
||||
}
|
||||
Value::Int(v) => {
|
||||
self.writer.write_all(v.to_string().as_bytes()).await?;
|
||||
}
|
||||
Value::Float(v) => {
|
||||
self.writer.write_all(v.to_string().as_bytes()).await?;
|
||||
}
|
||||
Value::Timestamp(v) => {
|
||||
self.writer
|
||||
.write_all(DateTime::from_timestamp(*v as i64).to_rfc3339().as_bytes())
|
||||
.await?;
|
||||
}
|
||||
Value::Duration(v) => {
|
||||
self.writer.write_all(v.to_string().as_bytes()).await?;
|
||||
self.writer.write_all("ms".as_bytes()).await?;
|
||||
}
|
||||
Value::Bytes(bytes) => {
|
||||
self.writer.write_all("base64:".as_bytes()).await?;
|
||||
self.writer
|
||||
.write_all(STANDARD.encode(bytes).as_bytes())
|
||||
.await?;
|
||||
}
|
||||
Value::Bool(true) => {
|
||||
self.writer.write_all("true".as_bytes()).await?;
|
||||
}
|
||||
Value::Bool(false) => {
|
||||
self.writer.write_all("false".as_bytes()).await?;
|
||||
}
|
||||
Value::Ipv4(v) => {
|
||||
self.writer.write_all(v.to_string().as_bytes()).await?;
|
||||
}
|
||||
Value::Ipv6(v) => {
|
||||
self.writer.write_all(v.to_string().as_bytes()).await?;
|
||||
}
|
||||
Value::Event(e) => {
|
||||
self.writer
|
||||
.write_all(e.0.inner.description().as_bytes())
|
||||
.await?;
|
||||
self.writer.write_all(" (".as_bytes()).await?;
|
||||
self.writer.write_all(e.0.inner.as_str().as_bytes()).await?;
|
||||
self.writer.write_all(")".as_bytes()).await?;
|
||||
if !e.0.keys.is_empty() {
|
||||
self.writer
|
||||
.write_all(if self.multiline { "\n" } else { " { " }.as_bytes())
|
||||
.await?;
|
||||
|
||||
self.write_keys(&e.0.keys, &[], indent + 1).await?;
|
||||
|
||||
if !self.multiline {
|
||||
self.writer.write_all(" }".as_bytes()).await?;
|
||||
}
|
||||
} else if self.multiline {
|
||||
self.writer.write_all("\n".as_bytes()).await?;
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
self.writer.write_all("[".as_bytes()).await?;
|
||||
for (pos, value) in arr.iter().enumerate() {
|
||||
if pos > 0 {
|
||||
self.writer.write_all(", ".as_bytes()).await?;
|
||||
}
|
||||
self.write_value(value, indent).await?;
|
||||
}
|
||||
self.writer.write_all("]".as_bytes()).await?;
|
||||
}
|
||||
Value::None => {
|
||||
self.writer.write_all("(null)".as_bytes()).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn flush(&mut self) -> std::io::Result<()> {
|
||||
self.writer.flush().await
|
||||
}
|
||||
|
||||
pub fn update_writer(&mut self, writer: T) {
|
||||
self.writer = writer;
|
||||
}
|
||||
}
|
||||
|
||||
impl Color {
|
||||
pub fn as_code(&self) -> &'static str {
|
||||
match self {
|
||||
Color::Black => "\x1b[30m",
|
||||
Color::Red => "\x1b[31m",
|
||||
Color::Green => "\x1b[32m",
|
||||
Color::Yellow => "\x1b[33m",
|
||||
Color::Blue => "\x1b[34m",
|
||||
Color::Magenta => "\x1b[35m",
|
||||
Color::Cyan => "\x1b[36m",
|
||||
Color::White => "\x1b[37m",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_code_bold(&self) -> &'static str {
|
||||
match self {
|
||||
Color::Black => "\x1b[30;1m",
|
||||
Color::Red => "\x1b[31;1m",
|
||||
Color::Green => "\x1b[32;1m",
|
||||
Color::Yellow => "\x1b[33;1m",
|
||||
Color::Blue => "\x1b[34;1m",
|
||||
Color::Magenta => "\x1b[35;1m",
|
||||
Color::Cyan => "\x1b[36;1m",
|
||||
Color::White => "\x1b[37;1m",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset() -> &'static str {
|
||||
"\x1b[0m"
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Value {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Value::String(value) => value.fmt(f),
|
||||
Value::UInt(value) => value.fmt(f),
|
||||
Value::Int(value) => value.fmt(f),
|
||||
Value::Float(value) => value.fmt(f),
|
||||
Value::Timestamp(value) => value.fmt(f),
|
||||
Value::Duration(value) => value.fmt(f),
|
||||
Value::Bytes(value) => STANDARD.encode(value).fmt(f),
|
||||
Value::Bool(value) => value.fmt(f),
|
||||
Value::Ipv4(value) => value.fmt(f),
|
||||
Value::Ipv6(value) => value.fmt(f),
|
||||
Value::Event(value) => {
|
||||
"{".fmt(f)?;
|
||||
value.fmt(f)?;
|
||||
"}".fmt(f)
|
||||
}
|
||||
Value::Array(value) => {
|
||||
f.write_str("[")?;
|
||||
for (i, value) in value.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
value.fmt(f)?;
|
||||
}
|
||||
f.write_str("]")
|
||||
}
|
||||
Value::None => "(null)".fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Error {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.inner.description().fmt(f)?;
|
||||
" (".fmt(f)?;
|
||||
self.0.inner.as_str().fmt(f)?;
|
||||
")".fmt(f)?;
|
||||
|
||||
if !self.0.keys.is_empty() {
|
||||
f.write_str(": ")?;
|
||||
for (i, (key, value)) in self.0.keys.iter().enumerate() {
|
||||
if i > 0 {
|
||||
f.write_str(", ")?;
|
||||
}
|
||||
key.as_str().fmt(f)?;
|
||||
f.write_str(" = ")?;
|
||||
value.fmt(f)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user