Skip to main content

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}