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
2 changes: 2 additions & 0 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ It also contains scenario demos under subdirectories:
- generate about 5K BOFH log entries, then export that `.logjet` file to Parquet through the external exporter plugin
- [`tui-view`](./tui-view)
- generate 1000 randomized log entries and open `ljx view` on the result
- [`visual-logtail`](./visual-logtail)
- append one fresh log record every half second and open `ljx view --tail` on the live file
- [`bridge-resume`](./bridge-resume)
- consumer restart resumes from persisted sequence state without replaying from zero
- [`upstream-reset-resume`](./upstream-reset-resume)
Expand Down
105 changes: 105 additions & 0 deletions demo/src/bin/visual-logtail-emitter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use std::fs::OpenOptions;
use std::io::{BufWriter, Write};
use std::path::PathBuf;
use std::thread;
use std::time::Duration;

use logjet::{LogjetWriter, RecordType, WriterConfig};
use otlp_demo::build_message_request_for_service;
use prost::Message;

fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut args = std::env::args().skip(1);
let output = match args.next() {
Some(path) => PathBuf::from(path),
None => {
eprintln!("usage: visual-logtail-emitter <output.logjet> [seed]");
std::process::exit(2);
}
};
let seed = args.next().map(|value| value.parse::<u64>()).transpose()?.unwrap_or(0x51a1_7a11);
let mut rng = Lcg::new(seed);
let mut seq = 1u64;

loop {
let service = SERVICES[rng.next_index(SERVICES.len())];
let severity = LEVELS[rng.next_index(LEVELS.len())];
let message = format_message(seq, &mut rng);
let request = build_message_request_for_service(seq, service, severity, message.clone());

let file = OpenOptions::new().create(true).append(true).open(&output)?;
let writer = BufWriter::new(file);
let mut logjet = LogjetWriter::with_config(writer, WriterConfig::default());
logjet.push(RecordType::Logs, seq, unix_time_nanos(seq), &request.encode_to_vec())?;
let mut writer = logjet.into_inner()?;
writer.flush()?;

eprintln!("#{seq} {severity:>5} {service}: {message}");
seq = seq.saturating_add(1);
thread::sleep(Duration::from_millis(500));
}
}

const SERVICES: &[&str] = &["visual-tail", "garage-rig", "bridge-alpha", "coffee-daemon", "night-shift", "kill-bill"];
const LEVELS: &[&str] = &["trace", "debug", "info", "warn", "error"];
const SUBJECTS: &[&str] = &[
"reindexed the replay cursor",
"shook loose a sleepy bridge",
"tickled the spool rotation",
"poked the checksum goblin",
"nudged the ingest guardrail",
"confused the on-call dashboard",
"reheated a stale batch",
"misplaced a highly motivated packet",
];
const CONTEXTS: &[&str] = &[
"during a suspiciously calm deploy",
"while the collector blinked twice",
"after a ceremonial config reload",
"under polite backpressure",
"while the spool muttered darkly",
"after a coffee-powered rollback",
"before the bridge could complain",
"while the checksum looked offended",
];
const OUTCOMES: &[&str] = &[
"and the logs kept flowing",
"but the operator remained unconvinced",
"so tail mode had something juicy to chew",
"and the file grew another tiny block",
"before the daemon could get grumpy",
"yet the replay queue stayed weirdly serene",
"and everyone blamed cosmic rays anyway",
"with absolutely no paperwork filed",
];

fn format_message(seq: u64, rng: &mut Lcg) -> String {
let subject = SUBJECTS[rng.next_index(SUBJECTS.len())];
let context = CONTEXTS[rng.next_index(CONTEXTS.len())];
let outcome = OUTCOMES[rng.next_index(OUTCOMES.len())];
format!("#{seq}: {subject} {context} {outcome}")
}

fn unix_time_nanos(seq: u64) -> u64 {
let base = 1_777_000_000_000_000_000u64;
base.saturating_add(seq.saturating_mul(500_000_000))
}

struct Lcg {
state: u64,
}

impl Lcg {
fn new(seed: u64) -> Self {
Self { state: seed }
}

fn next(&mut self) -> u64 {
self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
self.state
}

fn next_index(&mut self, len: usize) -> usize {
(self.next() % len as u64) as usize
}
}
30 changes: 30 additions & 0 deletions demo/visual-logtail/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Visual Logtail Demo

This demo starts a tiny emitter that appends one random OTLP log record to a `.logjet`
file every half second, then opens `ljx view --tail` on that file.

## Build First

From the project root:

```bash
cargo build -p ljx -p otlp-demo --bins
```

## Run

From this directory:

```bash
./run-demo.sh
```

## What It Does

1. creates `./logs/visual-logtail.logjet`
2. starts `visual-logtail-emitter` in the background
3. appends one fresh log record every 500 ms
4. opens `ljx view --tail ./logs/visual-logtail.logjet`

The viewer starts in tail mode automatically, so new records should keep landing at
the bottom until you press a key to stop tailing.
44 changes: 44 additions & 0 deletions demo/visual-logtail/run-demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/bin/sh
set -eu

SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
TARGET_DIR="$SCRIPT_DIR/../../target/debug"
EMITTER="$TARGET_DIR/visual-logtail-emitter"
LJX="$TARGET_DIR/ljx"
OUTPUT_DIR="$SCRIPT_DIR/logs"
OUTPUT_FILE="$OUTPUT_DIR/visual-logtail.logjet"
EMITTER_LOG="$OUTPUT_DIR/visual-logtail-emitter.log"
SEED=424242

if [ ! -x "$EMITTER" ] || [ ! -x "$LJX" ]; then
echo "missing demo binaries"
echo "build them first with: cargo build -p ljx -p otlp-demo --bins"
exit 1
fi

mkdir -p "$OUTPUT_DIR"
: > "$EMITTER_LOG"

if [ ! -f "$OUTPUT_FILE" ]; then
: > "$OUTPUT_FILE"
printf 'created fresh demo file -> %s\n' "$OUTPUT_FILE"
else
printf 'reusing existing demo file -> %s\n' "$OUTPUT_FILE"
fi

cleanup() {
if [ -n "${EMITTER_PID:-}" ]; then
kill "$EMITTER_PID" 2>/dev/null || true
wait "$EMITTER_PID" 2>/dev/null || true
fi
}
trap cleanup EXIT INT TERM

printf 'starting visual tail emitter -> %s\n' "$OUTPUT_FILE"
printf 'emitter stdout/stderr -> %s\n' "$EMITTER_LOG"
"$EMITTER" "$OUTPUT_FILE" "$SEED" >>"$EMITTER_LOG" 2>&1 &
EMITTER_PID=$!

sleep 1
printf 'opening ljx view --tail on %s\n' "$OUTPUT_FILE"
"$LJX" view --tail "$OUTPUT_FILE"
3 changes: 3 additions & 0 deletions ljx/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,9 @@ pub struct ViewArgs {

#[arg(long, default_value_t = false, help = "Show payload previews in hex")]
pub hex_payload: bool,

#[arg(long, default_value_t = false, help = "Start in tail mode after the initial scan completes")]
pub tail: bool,
}

#[derive(Debug, Clone, Copy, ValueEnum)]
Expand Down
Loading
Loading