Starting impl Sentry and tracing #3

Merged
qpismont merged 6 commits from tracing into main 2026-06-10 20:27:26 +02:00
10 changed files with 1456 additions and 84 deletions
Showing only changes of commit 39c2afa0a7 - Show all commits
+1
View File
@@ -11,6 +11,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
curl \ curl \
git \ git \
build-essential \ build-essential \
libssl-dev \
cmake \ cmake \
pkg-config \ pkg-config \
clang \ clang \
Generated
+1350 -16
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -11,8 +11,14 @@ tokio-util = "0.7"
futures-util = "0.3" futures-util = "0.3"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
sentry = { version = "0.48", features = ["tower-axum-matched-path"] }
Review

La dépendance sentry est configurée avec la feature tower-axum-matched-path, mais le fichier Cargo.lock inclut également sentry-actix, ce qui suggère que les features par défaut ne sont pas désactivées. Pour éviter de tirer des dépendances inutiles (comme actix), il est recommandé d'ajouter default-features = false. Exemple : sentry = { version = "0.48", default-features = false, features = ["tower-axum-matched-path"] }.

La dépendance `sentry` est configurée avec la feature `tower-axum-matched-path`, mais le fichier Cargo.lock inclut également `sentry-actix`, ce qui suggère que les features par défaut ne sont pas désactivées. Pour éviter de tirer des dépendances inutiles (comme `actix`), il est recommandé d'ajouter `default-features = false`. Exemple : `sentry = { version = "0.48", default-features = false, features = ["tower-axum-matched-path"] }`.
sentry-anyhow = "0.48"
openrouter-rs = "0.10" openrouter-rs = "0.10"
dotenvy = "0.15" dotenvy = "0.15"
tower = "0.5"
tower-http = {version = "0.6", features = ["trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features=["env-filter"] }
axum = "0.8" axum = "0.8"
anyhow = "1.0" anyhow = "1.0"
thiserror = "2.0" thiserror = "2.0"
+9 -1
View File
@@ -1,13 +1,17 @@
use axum::body::{Bytes, to_bytes}; use axum::body::{Body, Bytes, to_bytes};
use axum::extract::{FromRef, FromRequest, State}; use axum::extract::{FromRef, FromRequest, State};
Review

Le fichier importe des éléments d'Axum, alors que le projet semble utiliser Actix-Web (présent dans Cargo.lock). Cette incohérence peut causer des erreurs de compilation ou des comportements inattendus. Vérifiez si le projet est en cours de migration vers Axum ou s'il s'agit d'une erreur. Dans tous les cas, un seul framework HTTP doit être utilisé.

Le fichier importe des éléments d'Axum, alors que le projet semble utiliser Actix-Web (présent dans Cargo.lock). Cette incohérence peut causer des erreurs de compilation ou des comportements inattendus. Vérifiez si le projet est en cours de migration vers Axum ou s'il s'agit d'une erreur. Dans tous les cas, un seul framework HTTP doit être utilisé.
use axum::http::Request;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::{Json, Router}; use axum::{Json, Router};
use hmac::{Hmac, KeyInit, Mac}; use hmac::{Hmac, KeyInit, Mac};
use reqwest::StatusCode; use reqwest::StatusCode;
use sentry::integrations::tower::NewSentryLayer;
use serde_json::Value; use serde_json::Value;
use sha2::Sha256; use sha2::Sha256;
use subtle::ConstantTimeEq; use subtle::ConstantTimeEq;
use tower_http::trace::TraceLayer;
use tracing::info;
use crate::consts::{GITEA_EVENT_TYPE_HEADER_NAME, GITEA_SIG_HEADER_NAME, MAX_WEBHOOK_BODY_SIZE}; use crate::consts::{GITEA_EVENT_TYPE_HEADER_NAME, GITEA_SIG_HEADER_NAME, MAX_WEBHOOK_BODY_SIZE};
use crate::errors::AppError; use crate::errors::AppError;
@@ -18,11 +22,15 @@ pub async fn start(app_state: AppState) -> anyhow::Result<()> {
let http_port = app_state.config.http_port; let http_port = app_state.config.http_port;
let app = Router::new() let app = Router::new()
.layer(NewSentryLayer::<Request<Body>>::new_from_top())
.layer(TraceLayer::new_for_http())
.route("/", get(root)) .route("/", get(root))
.route("/webhook", post(webhook)) .route("/webhook", post(webhook))
.with_state(app_state); .with_state(app_state);
let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", http_port)).await?; let listener = tokio::net::TcpListener::bind(format!("0.0.0.0:{}", http_port)).await?;
info!("Listening API on port {}", http_port);
axum::serve(listener, app) axum::serve(listener, app)
.await .await
.map_err(anyhow::Error::from) .map_err(anyhow::Error::from)
+8 -40
View File
@@ -1,5 +1,6 @@
use serde::Deserialize; use serde::Deserialize;
use std::time::Duration; use std::time::Duration;
use tracing::{error, info};
use crate::{ use crate::{
env::EnvConfig, env::EnvConfig,
@@ -22,44 +23,6 @@ pub struct ReviewItem {
pub message: String, pub message: String,
} }
/// Map a filename to a markdown language identifier for syntax highlighting.
fn lang_from_filename(filename: &str) -> &str {
match std::path::Path::new(filename)
.extension()
.and_then(|e| e.to_str())
.unwrap_or("")
{
"rs" => "rust",
"py" => "python",
"js" | "mjs" => "javascript",
"ts" => "typescript",
"jsx" => "jsx",
"tsx" => "tsx",
"go" => "go",
"java" => "java",
"kt" | "kts" => "kotlin",
"scala" => "scala",
"c" | "h" => "c",
"cpp" | "cc" | "cxx" | "hpp" | "hxx" => "cpp",
"rb" => "ruby",
"php" => "php",
"swift" => "swift",
"sh" | "bash" | "zsh" => "bash",
"sql" => "sql",
"html" | "htm" => "html",
"css" => "css",
"scss" | "sass" => "scss",
"json" => "json",
"yaml" | "yml" => "yaml",
"xml" => "xml",
"toml" => "toml",
"md" | "mdx" => "markdown",
"dockerfile" | "Dockerfile" => "dockerfile",
"Makefile" => "makefile",
_ => "",
}
}
pub struct Bot { pub struct Bot {
config: EnvConfig, config: EnvConfig,
gitea_api: GiteaAPI, gitea_api: GiteaAPI,
@@ -90,6 +53,8 @@ impl Bot {
&self, &self,
mut rx: tokio::sync::mpsc::Receiver<WebhookType>, mut rx: tokio::sync::mpsc::Receiver<WebhookType>,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
info!("Bot started");
while let Some(wb) = rx.recv().await { while let Some(wb) = rx.recv().await {
self.exec(wb).await; self.exec(wb).await;
} }
1
@@ -110,8 +75,11 @@ impl Bot {
.await; .await;
match exec_result { match exec_result {
Ok(_) => println!("Task completed"), Ok(_) => info!("Task completed"),
Err(err) => println!("{}", err), Err(err) => {
error!("{}", err);
sentry_anyhow::capture_anyhow(&err);
}
} }
} }
} }
+20 -15
View File
@@ -16,7 +16,7 @@ pub async fn exec_review(
http_client: &reqwest::Client, http_client: &reqwest::Client,
model: &str, model: &str,
review_payload: ReviewPayload, review_payload: ReviewPayload,
) -> Result<(), AppError> { ) -> anyhow::Result<()> {
let new_comment = gitea_api let new_comment = gitea_api
.comment( .comment(
&BOT_PROCESS_MSG.replace("{model}", model), &BOT_PROCESS_MSG.replace("{model}", model),
@@ -41,9 +41,12 @@ pub async fn exec_review(
review_result.cost = chat_result.cost; review_result.cost = chat_result.cost;
let final_review_markdown = review_result_to_markdown(&review_result);
gitea_api gitea_api
.post_pull_request_review( .post_pull_request_review(
&review_result, &review_result,
&final_review_markdown,
&review_payload.repository.full_name, &review_payload.repository.full_name,
review_payload.pull_request.number, review_payload.pull_request.number,
) )
@@ -53,20 +56,22 @@ pub async fn exec_review(
} }
.await; .await;
let edit_msg = match bot_result { match bot_result {
Ok(bot_result) => review_result_to_markdown(&bot_result), Ok(_) => {
Err(e) => format!("Error while reviewing: {}", e), gitea_api
}; .delete_comment(&review_payload.repository.full_name, new_comment.id)
.await
gitea_api }
.edit_comment( Err(e) => {
&edit_msg, gitea_api
&review_payload.repository.full_name, .edit_comment(
new_comment.id, &format!("Error while reviewing: {}", e),
) &review_payload.repository.full_name,
.await?; new_comment.id,
)
Ok(()) .await
}
}
} }
fn review_result_to_markdown(review_result: &ReviewResult) -> String { fn review_result_to_markdown(review_result: &ReviewResult) -> String {
+1 -4
View File
@@ -1,5 +1,4 @@
use anyhow::anyhow; use anyhow::anyhow;
use dotenvy::dotenv;
#[derive(Clone)] #[derive(Clone)]
pub struct EnvConfig { pub struct EnvConfig {
@@ -15,8 +14,6 @@ pub struct EnvConfig {
} }
pub fn load_config() -> anyhow::Result<EnvConfig> { pub fn load_config() -> anyhow::Result<EnvConfig> {
dotenv().ok();
let http_port = try_get_env("HTTP_PORT")?.parse()?; let http_port = try_get_env("HTTP_PORT")?.parse()?;
let bot_name = try_get_env("BOT_NAME")?; let bot_name = try_get_env("BOT_NAME")?;
let webhook_secret = try_get_env("WEBHOOK_SIG_HEADER_SECRET")?; let webhook_secret = try_get_env("WEBHOOK_SIG_HEADER_SECRET")?;
@@ -40,7 +37,7 @@ pub fn load_config() -> anyhow::Result<EnvConfig> {
}) })
} }
fn try_get_env(key: &str) -> anyhow::Result<String> { pub fn try_get_env(key: &str) -> anyhow::Result<String> {
let env = std::env::var(key)?; let env = std::env::var(key)?;
if env.trim().is_empty() { if env.trim().is_empty() {
+7 -4
View File
@@ -54,10 +54,13 @@ impl IntoResponse for AppError {
StatusCode::UNAUTHORIZED, StatusCode::UNAUTHORIZED,
"WebHook sig header is invalid".to_string(), "WebHook sig header is invalid".to_string(),
), ),
AppError::Other(_) => ( AppError::Other(err) => {
StatusCode::INTERNAL_SERVER_ERROR, sentry_anyhow::capture_anyhow(&err);
"Internal server error".to_string(), (
), StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_string(),
)
}
} }
.into_response() .into_response()
} }
+21 -2
View File
@@ -84,9 +84,28 @@ impl GiteaAPI {
Ok(()) Ok(())
} }
pub async fn delete_comment(&self, full_name: &str, comment_id: u64) -> anyhow::Result<()> {
let url = format!(
"{}/api/v1/repos/{}/issues/comments/{}",
self.base_url, full_name, comment_id
);
let res = self.client.delete(url).send().await?;
if !res.status().is_success() {
return Err(anyhow::anyhow!(
"Failed to delete comment: {}",
res.status()
));
}
Ok(())
}
pub async fn post_pull_request_review( pub async fn post_pull_request_review(
&self, &self,
review_result: &ReviewResult, review_result: &ReviewResult,
final_comment: &str,
full_name: &str, full_name: &str,
index: u64, index: u64,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
@@ -95,7 +114,7 @@ impl GiteaAPI {
self.base_url, full_name, index self.base_url, full_name, index
); );
let comments = &&review_result let comments = &review_result
.reviews .reviews
.iter() .iter()
.filter(|r| r.line.is_some()) .filter(|r| r.line.is_some())
@@ -117,7 +136,7 @@ impl GiteaAPI {
.post(url) .post(url)
.json(&json!({ .json(&json!({
"event": "COMMENT", "event": "COMMENT",
"body": review_result.comment, "body": final_comment,
"comments": comments "comments": comments
})) }))
.send() .send()
+33 -2
View File
@@ -1,4 +1,7 @@
use crate::{bot::Bot, gitea::WebhookType, state::AppState}; use crate::{bot::Bot, gitea::WebhookType, state::AppState};
use dotenvy::dotenv;
use tracing::{info, warn};
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
mod api; mod api;
mod bot; mod bot;
@@ -10,8 +13,36 @@ mod gitea;
mod open_router; mod open_router;
mod state; mod state;
#[tokio::main] fn main() -> anyhow::Result<()> {
async fn main() -> anyhow::Result<()> { dotenv().ok();
tracing_subscriber::registry()
.with(fmt::layer())
.with(
EnvFilter::try_from_default_env() // lit RUST_LOG depuis l'env
.unwrap_or_else(|_| EnvFilter::new("info")),
)
.init();
if let Ok(sentry_dsn) = env::try_get_env("SENTRY_DSN") {
info!("Initialize sentry");
let _guard = sentry::init((
sentry_dsn,
sentry::ClientOptions {
release: sentry::release_name!(),
send_default_pii: true,
..Default::default()
},
));
} else {
warn!("SENTRY_DSN not set, sentry will not be initialized");
}
tokio::runtime::Runtime::new()?.block_on(run())
}
async fn run() -> anyhow::Result<()> {
let config = env::load_config()?; let config = env::load_config()?;
let bot = Bot::new(config.clone())?; let bot = Bot::new(config.clone())?;