tibba_middleware/tracker.rs
1// Copyright 2026 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::LOG_TARGET;
16use axum::extract::Request;
17use axum::extract::State;
18use axum::middleware::Next;
19use axum::response::Response;
20use tibba_error::Error;
21use tibba_state::CTX;
22use tracing::{error, info};
23
24type Result<T> = std::result::Result<T, Error>;
25
26#[derive(Clone, Copy)]
27pub struct TrackerParams {
28 pub name: &'static str,
29 pub step: &'static str,
30}
31
32impl From<(&'static str, &'static str)> for TrackerParams {
33 fn from((name, step): (&'static str, &'static str)) -> Self {
34 Self { name, step }
35 }
36}
37
38/// Middleware that records user behavior events for audit and analytics.
39///
40/// Each event captures:
41/// - Identity context: device_id, trace_id, account
42/// - Business context: operation name and step label
43/// - Outcome: HTTP status, success/failure result, elapsed time
44/// - Failure detail: error message, category, sub-category, and whether it
45/// was an infrastructure exception (vs. a normal business error)
46pub async fn user_tracker(
47 State(params): State<TrackerParams>,
48 req: Request,
49 next: Next,
50) -> Result<Response> {
51 let res = next.run(req).await;
52
53 let ctx = CTX.get();
54 // Milliseconds elapsed since the request entered the middleware stack
55 let elapsed = ctx.elapsed_ms();
56 let device_id = &ctx.device_id;
57 let trace_id = &ctx.trace_id;
58 // Authenticated account name; empty string when the user is not logged in
59 let account = ctx.get_account();
60 // HTTP status code — useful for correlating with access logs
61 let status = res.status().as_u16();
62
63 if status < 400 {
64 info!(
65 target: LOG_TARGET,
66 device_id,
67 trace_id,
68 name = params.name, // Logical operation name (e.g. "user_login")
69 account = %account,
70 step = params.step, // Fine-grained step within the operation
71 status,
72 elapsed,
73 result = "success",
74 "user tracker",
75 );
76 return Ok(res);
77 }
78
79 // Extract structured error details from the response extensions.
80 // If no Error is attached (e.g. the handler panicked), treat as an
81 // infrastructure exception so on-call alerts fire correctly.
82 let (error, error_category, error_sub_category, error_exception) = res
83 .extensions()
84 .get::<Error>()
85 .map(|err| {
86 (
87 Some(err.message.clone()),
88 Some(err.category.clone()),
89 err.sub_category.clone(),
90 // true when the error originated from infrastructure
91 // (network timeout, downstream failure, etc.)
92 err.exception.unwrap_or_default(),
93 )
94 })
95 .unwrap_or((None, None, None, true));
96
97 error!(
98 target: LOG_TARGET,
99 device_id,
100 trace_id,
101 name = params.name,
102 account = %account,
103 step = params.step,
104 status,
105 error,
106 error_category,
107 error_sub_category,
108 // Distinguishes infrastructure exceptions from normal business errors
109 error_exception,
110 elapsed,
111 result = "failure",
112 "user tracker",
113 );
114 Ok(res)
115}