Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds comprehensive PostgreSQL datastore support to EveBox as an alternative to SQLite and Elasticsearch. The implementation includes partition-based storage, efficient retention management, and full API integration.
Key Changes:
- Implements a complete PostgreSQL event repository with partitioning by day for efficient retention management
- Adds query builder infrastructure for constructing parameterized PostgreSQL queries with JSON path support
- Integrates PostgreSQL datastore across all API endpoints and event operations
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
src/postgres/mod.rs |
Module organization and exports for the new PostgreSQL subsystem |
src/postgres/connection.rs |
PostgreSQL connection pool management and database initialization with migrations |
src/postgres/partition.rs |
Time-based partition management with caching for efficient event storage and retention |
src/postgres/query_builder.rs |
Query builder for constructing parameterized PostgreSQL queries with JSON field support |
src/postgres/eventrepo.rs |
Core event repository implementation with all query operations (alerts, events, stats, DNS, DHCP) |
src/postgres/importer.rs |
Event sink for batch importing events with metrics tracking and error handling |
src/postgres/retention.rs |
Background task for automatic partition dropping based on retention policies |
src/eventrepo/mod.rs |
Integration of PostgreSQL variant into the EventRepo enum with routing to implementation |
src/eventrepo/stats.rs |
Routes stats aggregation queries to PostgreSQL implementation |
src/importer.rs |
Adds PostgreSQL event sink to the EventSink enum |
src/server/mod.rs |
Adds postgres_url configuration field to ServerConfig |
src/server/main.rs |
Configures PostgreSQL datastore with URL from config/env variables |
src/server/metrics.rs |
Adds PostgreSQL-specific metrics tracking |
src/server/api/*.rs |
Updates API handlers to support PostgreSQL datastore variant |
src/logger.rs |
Adjusts logging configuration to reduce sqlx query noise |
src/eve/reader.rs |
Changes log level from info to trace for incomplete line reads |
resources/postgres/migrations/0001_Initial.sql |
SQL schema with partitioned events table and management functions |
docker/postgres/* |
Docker configuration for running PostgreSQL for testing |
Cargo.toml |
Adds postgres feature to sqlx dependency |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| path.push_str(&format!("->>'{}' ", part)); | ||
| } else { | ||
| path.push_str(&format!("->'{}'", part)); | ||
| } | ||
| } | ||
| let p = builder.next_placeholder(); | ||
| builder | ||
| .push_where(format!("{} {} {}", path.trim(), op, p)) |
There was a problem hiding this comment.
There's a trailing space after the JSON path construction on this line. The code formats the path with a trailing space and then trims it on line 1060. Consider removing the trailing space from the format string on line 1055 to eliminate the need for trimming.
| path.push_str(&format!("->>'{}' ", part)); | |
| } else { | |
| path.push_str(&format!("->'{}'", part)); | |
| } | |
| } | |
| let p = builder.next_placeholder(); | |
| builder | |
| .push_where(format!("{} {} {}", path.trim(), op, p)) | |
| path.push_str(&format!("->>'{}'", part)); | |
| } else { | |
| path.push_str(&format!("->'{}'", part)); | |
| } | |
| } | |
| let p = builder.next_placeholder(); | |
| builder | |
| .push_where(format!("{} {} {}", path, op, p)) |
| partition_record RECORD; | ||
| dropped_count INTEGER := 0; | ||
| nanos_per_day BIGINT := 86400000000000; | ||
| BEGIN |
There was a problem hiding this comment.
Consider adding validation for negative or zero values of retention_days. While the calling code appears to check for days > 0, this function should be defensive and handle edge cases gracefully. A negative retention period could result in incorrect cutoff calculations.
| BEGIN | |
| BEGIN | |
| -- Validate retention_days to avoid incorrect cutoff calculations. | |
| -- For NULL, zero, or negative values, do nothing and return 0. | |
| IF retention_days IS NULL OR retention_days <= 0 THEN | |
| RETURN 0; | |
| END IF; |
src/postgres/partition.rs
Outdated
| pub fn partition_name(ts_nanos: i64) -> String { | ||
| let ts_seconds = ts_nanos / 1_000_000_000; | ||
| let dt = chrono::DateTime::from_timestamp(ts_seconds, 0) | ||
| .unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap()); |
There was a problem hiding this comment.
The unwrap_or_else fallback creates a timestamp at epoch 0 (1970-01-01), which may not be the intended behavior for invalid timestamps. Consider using a more appropriate error handling strategy, such as returning a Result or using a more meaningful default like the current time.
| .unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap()); | |
| .unwrap_or_else(|| chrono::Utc::now()); |
| let userinfo = match (user, password) { | ||
| (Some(u), Some(p)) => format!("{}:{}@", u, p), | ||
| (Some(u), None) => format!("{}@", u), | ||
| _ => String::new(), | ||
| }; | ||
|
|
||
| Some(format!( | ||
| "postgres://{}{}:{}/{}", | ||
| userinfo, host, port, database |
There was a problem hiding this comment.
The PostgreSQL URL construction does not URL-encode the username and password, which could lead to connection failures or security issues if these credentials contain special characters (e.g., @, :, /, ?). Consider using proper URL encoding for the userinfo component.
|
|
||
| if n == 0 { | ||
| warn!("Archive by event ID request did not update any events"); | ||
| bail!("postgres: event not found"); |
There was a problem hiding this comment.
The error message "postgres: event not found" uses lowercase "postgres" while other parts of the codebase use "PostgreSQL". For consistency with the rest of the error messages and logging, consider using "PostgreSQL" or "Postgres" with capital P.
| bail!("postgres: event not found"); | |
| bail!("PostgreSQL: event not found"); |
| } | ||
| EventRepo::SQLite(ds) => ds.comment_event_by_id(event_id, comment, session).await, | ||
| EventRepo::SQLite(ds) => { | ||
| ds.comment_event_by_id(event_id, comment, session.clone()) |
There was a problem hiding this comment.
The clone() operation in the parameter should be removed. This creates an unnecessary clone of the Arc before passing ownership.
| ds.comment_event_by_id(event_id, comment, session.clone()) | |
| ds.comment_event_by_id(event_id, comment, session) |
| match self { | ||
| EventRepo::Elastic(ds) => Ok(ds.agg(field, size, order, query).await?), | ||
| EventRepo::SQLite(ds) => Ok(ds.agg(field, size, order, query).await?), | ||
| EventRepo::SQLite(ds) => Ok(ds.agg(field, size, order, query.clone()).await?), |
There was a problem hiding this comment.
The clone() operation should be removed here. The SQLite variant now clones the query parameter while Elastic and Postgres don't. For consistency and to avoid unnecessary allocations, all variants should avoid cloning unless absolutely necessary. Consider if SQLite actually requires the clone or if it can be removed there as well.
| EventRepo::SQLite(ds) => Ok(ds.agg(field, size, order, query.clone()).await?), | |
| EventRepo::SQLite(ds) => Ok(ds.agg(field, size, order, query).await?), |
| impl EventQueryBuilder { | ||
| pub(crate) fn new() -> Self { | ||
| Self { | ||
| ..Default::default() |
There was a problem hiding this comment.
The default struct initialization syntax should use explicit field names for clarity. Instead of ..Default::default(), consider explicitly setting all fields. The current code creates an instance with all default values and then immediately overwrites them, which is unclear.
| ..Default::default() | |
| select: Vec::new(), | |
| from: Vec::new(), | |
| wheres: Vec::new(), | |
| order_by: None, | |
| limit: 0, | |
| args: PgArguments::default(), | |
| arg_count: 0, |
| path.push_str(&format!("->>'{}' ", part)); | ||
| } else { | ||
| path.push_str(&format!("->'{}' ", part)); | ||
| } | ||
| } | ||
| path.trim().to_string() |
There was a problem hiding this comment.
There's an extra space character after the JSON path expression that gets trimmed later. Consider removing the trailing space in the format string to make the code cleaner and avoid the need for the trim() call.
| path.push_str(&format!("->>'{}' ", part)); | |
| } else { | |
| path.push_str(&format!("->'{}' ", part)); | |
| } | |
| } | |
| path.trim().to_string() | |
| path.push_str(&format!("->>'{}'", part)); | |
| } else { | |
| path.push_str(&format!("->'{}'", part)); | |
| } | |
| } | |
| path |
| if i == parts.len() - 1 { | ||
| path.push_str(&format!("->>'{}' ", part)); | ||
| } else { | ||
| path.push_str(&format!("->'{}' ", part)); |
There was a problem hiding this comment.
Similar to line 1055, there's an extra space after the JSON path expression. Consider removing the trailing space in the format string on line 1057 as well for consistency.
| path.push_str(&format!("->'{}' ", part)); | |
| path.push_str(&format!("->'{}'", part)); |
- Add postgres module with connection pooling and lifecycle management - Implement EventRepository for PostgreSQL with full query/search capabilities - Add SQL migrations for initial schema with events, stats, and bookmarks tables - Implement Importer for inserting events into PostgreSQL - Add QueryBuilder for dynamic SQL construction from search filters - Implement table partitioning by date for performance - Add retention policy support for automatic old data cleanup - Update EventRepository trait to abstract datastore differences - Add Docker Compose setup for PostgreSQL development - Update server API layer to work with PostgreSQL backend - Add postgres feature flag to Cargo.toml Co-authored-by: Henri 🐕 <henri@codemonkey.net>
No description provided.