use crate::{
Bitmap, FieldName, FieldValuePair, Histogram, IndexError, Microseconds, Result, Seconds,
};
use journal_core::collections::{HashMap, HashSet};
use journal_core::file::{CurrentRowView, JournalFile, Mmap};
use journal_core::repository::File;
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::num::NonZeroU64;
use tracing::{error, trace};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "allocative", derive(allocative::Allocative))]
pub struct FileIndex {
file: File,
indexed_at: Seconds,
was_online: bool,
histogram: Histogram,
entry_offsets: Vec<u32>,
file_fields: HashSet<FieldName>,
indexed_fields: HashSet<FieldName>,
bitmaps: HashMap<FieldValuePair, Bitmap>,
}
impl FileIndex {
#[allow(clippy::too_many_arguments)]
pub fn new(
file: File,
indexed_at: Seconds,
was_online: bool,
histogram: Histogram,
entry_offsets: Vec<u32>,
fields: HashSet<FieldName>,
indexed_fields: HashSet<FieldName>,
bitmaps: HashMap<FieldValuePair, Bitmap>,
) -> Self {
Self {
file,
indexed_at,
was_online,
histogram,
entry_offsets,
file_fields: fields,
indexed_fields,
bitmaps,
}
}
pub fn bucket_duration(&self) -> Seconds {
Seconds(self.histogram.bucket_duration.get())
}
pub fn file(&self) -> &File {
&self.file
}
pub fn indexed_at(&self) -> Seconds {
self.indexed_at
}
pub fn online(&self) -> bool {
self.was_online
}
pub fn is_fresh(&self) -> bool {
if self.was_online {
let now = Seconds::now();
let age = now.get().saturating_sub(self.indexed_at.get());
age < 1
} else {
true
}
}
pub fn start_time(&self) -> Seconds {
self.histogram.start_time()
}
pub fn end_time(&self) -> Seconds {
self.histogram.end_time()
}
pub fn num_buckets(&self) -> usize {
self.histogram.num_buckets()
}
pub fn total_entries(&self) -> usize {
self.histogram.total_entries()
}
pub fn fields(&self) -> &HashSet<FieldName> {
&self.file_fields
}
pub fn bitmaps(&self) -> &HashMap<FieldValuePair, Bitmap> {
&self.bitmaps
}
pub fn is_indexed(&self, field: &FieldName) -> bool {
self.indexed_fields.contains(field)
}
pub fn count_entries_in_time_range(
&self,
bitmap: &Bitmap,
start_time: Seconds,
end_time: Seconds,
) -> Option<usize> {
self.histogram
.count_entries_in_time_range(bitmap, start_time, end_time)
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Direction {
#[default]
Forward,
Backward,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Anchor {
Timestamp(Microseconds),
Head,
Tail,
}
#[derive(Debug, Clone)]
pub struct LogQueryParams {
anchor: Anchor,
direction: Direction,
limit: Option<usize>,
source_timestamp_field: Option<super::FieldName>,
filter: Option<super::Filter>,
after: Option<Microseconds>,
before: Option<Microseconds>,
resume_position: Option<usize>,
regex: Option<Regex>,
}
impl LogQueryParams {
pub fn anchor(&self) -> Anchor {
self.anchor
}
pub fn direction(&self) -> Direction {
self.direction
}
pub fn limit(&self) -> Option<usize> {
self.limit
}
pub fn source_timestamp_field(&self) -> Option<&super::FieldName> {
self.source_timestamp_field.as_ref()
}
pub fn filter(&self) -> Option<&super::Filter> {
self.filter.as_ref()
}
pub fn after(&self) -> Option<Microseconds> {
self.after
}
pub fn before(&self) -> Option<Microseconds> {
self.before
}
pub fn resume_position(&self) -> Option<usize> {
self.resume_position
}
pub fn regex(&self) -> Option<&Regex> {
self.regex.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct LogQueryParamsBuilder {
anchor: Anchor,
direction: Direction,
limit: Option<usize>,
source_timestamp_field: Option<super::FieldName>,
filter: Option<super::Filter>,
after: Option<Microseconds>,
before: Option<Microseconds>,
resume_position: Option<usize>,
regex_pattern: Option<String>,
}
impl LogQueryParamsBuilder {
pub fn new(anchor: Anchor, direction: Direction) -> Self {
Self {
anchor,
direction,
limit: None,
source_timestamp_field: None,
filter: None,
after: None,
before: None,
resume_position: None,
regex_pattern: None,
}
}
pub fn with_limit(mut self, limit: usize) -> Self {
self.limit = Some(limit);
self
}
pub fn with_source_timestamp_field(mut self, field: Option<super::FieldName>) -> Self {
self.source_timestamp_field = field;
self
}
pub fn with_filter(mut self, filter: super::Filter) -> Self {
self.filter = Some(filter);
self
}
pub fn with_after(mut self, after: Microseconds) -> Self {
self.after = Some(after);
self
}
pub fn with_before(mut self, before: Microseconds) -> Self {
self.before = Some(before);
self
}
pub fn with_resume_position(mut self, position: usize) -> Self {
self.resume_position = Some(position);
self
}
pub fn with_regex(mut self, pattern: impl Into<String>) -> Self {
self.regex_pattern = Some(pattern.into());
self
}
pub fn build(self) -> Result<LogQueryParams> {
if let (Some(after), Some(before)) = (self.after, self.before) {
if after >= before {
return Err(IndexError::InvalidQueryTimeRange);
}
}
let regex = if let Some(pattern) = self.regex_pattern {
trace!("compiling regex pattern for log query: {:?}", pattern);
match Regex::new(&pattern) {
Ok(regex) => {
trace!("regex pattern compiled successfully");
Some(regex)
}
Err(e) => {
error!("failed to compile regex pattern {:?}: {}", pattern, e);
return Err(IndexError::InvalidRegex);
}
}
} else {
None
};
Ok(LogQueryParams {
anchor: self.anchor,
direction: self.direction,
limit: self.limit,
source_timestamp_field: self.source_timestamp_field,
filter: self.filter,
after: self.after,
before: self.before,
resume_position: self.resume_position,
regex,
})
}
}
fn get_timestamp_field(
journal_file: &JournalFile<Mmap>,
row: &mut CurrentRowView,
field_name: &super::FieldName,
entry_offset: NonZeroU64,
) -> Result<u64> {
row.load_entry(journal_file, entry_offset)?;
row.restart_data()?;
let result = (|| {
while let Some((_, payload)) = row.read_next_payload_with_offset(journal_file)? {
let payload = row.payload_slice(payload);
match crate::field_types::parse_timestamp_payload(field_name.as_bytes(), payload) {
Ok(timestamp) => return Ok(timestamp),
Err(IndexError::InvalidFieldPrefix) => continue,
Err(e) => return Err(e),
};
}
Err(IndexError::MissingFieldName)
})();
row.reset_data_state(journal_file)?;
result
}
fn get_entry_timestamp(
journal_file: &JournalFile<Mmap>,
row: &mut CurrentRowView,
source_timestamp_field: Option<&super::FieldName>,
entry_offset: NonZeroU64,
) -> Result<u64> {
if let Some(field_name) = source_timestamp_field {
match get_timestamp_field(journal_file, row, field_name, entry_offset) {
Ok(timestamp) => return Ok(timestamp),
Err(IndexError::MissingFieldName) => {
}
Err(e) => return Err(e),
}
}
let entry = journal_file.entry_ref(entry_offset)?;
Ok(entry.header.realtime)
}
fn partition_point_entries<F>(
entry_offsets: &[NonZeroU64],
left: usize,
right: usize,
mut predicate: F,
) -> Result<usize>
where
F: FnMut(NonZeroU64) -> Result<bool>,
{
let mut left = left;
let mut right = right;
debug_assert!(left <= right);
debug_assert!(right <= entry_offsets.len());
while left != right {
let mid = left.midpoint(right);
if predicate(entry_offsets[mid])? {
left = mid + 1;
} else {
right = mid;
}
}
Ok(left)
}
fn entry_matches_regex(
journal_file: &JournalFile<Mmap>,
row: &mut CurrentRowView,
entry_offset: NonZeroU64,
regex: &Regex,
data_match_cache: &mut HashMap<NonZeroU64, bool>,
) -> Result<bool> {
row.load_entry(journal_file, entry_offset)?;
row.restart_data()?;
let result = (|| {
for index in 0..row.data_offset_count() {
let Some(data_offset) = row.data_offset_at(index) else {
break;
};
if let Some(&matches) = data_match_cache.get(&data_offset) {
if matches {
return Ok(true);
}
continue;
}
let payload = row.read_payload_at(journal_file, data_offset)?;
let payload_bytes = row.payload_slice(payload);
let matches = if let Ok(payload_str) = std::str::from_utf8(payload_bytes) {
regex.is_match(payload_str)
} else {
false
};
data_match_cache.insert(data_offset, matches);
if matches {
return Ok(true);
}
}
Ok(false)
})();
row.reset_data_state(journal_file)?;
result
}
#[derive(Debug, Clone)]
pub struct LogEntryId {
pub file: File,
pub offset: u64,
pub timestamp: Microseconds,
pub position: usize,
}
impl FileIndex {
pub fn find_log_entries(
&self,
file: &File,
params: &LogQueryParams,
) -> Result<Vec<LogEntryId>> {
let anchor_usec = self.query_anchor_usec(params);
let bitmap = self.query_bitmap(params);
if bitmap.is_empty() {
return Ok(Vec::new());
}
let window_size = 32 * 1024 * 1024;
let journal_file = JournalFile::open(file, window_size)?;
let entry_offsets = self.candidate_entry_offsets(&bitmap);
let limit = query_limit(params.limit(), entry_offsets.len());
if limit == 0 {
return Ok(Vec::new());
}
EntryScanner::new(self, &journal_file, params, anchor_usec, limit).collect(&entry_offsets)
}
fn query_anchor_usec(&self, params: &LogQueryParams) -> Microseconds {
match params.anchor() {
Anchor::Timestamp(ts) => ts,
Anchor::Head => self.start_time().to_microseconds(),
Anchor::Tail => self.end_time().to_microseconds(),
}
}
fn query_bitmap(&self, params: &LogQueryParams) -> Bitmap {
params
.filter()
.map(|f| f.evaluate(self))
.unwrap_or_else(|| Bitmap::insert_range(0..self.entry_offsets.len() as u32))
}
fn candidate_entry_offsets(&self, bitmap: &Bitmap) -> Vec<NonZeroU64> {
bitmap
.iter()
.map(|idx| self.entry_offsets[idx as usize])
.filter(|offset| *offset != 0)
.map(|offset| NonZeroU64::new(offset as u64).expect("non-zero offset"))
.collect()
}
}
fn query_limit(limit: Option<usize>, candidate_count: usize) -> usize {
limit.unwrap_or(candidate_count)
}
enum BoundaryDecision {
Include,
Skip,
Stop,
}
fn forward_boundary_decision(timestamp: u64, params: &LogQueryParams) -> BoundaryDecision {
if params.after().is_some_and(|after| timestamp < after.get()) {
return BoundaryDecision::Skip;
}
if params
.before()
.is_some_and(|before| timestamp >= before.get())
{
return BoundaryDecision::Stop;
}
BoundaryDecision::Include
}
fn backward_boundary_decision(timestamp: u64, params: &LogQueryParams) -> BoundaryDecision {
if params
.before()
.is_some_and(|before| timestamp >= before.get())
{
return BoundaryDecision::Skip;
}
if params.after().is_some_and(|after| timestamp < after.get()) {
return BoundaryDecision::Stop;
}
BoundaryDecision::Include
}
struct EntryScanner<'a> {
file_index: &'a FileIndex,
journal_file: &'a JournalFile<Mmap>,
params: &'a LogQueryParams,
anchor_usec: Microseconds,
limit: usize,
row: CurrentRowView,
data_match_cache: HashMap<NonZeroU64, bool>,
regex_filtered_count: usize,
}
impl<'a> EntryScanner<'a> {
fn new(
file_index: &'a FileIndex,
journal_file: &'a JournalFile<Mmap>,
params: &'a LogQueryParams,
anchor_usec: Microseconds,
limit: usize,
) -> Self {
if params.regex().is_some() {
trace!("regex filtering enabled for query");
}
Self {
file_index,
journal_file,
params,
anchor_usec,
limit,
row: CurrentRowView::default(),
data_match_cache: HashMap::default(),
regex_filtered_count: 0,
}
}
fn collect(mut self, entry_offsets: &[NonZeroU64]) -> Result<Vec<LogEntryId>> {
let entries = match self.params.direction() {
Direction::Forward => self.collect_forward(entry_offsets)?,
Direction::Backward => self.collect_backward(entry_offsets)?,
};
self.trace_regex_result(entries.len());
Ok(entries)
}
fn collect_forward(&mut self, entry_offsets: &[NonZeroU64]) -> Result<Vec<LogEntryId>> {
let mut entries = Vec::with_capacity(self.limit.min(entry_offsets.len()));
let Some(start_idx) = self.forward_start_index(entry_offsets)? else {
return Ok(entries);
};
for (idx, &entry_offset) in entry_offsets[start_idx..].iter().enumerate() {
let timestamp = self.entry_timestamp(entry_offset)?;
match forward_boundary_decision(timestamp, self.params) {
BoundaryDecision::Include => {}
BoundaryDecision::Skip => continue,
BoundaryDecision::Stop => break,
}
if !self.matches_regex(entry_offset)? {
continue;
}
entries.push(self.log_entry(entry_offset, timestamp, start_idx + idx));
if entries.len() >= self.limit {
break;
}
}
Ok(entries)
}
fn collect_backward(&mut self, entry_offsets: &[NonZeroU64]) -> Result<Vec<LogEntryId>> {
let mut entries = Vec::with_capacity(self.limit.min(entry_offsets.len()));
let Some(start_idx) = self.backward_start_index(entry_offsets)? else {
return Ok(entries);
};
for (idx, &entry_offset) in entry_offsets[..=start_idx].iter().rev().enumerate() {
let timestamp = self.entry_timestamp(entry_offset)?;
match backward_boundary_decision(timestamp, self.params) {
BoundaryDecision::Include => {}
BoundaryDecision::Skip => continue,
BoundaryDecision::Stop => break,
}
if !self.matches_regex(entry_offset)? {
continue;
}
entries.push(self.log_entry(entry_offset, timestamp, start_idx - idx));
if entries.len() >= self.limit {
break;
}
}
Ok(entries)
}
fn forward_start_index(&mut self, entry_offsets: &[NonZeroU64]) -> Result<Option<usize>> {
let start_idx = if let Some(resume_pos) = self.params.resume_position() {
resume_pos + 1
} else {
partition_point_entries(entry_offsets, 0, entry_offsets.len(), |entry_offset| {
Ok(self.entry_timestamp(entry_offset)? < self.anchor_usec.get())
})?
};
Ok((start_idx < entry_offsets.len()).then_some(start_idx))
}
fn backward_start_index(&mut self, entry_offsets: &[NonZeroU64]) -> Result<Option<usize>> {
if let Some(resume_pos) = self.params.resume_position() {
return Ok((resume_pos > 0 && resume_pos < entry_offsets.len()).then(|| resume_pos - 1));
}
let partition_idx =
partition_point_entries(entry_offsets, 0, entry_offsets.len(), |entry_offset| {
Ok(self.entry_timestamp(entry_offset)? <= self.anchor_usec.get())
})?;
Ok((partition_idx > 0).then(|| partition_idx - 1))
}
fn entry_timestamp(&mut self, entry_offset: NonZeroU64) -> Result<u64> {
get_entry_timestamp(
self.journal_file,
&mut self.row,
self.params.source_timestamp_field(),
entry_offset,
)
}
fn matches_regex(&mut self, entry_offset: NonZeroU64) -> Result<bool> {
let Some(regex) = self.params.regex() else {
return Ok(true);
};
let matches = entry_matches_regex(
self.journal_file,
&mut self.row,
entry_offset,
regex,
&mut self.data_match_cache,
)?;
if !matches {
self.regex_filtered_count += 1;
}
Ok(matches)
}
fn log_entry(&self, entry_offset: NonZeroU64, timestamp: u64, position: usize) -> LogEntryId {
LogEntryId {
file: self.file_index.file.clone(),
offset: entry_offset.get(),
timestamp: Microseconds(timestamp),
position,
}
}
fn trace_regex_result(&self, matched_count: usize) {
if self.params.regex().is_some() {
trace!(
"regex filtering complete: {} entries matched, {} entries filtered out",
matched_count, self.regex_filtered_count
);
}
}
}