systemprompt_logging/models/
log_entry.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use systemprompt_identifiers::{LogId, SessionId, TraceId, UserId};
4
5use super::{LogLevel, LoggingError};
6use crate::attribution::{LogAttributionUnset, platform_owner_id};
7
8#[allow(clippy::struct_field_names)]
15#[derive(Debug, Clone)]
16pub struct LogActor {
17 pub user_id: UserId,
18 pub session_id: SessionId,
19 pub trace_id: TraceId,
20}
21
22impl LogActor {
23 #[must_use]
24 pub const fn new(user_id: UserId, session_id: SessionId, trace_id: TraceId) -> Self {
25 Self {
26 user_id,
27 session_id,
28 trace_id,
29 }
30 }
31
32 pub fn platform(trace_id: TraceId) -> Result<Self, LogAttributionUnset> {
37 Ok(Self {
38 user_id: platform_owner_id()?.clone(),
39 session_id: SessionId::system(),
40 trace_id,
41 })
42 }
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct LogEntry {
47 pub id: LogId,
48 pub timestamp: DateTime<Utc>,
49 pub level: LogLevel,
50 pub module: String,
51 pub message: String,
52 pub metadata: Option<serde_json::Value>,
53 pub user_id: UserId,
54 pub session_id: SessionId,
55 pub task_id: Option<systemprompt_identifiers::TaskId>,
56 pub trace_id: TraceId,
57 pub context_id: Option<systemprompt_identifiers::ContextId>,
58 pub client_id: Option<systemprompt_identifiers::ClientId>,
59}
60
61impl LogEntry {
62 pub fn new(
63 level: LogLevel,
64 module: impl Into<String>,
65 message: impl Into<String>,
66 actor: LogActor,
67 ) -> Self {
68 Self {
69 id: LogId::generate(),
70 timestamp: Utc::now(),
71 level,
72 module: module.into(),
73 message: message.into(),
74 metadata: None,
75 user_id: actor.user_id,
76 session_id: actor.session_id,
77 task_id: None,
78 trace_id: actor.trace_id,
79 context_id: None,
80 client_id: None,
81 }
82 }
83
84 #[must_use]
85 pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
86 self.metadata = Some(metadata);
87 self
88 }
89
90 #[must_use]
91 pub fn with_task_id(mut self, task_id: systemprompt_identifiers::TaskId) -> Self {
92 self.task_id = Some(task_id);
93 self
94 }
95
96 #[must_use]
97 pub fn with_context_id(mut self, context_id: systemprompt_identifiers::ContextId) -> Self {
98 self.context_id = Some(context_id);
99 self
100 }
101
102 #[must_use]
103 pub fn with_client_id(mut self, client_id: systemprompt_identifiers::ClientId) -> Self {
104 self.client_id = Some(client_id);
105 self
106 }
107
108 pub fn validate(&self) -> Result<(), LoggingError> {
109 if self.module.is_empty() {
110 return Err(LoggingError::EmptyModuleName);
111 }
112 if self.message.is_empty() {
113 return Err(LoggingError::EmptyMessage);
114 }
115 if let Some(metadata) = &self.metadata {
116 if !metadata.is_object()
117 && !metadata.is_array()
118 && !metadata.is_string()
119 && !metadata.is_null()
120 {
121 return Err(LoggingError::InvalidMetadata);
122 }
123 }
124 Ok(())
125 }
126}
127
128impl std::fmt::Display for LogEntry {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 let level_str = match self.level {
131 LogLevel::Error => "ERROR",
132 LogLevel::Warn => "WARN ",
133 LogLevel::Info => "INFO ",
134 LogLevel::Debug => "DEBUG",
135 LogLevel::Trace => "TRACE",
136 };
137
138 let timestamp_str = self.timestamp.format("%H:%M:%S");
139
140 if let Some(metadata) = &self.metadata {
141 write!(
142 f,
143 "{} [{}] {}: {} {}",
144 timestamp_str,
145 level_str,
146 self.module,
147 self.message,
148 serde_json::to_string(metadata).unwrap_or_else(|_| String::new())
149 )
150 } else {
151 write!(
152 f,
153 "{} [{}] {}: {}",
154 timestamp_str, level_str, self.module, self.message
155 )
156 }
157 }
158}