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}