| 1 | //! Regression test for indexing journals that contain the otel-plugin's |
| 2 | //! `ND_REMAPPING=1` bookkeeping field. |
| 3 | //! |
| 4 | //! `FileIndexer::collect_remapping_entry_offsets` used to hold a |
| 5 | //! `ValueGuard<DataObject>` (which keeps the journal file's window-manager |
| 6 | //! borrow alive) while calling `InlinedCursor::collect_offsets`. When the |
| 7 | //! `ND_REMAPPING=1` data object is referenced by more than one entry, |
| 8 | //! `collect_offsets` walks the entry-array chain, re-borrows the window |
| 9 | //! manager, and fails with `JournalError::ValueGuardInUse` ("previous object |
| 10 | //! is still in use"). That aborted indexing of every otel-plugin journal. |
| 11 | //! |
| 12 | //! Regular systemd journals do not contain the `ND_REMAPPING` field, so the |
| 13 | //! existing tests never exercised this path. This test reproduces the |
| 14 | //! multi-entry shape that triggered the bug and asserts indexing succeeds. |
| 15 | |
| 16 | use journal_common::Seconds; |
| 17 | use journal_core::field_map::REMAPPING_MARKER; |
| 18 | use journal_core::file::{JournalFile, JournalFileOptions, JournalWriter}; |
| 19 | use journal_core::repository::File; |
| 20 | use journal_index::{FieldName, FileIndexer}; |
| 21 | use std::fs; |
| 22 | use std::path::PathBuf; |
| 23 | use tempfile::TempDir; |
| 24 | use uuid::Uuid; |
| 25 | |
| 26 | fn create_test_journal_path(temp_dir: &TempDir) -> PathBuf { |
| 27 | let machine_id = Uuid::from_u128(0x12345678_1234_1234_1234_123456789abc); |
| 28 | let machine_dir = temp_dir.path().join(machine_id.to_string()); |
| 29 | fs::create_dir_all(&machine_dir).expect("create machine dir"); |
| 30 | machine_dir.join("system.journal") |
| 31 | } |
| 32 | |
| 33 | /// Build a journal that mirrors an otel-plugin journal: a handful of |
| 34 | /// `ND_REMAPPING=1` bookkeeping entries plus normal log entries. |
| 35 | /// |
| 36 | /// The bookkeeping entries all share one `ND_REMAPPING=1` DATA object (journald |
| 37 | /// dedups by payload), so with `num_marker_entries >= 2` that object's |
| 38 | /// entry-array chain is non-empty — the shape that made |
| 39 | /// `collect_remapping_entry_offsets` re-borrow the window manager and fail. |
| 40 | /// |
| 41 | /// The normal entries (which carry no marker) are excluded neither from the |
| 42 | /// histogram nor the bitmaps, so a fixed indexer produces a non-empty index |
| 43 | /// instead of erroring with `EmptyHistogramInput`. |
| 44 | fn create_remapping_journal(num_marker_entries: u64, num_real_entries: u64) -> (TempDir, File) { |
| 45 | let temp_dir = TempDir::new().expect("temp dir"); |
| 46 | let journal_path = create_test_journal_path(&temp_dir); |
| 47 | let file = File::from_path(&journal_path).expect("File::from_path"); |
| 48 | |
| 49 | let machine_id = Uuid::from_u128(0x12345678_1234_1234_1234_123456789abc); |
| 50 | let boot_id = Uuid::from_u128(0x11111111_1111_1111_1111_111111111111); |
| 51 | let seqnum_id = Uuid::from_u128(0x22222222_2222_2222_2222_222222222222); |
| 52 | |
| 53 | let options = JournalFileOptions::new(machine_id, boot_id, seqnum_id); |
| 54 | let mut journal_file = JournalFile::create(&file, options).expect("create journal"); |
| 55 | let mut writer = JournalWriter::new(&mut journal_file, 1, boot_id).expect("writer"); |
| 56 | |
| 57 | let mut timestamp = 1_000_000u64; // microseconds, strictly increasing |
| 58 | |
| 59 | // Bookkeeping entries: timestamp + the shared ND_REMAPPING=1 marker. |
| 60 | for _ in 0..num_marker_entries { |
| 61 | timestamp += 1; |
| 62 | let ts_field = format!("_SOURCE_REALTIME_TIMESTAMP={timestamp}").into_bytes(); |
| 63 | let items: Vec<&[u8]> = vec![ts_field.as_slice(), REMAPPING_MARKER]; |
| 64 | writer |
| 65 | .add_entry(&mut journal_file, &items, timestamp, timestamp) |
| 66 | .expect("add marker entry"); |
| 67 | } |
| 68 | |
| 69 | // Real log entries: no marker, so they survive into the index. |
| 70 | for i in 0..num_real_entries { |
| 71 | timestamp += 1; |
| 72 | let ts_field = format!("_SOURCE_REALTIME_TIMESTAMP={timestamp}").into_bytes(); |
| 73 | let message = format!("MESSAGE=log entry {i}").into_bytes(); |
| 74 | let items: Vec<&[u8]> = vec![ts_field.as_slice(), message.as_slice()]; |
| 75 | writer |
| 76 | .add_entry(&mut journal_file, &items, timestamp, timestamp) |
| 77 | .expect("add real entry"); |
| 78 | } |
| 79 | |
| 80 | (temp_dir, file) |
| 81 | } |
| 82 | |
| 83 | /// Indexing a journal whose `ND_REMAPPING=1` object spans multiple entries |
| 84 | /// must not fail with `ValueGuardInUse`. |
| 85 | #[test] |
| 86 | fn index_journal_with_multi_entry_remapping_marker() { |
| 87 | // Two marker entries already produce an entry-array chain (one inlined |
| 88 | // offset + one array object), the minimal shape that triggered the bug; |
| 89 | // use a few more to be robust against array-layout changes. |
| 90 | let (_temp_dir, file) = create_remapping_journal(3, 5); |
| 91 | |
| 92 | let mut indexer = FileIndexer::default(); |
| 93 | let source = FieldName::new("_SOURCE_REALTIME_TIMESTAMP").unwrap(); |
| 94 | let message = FieldName::new("MESSAGE").unwrap(); |
| 95 | |
| 96 | let result = indexer.index(&file, Some(&source), &[message], Seconds(15)); |
| 97 | |
| 98 | assert!( |
| 99 | result.is_ok(), |
| 100 | "indexing a journal with a multi-entry ND_REMAPPING marker must succeed, got: {:?}", |
| 101 | result.err() |
| 102 | ); |
| 103 | } |