client log sends to proxy

This commit is contained in:
2026-06-30 19:49:50 +07:00
parent f7a76353ea
commit 582ad714ae
8 changed files with 221 additions and 1 deletions
+21
View File
@@ -181,6 +181,27 @@ impl StreamHandler {
debug!(stream_id, "🏁 [Tunnel] Peer closed stream");
self.muxer.remove_stream(stream_id);
}
FrameType::Diag => {
// Диагностика клиента, доставленная по туннелю. Осмысленна только
// на сервере: пересылаем в сток вместе с id сессии (берём из
// muxer'а — на сервере это сессия этой ноги). На клиенте сток не
// поднят, поэтому отчёт просто отбрасывается. Никогда не идёт в
// локальные сокеты и не маршрутизируется как данные.
let session_id = self.muxer.session_id().to_string();
let json_line = String::from_utf8_lossy(&frame.payload).into_owned();
trace!(
session_id = %session_id,
bytes = json_line.len(),
"🩺 [Tunnel] Client diagnostics report received"
);
crate::net::diagnostics::report_client_diag(
crate::net::diagnostics::ClientDiagReport {
session_id,
json_line,
},
);
}
}
}
+22
View File
@@ -529,6 +529,28 @@ impl Muxer {
.await
}
/// Идентификатор сессии этого мультиплексора (для логов/топологии и для
/// именования пер-сессионных файлов диагностики на сервере).
pub fn session_id(&self) -> &str {
&self.session_id
}
/// Отправляет на сервер одну строку клиентской диагностики `Diag`-кадром.
///
/// Холодный путь: едет по контрольному каналу с best-effort семантикой (как и
/// прочий не-критичный контроль — при переполнении канала кадр дропается, не
/// блокируя). Возвращает `false`, только если живых ног нет вовсе — тогда
/// вызывающая сторона может оставить снапшот в очереди и повторить позже.
pub async fn send_diag_report(&self, payload: Bytes) -> bool {
self.send_to_network(MuxMessage {
stream_id: 0,
frame_type: FrameType::Diag,
data: payload,
})
.await
.is_ok()
}
/// Удобная обёртка для отправки управляющего кадра заданного типа.
pub(crate) async fn send_control(
&self,
+45
View File
@@ -54,6 +54,42 @@ pub fn send_diag_event(event: DiagnosticsEvent) {
}
}
// ── Client-diagnostics sink (server side) ─────────────────────────────────────
/// Отчёт диагностики клиента, доставленный по туннелю на сервер.
///
/// Клиент по триггеру строит снапшот, сериализует его в одну JSON-строку и шлёт
/// `Diag`-кадром. Сервер принимает кадр, оборачивает payload в этот тип (вместе с
/// `session_id` ноги, по которой он пришёл) и кладёт в сток
/// [`report_client_diag`], откуда серверный логгер пишет его в пер-сессионный файл.
#[derive(Debug, Clone)]
pub struct ClientDiagReport {
/// Идентификатор сессии (источник имени файла на сервере).
pub session_id: String,
/// Одна готовая JSON-строка снапшота (как прислал клиент).
pub json_line: String,
}
/// Sender end of the client-diagnostics sink. Set once by the server via
/// [`init_client_diag_sink`]; stays `None` on the client, where reports are dropped.
static GLOBAL_CLIENT_DIAG_TX: OnceLock<mpsc::UnboundedSender<ClientDiagReport>> = OnceLock::new();
/// Серверная сторона: вызвать один раз при старте. Возвращает приёмник
/// клиентских отчётов, который логгер держит и сливает в пер-сессионные файлы.
pub fn init_client_diag_sink() -> mpsc::UnboundedReceiver<ClientDiagReport> {
let (tx, rx) = mpsc::unbounded_channel();
let _ = GLOBAL_CLIENT_DIAG_TX.set(tx);
rx
}
/// Fire-and-forget: переслать клиентский отчёт в сток. No-op, если сток не поднят
/// (т.е. на клиенте) — ровно как [`send_diag_event`] относительно своего канала.
pub fn report_client_diag(report: ClientDiagReport) {
if let Some(tx) = GLOBAL_CLIENT_DIAG_TX.get() {
let _ = tx.send(report);
}
}
// ── Event types ───────────────────────────────────────────────────────────────
/// Событие, которое стало триггером снапшота (и единица потока событий).
@@ -184,6 +220,15 @@ pub struct DiagnosticsSnapshot {
pub error_totals: ErrorCounters,
}
impl DiagnosticsSnapshot {
/// Сериализует снапшот в одну компактную JSON-строку (без отступов и переводов
/// строк). Это формат, в котором клиент шлёт снапшот `Diag`-кадром, а сервер
/// дописывает его строкой в пер-сессионный JSONL-файл.
pub fn to_json_line(&self) -> String {
serde_json::to_string(self).unwrap_or_else(|e| format!("{{\"error\":\"{e}\"}}"))
}
}
// ── Atomic error counters (global, updated at event sites) ───────────────────
/// Глобальные атомарные счётчики событий, инкрементируемые в местах их