Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion crates/ourios-parquet/src/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@
//! columns (`tenant_id`, `attributes`, `resource_attributes`,
//! `body`, both `params` list-element leaves,
//! `separators.list.element`).
//! - Bloom filter on `template_id` (B2 predicate-pushdown).
//! - Bloom filters on `template_id` (B2 predicate-pushdown),
//! `trace_id` / `span_id` (RFC 0031 L3 exact-id lookup — random
//! ids defeat min/max statistics), and every promoted column.
//!
//! [`CLAUDE.md`]: ../../../../CLAUDE.md

Expand Down Expand Up @@ -806,6 +808,17 @@ fn writer_properties(zstd: ZstdLevel, promoted: &PromotedAttributes) -> WriterPr
let template_id = ColumnPath::new(vec![crate::columns::TEMPLATE_ID.to_string()]);
builder = builder.set_column_bloom_filter_enabled(template_id, true);

// Bloom filters on the trace-context ids: random 16/8-byte values
// defeat min/max statistics entirely, so an exact-id lookup (the
// RFC 0031 L3 class) degenerates to a whole-column scan without
// them — measured at 72.4 MB for a 9-row trace on the 4.9M-record
// otel-demo-v8 corpus (comparative run #12). Same §3.6 pattern as
// `template_id` and the promoted columns.
for column in [crate::columns::TRACE_ID, crate::columns::SPAN_ID] {
let path = ColumnPath::new(vec![column.to_string()]);
builder = builder.set_column_bloom_filter_enabled(path, true);
}

// RFC 0022 §3.1: promoted attribute columns are the attribute
// predicate-pushdown surface — bloom filter each (dictionary and
// page-level statistics are already the global defaults). A
Expand Down
1 change: 1 addition & 0 deletions crates/ourios-parquet/tests/it/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ mod round_trip;
mod row_vs_path_validation;
mod schema_pin;
mod sizing;
mod trace_bloom;
mod zstd_level;
72 changes: 72 additions & 0 deletions crates/ourios-parquet/tests/it/trace_bloom.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! RFC 0005 §3.6 — bloom filters on the trace-context id columns.
//!
//! Random 16/8-byte ids defeat min/max statistics entirely, so an
//! exact-id lookup (the RFC 0031 L3 class) degenerates to a
//! whole-column scan without blooms — measured at 72.4 MB for a 9-row
//! trace on the 4.9M-record otel-demo-v8 corpus (comparative run #12).
//! This pins the writer emitting blooms for `trace_id` and `span_id`,
//! alongside the pre-existing `template_id` one.

use ourios_core::audit::ParamType;
use ourios_core::record::{BodyKind, MinedRecord, Param};
use ourios_core::tenant::TenantId;
use ourios_parquet::{DEFAULT_ZSTD_LEVEL, columns, encode_records_to_parquet};
use parquet::file::reader::{FileReader, SerializedFileReader};

const TS0: u64 = 1_775_127_480_000_000_000;

fn rec(trace_id: Option<[u8; 16]>, span_id: Option<[u8; 8]>) -> MinedRecord {
MinedRecord {
tenant_id: TenantId::new("a"),
template_id: 1,
template_version: 1,
severity_number: 9,
severity_text: Some("INFO".to_string()),
scope_name: None,
scope_version: None,
scope_attributes: Vec::new(),
resource_schema_url: None,
scope_schema_url: None,
time_unix_nano: TS0,
observed_time_unix_nano: Some(TS0 + 1_000),
attributes: Vec::new(),
dropped_attributes_count: 0,
resource_attributes: Vec::new(),
trace_id,
span_id,
flags: 0x01,
event_name: None,
body_kind: BodyKind::String,
params: vec![Param {
type_tag: ParamType::Num,
value: "42".to_string(),
}],
separators: vec![String::new(), " ".to_string()],
body: None,
confidence: 1.0,
lossy_flag: false,
}
}

#[test]
fn trace_context_columns_carry_bloom_filters() {
let records = [
rec(Some([0xAB; 16]), Some([0xCD; 8])),
rec(Some([0x11; 16]), Some([0x22; 8])),
rec(None, None),
];
let bytes = encode_records_to_parquet(&records, DEFAULT_ZSTD_LEVEL).expect("encode");
let reader = SerializedFileReader::new(bytes::Bytes::from(bytes)).expect("footer");
let rg = reader.metadata().row_group(0);

for name in [columns::TRACE_ID, columns::SPAN_ID, columns::TEMPLATE_ID] {
let col = (0..rg.num_columns())
.map(|i| rg.column(i))
.find(|c| c.column_path().string() == name)
.expect("column chunk present");
assert!(
col.bloom_filter_offset().is_some(),
"{name}: bloom filter written",
);
}
}