132 lines
7.0 KiB
Rust
132 lines
7.0 KiB
Rust
//! 有界 HTTP 活动记录。保存请求/响应的可显示预览供 Network 工具读取,不负责执行请求。
|
|
//! 本模块限制预览字节数与记录条数;请求头白名单由 runtime/api::ObservedRequest 提前筛选。
|
|
//!
|
|
//! Bounded observation of this process's local HTTP API, never external traffic.
|
|
use std::{
|
|
collections::VecDeque,
|
|
sync::{Arc, Mutex},
|
|
time::{Instant, SystemTime, UNIX_EPOCH},
|
|
};
|
|
|
|
// HTTP worker 与原生 Network 工具共享同一有界日志;持锁读取快照/更新记录,不在这里执行网络请求。
|
|
pub type Activity = Arc<Mutex<ActivityLog>>;
|
|
// 预览正文按 UTF-8 字节限制;截断标记会额外占用少量字节。
|
|
pub const BODY_PREVIEW: usize = 32 * 1024;
|
|
// 只保留最近 80 次开始的请求,尚未完成的老请求也可能被淘汰。
|
|
const MAX_RECORDS: usize = 80;
|
|
|
|
#[derive(Clone, Debug)]
|
|
/// 一次 HTTP 请求/响应的有界观察记录,供 Network 工具查看。
|
|
pub struct Exchange {
|
|
// 由 begin 分配的日志身份,HTTP worker 后续只携带此 ID 更新,不持有队列元素引用。
|
|
pub id: u64,
|
|
// 请求方法由本地 API 观察器传入,按原文本保存;这里不校验或执行 HTTP 方法。
|
|
pub method: String,
|
|
// 请求 URL 的受限预览,不在此解析路由或读取所指文件。
|
|
pub path: String,
|
|
// ObservedRequest 传入 HTTP/Console 显示标签;本字段不参与认证或路由,也不在此截断。
|
|
pub origin: String,
|
|
// 开始时刻的 UTC 时分秒文本,不含日期;耗时另用 started 单调时钟计算。
|
|
pub timestamp: String,
|
|
// 初始为空,成功读取请求体后 body 覆盖预览;不是用于执行脚本的原始正文存储。
|
|
pub request: String,
|
|
// finish 记录已构造响应的正文预览,即使后续 socket 发送失败也可能有值。
|
|
pub response: String,
|
|
// None 表示尚未记录响应;完成后保存 HTTP 状态码,不据此判断业务调用是否成功。
|
|
pub status: Option<u16>,
|
|
// 初始为零,finish 时从 begin 起累计毫秒;不在界面每次绘制时持续刷新。
|
|
pub elapsed_ms: u128,
|
|
// 请求头已经过观察器白名单筛选,这里再截断文本;ActivityLog 自身不检查头名称。
|
|
pub request_headers: String,
|
|
// respond 提供已构造响应的全部头部文本,本模块仅限长,不沿用请求头的白名单。
|
|
pub response_headers: String,
|
|
// 使用单调时钟计算耗时,不受系统时间调整影响;timestamp 则用于显示 UTC 时刻。
|
|
started: Instant,
|
|
}
|
|
|
|
#[derive(Debug, Default)]
|
|
/// 按容量维护近期 HTTP 活动与请求身份,防止无限日志积累。
|
|
pub struct ActivityLog {
|
|
// 开始顺序队列,Network/Console 工具通过共享锁读取或复制;完成顺序不会重排记录。
|
|
pub records: VecDeque<Exchange>,
|
|
// 默认从零开始,begin 先递增再使用;计数仅属此日志实例,不是跨进程请求身份。
|
|
next_id: u64,
|
|
}
|
|
impl ActivityLog {
|
|
/// 为请求分配活动 ID 并登记其方法、来源和受限头部。
|
|
// runtime/api 的 ObservedRequest 构造时调用;先写日志再由调用方通知界面,本模块不主动唤醒窗口。
|
|
pub fn begin(&mut self, method: String, path: String, origin: String, headers: String) -> u64 {
|
|
// ID 在当前日志实例内递增,不从队列长度推导;淘汰旧项不会重置计数。
|
|
self.next_id += 1;
|
|
let id = self.next_id;
|
|
// 显示时间取 Unix 秒数的当日余数;系统时间早于纪元时回退到零。这里只显示时间,不保存日期。
|
|
let seconds = SystemTime::now()
|
|
.duration_since(UNIX_EPOCH)
|
|
.unwrap_or_default()
|
|
.as_secs()
|
|
% 86400;
|
|
// 登记开始状态,正文和响应留空,started 与墙上时钟分别服务耗时和时间显示。
|
|
self.records.push_back(Exchange {
|
|
id,
|
|
method,
|
|
path: preview(&path),
|
|
origin,
|
|
timestamp: format!(
|
|
"{:02}:{:02}:{:02} UTC",
|
|
seconds / 3600,
|
|
seconds / 60 % 60,
|
|
seconds % 60
|
|
),
|
|
request: String::new(),
|
|
response: String::new(),
|
|
status: None,
|
|
elapsed_ms: 0,
|
|
request_headers: preview(&headers),
|
|
response_headers: String::new(),
|
|
started: Instant::now(),
|
|
});
|
|
// 从队首淘汰最早开始的记录。后续 body/finish 若找不到其 ID 就忽略,不重新插入旧请求。
|
|
while self.records.len() > MAX_RECORDS {
|
|
self.records.pop_front();
|
|
}
|
|
id
|
|
}
|
|
/// 更新请求正文预览,内容长度仍由活动记录策略限制。
|
|
// 线性查找在最多 80 项范围内完成;本方法覆盖之前的请求预览,不拼接分块正文。
|
|
// ObservedRequest::record_body 在读取请求体后调用,后续 submit 仍使用原正文,日志只保存截断副本。
|
|
pub fn body(&mut self, id: u64, body: &str) {
|
|
// 按 ID 更新仍在队列内的记录;请求被容量淘汰时不重建,以免旧请求挤走较新的观察记录。
|
|
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
|
|
record.request = preview(body);
|
|
}
|
|
}
|
|
/// 为指定请求登记响应状态、正文/头部预览与单调时钟耗时;记录已淘汰时忽略。
|
|
// 仅更新仍保留的请求,响应正文和头部都重新截断;耗时从 begin 起计算。
|
|
// ObservedRequest::respond 在真实响应发送前调用,状态和耗时表示已构造响应,不证明客户端接收成功。
|
|
pub fn finish(&mut self, id: u64, status: u16, body: &str, headers: String) {
|
|
// finish 可能晚于容量淘汰,找不到 ID 时整个更新跳过;不为旧响应另建记录。
|
|
if let Some(record) = self.records.iter_mut().find(|r| r.id == id) {
|
|
record.status = Some(status);
|
|
record.response = preview(body);
|
|
record.elapsed_ms = record.started.elapsed().as_millis();
|
|
record.response_headers = preview(&headers);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 截取可显示的受限文本预览,避免日志无限保留完整大正文。
|
|
// begin/body/finish 统一使用 UTF-8 字节预算;输入未超限时仍返回独立 String,不借用请求缓冲。
|
|
pub fn preview(value: &str) -> String {
|
|
// 恰好等于上限也保留原文,不追加截断标记;长度判断按字节而不是字符数量。
|
|
if value.len() <= BODY_PREVIEW {
|
|
return value.to_owned();
|
|
}
|
|
// UTF-8 多字节字符不能从中间切开;从字节上限回退到合法边界,确保输出始终是有效字符串。
|
|
let mut end = BODY_PREVIEW;
|
|
// 最多回退到当前 UTF-8 字符起点,只保证字符边界,不承诺完整字素簇边界。
|
|
while !value.is_char_boundary(end) {
|
|
end -= 1;
|
|
}
|
|
format!("{}\n[preview truncated at 32 KiB]", &value[..end])
|
|
}
|