use std::{fmt, sync::Arc};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct RequestId(pub(crate) Uuid);
impl RequestId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
#[must_use]
pub const fn from_uuid(uuid: Uuid) -> Self {
Self(uuid)
}
}
impl Default for RequestId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for RequestId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RequestContext {
pub request_id: RequestId,
pub operation: Option<String>,
pub user_id: Option<String>,
pub org_id: Option<String>,
pub client_ip: Option<String>,
pub api_version: Option<String>,
}
impl RequestContext {
#[must_use]
pub fn new() -> Self {
Self {
request_id: RequestId::new(),
operation: None,
user_id: None,
org_id: None,
client_ip: None,
api_version: None,
}
}
#[must_use]
pub fn with_operation(mut self, operation: String) -> Self {
self.operation = Some(operation);
self
}
#[must_use]
pub fn with_user_id(mut self, user_id: String) -> Self {
self.user_id = Some(user_id);
self
}
#[must_use]
pub fn with_org_id(mut self, org_id: String) -> Self {
self.org_id = Some(org_id);
self
}
#[must_use]
pub fn with_client_ip(mut self, ip: String) -> Self {
self.client_ip = Some(ip);
self
}
#[must_use]
pub fn with_api_version(mut self, version: String) -> Self {
self.api_version = Some(version);
self
}
}
impl Default for RequestContext {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "UPPERCASE")]
#[non_exhaustive]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl fmt::Display for LogLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Trace => write!(f, "TRACE"),
Self::Debug => write!(f, "DEBUG"),
Self::Info => write!(f, "INFO"),
Self::Warn => write!(f, "WARN"),
Self::Error => write!(f, "ERROR"),
}
}
}
impl From<tracing::Level> for LogLevel {
fn from(level: tracing::Level) -> Self {
match level {
tracing::Level::TRACE => Self::Trace,
tracing::Level::DEBUG => Self::Debug,
tracing::Level::INFO => Self::Info,
tracing::Level::WARN => Self::Warn,
tracing::Level::ERROR => Self::Error,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StructuredLogEntry {
pub timestamp: String,
pub level: LogLevel,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_context: Option<RequestContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metrics: Option<LogMetrics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source: Option<SourceLocation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<serde_json::Value>,
}
impl StructuredLogEntry {
#[must_use]
pub fn new(level: LogLevel, message: String) -> Self {
Self {
timestamp: chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true),
level,
message,
request_context: None,
metrics: None,
error: None,
source: None,
context: None,
}
}
#[must_use]
pub fn with_request_context(mut self, context: RequestContext) -> Self {
self.request_context = Some(context);
self
}
#[must_use]
pub const fn with_metrics(mut self, metrics: LogMetrics) -> Self {
self.metrics = Some(metrics);
self
}
#[must_use]
pub fn with_error(mut self, error: ErrorDetails) -> Self {
self.error = Some(error);
self
}
#[must_use]
pub fn with_source(mut self, source: SourceLocation) -> Self {
self.source = Some(source);
self
}
#[must_use]
pub fn with_context(mut self, context: serde_json::Value) -> Self {
self.context = Some(context);
self
}
#[must_use]
pub fn to_json_string(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|_| {
format!(
r#"{{"level":"{}","message":"{}","error":"serialization failed"}}"#,
self.level, self.message
)
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LogMetrics {
#[serde(skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub complexity: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items_processed: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cache_hit: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub db_queries: Option<u32>,
}
impl LogMetrics {
#[must_use]
pub const fn new() -> Self {
Self {
duration_ms: None,
complexity: None,
items_processed: None,
cache_hit: None,
db_queries: None,
}
}
#[must_use]
pub const fn with_duration_ms(mut self, duration: f64) -> Self {
self.duration_ms = Some(duration);
self
}
#[must_use]
pub const fn with_complexity(mut self, complexity: u32) -> Self {
self.complexity = Some(complexity);
self
}
#[must_use]
pub const fn with_items_processed(mut self, count: u64) -> Self {
self.items_processed = Some(count);
self
}
#[must_use]
pub const fn with_cache_hit(mut self, hit: bool) -> Self {
self.cache_hit = Some(hit);
self
}
#[must_use]
pub const fn with_db_queries(mut self, count: u32) -> Self {
self.db_queries = Some(count);
self
}
}
impl Default for LogMetrics {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorDetails {
pub error_type: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stack_trace: Option<String>,
}
impl ErrorDetails {
#[must_use]
pub const fn new(error_type: String, message: String) -> Self {
Self {
error_type,
message,
code: None,
stack_trace: None,
}
}
#[must_use]
pub fn with_code(mut self, code: String) -> Self {
self.code = Some(code);
self
}
#[must_use]
pub fn with_stack_trace(mut self, trace: String) -> Self {
self.stack_trace = Some(trace);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SourceLocation {
pub file: String,
pub line: u32,
pub module: String,
}
impl SourceLocation {
#[must_use]
pub const fn new(file: String, line: u32, module: String) -> Self {
Self { file, line, module }
}
}
#[derive(Debug, Clone)]
pub struct RequestLogger {
context: Arc<RequestContext>,
}
impl RequestLogger {
#[must_use]
pub fn new(context: RequestContext) -> Self {
Self {
context: Arc::new(context),
}
}
#[must_use]
pub fn with_request_id(request_id: RequestId) -> Self {
Self::new(RequestContext {
request_id,
..Default::default()
})
}
#[must_use]
pub fn context(&self) -> &RequestContext {
&self.context
}
pub fn info(&self, message: impl Into<String>) -> StructuredLogEntry {
StructuredLogEntry::new(LogLevel::Info, message.into())
.with_request_context((*self.context).clone())
}
pub fn warn(&self, message: impl Into<String>) -> StructuredLogEntry {
StructuredLogEntry::new(LogLevel::Warn, message.into())
.with_request_context((*self.context).clone())
}
pub fn error(&self, message: impl Into<String>) -> StructuredLogEntry {
StructuredLogEntry::new(LogLevel::Error, message.into())
.with_request_context((*self.context).clone())
}
pub fn debug(&self, message: impl Into<String>) -> StructuredLogEntry {
StructuredLogEntry::new(LogLevel::Debug, message.into())
.with_request_context((*self.context).clone())
}
}