Анализатор: Claude (Opus 4.5) Дата: 2025-12-25 Общий объём кода: 9015 строк Rust Статус: Код в процессе разработки, только чтение
- Состояние проекта
- Анализ парсера AST
- Анализ движка eval
- Рефакторинг для читаемости
- Оптимизация производительности
- Технические риски и ошибки
- PromQL-специфика
- Rust-идиоматичность
- unwrap(), panic! и падения
- Обработка ошибок
- Строковые литералы
promql/src/ СТРОК СТАТУС
├── lib.rs 12 ✅ Отлично
├── parser.rs 27 ✅ Отлично
├── duration.rs 104 ✅ Отлично
├── ast/mod.rs 142 ✅ Отлично
├── parser/ast_parser.rs 1243 ⚠️ Допустимо
└── engine/
├── mod.rs 943 ✅ Хорошо
├── types.rs 259 ✅ Отлично
├── storage.rs 76 ✅ Отлично
├── regex.rs 83 ✅ Отлично
├── time.rs 104 ✅ Отлично
├── histogram.rs 575 ⚠️ Сложно
├── aggregate.rs 748 ⚠️ Требует рефакторинга
├── vector_ops.rs 421 ✅ Хорошо
└── functions/
├── mod.rs 434 ✅ Отлично
├── math.rs 226 ✅ Отлично
├── trig.rs 65 ✅ Отлично
├── time.rs 102 ✅ Отлично
├── label.rs 156 ✅ Отлично
├── sort.rs 192 ✅ Хорошо
├── range.rs 726 ⚠️ Большой файл
├── rate.rs 1323 ❌ Слишком большой
├── histogram_fn.rs 960 ⚠️ Большой файл
└── info.rs 94 ✅ Отлично
| Критерий | Оценка | Комментарий |
|---|---|---|
| Архитектура | ★★★★☆ | Хорошая модульность |
| Типобезопасность | ★★★☆☆ | Aggregate.op — String |
| Производительность | ★★★★☆ | Dispatch table, regex cache |
| Обработка ошибок | ★★★☆☆ | Много unwrap() |
| Читаемость | ★★★☆☆ | Большие файлы |
| PromQL-совместимость | ★★★★☆ | ~95% покрытие |
Сильные стороны:
- Идиоматичное использование enum:
pub enum Expr {
Number(f64),
String(String),
VectorSelector(VectorSelector),
MatrixSelector(MatrixSelector),
Subquery(SubqueryExpr),
Call { name: String, args: Vec<Expr> },
Aggregate { op: String, ... }, // ← Единственная проблема
Unary { op: UnaryOp, expr: Box<Expr> },
Binary { lhs: Box<Expr>, op: BinaryOp, ... },
}-
Правильное использование Box для рекурсивных структур
-
BinaryOp как enum (правильно!):
pub enum BinaryOp {
Add, Sub, Mul, Div, Mod, Atan2, Pow,
Eq, NotEq, Gt, Ge, Lt, Le,
And, Or, Unless,
}Критическая проблема:
Aggregate {
op: String, // ❌ Должен быть enum AggregateOp
...
}Сильные стороны:
- Правильный приоритет операторов (recursive descent):
parse_or → parse_and_unless → parse_comparison →
parse_add_sub → parse_mul_div_mod_atan2 → parse_unary → parse_pow → parse_primary
-
Полная поддержка escape-последовательностей:
\n, \r, \t, \x00, \u0000, \U00000000, октальные
-
Чистые ошибки с позицией:
ParseError { message: String, offset: usize }Проблемы:
- Строковые литералы в
is_aggregate_op:
// ast_parser.rs:1189-1195
fn is_aggregate_op(name: &str) -> bool {
match name.to_ascii_lowercase().as_str() {
"sum" | "avg" | "min" | "max" | "count" | "stddev" | "stdvar"
| "topk" | "bottomk" | "quantile" | "count_values" | "group"
| "limitk" | "limit_ratio" => true,
_ => false,
}
}- Потенциально небезопасные unwrap() (хотя после проверки длины):
// ast_parser.rs:428-433
1 => Ok((None, Box::new(args.into_iter().next().unwrap()))),
2 => {
let param = it.next().unwrap();
let expr = it.next().unwrap();
...
}Сильные стороны:
- Чистая структура контекста:
pub struct EvalContext<'a> {
pub ts_ms: TimestampMs,
pub eval_step_ms: Option<TimestampMs>,
pub query_start_ms: Option<TimestampMs>,
pub query_end_ms: Option<TimestampMs>,
pub lookback_delta_ms: TimestampMs,
pub storage: Option<&'a dyn Storage>,
}- Разделение eval/finalize:
pub fn eval_instant(expr: &Expr, ctx: EvalContext<'_>) -> Result<Value, EvalError>
fn eval_instant_raw(expr: &Expr, ctx: EvalContext<'_>) -> Result<Value, EvalError>
fn finalize_value(value: Value) -> Result<Value, EvalError>- Использование BinaryOp enum (правильно!):
fn apply_binop(op: BinaryOp, a: f64, b: f64) -> f64 {
match op {
BinaryOp::Add => a + b,
BinaryOp::Sub => a - b,
// ...
}
}Отличная реализация dispatch table:
type FnImpl = fn(&[Expr], EvalContext<'_>) -> Result<Value, EvalError>;
static FUNCTIONS: OnceLock<HashMap<&'static str, FnImpl>> = OnceLock::new();
pub(super) fn eval_call(name: &str, args: &[Expr], ctx: EvalContext<'_>) -> Result<Value, EvalError> {
let name_lc = name.to_ascii_lowercase();
if let Some(f) = FUNCTIONS.get_or_init(init_functions).get(name_lc.as_str()).copied() {
return f(args, ctx);
}
Err(EvalError::UnsupportedOwned(format!("function call {name}")))
}Преимущества:
- O(1) lookup вместо O(n) if-chains
- Thread-safe с OnceLock
- Легко расширяемо
Проблемы:
- Массовое использование строковых литералов:
// aggregate.rs:20-277
match op {
"topk" | "bottomk" => { ... }
"limitk" => { ... }
"limit_ratio" => { ... }
"count_values" => { ... }
"quantile" => { ... }
"group" => { ... }
"stddev" | "stdvar" => { ... }
_ => {}
}
if op == "count" { ... }
if op == "sum" || op == "avg" { ... }- Сложная структура AggState:
struct AggState {
sum: f64, sum_c: f64,
sum_has_nan: bool, sum_has_pos_inf: bool, sum_has_neg_inf: bool,
count: usize, min: Option<f64>, max: Option<f64>,
drop_meta: bool,
avg_max_abs: f64, avg_scaled_sum: f64, avg_scaled_c: f64,
avg_has_nan: bool, avg_has_pos_inf: bool, avg_has_neg_inf: bool,
}Слишком много полей — нужно разбить на специализированные структуры.
Сильные стороны:
- Neumaier summation для численной стабильности
- Welford variance для stddev/stdvar
| Файл | Строк | Рекомендация |
|---|---|---|
rate.rs |
1323 | Разбить на: float_rate.rs, histogram_rate.rs, deriv.rs |
histogram_fn.rs |
960 | Разбить на: quantile.rs, fraction.rs, stats.rs |
aggregate.rs |
748 | Вынести AggState в отдельный модуль |
eval_rate в rate.rs (~120 строк):
// Текущая структура:
pub fn eval_rate(args: &[Expr], ctx: EvalContext<'_>) -> Result<Value, EvalError> {
// 1. Проверка аргументов
// 2. Извлечение range параметров из MatrixSelector или Subquery
// 3. Обработка float серий
// 4. Обработка histogram серий
// 5. Формирование результата
}
// Рекомендация — разбить на:
fn extract_range_params(...) -> Result<RangeParams, EvalError>;
fn rate_float_series(...) -> Option<f64>;
fn rate_histogram_series(...) -> Result<Option<Histogram>, EvalError>;Повторяющийся паттерн проверки типов:
// Встречается 15+ раз:
let series = match eval_instant_raw(&args[0], ctx)? {
Value::RangeVector(series) => series,
other => {
return Err(EvalError::TypeError {
expected: "range-vector",
got: other.kind(),
})
}
};
// Рекомендация — вспомогательная функция:
fn expect_range_vector(expr: &Expr, ctx: EvalContext<'_>) -> Result<Vec<RangeSeries>, EvalError> {
match eval_instant_raw(expr, ctx)? {
Value::RangeVector(v) => Ok(v),
other => Err(EvalError::TypeError { expected: "range-vector", got: other.kind() }),
}
}Найдено 69 вызовов .clone() в проекте.
Примеры неоптимального клонирования:
// histogram.rs:206-207 — можно избежать clone через take
let min = *mapped.keys().next().unwrap();
let max = *mapped.keys().next_back().unwrap();
// vector_ops.rs:14 — clone в цикле
out.push((name.clone(), v.to_owned()));
// rate.rs:67 — clone гистограммы в цикле
PointValue::Histogram(h) => hists.push((p.ts_ms, h.clone())),Рекомендации:
- Использовать
Cow<'_, str>для временных строк - Применять
std::mem::takeвместо clone где возможно - Рассмотреть
Arc<Histogram>для shared ownership
// regex.rs:49-56
while self.order.len() > self.cap {
let Some(oldest) = self.order.first().cloned() else { break };
self.order.remove(0); // O(n) операция!
self.map.remove(&oldest);
}Проблема: Vec::remove(0) — O(n) операция.
Рекомендация: Использовать VecDeque или готовую реализацию LRU:
use lru::LruCache;
// или std::collections::VecDeque для O(1) pop_front// functions/mod.rs:271-272 — аллокация на каждый вызов функции
let name_lc = name.to_ascii_lowercase(); // новая String!Рекомендация: Case-insensitive HashMap или предварительная нормализация.
// Встречается ~20 раз:
out.sort_by(|a, b| a.labels.cmp(&b.labels));Рекомендация: Рассмотреть IndexSet или сортировку только при необходимости.
// Найдено 78 приведений типов (as i64, as u64, as f64, as usize)
// time.rs:97 — потенциальная потеря точности
let millis: u64 = secs.checked_mul(1_000)...
// rate.rs:95
let sampled_interval = (last_t.saturating_sub(first_t) as f64) / 1000.0;Риски:
as f64для больших i64 теряет точность при |x| > 2^53as usizeможет паниковать на 32-bit платформах
Рекомендация: Использовать TryFrom с обработкой ошибок.
// mod.rs:436-441
while t <= end_ms {
// ...
let next = t.saturating_add(step_ms);
if next <= t { break; } // Защита есть ✓
t = next;
}Защита от бесконечного цикла есть — хорошо.
// regex.rs:59-74
fn compile_promql_matcher_regex(pattern: &str) -> Result<Regex, EvalError> {
let anchored = format!("^(?:{pattern})$"); // pattern от пользователя!
let re = Regex::new(&anchored)...
}Риски:
- ReDoS атаки через сложные regex
- Нет лимита на сложность regex
Рекомендация: Добавить таймаут или ограничение сложности.
// histogram.rs:141-142
fn is_zero(&self) -> bool {
self.sum == 0.0 && self.count == 0.0 ... // Не учитывает -0.0
}-0.0 == 0.0 в Rust возвращает true, но для полноты:
Рекомендация: Использовать self.sum.abs() == 0.0 если нужна строгость.
// types.rs:117-121
pub enum SampleValue {
Float(f64),
Histogram(super::Histogram),
Stale, // ✓ Правильно реализовано
}// mod.rs:23
pub const DEFAULT_LOOKBACK_DELTA_MS: TimestampMs = 300_000; // 5m ✓// rate.rs:86-93 — правильная обработка сбросов счётчика
let mut result = last_v - first_v;
let mut prev = first_v;
for (_, curr) in floats.iter().skip(1) {
if *curr < prev {
result += prev; // ✓ Корректно
}
prev = *curr;
}Не реализовано (или не обнаружено):
histogram_stddev_over_time/histogram_stdvar_over_timehistogram_quantileс Native Histogram (проверить покрытие)- Некоторые edge cases в
offsetмодификаторе с отрицательными значениями
// rate.rs:100-130 — реализована экстраполяция ✓
let mut duration_to_start = (first_t.saturating_sub(range_start_ms) as f64) / 1000.0;
// ... extrapolation logic✅ Использование Result<T, E> вместо exceptions
✅ #[derive(Debug, Clone, PartialEq)] для типов данных
✅ impl std::error::Error для типов ошибок
✅ Lifetime параметры (EvalContext<'a>)
✅ Option<T> вместо null
✅ Pattern matching
✅ Итераторы вместо циклов с индексами
1. Строки вместо enum:
// ❌ Плохо
Aggregate { op: String, ... }
match op { "sum" | "avg" => ... }
// ✓ Хорошо (как BinaryOp)
Aggregate { op: AggregateOp, ... }
match op { AggregateOp::Sum | AggregateOp::Avg => ... }2. Избыточное клонирование:
// ❌ Плохо
for (k, v) in labels.iter() {
out.push((k.to_owned(), v.to_owned()));
}
// ✓ Лучше (если labels можно consume)
out.extend(labels.into_iter());3. Magic numbers:
// ❌ Плохо
RegexCache::new(128) // Почему 128?
// ✓ Лучше
const REGEX_CACHE_SIZE: usize = 128;
RegexCache::new(REGEX_CACHE_SIZE)4. Отсутствие документации:
// ❌ Публичные функции без документации
pub fn eval_instant(expr: &Expr, ctx: EvalContext<'_>) -> Result<Value, EvalError>
// ✓ Рекомендуется
/// Evaluates a PromQL expression at a single instant in time.
///
/// # Arguments
/// * `expr` - The parsed PromQL expression
/// * `ctx` - Evaluation context containing timestamp and storage
///
/// # Returns
/// The evaluated value or an error
pub fn eval_instant(...)// ast_parser.rs:428 — после проверки args.len() == 1
1 => Ok((None, Box::new(args.into_iter().next().unwrap()))),
// rate.rs:81-84 — после проверки floats.len() >= 2
if floats.len() < 2 { continue; }
let first_t = floats.first().unwrap().0;// histogram.rs:206-207 — mapped может быть пустым?
let min = *mapped.keys().next().unwrap();
let max = *mapped.keys().next_back().unwrap();Проверка контекста:
// Есть проверка выше:
if self.buckets.is_empty() && self.n_buckets.is_empty() {
return self.clone();
}Безопасно — проверка есть.
1. Заменить unwrap() на expect() с сообщением:
// ❌ Плохо
floats.first().unwrap()
// ✓ Лучше (для отладки)
floats.first().expect("floats guaranteed non-empty after len check")2. Использовать if-let где возможно:
// ❌ Плохо
let first = samples.first().unwrap();
// ✓ Лучше
let Some(first) = samples.first() else { continue };3. Единственный unreachable!:
// types.rs:251
_ => unreachable!(),Этот unreachable находится внутри exhaustive match на MatchOp::RegexMatch | MatchOp::RegexNoMatch, поэтому действительно недостижим. Однако для безопасности:
// ✓ Рекомендация — использовать match guard вместо nested match
match m.op {
MatchOp::Equal => { ... }
MatchOp::NotEqual => { ... }
MatchOp::RegexMatch if !is_match => return Ok(false),
MatchOp::RegexNoMatch if is_match => return Ok(false),
MatchOp::RegexMatch | MatchOp::RegexNoMatch => {}
}// types.rs:87-94
pub enum EvalError {
Unsupported(&'static str),
UnsupportedOwned(String),
TypeError { expected: &'static str, got: &'static str },
}1. Недостаточная детализация ошибок:
Err(EvalError::Unsupported("rate arity")) // Какая arity ожидалась?2. Нет контекста позиции:
// При ошибке eval неясно, какое выражение вызвало проблему3. Нет цепочки ошибок:
// Потеря контекста при конвертации
re.map_err(|_| EvalError::Unsupported("invalid regex matcher"))
// ↑ Оригинальная ошибка regex теряется1. Расширить EvalError:
pub enum EvalError {
Unsupported { feature: &'static str, context: Option<String> },
TypeError { expected: &'static str, got: &'static str, expr: Option<String> },
ArityError { function: String, expected: usize, got: usize },
RegexError { pattern: String, source: regex::Error },
StorageError(Box<dyn std::error::Error + Send + Sync>),
}2. Использовать thiserror crate:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum EvalError {
#[error("unsupported: {feature}")]
Unsupported { feature: &'static str },
#[error("type error: expected {expected}, got {got}")]
TypeError { expected: &'static str, got: &'static str },
#[error("invalid regex `{pattern}`: {source}")]
RegexError { pattern: String, #[source] source: regex::Error },
}3. Добавить span information:
pub struct EvalErrorWithSpan {
pub error: EvalError,
pub span: Option<Span>, // Позиция в исходном запросе
}1. Агрегации (критично — должен быть enum):
// aggregate.rs
match op {
"topk" | "bottomk" => ...
"sum" | "avg" => ...
"min" | "max" => ...
}2. Специальные имена меток (допустимо):
// Константы для метаданных
k == "__name__"
k == "__type__"
k == "__unit__"Рекомендация: Вынести в константы:
pub const LABEL_NAME: &str = "__name__";
pub const LABEL_TYPE: &str = "__type__";
pub const LABEL_UNIT: &str = "__unit__";3. Парсинг histogram (допустимо для DSL):
// histogram.rs
match key {
"schema" => ...
"sum" => ...
"count" => ...
}4. Парсинг специальных значений (допустимо):
match s {
"NaN" | "nan" => Some(f64::NAN),
"+Inf" | "Inf" => Some(f64::INFINITY),
}// Добавить в ast/mod.rs:
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AggregateOp {
Sum, Avg, Min, Max, Count,
Stddev, Stdvar,
Topk, Bottomk,
Quantile, CountValues,
Group, Limitk, LimitRatio,
}
impl AggregateOp {
pub fn from_str(s: &str) -> Option<Self> {
Some(match s.to_ascii_lowercase().as_str() {
"sum" => Self::Sum,
"avg" => Self::Avg,
// ...
_ => return None,
})
}
}
// Изменить Expr:
Aggregate {
op: AggregateOp, // вместо String
...
}
// Изменить aggregate.rs:
match op {
AggregateOp::Topk | AggregateOp::Bottomk => ...
AggregateOp::Sum | AggregateOp::Avg => ...
}Преимущества:
- Compile-time проверка
- Exhaustive matching (компилятор покажет пропущенные cases)
- Нет опечаток
- IDE autocomplete
- Лучшая производительность (нет string comparison)
| # | Задача | Приоритет | Сложность | Влияние |
|---|---|---|---|---|
| 1 | Создать AggregateOp enum |
🔴 Высокий | Средняя | Высокое |
| 2 | Заменить опасные unwrap() на expect() |
🔴 Высокий | Низкая | Безопасность |
| 3 | Разбить rate.rs на модули |
🟡 Средний | Средняя | Читаемость |
| 4 | Улучшить LRU кэш regex (VecDeque) | 🟡 Средний | Низкая | Производительность |
| 5 | Вынести константы меток | 🟢 Низкий | Низкая | Maintainability |
| 6 | Добавить документацию к pub функциям | 🟢 Низкий | Средняя | DX |
| 7 | Расширить EvalError | 🟡 Средний | Средняя | Отладка |
| 8 | Разбить histogram_fn.rs |
🟢 Низкий | Средняя | Читаемость |
Проект имеет хорошую архитектуру после проведённого рефакторинга:
- Dispatch table вместо if-chains для функций
- Модульная структура
- Regex кэширование
- Правильная обработка Native Histograms
Главные проблемы:
Aggregate.op: String— должен быть enum- Много
unwrap()безexpect() - Большие файлы (
rate.rs: 1323 строки) - Недостаточно детализированные ошибки
Код production-ready для MVP, но требует доработки для долгосрочной поддержки.