systemprompt_api/services/gateway/audit/
mod.rs1mod complete;
14mod message_text;
15mod open;
16pub mod payload;
17
18#[cfg(feature = "test-api")]
19pub mod test_api {
20 pub use super::message_text::flatten_message_content;
21}
22
23use std::sync::{Arc, Mutex};
24use std::time::Instant;
25
26use anyhow::Result;
27use systemprompt_ai::models::RequestStatus;
28use systemprompt_ai::repository::{AiRequestPayloadRepository, AiRequestRepository};
29use systemprompt_identifiers::{
30 AiRequestId, ClientId, ContextId, GatewayConversationId, SessionId, TraceId, UserId,
31};
32use systemprompt_security::policy::types::AccessScope;
33
34#[derive(Debug, Clone)]
38pub struct GatewayAccessLog {
39 pub method: String,
40 pub path: String,
41 pub started: Instant,
42}
43
44#[derive(Debug, Clone)]
45pub struct GatewayRequestContext {
46 pub ai_request_id: AiRequestId,
47 pub user_id: UserId,
48 pub session_id: Option<SessionId>,
49 pub context_id: ContextId,
50 pub gateway_conversation_id: Option<GatewayConversationId>,
51 pub trace_id: Option<TraceId>,
52 pub access_scope: AccessScope,
55 pub client_id: Option<ClientId>,
56 pub provider: String,
57 pub model: String,
58 pub requested_model: Option<String>,
59 pub max_tokens: Option<u32>,
60 pub is_streaming: bool,
61 pub wire_protocol: String,
62 pub access_log: Option<GatewayAccessLog>,
63}
64
65#[expect(
66 missing_debug_implementations,
67 reason = "service type holds repository clients that intentionally do not implement Debug"
68)]
69pub struct GatewayAudit {
70 requests: Arc<AiRequestRepository>,
71 payloads: Arc<AiRequestPayloadRepository>,
72 context_materializer: systemprompt_traits::DynContextMaterializer,
73 pub ctx: GatewayRequestContext,
74 served_model: Mutex<Option<String>>,
75 started_at: Instant,
76}
77
78impl GatewayAudit {
79 pub fn new(repos: &super::GatewayRepositories, ctx: GatewayRequestContext) -> Self {
80 Self {
81 requests: Arc::clone(&repos.requests),
82 payloads: Arc::clone(&repos.payloads),
83 context_materializer: Arc::clone(&repos.context_materializer),
84 ctx,
85 served_model: Mutex::new(None),
86 started_at: Instant::now(),
87 }
88 }
89
90 pub async fn set_served_model(&self, model: &str) {
91 if model.is_empty() || model == self.ctx.model {
92 return;
93 }
94 if let Ok(mut slot) = self.served_model.lock() {
95 *slot = Some(model.to_owned());
96 }
97 if let Err(e) = self
98 .requests
99 .update_model(&self.ctx.ai_request_id, model)
100 .await
101 {
102 tracing::warn!(error = %e, "update_model failed");
103 }
104 }
105
106 pub async fn set_prepared_body_digest(&self, body: &[u8]) {
107 let sha256 = payload::digest_hex(body);
108 if let Err(e) = self
109 .payloads
110 .upsert_prepared_sha256(&self.ctx.ai_request_id, &sha256)
111 .await
112 {
113 tracing::warn!(error = %e, ai_request_id = %self.ctx.ai_request_id, "prepared body digest write failed");
114 }
115 }
116
117 pub async fn set_system_prompt_override(&self, descriptor: &str) {
118 if let Err(e) = self
119 .requests
120 .update_system_prompt_override(&self.ctx.ai_request_id, descriptor)
121 .await
122 {
123 tracing::warn!(error = %e, "update_system_prompt_override failed");
124 }
125 }
126
127 pub async fn set_route_match(&self, descriptor: &str) {
128 if let Err(e) = self
129 .requests
130 .update_route_match(&self.ctx.ai_request_id, descriptor)
131 .await
132 {
133 tracing::warn!(error = %e, "update_route_match failed");
134 }
135 }
136
137 pub async fn fail(&self, error: &str) -> Result<()> {
138 let latency_ms = self.elapsed_ms();
139 if let Err(e) = self
140 .requests
141 .update_error(&self.ctx.ai_request_id, RequestStatus::Failed, error)
142 .await
143 {
144 tracing::warn!(error = %e, "audit fail update failed");
145 }
146 tracing::warn!(
150 ai_request_id = %self.ctx.ai_request_id,
151 user_id = %self.ctx.user_id,
152 provider = %self.ctx.provider,
153 model = %self.effective_model(),
154 requested_model = %self.ctx.model,
155 wire_protocol = %self.ctx.wire_protocol,
156 status = RequestStatus::Failed.as_str(),
157 latency_ms,
158 tokens_recorded = false,
159 error,
160 "Gateway audit: request failed"
161 );
162 Ok(())
163 }
164
165 pub(crate) fn elapsed_ms(&self) -> i32 {
166 self.started_at.elapsed().as_millis().min(i32::MAX as u128) as i32
167 }
168}