From 59ec959ff62d6f2874955579d0645aa94ccf23df Mon Sep 17 00:00:00 2001 From: rizkidarmawan21 Date: Sun, 18 Jan 2026 11:58:30 +0700 Subject: [PATCH 1/2] feat: add HTTP outbound logging for Telegram API requests - Add JSON formatted logging for all Telegram API HTTP requests - Log request: status, method, url, body - Log response with elapsed_ms - Mask bot token in URL for security - Add serde_json dependency for JSON logging --- pentaract/Cargo.toml | 1 + pentaract/src/common/telegram_api/bot_api.rs | 177 +++++++++++++++++-- pentaract/src/common/telegram_api/schemas.rs | 1 + 3 files changed, 163 insertions(+), 16 deletions(-) diff --git a/pentaract/Cargo.toml b/pentaract/Cargo.toml index 13eed2df..c6fc6264 100644 --- a/pentaract/Cargo.toml +++ b/pentaract/Cargo.toml @@ -16,6 +16,7 @@ tower-http = { version = "0.4.4", features = ["fs", "trace", "cors"], default_fe # serialization/deserialization serde = { version = "1.0.189", features = ["derive"] } +serde_json = "1.0" # auth pwhash = "1.0.0" diff --git a/pentaract/src/common/telegram_api/bot_api.rs b/pentaract/src/common/telegram_api/bot_api.rs index 793b3591..186a1a49 100644 --- a/pentaract/src/common/telegram_api/bot_api.rs +++ b/pentaract/src/common/telegram_api/bot_api.rs @@ -1,4 +1,7 @@ +use std::time::Instant; + use reqwest::multipart; +use serde_json::json; use uuid::Uuid; use crate::{ @@ -21,12 +24,29 @@ impl<'t> TelegramBotApi<'t> { } } + /// Masks the bot token in URL for safe logging + fn mask_url(&self, url: &str) -> String { + // Replace bot token with *** + if let Some(bot_idx) = url.find("/bot") { + if let Some(slash_idx) = url[bot_idx + 4..].find('/') { + let masked = format!( + "{}/bot***{}", + &url[..bot_idx], + &url[bot_idx + 4 + slash_idx..] + ); + return masked; + } + } + url.to_string() + } + pub async fn upload( &self, file: &[u8], chat_id: ChatId, storage_id: Uuid, ) -> PentaractResult { + let original_chat_id = chat_id; let chat_id = { // inserting 100 between minus sign and chat id // cause telegram devs are complete retards and it works this way only @@ -39,23 +59,70 @@ impl<'t> TelegramBotApi<'t> { let token = self.scheduler.get_token(storage_id).await?; let url = self.build_url("", "sendDocument", token); + let masked_url = self.mask_url(&url); let file_part = multipart::Part::bytes(file.to_vec()).file_name("pentaract_chunk.bin"); let form = multipart::Form::new() .text("chat_id", chat_id.to_string()) .part("document", file_part); + let start = Instant::now(); let response = reqwest::Client::new() - .post(url) + .post(&url) .multipart(form) .send() .await?; + let elapsed_ms = start.elapsed().as_millis() as u64; + + let status = response.status(); - match response.error_for_status() { - // https://stackoverflow.com/a/32679930/12255756 - Ok(r) => Ok(r.json::().await?.result.document), - Err(e) => Err(e.into()), + if !status.is_success() { + let error_body = response.text().await.unwrap_or_default(); + tracing::error!( + target: "http_outbound", + "{}", + json!({ + "status": status.as_u16(), + "method": "POST", + "url": masked_url, + "body": { + "chat_id": chat_id, + "original_chat_id": original_chat_id, + "file_size_bytes": file.len(), + "storage_id": storage_id.to_string() + }, + "response": error_body, + "elapsed_ms": elapsed_ms + }) + ); + return Err(crate::errors::PentaractError::TelegramAPIError( + format!("{}: {}", status, error_body) + )); } + + let result = response.json::().await?; + + tracing::info!( + target: "http_outbound", + "{}", + json!({ + "status": status.as_u16(), + "method": "POST", + "url": masked_url, + "body": { + "chat_id": chat_id, + "original_chat_id": original_chat_id, + "file_size_bytes": file.len(), + "storage_id": storage_id.to_string() + }, + "response": { + "telegram_file_id": result.result.document.file_id + }, + "elapsed_ms": elapsed_ms + }) + ); + + Ok(result.result.document) } pub async fn download( @@ -63,26 +130,104 @@ impl<'t> TelegramBotApi<'t> { telegram_file_id: &str, storage_id: Uuid, ) -> PentaractResult> { - // getting file path + // Step 1: Get file path from Telegram let token = self.scheduler.get_token(storage_id).await?; let url = self.build_url("", "getFile", token); + let masked_url = self.mask_url(&url); + // TODO: add retries with their number taking from env - let body: DownloadBodySchema = reqwest::Client::new() - .get(url) + let start = Instant::now(); + let response = reqwest::Client::new() + .get(&url) .query(&[("file_id", telegram_file_id)]) .send() - .await? - .json() .await?; + let elapsed_ms = start.elapsed().as_millis() as u64; - // downloading the file itself + let status = response.status(); + + if !status.is_success() { + let error_body = response.text().await.unwrap_or_default(); + tracing::error!( + target: "http_outbound", + "{}", + json!({ + "status": status.as_u16(), + "method": "GET", + "url": format!("{}?file_id={}", masked_url, telegram_file_id), + "body": null, + "response": error_body, + "elapsed_ms": elapsed_ms + }) + ); + return Err(crate::errors::PentaractError::TelegramAPIError( + format!("{}: {}", status, error_body) + )); + } + + let body: DownloadBodySchema = response.json().await?; + + tracing::info!( + target: "http_outbound", + "{}", + json!({ + "status": status.as_u16(), + "method": "GET", + "url": format!("{}?file_id={}", masked_url, telegram_file_id), + "body": null, + "response": { + "file_path": body.result.file_path, + "file_size": body.result.file_size + }, + "elapsed_ms": elapsed_ms + }) + ); + + // Step 2: Download the file itself let token = self.scheduler.get_token(storage_id).await?; let url = self.build_url("file/", &body.result.file_path, token); - let file = reqwest::get(url) - .await? - .bytes() - .await - .map(|file| file.to_vec())?; + let masked_url = self.mask_url(&url); + + let start = Instant::now(); + let response = reqwest::get(&url).await?; + let elapsed_ms = start.elapsed().as_millis() as u64; + + let status = response.status(); + if !status.is_success() { + let error_body = response.text().await.unwrap_or_default(); + tracing::error!( + target: "http_outbound", + "{}", + json!({ + "status": status.as_u16(), + "method": "GET", + "url": masked_url, + "body": null, + "response": error_body, + "elapsed_ms": elapsed_ms + }) + ); + return Err(crate::errors::PentaractError::TelegramAPIError( + format!("{}: {}", status, error_body) + )); + } + + let file = response.bytes().await.map(|file| file.to_vec())?; + + tracing::info!( + target: "http_outbound", + "{}", + json!({ + "status": status.as_u16(), + "method": "GET", + "url": masked_url, + "body": null, + "response": { + "downloaded_bytes": file.len() + }, + "elapsed_ms": elapsed_ms + }) + ); Ok(file) } diff --git a/pentaract/src/common/telegram_api/schemas.rs b/pentaract/src/common/telegram_api/schemas.rs index 715fdcea..1059ebfd 100644 --- a/pentaract/src/common/telegram_api/schemas.rs +++ b/pentaract/src/common/telegram_api/schemas.rs @@ -23,4 +23,5 @@ pub struct DownloadBodySchema { #[derive(Deserialize)] pub struct DownloadSchema { pub file_path: String, + pub file_size: Option, } From 88ee17f14f0e01f50c830a92b6c8e811c09522f9 Mon Sep 17 00:00:00 2001 From: rizkidarmawan21 Date: Sun, 18 Jan 2026 12:05:39 +0700 Subject: [PATCH 2/2] chore: update Cargo.lock to version 4 and add serde_json dependency --- pentaract/Cargo.lock | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pentaract/Cargo.lock b/pentaract/Cargo.lock index 288d7442..a08179dd 100644 --- a/pentaract/Cargo.lock +++ b/pentaract/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1237,6 +1237,7 @@ dependencies = [ "pwhash", "reqwest", "serde", + "serde_json", "sqlx", "thiserror", "tokio",