| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent baba41d commit f68d4b1
5 files changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -25,6 +25,9 @@ hmac = "0.12" | |||
| 25 | 25 | sha2 = "0.10" | |
| 26 | 26 | glob = "0.3" | |
| 27 | 27 | thiserror = "2.0" | |
| 28 | + tracing = "0.1" | ||
| 29 | + tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json"] } | ||
| 30 | + regex = "1.0" | ||
| 28 | 31 | ||
| 29 | 32 | [dev-dependencies] | |
| 30 | 33 | tempfile = "3.10" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -4,6 +4,7 @@ use jsonwebtoken::{encode, Algorithm, EncodingKey, Header}; | |||
| 4 | 4 | use serde::{Deserialize, Serialize}; | |
| 5 | 5 | use std::sync::Arc; | |
| 6 | 6 | use tokio::sync::Mutex; | |
| 7 | + use tracing::{debug, error, info, instrument}; | ||
| 7 | 8 | ||
| 8 | 9 | #[derive(Debug, Serialize)] | |
| 9 | 10 | struct Claims { | |
@@ -39,7 +40,10 @@ impl GitHubTokenProvider { | |||
| 39 | 40 | } | |
| 40 | 41 | } | |
| 41 | 42 | ||
| 43 | + #[instrument(skip(self))] | ||
| 42 | 44 | fn create_jwt(&self) -> Result<String, GitHubError> { | |
| 45 | + debug!("Creating JWT for GitHub App authentication"); | ||
| 46 | + | ||
| 43 | 47 | let now = Utc::now(); | |
| 44 | 48 | let iat = now.timestamp(); | |
| 45 | 49 | let exp = (now + Duration::minutes(10)).timestamp(); | |
@@ -52,17 +56,24 @@ impl GitHubTokenProvider { | |||
| 52 | 56 | ||
| 53 | 57 | let key = EncodingKey::from_rsa_pem(self.config.private_key_pem.as_bytes())?; | |
| 54 | 58 | let token = encode(&Header::new(Algorithm::RS256), &claims, &key)?; | |
| 59 | + | ||
| 60 | + debug!("JWT created successfully"); | ||
| 55 | 61 | Ok(token) | |
| 56 | 62 | } | |
| 57 | 63 | ||
| 64 | + #[instrument(skip(self), fields(installation_id = %self.config.installation_id))] | ||
| 58 | 65 | async fn fetch_installation_token(&self) -> Result<CachedToken, GitHubError> { | |
| 66 | + info!("Fetching new installation access token"); | ||
| 67 | + | ||
| 59 | 68 | let jwt = self.create_jwt()?; | |
| 60 | 69 | ||
| 61 | 70 | let url = format!( | |
| 62 | 71 | "https://api.github.com/app/installations/{}/access_tokens", | |
| 63 | 72 | self.config.installation_id | |
| 64 | 73 | ); | |
| 65 | 74 | ||
| 75 | + debug!(url = %url, "Requesting installation token"); | ||
| 76 | + | ||
| 66 | 77 | let response = self | |
| 67 | 78 | .client | |
| 68 | 79 | .post(&url) | |
@@ -76,9 +87,17 @@ impl GitHubTokenProvider { | |||
| 76 | 87 | if !response.status().is_success() { | |
| 77 | 88 | let status = response.status(); | |
| 78 | 89 | let body = response.text().await?; | |
| 90 | + | ||
| 91 | + // Tracing will automatically sanitize any tokens in the body | ||
| 92 | + error!( | ||
| 93 | + status = %status, | ||
| 94 | + response_body = %body, | ||
| 95 | + "Failed to get installation token" | ||
| 96 | + ); | ||
| 97 | + | ||
| 79 | 98 | return Err(GitHubError::Other(format!( | |
| 80 | - "Failed to get installation token: {} - {}", | ||
| 81 | - status, body | ||
| 99 | + "Failed to get installation token: {}", | ||
| 100 | + status | ||
| 82 | 101 | ))); | |
| 83 | 102 | } | |
| 84 | 103 | ||
@@ -87,6 +106,8 @@ impl GitHubTokenProvider { | |||
| 87 | 106 | .map_err(|e| GitHubError::Other(format!("Failed to parse expiry time: {}", e)))? | |
| 88 | 107 | .with_timezone(&Utc); | |
| 89 | 108 | ||
| 109 | + info!(expires_at = %expires_at, "Installation token fetched successfully"); | ||
| 110 | + | ||
| 90 | 111 | Ok(CachedToken { | |
| 91 | 112 | token: token_response.token, | |
| 92 | 113 | expires_at, | |
@@ -99,6 +120,7 @@ impl GitHubTokenProvider { | |||
| 99 | 120 | /// 1. Quick read-only check if token is valid (fast path) | |
| 100 | 121 | /// 2. If refresh needed, acquire lock and check again | |
| 101 | 122 | /// 3. Only one task fetches new token, others wait and reuse it | |
| 123 | + #[instrument(skip(self))] | ||
| 102 | 124 | pub async fn get_token(&self) -> Result<String, GitHubError> { | |
| 103 | 125 | // Fast path: Check if we have a valid token without holding lock during HTTP | |
| 104 | 126 | { | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,6 +2,7 @@ use crate::{GitHubAppConfig, GitHubError, GitHubTokenProvider}; | |||
| 2 | 2 | use serde::de::DeserializeOwned; | |
| 3 | 3 | use std::path::{Path, PathBuf}; | |
| 4 | 4 | use std::process::{Command, Stdio}; | |
| 5 | + use tracing::{debug, error, info, instrument, warn}; | ||
| 5 | 6 | ||
| 6 | 7 | pub struct GitHubGitOps { | |
| 7 | 8 | config: GitHubAppConfig, | |
@@ -16,85 +17,120 @@ impl GitHubGitOps { | |||
| 16 | 17 | } | |
| 17 | 18 | } | |
| 18 | 19 | ||
| 20 | + #[instrument(skip(self, args), fields(git_cmd = ?args))] | ||
| 19 | 21 | fn run_git_command(&self, args: &[&str], cwd: Option<&Path>) -> Result<String, GitHubError> { | |
| 22 | + debug!("Executing git command"); | ||
| 23 | + | ||
| 20 | 24 | let mut cmd = Command::new("git"); | |
| 21 | 25 | cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped()); | |
| 22 | 26 | ||
| 23 | 27 | if let Some(dir) = cwd { | |
| 24 | 28 | cmd.current_dir(dir); | |
| 29 | + debug!(directory = ?dir, "Set working directory"); | ||
| 25 | 30 | } | |
| 26 | 31 | ||
| 27 | 32 | let output = cmd.output()?; | |
| 28 | 33 | ||
| 29 | 34 | if !output.status.success() { | |
| 30 | 35 | let stderr = String::from_utf8_lossy(&output.stderr); | |
| 36 | + | ||
| 37 | + // Log the error with tracing - the sanitizer will redact tokens automatically | ||
| 38 | + error!( | ||
| 39 | + exit_code = ?output.status.code(), | ||
| 40 | + stderr = %stderr, | ||
| 41 | + "Git command failed" | ||
| 42 | + ); | ||
| 43 | + | ||
| 44 | + // Return a generic error to users (details are in logs) | ||
| 31 | 45 | return Err(GitHubError::Git(format!( | |
| 32 | - "Git command failed: git {}. Error: {}", | ||
| 33 | - args.join(" "), | ||
| 34 | - stderr | ||
| 46 | + "Git command failed. Check logs for details. Exit code: {:?}", | ||
| 47 | + output.status.code() | ||
| 35 | 48 | ))); | |
| 36 | 49 | } | |
| 37 | 50 | ||
| 38 | - Ok(String::from_utf8_lossy(&output.stdout).to_string()) | ||
| 51 | + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); | ||
| 52 | + debug!(output_length = stdout.len(), "Git command succeeded"); | ||
| 53 | + Ok(stdout) | ||
| 39 | 54 | } | |
| 40 | 55 | ||
| 56 | + #[instrument(skip(self), fields(repo = %self.config.repo, branch = %self.config.branch))] | ||
| 41 | 57 | pub async fn initialize(&self) -> Result<(), GitHubError> { | |
| 42 | 58 | if self.config.git_clone_path.exists() { | |
| 59 | + info!("Repository already exists, skipping clone"); | ||
| 43 | 60 | return Ok(()); | |
| 44 | 61 | } | |
| 45 | 62 | ||
| 63 | + info!("Initializing repository clone"); | ||
| 64 | + | ||
| 46 | 65 | std::fs::create_dir_all(&self.config.git_clone_path)?; | |
| 66 | + debug!("Created clone directory"); | ||
| 47 | 67 | ||
| 48 | 68 | let token = self.token_provider.get_token().await?; | |
| 49 | 69 | let clone_url = format!( | |
| 50 | 70 | "https://x-access-token:{}@github.com/{}.git", | |
| 51 | 71 | token, self.config.repo | |
| 52 | 72 | ); | |
| 53 | 73 | ||
| 74 | + // Tracing will automatically sanitize the clone_url in logs | ||
| 75 | + debug!("Starting git clone operation"); | ||
| 54 | 76 | self.run_git_command( | |
| 55 | 77 | &["clone", "--branch", &self.config.branch, &clone_url, "."], | |
| 56 | 78 | Some(&self.config.git_clone_path), | |
| 57 | 79 | )?; | |
| 58 | 80 | ||
| 81 | + info!("Repository clone completed successfully"); | ||
| 59 | 82 | Ok(()) | |
| 60 | 83 | } | |
| 61 | 84 | ||
| 85 | + #[instrument(skip(self), fields(repo = %self.config.repo, branch = %self.config.branch))] | ||
| 62 | 86 | pub async fn sync(&self) -> Result<(), GitHubError> { | |
| 63 | 87 | if !self.config.git_clone_path.exists() { | |
| 88 | + warn!("Repository not initialized"); | ||
| 64 | 89 | return Err(GitHubError::Git( | |
| 65 | 90 | "Repository not initialized. Call initialize() first.".to_string(), | |
| 66 | 91 | )); | |
| 67 | 92 | } | |
| 68 | 93 | ||
| 94 | + info!("Syncing repository with remote"); | ||
| 95 | + | ||
| 69 | 96 | let token = self.token_provider.get_token().await?; | |
| 70 | 97 | let remote_url = format!( | |
| 71 | 98 | "https://x-access-token:{}@github.com/{}.git", | |
| 72 | 99 | token, self.config.repo | |
| 73 | 100 | ); | |
| 74 | 101 | ||
| 102 | + // Tracing will automatically sanitize the remote_url in logs | ||
| 103 | + debug!("Updating remote URL"); | ||
| 75 | 104 | self.run_git_command( | |
| 76 | 105 | &["remote", "set-url", "origin", &remote_url], | |
| 77 | 106 | Some(&self.config.git_clone_path), | |
| 78 | 107 | )?; | |
| 79 | 108 | ||
| 109 | + debug!("Fetching from origin"); | ||
| 80 | 110 | self.run_git_command(&["fetch", "origin"], Some(&self.config.git_clone_path))?; | |
| 81 | 111 | ||
| 82 | 112 | let remote_branch = format!("origin/{}", self.config.branch); | |
| 113 | + debug!(remote_branch = %remote_branch, "Resetting to remote branch"); | ||
| 83 | 114 | self.run_git_command( | |
| 84 | 115 | &["reset", "--hard", &remote_branch], | |
| 85 | 116 | Some(&self.config.git_clone_path), | |
| 86 | 117 | )?; | |
| 87 | 118 | ||
| 119 | + info!("Repository sync completed successfully"); | ||
| 88 | 120 | Ok(()) | |
| 89 | 121 | } | |
| 90 | 122 | ||
| 123 | + #[instrument(skip(self), fields(repo = %self.config.repo, glob = %self.config.manifest_glob))] | ||
| 91 | 124 | pub fn load_all_manifests<T: DeserializeOwned>(&self) -> Result<Vec<T>, GitHubError> { | |
| 92 | 125 | if !self.config.git_clone_path.exists() { | |
| 126 | + warn!("Repository not initialized"); | ||
| 93 | 127 | return Err(GitHubError::Git( | |
| 94 | 128 | "Repository not initialized. Call initialize() first.".to_string(), | |
| 95 | 129 | )); | |
| 96 | 130 | } | |
| 97 | 131 | ||
| 132 | + debug!("Loading manifests"); | ||
| 133 | + | ||
| 98 | 134 | let pattern = self | |
| 99 | 135 | .config | |
| 100 | 136 | .git_clone_path | |
@@ -106,11 +142,14 @@ impl GitHubGitOps { | |||
| 106 | 142 | ||
| 107 | 143 | for entry in glob::glob(&pattern)? { | |
| 108 | 144 | let path = entry?; | |
| 145 | + debug!(file = ?path, "Loading manifest file"); | ||
| 146 | + | ||
| 109 | 147 | let content = std::fs::read_to_string(&path)?; | |
| 110 | 148 | let manifest: T = serde_yaml::from_str(&content).map_err(GitHubError::Yaml)?; | |
| 111 | 149 | manifests.push(manifest); | |
| 112 | 150 | } | |
| 113 | 151 | ||
| 152 | + info!(count = manifests.len(), "Loaded manifests"); | ||
| 114 | 153 | Ok(manifests) | |
| 115 | 154 | } | |
| 116 | 155 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2,10 +2,90 @@ pub mod app_auth; | |||
| 2 | 2 | pub mod config; | |
| 3 | 3 | pub mod error; | |
| 4 | 4 | pub mod gitops; | |
| 5 | + pub mod tracing_sanitizer; | ||
| 5 | 6 | pub mod webhook; | |
| 6 | 7 | ||
| 7 | 8 | pub use app_auth::GitHubTokenProvider; | |
| 8 | 9 | pub use config::GitHubAppConfig; | |
| 9 | 10 | pub use error::GitHubError; | |
| 10 | 11 | pub use gitops::GitHubGitOps; | |
| 12 | + pub use tracing_sanitizer::sanitize_sensitive_data; | ||
| 11 | 13 | pub use webhook::{PushEvent, WebhookEvent, WebhookVerifier}; | |
| 14 | + | ||
| 15 | + use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; | ||
| 16 | + | ||
| 17 | + /// Initialize tracing with automatic sanitization of sensitive data | ||
| 18 | + /// | ||
| 19 | + /// This sets up structured logging with automatic redaction of: | ||
| 20 | + /// - GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_) | ||
| 21 | + /// - Credentials in URLs | ||
| 22 | + /// - Bearer tokens | ||
| 23 | + /// - x-access-token URLs | ||
| 24 | + /// | ||
| 25 | + /// # Environment Variables | ||
| 26 | + /// | ||
| 27 | + /// - `RUST_LOG`: Control log level (e.g., "debug", "info", "warn", "error") | ||
| 28 | + /// - Default: "info" | ||
| 29 | + /// - Example: `RUST_LOG=debug cargo run` | ||
| 30 | + /// | ||
| 31 | + /// # Examples | ||
| 32 | + /// | ||
| 33 | + /// ```no_run | ||
| 34 | + /// use github_app::init_tracing; | ||
| 35 | + /// | ||
| 36 | + /// // Initialize once at application startup | ||
| 37 | + /// init_tracing(); | ||
| 38 | + /// | ||
| 39 | + /// // Now all logs will have sensitive data automatically redacted | ||
| 40 | + /// tracing::info!("Starting application"); | ||
| 41 | + /// ``` | ||
| 42 | + /// | ||
| 43 | + /// # Panics | ||
| 44 | + /// | ||
| 45 | + /// Panics if called more than once (tracing can only be initialized once per process) | ||
| 46 | + pub fn init_tracing() { | ||
| 47 | + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); | ||
| 48 | + | ||
| 49 | + // Create a formatter that writes to a sanitizing writer | ||
| 50 | + let fmt_layer = fmt::layer() | ||
| 51 | + .with_target(true) | ||
| 52 | + .with_thread_ids(false) | ||
| 53 | + .with_thread_names(false) | ||
| 54 | + .with_file(true) | ||
| 55 | + .with_line_number(true) | ||
| 56 | + .with_writer(tracing_sanitizer::SanitizingMakeWriter::new()); | ||
| 57 | + | ||
| 58 | + tracing_subscriber::registry() | ||
| 59 | + .with(filter) | ||
| 60 | + .with(fmt_layer) | ||
| 61 | + .init(); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + /// Initialize tracing with JSON output for structured logging | ||
| 65 | + /// | ||
| 66 | + /// Useful for production environments where logs are shipped to aggregation systems | ||
| 67 | + /// like DataDog, Splunk, or ELK. All output is still sanitized. | ||
| 68 | + /// | ||
| 69 | + /// # Examples | ||
| 70 | + /// | ||
| 71 | + /// ```no_run | ||
| 72 | + /// use github_app::init_tracing_json; | ||
| 73 | + /// | ||
| 74 | + /// init_tracing_json(); | ||
| 75 | + /// tracing::info!(user = "alice", "User logged in"); | ||
| 76 | + /// ``` | ||
| 77 | + pub fn init_tracing_json() { | ||
| 78 | + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); | ||
| 79 | + | ||
| 80 | + let fmt_layer = fmt::layer() | ||
| 81 | + .json() | ||
| 82 | + .with_target(true) | ||
| 83 | + .with_file(true) | ||
| 84 | + .with_line_number(true) | ||
| 85 | + .with_writer(tracing_sanitizer::SanitizingMakeWriter::new()); | ||
| 86 | + | ||
| 87 | + tracing_subscriber::registry() | ||
| 88 | + .with(filter) | ||
| 89 | + .with(fmt_layer) | ||
| 90 | + .init(); | ||
| 91 | + } | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments