Skip to content

Add support for PostgreSQL datastore#361

Closed
jasonish wants to merge 1 commit intomainfrom
postgres/v1
Closed

Add support for PostgreSQL datastore#361
jasonish wants to merge 1 commit intomainfrom
postgres/v1

Conversation

@jasonish
Copy link
Owner

@jasonish jasonish commented Jan 3, 2026

No description provided.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +414 to +421
path.push_str(&format!("->>'{}' ", part));
} else {
path.push_str(&format!("->'{}'", part));
}
}
let p = builder.next_placeholder();
builder
.push_where(format!("{} {} {}", path.trim(), op, p))
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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))

Copilot uses AI. Check for mistakes.
partition_record RECORD;
dropped_count INTEGER := 0;
nanos_per_day BIGINT := 86400000000000;
BEGIN
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
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());
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
.unwrap_or_else(|| chrono::DateTime::from_timestamp(0, 0).unwrap());
.unwrap_or_else(|| chrono::Utc::now());

Copilot uses AI. Check for mistakes.
Comment on lines +675 to +683
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
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.

if n == 0 {
warn!("Archive by event ID request did not update any events");
bail!("postgres: event not found");
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
bail!("postgres: event not found");
bail!("PostgreSQL: event not found");

Copilot uses AI. Check for mistakes.
}
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())
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The clone() operation in the parameter should be removed. This creates an unnecessary clone of the Arc before passing ownership.

Suggested change
ds.comment_event_by_id(event_id, comment, session.clone())
ds.comment_event_by_id(event_id, comment, session)

Copilot uses AI. Check for mistakes.
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?),
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
EventRepo::SQLite(ds) => Ok(ds.agg(field, size, order, query.clone()).await?),
EventRepo::SQLite(ds) => Ok(ds.agg(field, size, order, query).await?),

Copilot uses AI. Check for mistakes.
impl EventQueryBuilder {
pub(crate) fn new() -> Self {
Self {
..Default::default()
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
..Default::default()
select: Vec::new(),
from: Vec::new(),
wheres: Vec::new(),
order_by: None,
limit: 0,
args: PgArguments::default(),
arg_count: 0,

Copilot uses AI. Check for mistakes.
Comment on lines +1055 to +1060
path.push_str(&format!("->>'{}' ", part));
} else {
path.push_str(&format!("->'{}' ", part));
}
}
path.trim().to_string()
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
if i == parts.len() - 1 {
path.push_str(&format!("->>'{}' ", part));
} else {
path.push_str(&format!("->'{}' ", part));
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
path.push_str(&format!("->'{}' ", part));
path.push_str(&format!("->'{}'", part));

Copilot uses AI. Check for mistakes.
- 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>
@jasonish jasonish closed this Jan 8, 2026
@jasonish jasonish deleted the postgres/v1 branch January 8, 2026 23:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant