diff --git a/Cargo.lock b/Cargo.lock index e6c3330..71a9d42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,18 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -334,6 +346,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "deranged" version = "0.5.5" @@ -467,6 +494,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + [[package]] name = "font-kit" version = "0.14.3" @@ -668,6 +701,9 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] [[package]] name = "heck" @@ -1115,6 +1151,52 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +[[package]] +name = "metrics" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5312e9ba3771cfa961b585728215e3d972c950a3eed9252aa093d6301277e8" +dependencies = [ + "ahash", + "portable-atomic", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3589659543c04c7dc5526ec858591015b87cd8746583b51b48ef4353f99dbcda" +dependencies = [ + "base64", + "http-body-util", + "hyper", + "hyper-util", + "indexmap", + "ipnet", + "metrics", + "metrics-util", + "quanta", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "metrics-util" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdfb1365fea27e6dd9dc1dbc19f570198bc86914533ad639dae939635f096be4" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown", + "metrics", + "quanta", + "rand", + "rand_xoshiro", + "sketches-ddsketch", +] + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -1306,6 +1388,12 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "potential_utf" version = "0.1.4" @@ -1367,6 +1455,21 @@ dependencies = [ "url", ] +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quinn" version = "0.11.9" @@ -1466,6 +1569,24 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags 2.10.0", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -1749,6 +1870,7 @@ dependencies = [ "futures-util", "humantime", "jsonrpsee", + "metrics", "prometheus-http-client", "rand", "regex", @@ -1770,6 +1892,7 @@ version = "0.1.0" dependencies = [ "async-trait", "flate2", + "globset", "regex", "reqwest", "serde", @@ -1799,6 +1922,7 @@ dependencies = [ "chrono", "conf", "conf-extra", + "metrics", "reqwest", "serde", "serde_json", @@ -1822,6 +1946,7 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", + "metrics-exporter-prometheus", "reqwest", "serde", "serde_json", @@ -1873,6 +1998,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" +[[package]] +name = "sketches-ddsketch" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1e9a774a6c28142ac54bb25d25562e6bcf957493a184f15ad4eebccb23e410a" + [[package]] name = "slab" version = "0.4.11" @@ -2330,6 +2461,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 26031cb..71506eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,10 +39,13 @@ conf-extra = "0.1" displaydoc = "0.2" dotenvy = "0.15" futures-util = "0.3" +globset = "0.4" http = "1" http-body = "1" http-body-util = "0.1" humantime = "2" +metrics = "0.24" +metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] } hyper = { version = "1.7", features = ["server", "http1", "http2"] } hyper-util = { version = "0.1", features = ["tokio", "server", "server-auto"] } jsonrpsee = { version = "0.26", features = ["macros", "async-client"] } diff --git a/signal-gateway-app-code/Cargo.toml b/signal-gateway-app-code/Cargo.toml index 50dfd16..58e36db 100644 --- a/signal-gateway-app-code/Cargo.toml +++ b/signal-gateway-app-code/Cargo.toml @@ -11,6 +11,7 @@ signal-gateway-assistant = { workspace = true } async-trait = { workspace = true } flate2 = { workspace = true } +globset = { workspace = true } regex = { workspace = true } reqwest = { workspace = true, features = ["rustls-tls"] } serde = { workspace = true } diff --git a/signal-gateway-app-code/src/lib.rs b/signal-gateway-app-code/src/lib.rs index 303e7b7..aaae314 100644 --- a/signal-gateway-app-code/src/lib.rs +++ b/signal-gateway-app-code/src/lib.rs @@ -5,6 +5,7 @@ use async_trait::async_trait; use flate2::read::GzDecoder; +use globset::{Glob, GlobSet, GlobSetBuilder}; use regex::Regex; use serde::Deserialize; use signal_gateway_assistant::{Tool, ToolExecutor, ToolResult}; @@ -62,6 +63,15 @@ pub struct AppCodeConfig { /// Path to file containing the GitHub personal access token. /// Optional for public repositories (unauthenticated access has lower rate limits). pub token_file: Option, + /// Glob patterns to filter which files are included from the tarball. + /// If non-empty, only files matching at least one pattern are kept. + /// Uses gitignore-style glob syntax (e.g., "*.rs", "src/**/*.rs"). + #[serde(default)] + pub glob: Vec, + /// Include files that aren't valid UTF-8 (using lossy conversion). + /// By default (false), non-UTF-8 files are skipped entirely. + #[serde(default)] + pub include_non_utf8: bool, } /// A file stored in memory from the tarball. @@ -94,6 +104,7 @@ pub type ShaCallback = Arc< pub struct AppCode { config: AppCodeConfig, token: Option, + glob_filter: Option, get_sha: ShaCallback, client: reqwest::Client, cache: Mutex>, @@ -111,9 +122,26 @@ impl AppCode { .map(|path| std::fs::read_to_string(path).map(|s| s.trim().to_string())) .transpose()?; + // Compile glob patterns if any are specified + let glob_filter = if config.glob.is_empty() { + None + } else { + let mut builder = GlobSetBuilder::new(); + for pattern in &config.glob { + let glob = Glob::new(pattern).map_err(|e| { + std::io::Error::other(format!("invalid glob pattern '{}': {}", pattern, e)) + })?; + builder.add(glob); + } + Some(builder.build().map_err(|e| { + std::io::Error::other(format!("failed to build glob set: {}", e)) + })?) + }; + Ok(Self { config, token, + glob_filter, get_sha, client: reqwest::Client::new(), cache: Mutex::new(None), @@ -254,14 +282,32 @@ impl AppCode { continue; } + // Apply glob filter if configured + if let Some(ref glob_filter) = self.glob_filter { + if !glob_filter.is_match(&path) { + continue; + } + } + // Read file contents let mut contents = Vec::new(); if entry.read_to_end(&mut contents).is_err() { continue; // Skip files we can't read } - // Convert to string (lossy) - let content = String::from_utf8_lossy(&contents).to_string(); + // Convert to string, handling non-UTF-8 based on config + let content = match String::from_utf8(contents) { + Ok(s) => s, + Err(e) => { + if self.config.include_non_utf8 { + // Use lossy conversion if configured to include non-UTF-8 + String::from_utf8_lossy(e.as_bytes()).into_owned() + } else { + // Skip non-UTF-8 files by default + continue; + } + } + }; files.insert(path, CachedFile { content }); } diff --git a/signal-gateway-app-code/tests/integration.rs b/signal-gateway-app-code/tests/integration.rs index f781c03..a3c2bd2 100644 --- a/signal-gateway-app-code/tests/integration.rs +++ b/signal-gateway-app-code/tests/integration.rs @@ -13,6 +13,10 @@ const TEST_REPO: &str = "ver-stub-rs"; const TEST_SHA: &str = "79b98e25f27ae4f5dd73a5a3d8f37dad655a57e8"; fn create_test_app_code() -> AppCode { + create_test_app_code_with_glob(vec![]) +} + +fn create_test_app_code_with_glob(glob: Vec) -> AppCode { let config = AppCodeConfig { name: "test-app".to_string(), github: GitHubRepo { @@ -20,6 +24,8 @@ fn create_test_app_code() -> AppCode { repo: TEST_REPO.to_string(), }, token_file: None, // Public repo, no auth needed + glob, + include_non_utf8: false, }; let sha = TEST_SHA.to_string(); @@ -173,3 +179,78 @@ async fn test_search_no_matches() { assert!(result.contains("No matches"), "Should report no matches"); } + +#[tokio::test] +async fn test_glob_filter_rust_files_only() { + // Only include .rs files + let app = create_test_app_code_with_glob(vec!["**/*.rs".to_string()]); + + let result = app.find(Some("*")).await.expect("find failed"); + + // Should find Rust files + assert!(result.contains(".rs"), "Should contain .rs files"); + + // Should NOT find non-Rust files + assert!(!result.contains("Cargo.toml"), "Should not contain Cargo.toml"); + assert!(!result.contains("README.md"), "Should not contain README.md"); + assert!(!result.contains("tests.sh"), "Should not contain tests.sh"); +} + +#[tokio::test] +async fn test_glob_filter_specific_directory() { + // Only include files in ver-stub/src + let app = create_test_app_code_with_glob(vec!["ver-stub/src/**".to_string()]); + + let result = app.find(Some("*")).await.expect("find failed"); + + // Should find files in ver-stub/src + assert!( + result.contains("ver-stub/src/lib.rs"), + "Should contain ver-stub/src/lib.rs" + ); + + // Should NOT find files outside ver-stub/src + assert!( + !result.contains("ver-stub-build/"), + "Should not contain ver-stub-build files" + ); + assert!(!result.contains("Cargo.toml"), "Should not contain root Cargo.toml"); +} + +#[tokio::test] +async fn test_glob_filter_multiple_patterns() { + // Include both Cargo.toml files and shell scripts + let app = create_test_app_code_with_glob(vec![ + "**/Cargo.toml".to_string(), + "*.sh".to_string(), + ]); + + let result = app.find(Some("*")).await.expect("find failed"); + + // Should find Cargo.toml files + assert!(result.contains("Cargo.toml"), "Should contain Cargo.toml"); + + // Should find shell scripts + assert!(result.contains("tests.sh"), "Should contain tests.sh"); + + // Should NOT find other files + assert!(!result.contains("README.md"), "Should not contain README.md"); + assert!(!result.contains(".rs"), "Should not contain .rs files"); +} + +#[tokio::test] +async fn test_glob_filter_ls_shows_filtered_dirs() { + // Only include files in ver-stub directory + let app = create_test_app_code_with_glob(vec!["ver-stub/**".to_string()]); + + let result = app.ls(None).await.expect("ls failed"); + + // Root ls should only show ver-stub/ since other dirs are empty after filtering + assert!(result.contains("ver-stub/"), "Should show ver-stub/"); + + // Other directories should not appear (they have no matching files) + assert!( + !result.contains("ver-stub-build/"), + "Should not show ver-stub-build/" + ); +} diff --git a/signal-gateway-assistant/claude/Cargo.toml b/signal-gateway-assistant/claude/Cargo.toml index 6c8cd1d..61f7ba0 100644 --- a/signal-gateway-assistant/claude/Cargo.toml +++ b/signal-gateway-assistant/claude/Cargo.toml @@ -17,6 +17,7 @@ async-trait = { workspace = true } chrono = { workspace = true } conf = { workspace = true } conf-extra = { workspace = true } +metrics = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/signal-gateway-assistant/claude/src/lib.rs b/signal-gateway-assistant/claude/src/lib.rs index 6a563bd..84a7359 100644 --- a/signal-gateway-assistant/claude/src/lib.rs +++ b/signal-gateway-assistant/claude/src/lib.rs @@ -8,11 +8,21 @@ use api::{ }; use conf::Conf; use message_buffer::MessageBuffer; +use metrics::{counter, gauge}; use signal_gateway_assistant::{Assistant, AssistantResponse, ChatMessage, ToolExecutor}; use std::{path::PathBuf, sync::Weak, time::Duration}; use tokio_util::sync::CancellationToken; use tracing::{error, info, warn}; +// Metric names +const METRIC_API_CALLS: &str = "claude_api_calls_total"; +const METRIC_REQUEST_CHARS: &str = "claude_request_chars"; +const METRIC_CHARS_SENT: &str = "claude_chars_sent_total"; +const METRIC_TOOL_USE: &str = "claude_tool_use_total"; +const METRIC_BUFFER_SIZE: &str = "claude_buffer_size_chars"; +const METRIC_SUMMARY_SIZE: &str = "claude_summary_size_chars"; +const METRIC_COMPACTIONS: &str = "claude_compactions_total"; + /// The Anthropic API version header value. const ANTHROPIC_API_VERSION: &str = "2023-06-01"; @@ -146,6 +156,12 @@ impl ClaudeAssistant { }) } + /// Update the buffer and summary size gauge metrics. + fn update_size_metrics(&self) { + gauge!(METRIC_BUFFER_SIZE).set(self.messages.total_chars() as f64); + gauge!(METRIC_SUMMARY_SIZE).set(self.summary.len() as f64); + } + /// Check if automatic compaction should be triggered and handle it. async fn maybe_compact(&mut self) { let buffer_chars = self.messages.total_chars(); @@ -199,6 +215,10 @@ impl ClaudeAssistant { return; } + // Record compaction event + let compaction_type = if is_automatic { "automatic" } else { "manual" }; + counter!(METRIC_COMPACTIONS, "type" => compaction_type).increment(1); + if is_automatic { self.last_auto_compaction = Some(std::time::Instant::now()); } @@ -349,6 +369,12 @@ impl ClaudeAssistant { tools: tools.clone(), }; + // Calculate request size for metrics + let request_json = serde_json::to_string(&request_body).unwrap_or_default(); + let request_chars = request_json.len(); + gauge!(METRIC_REQUEST_CHARS).set(request_chars as f64); + counter!(METRIC_CHARS_SENT).increment(request_chars as u64); + let response = tokio::select! { result = self .client @@ -361,6 +387,9 @@ impl ClaudeAssistant { _ = cancel.cancelled() => return Ok(None), }; + // Record API call + counter!(METRIC_API_CALLS).increment(1); + if !response.status().is_success() { let error: ErrorResponse = response.json().await?; return Err(ClaudeError::ApiError(error.error.message)); @@ -386,6 +415,9 @@ impl ClaudeAssistant { return Ok(None); } + // Record tool use + counter!(METRIC_TOOL_USE, "tool" => name.to_string()).increment(1); + info!("Claude tool use: {}({})", name, input); let tool_result = tokio::select! { result = executor.execute(name, input) => result, @@ -442,6 +474,7 @@ impl Assistant for ClaudeAssistant { let mc = message_to_content(message); self.messages.push(mc); self.maybe_compact().await; + self.update_size_metrics(); } async fn prompt( @@ -454,15 +487,19 @@ impl Assistant for ClaudeAssistant { self.maybe_compact().await; if cancel.is_cancelled() { + self.update_size_metrics(); return Ok(None); } - Ok(self.handle_request(&cancel).await?) + let result = self.handle_request(&cancel).await?; + self.update_size_metrics(); + Ok(result) } async fn compact(&mut self, cancel: CancellationToken) { // Manual compaction always runs (is_automatic = false) self.do_compact(false, Some(&cancel)).await; + self.update_size_metrics(); } fn debug_log(&mut self) { diff --git a/signal-gateway-bin/Cargo.toml b/signal-gateway-bin/Cargo.toml index c54de95..f7a97b5 100644 --- a/signal-gateway-bin/Cargo.toml +++ b/signal-gateway-bin/Cargo.toml @@ -30,6 +30,7 @@ http-body = { workspace = true } http-body-util = { workspace = true } hyper = { workspace = true } hyper-util = { workspace = true } +metrics-exporter-prometheus = { workspace = true } reqwest = { workspace = true } url = { workspace = true } serde = { workspace = true } diff --git a/signal-gateway-bin/src/main.rs b/signal-gateway-bin/src/main.rs index b536fc3..08338d3 100644 --- a/signal-gateway-bin/src/main.rs +++ b/signal-gateway-bin/src/main.rs @@ -3,6 +3,7 @@ #![deny(missing_docs)] use conf::Conf; +use metrics_exporter_prometheus::PrometheusBuilder; use signal_gateway::{CommandRouter, Gateway, GatewayConfig, Handling}; use signal_gateway_app_code::AppCodeTools; use signal_gateway_assistant_claude::{ClaudeAssistant, ClaudeConfig}; @@ -38,6 +39,9 @@ pub struct Config { /// Socket to listen for HTTP requests (GET /health, POST /alert) #[conf(long, env, default_value = "0.0.0.0:8000")] http_listen_addr: SocketAddr, + /// Socket to listen for Prometheus metrics scraping. + #[conf(long, env, default_value = "0.0.0.0:9000")] + metrics_listen_addr: SocketAddr, #[conf(flatten, prefix)] syslog: Option, #[conf(flatten, prefix)] @@ -111,6 +115,16 @@ async fn main() -> Result<(), Box> { return Ok(()); } + // Initialize Prometheus metrics exporter + PrometheusBuilder::new() + .with_http_listener(config.metrics_listen_addr) + .install() + .expect("failed to install Prometheus metrics recorder"); + info!( + "Prometheus metrics endpoint at http://{}/metrics", + config.metrics_listen_addr + ); + let token = CancellationToken::new(); // Build the command router diff --git a/signal-gateway/Cargo.toml b/signal-gateway/Cargo.toml index d3bc919..d280701 100644 --- a/signal-gateway/Cargo.toml +++ b/signal-gateway/Cargo.toml @@ -23,6 +23,7 @@ displaydoc = { workspace = true } futures-util = { workspace = true } humantime = { workspace = true } jsonrpsee = { workspace = true } +metrics = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/signal-gateway/src/gateway/mod.rs b/signal-gateway/src/gateway/mod.rs index 932933e..210efee 100644 --- a/signal-gateway/src/gateway/mod.rs +++ b/signal-gateway/src/gateway/mod.rs @@ -16,6 +16,7 @@ use async_trait::async_trait; use chrono::Utc; use conf::{Conf, Subcommands}; use futures_util::FutureExt; +use metrics::counter; use prometheus_http_client::{AlertStatus, ExtractLabels}; use std::{ fmt::Write, @@ -31,6 +32,12 @@ use tokio::{ use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; +// Metric names +const METRIC_LOG_MESSAGES: &str = "gateway_log_messages_total"; +const METRIC_SIGNAL_RECEIVED: &str = "gateway_signal_messages_received_total"; +const METRIC_SIGNAL_SENT: &str = "gateway_signal_messages_sent_total"; +const METRIC_ALERTS: &str = "gateway_alerts_total"; + mod signal_trust_set; pub use signal_trust_set::SignalTrustSet; @@ -502,6 +509,7 @@ impl Gateway { message: message.clone(), attachments, }.send(signal_cli).await?; + counter!(METRIC_SIGNAL_SENT, "type" => "alert").increment(1); if let Some(assistant) = self.assistant.get() { assistant.record_message(SentBy::System, &message, Utc::now().timestamp_millis() as u64); @@ -523,6 +531,7 @@ impl Gateway { return Err(RpcClientError::ParseError(err)); } Some(Ok(msg)) => { + counter!(METRIC_SIGNAL_RECEIVED).increment(1); //info!("Signal Rx: {msg:?}"); let Some(data_message) = &msg.envelope.data_message else { debug!("Ignoring message which was not a data message: {msg:?}"); @@ -576,6 +585,7 @@ impl Gateway { message: sourced.resp.text.clone(), attachments, }.send(signal_cli).await?; + counter!(METRIC_SIGNAL_SENT, "type" => "response").increment(1); if let Some(assistant) = self.assistant.get() { assistant.record_message(sourced.source, &sourced.resp.text, Utc::now().timestamp_millis() as u64); @@ -840,6 +850,8 @@ impl Gateway { /// Handle a POST body from alertmanager pub async fn handle_alertmanager_post(&self, alert_msg: AlertPost) -> Result<(), &'static str> { + counter!(METRIC_ALERTS).increment(alert_msg.alerts.len() as u64); + let text = self .format_alert_text(&alert_msg) .unwrap_or_else(|err| format!("error formatting alert text: {err}:\n{alert_msg:#?}")); @@ -928,6 +940,8 @@ impl Gateway { /// Process an incoming log message, buffering it and potentially triggering an alert. pub async fn handle_log_message(&self, log_msg: impl Into) { + counter!(METRIC_LOG_MESSAGES).increment(1); + let mut log_msg = log_msg.into(); if let Some(n_fn) = self.path_normalization_fn.as_ref() && let Some(file) = log_msg.file.as_ref()