enya_analyzer/scanner/mod.rs
1//! Language-agnostic scanner framework.
2//!
3//! Provides a trait-based architecture for scanning source files to discover
4//! metric instrumentation points, usage sites, and alert rule definitions. Each
5//! language/library combination can implement the [`Scanner`] trait to support
6//! different ecosystems.
7//!
8//! # Architecture
9//!
10//! - [`Scanner`] - Trait for language-specific scanners
11//! - [`ScannerRegistry`] - Collection of registered scanners
12//! - [`MetricInstrumentation`] - Language-agnostic metric definition
13//! - [`MetricUsage`] - Where a metric is recorded/updated (hot paths)
14//! - [`MetricKind`] - Counter, Gauge, or Histogram
15//! - [`AlertRule`] - Prometheus alert rule definition
16
17mod go;
18mod javascript;
19mod python;
20mod rust;
21mod typescript;
22mod yaml;
23
24pub use go::GoPrometheusScanner;
25pub use javascript::JavaScriptPromClientScanner;
26pub use python::PythonPrometheusScanner;
27pub use rust::RustMetricsScanner;
28pub use typescript::TypeScriptPromClientScanner;
29pub use yaml::YamlAlertScanner;
30
31use std::path::{Path, PathBuf};
32
33use crate::parser::ParseError;
34
35/// The kind of metric instrumentation.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum MetricKind {
38 Counter,
39 Gauge,
40 Histogram,
41}
42
43impl MetricKind {
44 /// Returns the display name for this kind.
45 #[must_use]
46 pub fn as_str(&self) -> &'static str {
47 match self {
48 Self::Counter => "counter",
49 Self::Gauge => "gauge",
50 Self::Histogram => "histogram",
51 }
52 }
53}
54
55impl std::fmt::Display for MetricKind {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(f, "{}", self.as_str())
58 }
59}
60
61/// A discovered metric instrumentation point in source code.
62///
63/// This is a language-agnostic representation of where a metric is defined
64/// or recorded in a codebase. Different [`Scanner`] implementations produce
65/// these from language-specific patterns.
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct MetricInstrumentation {
68 /// The kind of metric (counter, gauge, histogram).
69 pub kind: MetricKind,
70 /// The metric name (e.g., `http_requests_total`).
71 pub name: String,
72 /// Label keys used with this metric (e.g., `["method", "status"]`).
73 pub labels: Vec<String>,
74 /// The file path where this metric is defined.
75 pub file: PathBuf,
76 /// Line number (1-indexed).
77 pub line: usize,
78 /// Column number (0-indexed).
79 pub column: usize,
80 /// The function containing this metric (e.g., `handle_request`).
81 pub function_name: Option<String>,
82 /// The impl type if inside an impl block (e.g., `Handler`).
83 pub impl_type: Option<String>,
84}
85
86/// The kind of operation performed on a metric.
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
88pub enum UsageKind {
89 /// Incrementing a counter (`inc()`, `Add()`)
90 Increment,
91 /// Setting a gauge value (`set()`, `Set()`)
92 Set,
93 /// Adding to a gauge (`add()`, `Add()`)
94 Add,
95 /// Subtracting from a gauge (`sub()`, `Sub()`)
96 Sub,
97 /// Recording a histogram/summary observation (`observe()`, `Observe()`)
98 Observe,
99 /// Timing an operation (`time()`, wrapping a block)
100 Time,
101 /// Setting gauge to current time (`set_to_current_time()`)
102 SetToCurrentTime,
103 /// Incrementing/decrementing around a block (`track_inprogress()`)
104 TrackInProgress,
105}
106
107impl UsageKind {
108 /// Returns the display name for this usage kind.
109 #[must_use]
110 pub fn as_str(&self) -> &'static str {
111 match self {
112 Self::Increment => "increment",
113 Self::Set => "set",
114 Self::Add => "add",
115 Self::Sub => "sub",
116 Self::Observe => "observe",
117 Self::Time => "time",
118 Self::SetToCurrentTime => "set_to_current_time",
119 Self::TrackInProgress => "track_inprogress",
120 }
121 }
122}
123
124impl std::fmt::Display for UsageKind {
125 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
126 write!(f, "{}", self.as_str())
127 }
128}
129
130/// A discovered metric usage point in source code.
131///
132/// Represents where a metric is actually recorded/updated, as opposed to where
133/// it's defined. This helps identify "hot paths" in the code where metrics are
134/// being actively used.
135///
136/// # Examples
137///
138/// - Python: `counter.inc()`, `histogram.observe(value)`
139/// - Go: `counter.Inc()`, `histogram.Observe(value)`
140/// - JavaScript: `counter.inc()`, `histogram.observe(value)`
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct MetricUsage {
143 /// The kind of operation (increment, observe, set, etc.).
144 pub usage_kind: UsageKind,
145 /// The variable name holding the metric (e.g., `request_counter`).
146 pub variable_name: String,
147 /// Label values used at this call site, if statically determinable.
148 pub label_values: Vec<String>,
149 /// The file path where this usage occurs.
150 pub file: PathBuf,
151 /// Line number (1-indexed).
152 pub line: usize,
153 /// Column number (0-indexed).
154 pub column: usize,
155 /// The function containing this usage.
156 pub function_name: Option<String>,
157 /// The impl/class type if inside one.
158 pub impl_type: Option<String>,
159}
160
161/// A discovered Prometheus alert rule.
162///
163/// Represents an alert rule found in YAML files that references a metric
164/// via its `PromQL` expression.
165#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct AlertRule {
167 /// The alert name (e.g., `HighErrorRate`).
168 pub name: String,
169 /// The `PromQL` expression for this alert.
170 pub expr: String,
171 /// The primary metric name extracted from the expression.
172 pub metric_name: Option<String>,
173 /// Alert severity (if specified in labels).
174 pub severity: Option<String>,
175 /// Alert message (from annotations).
176 pub message: Option<String>,
177 /// Runbook URL (from annotations).
178 pub runbook_url: Option<String>,
179 /// The file path where this alert is defined.
180 pub file: PathBuf,
181 /// Line number (1-indexed) where the alert starts.
182 pub line: usize,
183 /// Column number (0-indexed).
184 pub column: usize,
185}
186
187/// Trait for language-specific metric scanners.
188///
189/// Implement this trait to add support for a new language or metrics library.
190/// The scanner is responsible for:
191/// 1. Declaring which file extensions it handles
192/// 2. Parsing source files and finding metric instrumentation points
193///
194/// # Example
195///
196/// ```ignore
197/// pub struct GoPrometheusScanner;
198///
199/// impl Scanner for GoPrometheusScanner {
200/// fn extensions(&self) -> &[&str] {
201/// &["go"]
202/// }
203///
204/// fn scan_file(&self, path: &Path) -> Result<Vec<MetricInstrumentation>, ParseError> {
205/// // Use tree-sitter-go to find prometheus.NewCounter(), etc.
206/// }
207/// }
208/// ```
209pub trait Scanner: Send + Sync {
210 /// File extensions this scanner handles (e.g., `["rs"]` for Rust).
211 fn extensions(&self) -> &[&str];
212
213 /// Scan a source file for metric instrumentation points (definitions).
214 ///
215 /// Returns all metric definitions found in the file, or an error if parsing fails.
216 ///
217 /// # Errors
218 ///
219 /// Returns a [`ParseError`] if the file cannot be read or parsed.
220 fn scan_file(&self, path: &Path) -> Result<Vec<MetricInstrumentation>, ParseError>;
221
222 /// Scan a source file for metric usage points (where metrics are recorded).
223 ///
224 /// Returns all metric usages found in the file. Default implementation
225 /// returns an empty vector for scanners that don't support usage tracking.
226 ///
227 /// # Errors
228 ///
229 /// Returns a [`ParseError`] if the file cannot be read or parsed.
230 fn scan_usages(&self, path: &Path) -> Result<Vec<MetricUsage>, ParseError> {
231 let _ = path;
232 Ok(Vec::new())
233 }
234}
235
236/// Registry of available scanners.
237///
238/// Maintains a collection of [`Scanner`] implementations and routes files
239/// to the appropriate scanner based on extension.
240pub struct ScannerRegistry {
241 scanners: Vec<Box<dyn Scanner>>,
242}
243
244impl ScannerRegistry {
245 /// Creates an empty registry.
246 #[must_use]
247 pub fn new() -> Self {
248 Self {
249 scanners: Vec::new(),
250 }
251 }
252
253 /// Registers a scanner with this registry.
254 pub fn register(&mut self, scanner: Box<dyn Scanner>) {
255 self.scanners.push(scanner);
256 }
257
258 /// Finds a scanner that can handle the given file path.
259 ///
260 /// Returns `None` if no registered scanner handles this file type.
261 #[must_use]
262 pub fn scanner_for(&self, path: &Path) -> Option<&dyn Scanner> {
263 let ext = path.extension()?.to_str()?;
264 self.scanners
265 .iter()
266 .find(|s| s.extensions().contains(&ext))
267 .map(AsRef::as_ref)
268 }
269
270 /// Returns all file extensions supported by registered scanners.
271 #[must_use]
272 pub fn all_extensions(&self) -> Vec<&str> {
273 self.scanners
274 .iter()
275 .flat_map(|s| s.extensions().iter().copied())
276 .collect()
277 }
278}
279
280impl Default for ScannerRegistry {
281 fn default() -> Self {
282 let mut registry = Self::new();
283 registry.register(Box::new(RustMetricsScanner::new()));
284 registry.register(Box::new(PythonPrometheusScanner::new()));
285 registry.register(Box::new(GoPrometheusScanner::new()));
286 registry.register(Box::new(JavaScriptPromClientScanner::new()));
287 registry.register(Box::new(TypeScriptPromClientScanner::new()));
288 registry
289 }
290}
291
292impl ScannerRegistry {
293 /// Creates a registry with only the scanner for a specific language.
294 ///
295 /// Supported languages: "rust", "python", "go", "javascript", "typescript"
296 /// If the language is not recognized or empty, returns a registry with all scanners.
297 #[must_use]
298 pub fn for_language(language: &str) -> Self {
299 let mut registry = Self::new();
300 match language.to_lowercase().as_str() {
301 "rust" | "rs" => {
302 registry.register(Box::new(RustMetricsScanner::new()));
303 }
304 "python" | "py" => {
305 registry.register(Box::new(PythonPrometheusScanner::new()));
306 }
307 "go" | "golang" => {
308 registry.register(Box::new(GoPrometheusScanner::new()));
309 }
310 "javascript" | "js" => {
311 registry.register(Box::new(JavaScriptPromClientScanner::new()));
312 }
313 "typescript" | "ts" => {
314 registry.register(Box::new(TypeScriptPromClientScanner::new()));
315 }
316 _ => {
317 // Unknown or empty language, use all scanners
318 return Self::default();
319 }
320 }
321 registry
322 }
323}