1use appcore_core::redact_text_with_limit;
14use parking_lot::{Mutex, RwLock};
15use serde::{Deserialize, Serialize};
16use std::collections::{BTreeMap, VecDeque};
17use std::sync::Arc;
18
19pub const MAX_OBSERVATION_ATTRIBUTES: usize = 32;
21pub const MAX_OBSERVATION_NAME_BYTES: usize = 128;
23pub const MAX_OBSERVATION_KEY_BYTES: usize = 64;
25pub const MAX_OBSERVATION_VALUE_BYTES: usize = 1_024;
27pub const MAX_OBSERVATION_TRACE_BYTES: usize = 256;
29pub const MAX_IN_MEMORY_OBSERVATION_ITEMS: usize = 65_536;
31pub const MAX_IN_MEMORY_OBSERVATION_BYTES: usize = 16 * 1024 * 1024;
33pub const MAX_OBSERVATION_DRAINS: usize = 32;
35const OBSERVATION_FIXED_BYTES: usize = std::mem::size_of::<ObservationEvent>()
36 + std::mem::size_of::<Arc<ObservationEvent>>()
37 + std::mem::size_of::<usize>() * 2;
38const ATTRIBUTE_FIXED_BYTES: usize =
39 std::mem::size_of::<(String, String)>() + std::mem::size_of::<usize>() * 4;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44pub enum ObservationKind {
45 Lifecycle,
47 Configuration,
49 Health,
51 Security,
53 Storage,
55 ControlPlane,
57 PeerRpc,
59 Scheduler,
61 Sync,
63 Audit,
65 Diagnostic,
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub enum ObservationSeverity {
73 Debug,
75 Info,
77 Warning,
79 Error,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
85pub struct ObservationEvent {
86 pub kind: ObservationKind,
88 pub severity: ObservationSeverity,
90 pub name: String,
92 pub timestamp_ms: u64,
94 pub trace_id: Option<String>,
96 pub attributes: BTreeMap<String, String>,
98}
99
100impl ObservationEvent {
101 pub fn new(
103 kind: ObservationKind,
104 severity: ObservationSeverity,
105 name: impl Into<String>,
106 timestamp_ms: u64,
107 ) -> Self {
108 Self {
109 kind,
110 severity,
111 name: name.into(),
112 timestamp_ms,
113 trace_id: None,
114 attributes: BTreeMap::new(),
115 }
116 }
117
118 pub fn with_trace_id(mut self, trace_id: impl Into<String>) -> Self {
120 self.trace_id = Some(trace_id.into());
121 self
122 }
123
124 pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
126 let key = key.into();
127 let sensitive = is_sensitive_key(&key);
128 let key = redact_text_with_limit(&key, MAX_OBSERVATION_KEY_BYTES);
129 if self.attributes.len() >= MAX_OBSERVATION_ATTRIBUTES
130 && !self.attributes.contains_key(&key)
131 {
132 return self;
133 }
134 let value = if sensitive {
135 "[REDACTED]".to_string()
136 } else {
137 redact_text_with_limit(&value.into(), MAX_OBSERVATION_VALUE_BYTES)
138 };
139 self.attributes.insert(key, value);
140 self
141 }
142
143 pub(crate) fn redacted(mut self) -> Self {
144 self.name = redact_text_with_limit(&self.name, MAX_OBSERVATION_NAME_BYTES);
145 self.name.shrink_to_fit();
146 self.trace_id = self.trace_id.map(|value| {
147 let mut value = redact_text_with_limit(&value, MAX_OBSERVATION_TRACE_BYTES);
148 value.shrink_to_fit();
149 value
150 });
151 let mut attributes = BTreeMap::new();
152 for (key, value) in std::mem::take(&mut self.attributes)
153 .into_iter()
154 .take(MAX_OBSERVATION_ATTRIBUTES)
155 {
156 let sensitive = is_sensitive_key(&key);
157 let mut key = redact_text_with_limit(&key, MAX_OBSERVATION_KEY_BYTES);
158 let mut value = if sensitive {
159 "[REDACTED]".to_string()
160 } else {
161 redact_text_with_limit(&value, MAX_OBSERVATION_VALUE_BYTES)
162 };
163 key.shrink_to_fit();
164 value.shrink_to_fit();
165 attributes.insert(key, value);
166 }
167 self.attributes = attributes;
168 self
169 }
170}
171
172#[derive(Debug, Clone)]
177pub struct SharedObservationEvent {
178 event: Arc<ObservationEvent>,
179}
180
181impl SharedObservationEvent {
182 pub fn new(event: ObservationEvent) -> Self {
184 Self {
185 event: Arc::new(event.redacted()),
186 }
187 }
188
189 pub fn as_event(&self) -> &ObservationEvent {
191 &self.event
192 }
193
194 fn clone_event_arc(&self) -> Arc<ObservationEvent> {
195 Arc::clone(&self.event)
196 }
197}
198
199pub trait ObservationSink: Send + Sync {
201 fn emit(&self, event: ObservationEvent);
203
204 fn emit_shared(&self, event: &SharedObservationEvent) {
210 self.emit(event.as_event().clone());
211 }
212}
213
214#[derive(Debug, Clone, Default)]
216pub struct ObservationSnapshot {
217 events: Arc<VecDeque<Arc<ObservationEvent>>>,
218}
219
220impl ObservationSnapshot {
221 pub fn len(&self) -> usize {
223 self.events.len()
224 }
225
226 pub fn is_empty(&self) -> bool {
228 self.events.is_empty()
229 }
230
231 pub fn iter(&self) -> impl DoubleEndedIterator<Item = &ObservationEvent> {
233 self.events.iter().map(AsRef::as_ref)
234 }
235
236 pub fn recent(&self, limit: usize) -> impl Iterator<Item = &ObservationEvent> {
238 self.events
239 .iter()
240 .skip(self.events.len().saturating_sub(limit))
241 .map(AsRef::as_ref)
242 }
243}
244
245#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
247pub struct InMemoryObservationPressure {
248 pub entries: usize,
250 pub max_entries: usize,
252 pub used_bytes: usize,
254 pub peak_bytes: usize,
256 pub max_bytes: usize,
258 pub evictions: u64,
260 pub oversized_rejections: u64,
262 pub drain_rejections: u64,
264}
265
266#[derive(Debug)]
267struct ObservationState {
268 events: Arc<VecDeque<Arc<ObservationEvent>>>,
269 pressure: InMemoryObservationPressure,
270}
271
272type ObservationDrain = Arc<dyn ObservationSink>;
273type ObservationDrainGeneration = Arc<Vec<ObservationDrain>>;
274
275#[derive(Clone)]
280pub struct InMemoryObservationSink {
281 capacity: usize,
282 state: Arc<Mutex<ObservationState>>,
283 drains: Arc<RwLock<ObservationDrainGeneration>>,
284}
285
286impl std::fmt::Debug for InMemoryObservationSink {
287 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 formatter
289 .debug_struct("InMemoryObservationSink")
290 .field("capacity", &self.capacity)
291 .field("event_count", &self.len())
292 .field("pressure", &self.pressure())
293 .field("drain_count", &self.drains.read().len())
294 .finish()
295 }
296}
297
298impl InMemoryObservationSink {
299 pub fn new(capacity: usize) -> Self {
301 let capacity = capacity.clamp(1, MAX_IN_MEMORY_OBSERVATION_ITEMS);
302 let max_bytes = default_max_bytes(capacity);
303 Self::with_limits(capacity, max_bytes)
304 }
305
306 pub fn with_max_bytes(capacity: usize, max_bytes: usize) -> Self {
312 let capacity = capacity.clamp(1, MAX_IN_MEMORY_OBSERVATION_ITEMS);
313 Self::with_limits(capacity, max_bytes.clamp(1, default_max_bytes(capacity)))
314 }
315
316 fn with_limits(capacity: usize, max_bytes: usize) -> Self {
317 Self {
318 capacity,
319 state: Arc::new(Mutex::new(ObservationState {
320 events: Arc::new(VecDeque::new()),
321 pressure: InMemoryObservationPressure {
322 max_entries: capacity,
323 max_bytes,
324 ..InMemoryObservationPressure::default()
325 },
326 })),
327 drains: Arc::new(RwLock::new(Arc::new(Vec::new()))),
328 }
329 }
330
331 pub fn add_drain(&self, drain: Arc<dyn ObservationSink>) {
333 let _ = self.try_add_drain(drain);
334 }
335
336 pub fn try_add_drain(&self, drain: Arc<dyn ObservationSink>) -> bool {
338 let accepted = {
339 let mut drains = self.drains.write();
340 if drains.len() >= MAX_OBSERVATION_DRAINS {
341 false
342 } else {
343 Arc::make_mut(&mut drains).push(drain);
344 true
345 }
346 };
347 if !accepted {
348 let mut state = self.state.lock();
349 state.pressure.drain_rejections = state.pressure.drain_rejections.saturating_add(1);
350 }
351 accepted
352 }
353
354 pub fn drain_count(&self) -> usize {
356 self.drains.read().len()
357 }
358
359 pub fn snapshot(&self) -> Vec<ObservationEvent> {
361 self.shared_snapshot().iter().cloned().collect()
362 }
363
364 pub fn shared_snapshot(&self) -> ObservationSnapshot {
366 ObservationSnapshot {
367 events: Arc::clone(&self.state.lock().events),
368 }
369 }
370
371 pub fn pressure(&self) -> InMemoryObservationPressure {
373 self.state.lock().pressure
374 }
375
376 pub fn len(&self) -> usize {
378 self.state.lock().pressure.entries
379 }
380
381 pub fn is_empty(&self) -> bool {
383 self.len() == 0
384 }
385}
386
387impl Default for InMemoryObservationSink {
388 fn default() -> Self {
389 Self::new(1_024)
390 }
391}
392
393impl ObservationSink for InMemoryObservationSink {
394 fn emit(&self, event: ObservationEvent) {
395 self.emit_shared(&SharedObservationEvent::new(event));
396 }
397
398 fn emit_shared(&self, event: &SharedObservationEvent) {
399 let retained_bytes = observation_retained_bytes(event.as_event());
400 let mut state = self.state.lock();
401 if retained_bytes > state.pressure.max_bytes {
402 state.pressure.oversized_rejections =
403 state.pressure.oversized_rejections.saturating_add(1);
404 } else {
405 while state.pressure.entries >= self.capacity
406 || state.pressure.used_bytes.saturating_add(retained_bytes)
407 > state.pressure.max_bytes
408 {
409 let Some(removed) = Arc::make_mut(&mut state.events).pop_front() else {
410 break;
411 };
412 state.pressure.entries = state.pressure.entries.saturating_sub(1);
413 state.pressure.used_bytes = state
414 .pressure
415 .used_bytes
416 .saturating_sub(observation_retained_bytes(&removed));
417 state.pressure.evictions = state.pressure.evictions.saturating_add(1);
418 }
419 Arc::make_mut(&mut state.events).push_back(event.clone_event_arc());
420 state.pressure.entries = state.pressure.entries.saturating_add(1);
421 state.pressure.used_bytes = state.pressure.used_bytes.saturating_add(retained_bytes);
422 state.pressure.peak_bytes = state.pressure.peak_bytes.max(state.pressure.used_bytes);
423 }
424 drop(state);
425 let drains = Arc::clone(&self.drains.read());
426 for drain in drains.iter() {
427 drain.emit_shared(event);
428 }
429 }
430}
431
432fn default_max_bytes(capacity: usize) -> usize {
433 capacity
434 .saturating_mul(maximum_observation_retained_bytes())
435 .clamp(1, MAX_IN_MEMORY_OBSERVATION_BYTES)
436}
437
438fn maximum_observation_retained_bytes() -> usize {
439 OBSERVATION_FIXED_BYTES
440 .saturating_add(MAX_OBSERVATION_NAME_BYTES)
441 .saturating_add(MAX_OBSERVATION_TRACE_BYTES)
442 .saturating_add(
443 MAX_OBSERVATION_ATTRIBUTES.saturating_mul(
444 ATTRIBUTE_FIXED_BYTES
445 .saturating_add(MAX_OBSERVATION_KEY_BYTES)
446 .saturating_add(MAX_OBSERVATION_VALUE_BYTES),
447 ),
448 )
449}
450
451fn observation_retained_bytes(event: &ObservationEvent) -> usize {
452 OBSERVATION_FIXED_BYTES
453 .saturating_add(event.name.capacity())
454 .saturating_add(event.trace_id.as_ref().map_or(0, String::capacity))
455 .saturating_add(event.attributes.iter().fold(0usize, |total, (key, value)| {
456 total
457 .saturating_add(ATTRIBUTE_FIXED_BYTES)
458 .saturating_add(key.capacity())
459 .saturating_add(value.capacity())
460 }))
461}
462
463fn is_sensitive_key(key: &str) -> bool {
464 ["secret", "password", "token", "credential", "private_key"]
465 .iter()
466 .any(|fragment| {
467 key.as_bytes()
468 .windows(fragment.len())
469 .any(|candidate| candidate.eq_ignore_ascii_case(fragment.as_bytes()))
470 })
471}
472
473#[cfg(test)]
474#[path = "observation_tests.rs"]
475mod tests;