Skip to main content

appcore_log/
event.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: event.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: unknown by dnettoRaw
7//    ##   ## ##   ##    U: working-tree by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Event values deliberately kept independent from sinks and process hosting.
12
13use crate::Sensitivity;
14use serde::{Deserialize, Serialize};
15
16/// Maximum UTF-8 bytes retained in one message or identity-like field.
17pub const MAX_LOG_TEXT_BYTES: usize = 4 * 1024;
18/// Maximum structured fields attached to one event.
19pub const MAX_LOG_FIELDS: usize = 32;
20/// Maximum UTF-8 bytes retained in one structured field key.
21pub const MAX_LOG_FIELD_KEY_BYTES: usize = 128;
22/// Maximum UTF-8 bytes retained in one structured field value.
23pub const MAX_LOG_FIELD_VALUE_BYTES: usize = 4 * 1024;
24
25/// Event rejected before sanitization or sink delivery.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum LogEventError {
28    /// A text field exceeds its public byte ceiling.
29    TextTooLong,
30    /// The field count exceeds the public ceiling.
31    TooManyFields,
32    /// A structured field key exceeds its public byte ceiling.
33    FieldKeyTooLong,
34    /// A structured field value exceeds its public byte ceiling.
35    FieldValueTooLong,
36}
37
38/// Operational impact of an event.
39#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
40pub enum Severity {
41    /// Fine-grained diagnostic event.
42    Trace,
43    /// Developer-oriented diagnostic event.
44    Debug,
45    /// Normal operational event.
46    Info,
47    /// Recoverable or degraded condition.
48    Warn,
49    /// Failed operation.
50    Error,
51    /// Failure requiring urgent operator attention.
52    Critical,
53}
54
55/// Detail threshold from V1 (critical) through V9 (deep diagnostics).
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
57pub struct Verbosity(u8);
58
59/// Invalid public verbosity outside the V1–V9 contract.
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
61pub struct VerbosityError;
62
63impl Verbosity {
64    /// Critical failures only.
65    pub const V1: Self = Self(1);
66    /// Important recoverable errors.
67    pub const V2: Self = Self(2);
68    /// Relevant warnings.
69    pub const V3: Self = Self(3);
70    /// Essential application events.
71    pub const V4: Self = Self(4);
72    /// Normal operation.
73    pub const V5: Self = Self(5);
74    /// Flow details.
75    pub const V6: Self = Self(6);
76    /// Technical debugging.
77    pub const V7: Self = Self(7);
78    /// I/O, queue and timing diagnostics.
79    pub const V8: Self = Self(8);
80    /// Deep memory and internal diagnostics.
81    pub const V9: Self = Self(9);
82
83    /// Validates one public verbosity value.
84    pub const fn new(value: u8) -> Option<Self> {
85        if value >= 1 && value <= 9 {
86            Some(Self(value))
87        } else {
88            None
89        }
90    }
91    /// Returns the numeric level.
92    pub const fn value(self) -> u8 {
93        self.0
94    }
95}
96
97/// One structured field; paths must be supplied as fields instead of embedded text.
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99pub struct LogField {
100    /// Stable field name.
101    pub key: String,
102    /// Field value.
103    pub value: String,
104    /// Whether the value is a local path.
105    pub path: bool,
106    /// Whether ordinary logs must redact the value.
107    pub sensitive: bool,
108}
109
110/// Structured operational event passed through the sanitizer before normal sinks.
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct LogEvent {
113    /// Unix timestamp in milliseconds supplied by the caller or clock boundary.
114    pub timestamp_ms: u64,
115    /// Operational impact.
116    pub severity: Severity,
117    /// Detail threshold independent from severity.
118    pub verbosity: Verbosity,
119    /// Human-readable message.
120    pub message: String,
121    /// Stable subsystem name used for policy overrides.
122    pub component: String,
123    /// Optional source module when the caller has a stable module name.
124    pub module: Option<String>,
125    /// Optional named operation being observed.
126    pub operation: Option<String>,
127    /// Requested handling class; a policy can only preserve or reduce detail.
128    pub sensitivity: Sensitivity,
129    /// Optional application identity.
130    pub application_id: Option<String>,
131    /// Optional node identity.
132    pub node_id: Option<String>,
133    /// Optional tenant identity.
134    pub tenant_id: Option<String>,
135    /// Optional trace correlation identifier.
136    pub trace_id: Option<String>,
137    /// Optional request correlation identifier.
138    pub request_id: Option<String>,
139    /// Extra bounded structured values.
140    pub fields: Vec<LogField>,
141}
142
143impl LogEvent {
144    /// Creates a normal operational event without allocating optional metadata.
145    pub fn new(
146        timestamp_ms: u64,
147        severity: Severity,
148        verbosity: Verbosity,
149        component: impl Into<String>,
150        message: impl Into<String>,
151    ) -> Self {
152        Self {
153            timestamp_ms,
154            severity,
155            verbosity,
156            message: message.into(),
157            component: component.into(),
158            module: None,
159            operation: None,
160            sensitivity: Sensitivity::Safe,
161            application_id: None,
162            node_id: None,
163            tenant_id: None,
164            trace_id: None,
165            request_id: None,
166            fields: Vec::new(),
167        }
168    }
169
170    /// Adds one structured field.
171    #[must_use]
172    pub fn field(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
173        self.fields.push(LogField {
174            key: key.into(),
175            value: value.into(),
176            path: false,
177            sensitive: false,
178        });
179        self
180    }
181
182    /// Adds one path field which is alias-sanitized by normal policies.
183    #[must_use]
184    pub fn path(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
185        self.fields.push(LogField {
186            key: key.into(),
187            value: value.into(),
188            path: true,
189            sensitive: false,
190        });
191        self
192    }
193
194    /// Adds one secret field which ordinary policies redact before dispatch.
195    #[must_use]
196    pub fn secret(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
197        self.fields.push(LogField {
198            key: key.into(),
199            value: value.into(),
200            path: false,
201            sensitive: true,
202        });
203        self
204    }
205
206    /// Adds the stable source module without inferring it from compiler state.
207    #[must_use]
208    pub fn module(mut self, module: impl Into<String>) -> Self {
209        self.module = Some(module.into());
210        self
211    }
212
213    /// Adds a bounded caller-defined operation name.
214    #[must_use]
215    pub fn operation(mut self, operation: impl Into<String>) -> Self {
216        self.operation = Some(operation.into());
217        self
218    }
219
220    /// Requests a handling class; sensitive delivery still requires a policy.
221    #[must_use]
222    pub fn sensitivity(mut self, sensitivity: Sensitivity) -> Self {
223        self.sensitivity = sensitivity;
224        self
225    }
226
227    /// Validates bounded event storage before sanitization and sink I/O.
228    pub fn validate(&self) -> Result<(), LogEventError> {
229        for value in [
230            &self.message,
231            &self.component,
232            self.module.as_deref().unwrap_or_default(),
233            self.operation.as_deref().unwrap_or_default(),
234            self.application_id.as_deref().unwrap_or_default(),
235            self.node_id.as_deref().unwrap_or_default(),
236            self.tenant_id.as_deref().unwrap_or_default(),
237            self.trace_id.as_deref().unwrap_or_default(),
238            self.request_id.as_deref().unwrap_or_default(),
239        ] {
240            if value.len() > MAX_LOG_TEXT_BYTES {
241                return Err(LogEventError::TextTooLong);
242            }
243        }
244        if self.fields.len() > MAX_LOG_FIELDS {
245            return Err(LogEventError::TooManyFields);
246        }
247        for field in &self.fields {
248            if field.key.len() > MAX_LOG_FIELD_KEY_BYTES {
249                return Err(LogEventError::FieldKeyTooLong);
250            }
251            if field.value.len() > MAX_LOG_FIELD_VALUE_BYTES {
252                return Err(LogEventError::FieldValueTooLong);
253            }
254        }
255        Ok(())
256    }
257
258    /// Estimates retained heap bytes without serializing or allocating.
259    pub fn retained_bytes(&self) -> usize {
260        let optional = [
261            &self.module,
262            &self.operation,
263            &self.application_id,
264            &self.node_id,
265            &self.tenant_id,
266            &self.trace_id,
267            &self.request_id,
268        ]
269        .into_iter()
270        .flatten()
271        .map(String::capacity)
272        .sum::<usize>();
273        let fields = self
274            .fields
275            .iter()
276            .map(|field| field.key.capacity().saturating_add(field.value.capacity()))
277            .sum::<usize>();
278        std::mem::size_of::<Self>()
279            .saturating_add(self.message.capacity())
280            .saturating_add(self.component.capacity())
281            .saturating_add(
282                self.fields
283                    .capacity()
284                    .saturating_mul(std::mem::size_of::<LogField>()),
285            )
286            .saturating_add(optional)
287            .saturating_add(fields)
288    }
289}