use crate::constants::defaults;
use bon::builder;
use opentelemetry::KeyValue;
use std::sync::OnceLock;
use std::{env, time::SystemTime};
use tracing_opentelemetry::OpenTelemetrySpanExt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum EventLevel {
Trace = 1,
Debug = 5,
Info = 9,
Warn = 13,
Error = 17,
}
impl From<EventLevel> for tracing::Level {
fn from(level: EventLevel) -> Self {
match level {
EventLevel::Trace => tracing::Level::TRACE,
EventLevel::Debug => tracing::Level::DEBUG,
EventLevel::Info => tracing::Level::INFO,
EventLevel::Warn => tracing::Level::WARN,
EventLevel::Error => tracing::Level::ERROR,
}
}
}
impl From<EventLevel> for u8 {
fn from(level: EventLevel) -> Self {
level as u8
}
}
fn level_text(level: EventLevel) -> &'static str {
match level {
EventLevel::Trace => "TRACE",
EventLevel::Debug => "DEBUG",
EventLevel::Info => "INFO",
EventLevel::Warn => "WARN",
EventLevel::Error => "ERROR",
}
}
static MIN_LEVEL: OnceLock<EventLevel> = OnceLock::new();
fn get_min_level() -> EventLevel {
*MIN_LEVEL.get_or_init(|| get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL"))
}
fn get_min_level_with_env_vars(primary_var: &str, fallback_var: &str) -> EventLevel {
let level = env::var(primary_var)
.or_else(|_| env::var(fallback_var))
.unwrap_or_else(|_| defaults::EVENT_LEVEL.to_string())
.to_uppercase();
match level.as_str() {
"ERROR" => EventLevel::Error,
"WARN" => EventLevel::Warn,
"INFO" => EventLevel::Info,
"DEBUG" => EventLevel::Debug,
"TRACE" => EventLevel::Trace,
_ => EventLevel::Info, }
}
pub fn record_event(
level: EventLevel,
message: impl AsRef<str>,
attributes: Vec<KeyValue>,
timestamp: Option<SystemTime>,
) {
record_event_impl(level, message.as_ref(), attributes, timestamp);
}
#[builder]
pub fn event(
#[builder(field)] attributes: Vec<KeyValue>,
#[builder(default = EventLevel::Info)] level: EventLevel,
#[builder(into, default = "")] message: String,
timestamp: Option<SystemTime>,
) {
record_event_impl(level, &message, attributes, timestamp);
}
fn record_event_impl(
level: EventLevel,
message: &str,
attributes: Vec<KeyValue>,
timestamp: Option<SystemTime>,
) {
if level < get_min_level() {
return;
}
let span = tracing::Span::current();
if span.is_disabled() {
return;
}
let mut event_attributes = Vec::with_capacity(attributes.len() + 3);
event_attributes.extend_from_slice(&[
KeyValue::new("event.severity_text", level_text(level)),
KeyValue::new("event.severity_number", u8::from(level) as i64),
]);
if !message.is_empty() {
event_attributes.push(KeyValue::new("event.body", message.to_string()));
}
event_attributes.extend(attributes);
if let Some(ts) = timestamp {
span.add_event_with_timestamp("event", ts, event_attributes);
} else {
span.add_event("event", event_attributes);
}
}
impl<S: event_builder::State> EventBuilder<S> {
pub fn attribute(
mut self,
key: impl Into<String>,
value: impl Into<opentelemetry::Value>,
) -> Self {
self.attributes
.push(KeyValue::new(key.into(), value.into()));
self
}
pub fn add_attributes(mut self, attrs: Vec<KeyValue>) -> Self {
self.attributes.extend(attrs);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use sealed_test::prelude::*;
use std::env;
#[test]
fn test_event_level_ordering() {
assert!(EventLevel::Trace < EventLevel::Debug);
assert!(EventLevel::Debug < EventLevel::Info);
assert!(EventLevel::Info < EventLevel::Warn);
assert!(EventLevel::Warn < EventLevel::Error);
}
#[test]
fn test_event_level_to_tracing_level() {
assert_eq!(
tracing::Level::TRACE,
tracing::Level::from(EventLevel::Trace)
);
assert_eq!(
tracing::Level::DEBUG,
tracing::Level::from(EventLevel::Debug)
);
assert_eq!(tracing::Level::INFO, tracing::Level::from(EventLevel::Info));
assert_eq!(tracing::Level::WARN, tracing::Level::from(EventLevel::Warn));
assert_eq!(
tracing::Level::ERROR,
tracing::Level::from(EventLevel::Error)
);
}
#[test]
fn test_event_level_to_u8() {
assert_eq!(1u8, u8::from(EventLevel::Trace));
assert_eq!(5u8, u8::from(EventLevel::Debug));
assert_eq!(9u8, u8::from(EventLevel::Info));
assert_eq!(13u8, u8::from(EventLevel::Warn));
assert_eq!(17u8, u8::from(EventLevel::Error));
}
#[test]
fn test_level_text() {
assert_eq!("TRACE", level_text(EventLevel::Trace));
assert_eq!("DEBUG", level_text(EventLevel::Debug));
assert_eq!("INFO", level_text(EventLevel::Info));
assert_eq!("WARN", level_text(EventLevel::Warn));
assert_eq!("ERROR", level_text(EventLevel::Error));
}
#[sealed_test]
fn test_get_min_level_aws_lambda_log_level() {
env::set_var("AWS_LAMBDA_LOG_LEVEL", "DEBUG");
env::remove_var("LOG_LEVEL");
let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
assert_eq!(level, EventLevel::Debug);
}
#[sealed_test]
fn test_get_min_level_log_level_fallback() {
env::remove_var("AWS_LAMBDA_LOG_LEVEL");
env::set_var("LOG_LEVEL", "WARN");
let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
assert_eq!(level, EventLevel::Warn);
}
#[sealed_test]
fn test_get_min_level_default() {
env::remove_var("AWS_LAMBDA_LOG_LEVEL");
env::remove_var("LOG_LEVEL");
let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
assert_eq!(level, EventLevel::Info);
}
#[sealed_test]
fn test_get_min_level_invalid() {
env::set_var("AWS_LAMBDA_LOG_LEVEL", "INVALID");
env::remove_var("LOG_LEVEL");
let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
assert_eq!(level, EventLevel::Info);
}
#[sealed_test]
fn test_get_min_level_case_insensitive() {
env::set_var("AWS_LAMBDA_LOG_LEVEL", "error");
env::remove_var("LOG_LEVEL");
let level = get_min_level_with_env_vars("AWS_LAMBDA_LOG_LEVEL", "LOG_LEVEL");
assert_eq!(level, EventLevel::Error);
}
#[test]
fn test_record_event_function_api() {
record_event(
EventLevel::Info,
"Test event",
vec![KeyValue::new("test_key", "test_value")],
None,
);
}
#[test]
fn test_event_builder_api_basic() {
event()
.level(EventLevel::Info)
.message("test message")
.call();
}
#[test]
fn test_event_builder_individual_attributes() {
event()
.level(EventLevel::Info)
.message("test message")
.attribute("user_id", "123")
.attribute("count", 42)
.attribute("is_admin", true)
.attribute("score", 98.5)
.call();
}
#[test]
fn test_event_builder_mixed_attributes() {
use opentelemetry::KeyValue;
event()
.level(EventLevel::Warn)
.message("mixed attributes test")
.attribute("single_attr", "value")
.add_attributes(vec![
KeyValue::new("batch1", "value1"),
KeyValue::new("batch2", "value2"),
])
.attribute("another_single", 999)
.call();
}
#[test]
fn test_both_apis_work() {
record_event(EventLevel::Info, "Function API", vec![], None);
event()
.level(EventLevel::Info)
.message("Builder API")
.call();
}
}