1use std::cell::RefCell;
30use std::path::Path;
31
32use tracing::Level;
33
34pub const TARGET: &str = "futu_audit";
36
37const MAX_AUDIT_CONTEXT_FIELD_LEN: usize = 128;
38
39#[derive(Debug, Clone, Default, PartialEq, Eq)]
46pub struct AuditContext {
47 remote_addr: String,
48 session_id: String,
49}
50
51impl AuditContext {
52 #[must_use]
53 pub fn new<R, S>(remote_addr: Option<R>, session_id: Option<S>) -> Self
54 where
55 R: AsRef<str>,
56 S: AsRef<str>,
57 {
58 Self {
59 remote_addr: sanitize_context_field(remote_addr),
60 session_id: sanitize_context_field(session_id),
61 }
62 }
63
64 #[must_use]
65 pub fn empty() -> Self {
66 Self::default()
67 }
68
69 #[must_use]
70 pub fn remote_addr(&self) -> &str {
71 &self.remote_addr
72 }
73
74 #[must_use]
75 pub fn session_id(&self) -> &str {
76 &self.session_id
77 }
78}
79
80thread_local! {
81 static CURRENT_CONTEXT: RefCell<AuditContext> = RefCell::new(AuditContext::empty());
82}
83
84pub fn with_context<T>(ctx: AuditContext, f: impl FnOnce() -> T) -> T {
91 let previous = CURRENT_CONTEXT.with(|cell| std::mem::replace(&mut *cell.borrow_mut(), ctx));
92 let _guard = AuditContextGuard {
93 previous: Some(previous),
94 };
95 f()
96}
97
98fn current_context() -> AuditContext {
99 CURRENT_CONTEXT.with(|cell| cell.borrow().clone())
100}
101
102struct AuditContextGuard {
103 previous: Option<AuditContext>,
104}
105
106impl Drop for AuditContextGuard {
107 fn drop(&mut self) {
108 if let Some(previous) = self.previous.take() {
109 CURRENT_CONTEXT.with(|cell| {
110 *cell.borrow_mut() = previous;
111 });
112 }
113 }
114}
115
116fn sanitize_context_field<T>(value: Option<T>) -> String
117where
118 T: AsRef<str>,
119{
120 value
121 .map(|v| {
122 v.as_ref()
123 .chars()
124 .filter(|c| !c.is_control())
125 .take(MAX_AUDIT_CONTEXT_FIELD_LEN)
126 .collect()
127 })
128 .unwrap_or_default()
129}
130
131pub fn reject(iface: &str, endpoint: &str, key_id: &str, reason: &str) {
133 let ctx = current_context();
134 tracing::event!(
135 target: TARGET,
136 Level::WARN,
137 iface = iface,
138 endpoint = endpoint,
139 key_id = key_id,
140 outcome = "reject",
141 reason = reason,
142 remote_addr = ctx.remote_addr(),
143 session_id = ctx.session_id(),
144 "auth reject"
145 );
146 crate::metrics::bump_auth_event(iface, "reject", key_id);
147 if let Some(rest) = reason.strip_prefix("limit: ") {
149 crate::metrics::bump_limit_reject(iface, key_id, rest);
150 } else if reason.starts_with("rate limit")
151 || reason.starts_with("daily value")
152 || reason.starts_with("order value")
153 {
154 crate::metrics::bump_limit_reject(iface, key_id, reason);
155 }
156}
157
158pub fn allow(iface: &str, endpoint: &str, key_id: &str, scope: Option<&str>) {
160 let ctx = current_context();
161 tracing::event!(
162 target: TARGET,
163 Level::INFO,
164 iface = iface,
165 endpoint = endpoint,
166 key_id = key_id,
167 outcome = "allow",
168 scope = scope.unwrap_or(""),
169 remote_addr = ctx.remote_addr(),
170 session_id = ctx.session_id(),
171 "auth allow"
172 );
173 crate::metrics::bump_auth_event(iface, "allow", key_id);
174}
175
176pub fn trade(
178 iface: &str,
179 tool: &str,
180 key_id: &str,
181 args_hash: &str,
182 outcome: &str,
183 reason: Option<&str>,
184) {
185 let ctx = current_context();
186 tracing::event!(
187 target: TARGET,
188 Level::WARN,
189 iface = iface,
190 endpoint = tool,
191 key_id = key_id,
192 outcome = outcome,
193 args_hash = args_hash,
194 reason = reason.unwrap_or(""),
195 remote_addr = ctx.remote_addr(),
196 session_id = ctx.session_id(),
197 "trade event"
198 );
199 crate::metrics::bump_auth_event(iface, outcome, key_id);
200}
201
202pub fn open_writer(
213 path: &Path,
214) -> std::io::Result<(
215 tracing_appender::non_blocking::NonBlocking,
216 tracing_appender::non_blocking::WorkerGuard,
217)> {
218 futu_core::audit_log_writer::open_writer(path)
219}
220
221#[cfg(test)]
222mod tests;