lambda_otel_lite/events.rs
1//! Structured event recording for OpenTelemetry spans in AWS Lambda functions.
2//!
3//! This module provides functionality for recording structured events within OpenTelemetry spans.
4//! Events are queryable data points that provide additional context and business logic markers
5//! within the execution of Lambda functions.
6//!
7//! # Key Features
8//!
9//! - **Dual API**: Both function-based and builder-based interfaces
10//! - **Level-based filtering**: Events can be filtered by severity level
11//! - **Structured attributes**: Attach custom key-value pairs to events
12//! - **OpenTelemetry compliance**: Uses standard OpenTelemetry event semantics
13//! - **Lambda-optimized**: Designed for AWS Lambda execution patterns
14//! - **Performance-conscious**: Early filtering to minimize overhead
15//!
16//! # Use Cases
17//!
18//! Events are particularly useful for:
19//! - **Business logic markers**: Track significant application state changes
20//! - **Audit trails**: Record user actions and system decisions
21//! - **Debugging context**: Add structured data for troubleshooting
22//! - **Performance insights**: Mark important execution milestones
23//! - **Compliance logging**: Record security and regulatory events
24//!
25//! # API Styles
26//!
27//! ## Function-based API (Direct)
28//!
29//! ```rust
30//! use lambda_otel_lite::events::{record_event, EventLevel};
31//! use opentelemetry::KeyValue;
32//!
33//! record_event(
34//! EventLevel::Info,
35//! "User logged in",
36//! vec![
37//! KeyValue::new("user_id", "123"),
38//! KeyValue::new("method", "oauth"),
39//! ],
40//! None, // timestamp
41//! );
42//! ```
43//!
44//! ## Builder-based API (Ergonomic)
45//!
46//! ```rust
47//! use lambda_otel_lite::events::{event, EventLevel};
48//!
49//! // Simple event
50//! event()
51//! .level(EventLevel::Info)
52//! .message("User logged in")
53//! .call();
54//!
55//! // Event with individual attributes
56//! event()
57//! .level(EventLevel::Info)
58//! .message("User logged in")
59//! .attribute("user_id", "123")
60//! .attribute("method", "oauth")
61//! .call();
62//! ```
63//!
64//! # Environment Configuration
65//!
66//! The event level can be controlled via the `AWS_LAMBDA_LOG_LEVEL` environment variable
67//! (with fallback to `LOG_LEVEL`), same as the internal logging system:
68//! - `TRACE`: All events (most verbose)
69//! - `DEBUG`: Debug, Info, Warn, Error events
70//! - `INFO`: Info, Warn, Error events (default)
71//! - `WARN`: Warn, Error events only
72//! - `ERROR`: Error events only
73
74use crate::constants::defaults;
75use bon::builder;
76use opentelemetry::KeyValue;
77use std::sync::OnceLock;
78use std::{env, time::SystemTime};
79use tracing_opentelemetry::OpenTelemetrySpanExt;
80
81/// Event severity levels for filtering and categorization.
82///
83/// These levels follow the same semantics as standard logging levels
84/// and are used both for filtering events and setting their severity in OpenTelemetry.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
86pub enum EventLevel {
87 /// Trace-level events (most verbose)
88 Trace = 1,
89 /// Debug-level events
90 Debug = 5,
91 /// Informational events (default)
92 Info = 9,
93 /// Warning events
94 Warn = 13,
95 /// Error events (least verbose)
96 Error = 17,
97}
98
99impl From<EventLevel> for tracing::Level {
100 fn from(level: EventLevel) -> Self {
101 match level {
102 EventLevel::Trace => tracing::Level::TRACE,
103 EventLevel::Debug => tracing::Level::DEBUG,
104 EventLevel::Info => tracing::Level::INFO,
105 EventLevel::Warn => tracing::Level::WARN,
106 EventLevel::Error => tracing::Level::ERROR,
107 }
108 }
109}
110
111impl From<EventLevel> for u8 {
112 fn from(level: EventLevel) -> Self {
113 level as u8
114 }
115}
116
117/// Convert event level to standard text representation
118fn level_text(level: EventLevel) -> &'static str {
119 match level {
120 EventLevel::Trace => "TRACE",
121 EventLevel::Debug => "DEBUG",
122 EventLevel::Info => "INFO",
123 EventLevel::Warn => "WARN",
124 EventLevel::Error => "ERROR",
125 }
126}
127
128/// Cached minimum event level for performance
129static MIN_LEVEL: OnceLock<EventLevel> = OnceLock::new();
130
131/// Get the minimum event level from environment configuration
132fn get_min_level() -> EventLevel {
133 *MIN_LEVEL.get_or_init(|| get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL"))
134}
135
136/// Helper function for testing that allows custom environment variable names
137fn get_min_level_with_env_vars(primary_var: &str, fallback_var: &str) -> EventLevel {
138 // Use the same environment variable logic as the logger module for consistency
139 let level = env::var(primary_var)
140 .or_else(|_| env::var(fallback_var))
141 .unwrap_or_else(|_| defaults::EVENT_LEVEL.to_string())
142 .to_uppercase();
143
144 match level.as_str() {
145 "ERROR" => EventLevel::Error,
146 "WARN" => EventLevel::Warn,
147 "INFO" => EventLevel::Info,
148 "DEBUG" => EventLevel::Debug,
149 "TRACE" => EventLevel::Trace,
150 _ => EventLevel::Info, // Default fallback
151 }
152}
153
154/// Record a structured event within the current OpenTelemetry span (function-based API).
155///
156/// This is the direct function interface for recording events. For a more ergonomic
157/// builder-based API, see the [`event()`] function.
158///
159/// # Arguments
160///
161/// * `level` - The severity level of the event
162/// * `message` - Human-readable description of the event
163/// * `attributes` - Additional structured attributes as key-value pairs
164/// * `timestamp` - Optional custom timestamp (uses current time if None)
165///
166/// # Examples
167///
168/// ```rust
169/// use lambda_otel_lite::events::{record_event, EventLevel};
170/// use opentelemetry::KeyValue;
171///
172/// // Simple event
173/// record_event(
174/// EventLevel::Info,
175/// "User logged in",
176/// vec![],
177/// None,
178/// );
179///
180/// // Event with attributes and custom timestamp
181/// record_event(
182/// EventLevel::Warn,
183/// "Rate limit approaching",
184/// vec![
185/// KeyValue::new("user_id", "123"),
186/// KeyValue::new("requests_remaining", 10),
187/// ],
188/// Some(std::time::SystemTime::now()),
189/// );
190/// ```
191pub fn record_event(
192 level: EventLevel,
193 message: impl AsRef<str>,
194 attributes: Vec<KeyValue>,
195 timestamp: Option<SystemTime>,
196) {
197 record_event_impl(level, message.as_ref(), attributes, timestamp);
198}
199
200/// Create an event builder for ergonomic event construction (builder-based API).
201///
202/// This returns a builder that allows you to configure the event through method chaining
203/// before calling `.call()` to record it. For a direct function interface, see [`record_event()`].
204///
205/// # Examples
206///
207/// ```rust
208/// use lambda_otel_lite::events::{event, EventLevel};
209///
210/// // Basic event
211/// event()
212/// .level(EventLevel::Info)
213/// .message("User action completed")
214/// .call();
215///
216/// // Event with individual attributes
217/// event()
218/// .level(EventLevel::Info)
219/// .message("User logged in")
220/// .attribute("user_id", "123")
221/// .attribute("count", 42)
222/// .attribute("is_admin", true)
223/// .call();
224/// ```
225#[builder]
226pub fn event(
227 #[builder(field)] attributes: Vec<KeyValue>,
228
229 #[builder(default = EventLevel::Info)] level: EventLevel,
230
231 #[builder(into, default = "")] message: String,
232
233 timestamp: Option<SystemTime>,
234) {
235 record_event_impl(level, &message, attributes, timestamp);
236}
237
238/// Internal implementation that both APIs call
239fn record_event_impl(
240 level: EventLevel,
241 message: &str,
242 attributes: Vec<KeyValue>,
243 timestamp: Option<SystemTime>,
244) {
245 // Early return if event level is below threshold
246 if level < get_min_level() {
247 return;
248 }
249
250 // Get the current span and check if it's valid
251 let span = tracing::Span::current();
252 if span.is_disabled() {
253 return;
254 }
255
256 // Create the event attributes with OpenTelemetry semantic conventions
257 let mut event_attributes = Vec::with_capacity(attributes.len() + 3);
258 event_attributes.extend_from_slice(&[
259 KeyValue::new("event.severity_text", level_text(level)),
260 KeyValue::new("event.severity_number", u8::from(level) as i64),
261 ]);
262
263 // Add the message as event.body if provided
264 if !message.is_empty() {
265 event_attributes.push(KeyValue::new("event.body", message.to_string()));
266 }
267
268 // Add custom attributes
269 event_attributes.extend(attributes);
270
271 // Add the event to the span
272 if let Some(ts) = timestamp {
273 span.add_event_with_timestamp("event", ts, event_attributes);
274 } else {
275 span.add_event("event", event_attributes);
276 }
277}
278
279/// Custom methods for the event builder to support individual attribute calls
280impl<S: event_builder::State> EventBuilder<S> {
281 /// Add a single attribute to the event.
282 ///
283 /// This method accepts any value that can be converted to an OpenTelemetry `Value`,
284 /// including strings, numbers, booleans, and other supported types.
285 ///
286 /// # Examples
287 ///
288 /// ```rust
289 /// use lambda_otel_lite::events::{event, EventLevel};
290 ///
291 /// event()
292 /// .level(EventLevel::Info)
293 /// .message("User action")
294 /// .attribute("user_id", "123") // String
295 /// .attribute("count", 42) // Integer
296 /// .attribute("score", 98.5) // Float
297 /// .attribute("is_admin", true) // Boolean
298 /// .call();
299 /// ```
300 pub fn attribute(
301 mut self,
302 key: impl Into<String>,
303 value: impl Into<opentelemetry::Value>,
304 ) -> Self {
305 self.attributes
306 .push(KeyValue::new(key.into(), value.into()));
307 self
308 }
309
310 /// Add multiple attributes at once from a vector.
311 ///
312 /// This is useful when you have a collection of attributes to add.
313 ///
314 /// # Examples
315 ///
316 /// ```rust
317 /// use lambda_otel_lite::events::{event, EventLevel};
318 /// use opentelemetry::KeyValue;
319 ///
320 /// event()
321 /// .level(EventLevel::Info)
322 /// .message("Batch operation")
323 /// .add_attributes(vec![
324 /// KeyValue::new("batch_id", "batch_123"),
325 /// KeyValue::new("item_count", 100),
326 /// ])
327 /// .call();
328 /// ```
329 pub fn add_attributes(mut self, attrs: Vec<KeyValue>) -> Self {
330 self.attributes.extend(attrs);
331 self
332 }
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338 use sealed_test::prelude::*;
339 use std::env;
340
341 #[test]
342 fn test_event_level_ordering() {
343 assert!(EventLevel::Trace < EventLevel::Debug);
344 assert!(EventLevel::Debug < EventLevel::Info);
345 assert!(EventLevel::Info < EventLevel::Warn);
346 assert!(EventLevel::Warn < EventLevel::Error);
347 }
348
349 #[test]
350 fn test_event_level_to_tracing_level() {
351 assert_eq!(
352 tracing::Level::TRACE,
353 tracing::Level::from(EventLevel::Trace)
354 );
355 assert_eq!(
356 tracing::Level::DEBUG,
357 tracing::Level::from(EventLevel::Debug)
358 );
359 assert_eq!(tracing::Level::INFO, tracing::Level::from(EventLevel::Info));
360 assert_eq!(tracing::Level::WARN, tracing::Level::from(EventLevel::Warn));
361 assert_eq!(
362 tracing::Level::ERROR,
363 tracing::Level::from(EventLevel::Error)
364 );
365 }
366
367 #[test]
368 fn test_event_level_to_u8() {
369 assert_eq!(1u8, u8::from(EventLevel::Trace));
370 assert_eq!(5u8, u8::from(EventLevel::Debug));
371 assert_eq!(9u8, u8::from(EventLevel::Info));
372 assert_eq!(13u8, u8::from(EventLevel::Warn));
373 assert_eq!(17u8, u8::from(EventLevel::Error));
374 }
375
376 #[test]
377 fn test_level_text() {
378 assert_eq!("TRACE", level_text(EventLevel::Trace));
379 assert_eq!("DEBUG", level_text(EventLevel::Debug));
380 assert_eq!("INFO", level_text(EventLevel::Info));
381 assert_eq!("WARN", level_text(EventLevel::Warn));
382 assert_eq!("ERROR", level_text(EventLevel::Error));
383 }
384
385 #[sealed_test]
386 fn test_get_min_level_aws_lambda_log_level() {
387 env::set_var("AWS_LAMBDA_LOG_LEVEL", "DEBUG");
388 env::remove_var("LOG_LEVEL");
389
390 let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
391 assert_eq!(level, EventLevel::Debug);
392 }
393
394 #[sealed_test]
395 fn test_get_min_level_log_level_fallback() {
396 env::remove_var("AWS_LAMBDA_LOG_LEVEL");
397 env::set_var("LOG_LEVEL", "WARN");
398
399 let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
400 assert_eq!(level, EventLevel::Warn);
401 }
402
403 #[sealed_test]
404 fn test_get_min_level_default() {
405 env::remove_var("AWS_LAMBDA_LOG_LEVEL");
406 env::remove_var("LOG_LEVEL");
407
408 let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
409 assert_eq!(level, EventLevel::Info);
410 }
411
412 #[sealed_test]
413 fn test_get_min_level_invalid() {
414 env::set_var("AWS_LAMBDA_LOG_LEVEL", "INVALID");
415 env::remove_var("LOG_LEVEL");
416
417 let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
418 assert_eq!(level, EventLevel::Info);
419 }
420
421 #[sealed_test]
422 fn test_get_min_level_case_insensitive() {
423 env::set_var("AWS_LAMBDA_LOG_LEVEL", "error");
424 env::remove_var("LOG_LEVEL");
425
426 let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
427 assert_eq!(level, EventLevel::Error);
428 }
429
430 #[test]
431 fn test_record_event_function_api() {
432 // Test the direct function API
433 record_event(
434 EventLevel::Info,
435 "Test event",
436 vec![KeyValue::new("test_key", "test_value")],
437 None,
438 );
439 }
440
441 #[test]
442 fn test_event_builder_api_basic() {
443 // Test the basic builder API compiles and works
444 event()
445 .level(EventLevel::Info)
446 .message("test message")
447 .call();
448 }
449
450 #[test]
451 fn test_event_builder_individual_attributes() {
452 // Test the individual attribute API
453 event()
454 .level(EventLevel::Info)
455 .message("test message")
456 .attribute("user_id", "123")
457 .attribute("count", 42)
458 .attribute("is_admin", true)
459 .attribute("score", 98.5)
460 .call();
461 }
462
463 #[test]
464 fn test_event_builder_mixed_attributes() {
465 // Test mixing individual attributes with batch attributes
466 use opentelemetry::KeyValue;
467
468 event()
469 .level(EventLevel::Warn)
470 .message("mixed attributes test")
471 .attribute("single_attr", "value")
472 .add_attributes(vec![
473 KeyValue::new("batch1", "value1"),
474 KeyValue::new("batch2", "value2"),
475 ])
476 .attribute("another_single", 999)
477 .call();
478 }
479
480 #[test]
481 fn test_both_apis_work() {
482 // Test that both APIs can be used together
483 record_event(EventLevel::Info, "Function API", vec![], None);
484 event()
485 .level(EventLevel::Info)
486 .message("Builder API")
487 .call();
488 }
489}