// Copyright 2024 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! End-to-end check that a traced runtime actually records events to disk.
//!
//! Guards against the regression where `TracedRuntime::builder()...build(..)`
//! was used instead of `build_and_start(..)`: that returns a guard with
//! recording disabled, so telemetry appeared healthy and produced empty traces.
//!
//! Configuration comes from the environment (the crate forbids `unsafe`, so this
//! cannot call `set_var`). Run with:
//!
//! ```bash
//! RUSTFLAGS="--cfg tokio_unstable" \
//! RUSTFS_RUNTIME_DIAL9_ENABLED=true \
//! RUSTFS_RUNTIME_DIAL9_OUTPUT_DIR=/tmp/rustfs-dial9-smoke \
//! RUSTFS_RUNTIME_DIAL9_MAX_FILE_SIZE=65536 \
//! RUSTFS_RUNTIME_DIAL9_ROTATION_COUNT=4 \
//! cargo run -p rustfs-obs --features dial9 --example dial9_smoke
//! ```
use rustfs_obs::dial9::{Dial9Config, build_traced_runtime};
use std::path::Path;
const USAGE: &str = "\
set RUSTFS_RUNTIME_DIAL9_ENABLED=true and RUSTFS_RUNTIME_DIAL9_OUTPUT_DIR=
\
before running this example (see the module docs for a full command line)";
/// A segment that was opened but never recorded an event still carries a header
/// (~292 bytes when this was written). A workload like the one below produces
/// several KiB. Anything under this threshold means events are not being
/// recorded, even though the file exists and the guard looks healthy.
const MIN_EXPECTED_TRACE_BYTES: u64 = 2048;
fn segment_bytes(dir: &Path, prefix: &str) -> u64 {
let Ok(entries) = std::fs::read_dir(dir) else {
return 0;
};
entries
.filter_map(Result::ok)
.filter(|e| e.file_name().to_str().is_some_and(|n| n.starts_with(prefix)))
.filter_map(|e| e.metadata().ok())
.map(|m| m.len())
.sum()
}
fn main() {
let config = Dial9Config::from_env();
if !config.enabled {
eprintln!("{USAGE}");
std::process::exit(2);
}
let output_dir = std::path::PathBuf::from(&config.output_dir);
println!("output_dir = {}", config.output_dir);
println!("disk budget = {} bytes", config.total_disk_budget());
let mut builder = tokio::runtime::Builder::new_multi_thread();
builder.worker_threads(2).enable_all();
let (runtime, guard) = build_traced_runtime(builder).expect("traced runtime should build");
// Necessary but not sufficient: a guard built without `build_and_start` also
// reports active. Only the byte count below proves events were recorded.
assert!(guard.is_active(), "telemetry session must be live");
println!("session active = {}", guard.is_active());
// Generate poll/park/spawn traffic, including a deliberately long poll.
runtime.block_on(async {
let mut handles = Vec::new();
for i in 0..64_u64 {
handles.push(tokio::spawn(async move {
if i % 16 == 0 {
// Block the worker: exactly the fault dial9 exists to surface.
std::thread::sleep(std::time::Duration::from_millis(5));
}
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
i * 2
}));
}
for h in handles {
h.await.expect("task should not panic");
}
});
// Dropping the guard flushes buffered events and seals the active segment.
drop(guard);
drop(runtime);
let bytes = segment_bytes(&output_dir, &config.file_prefix);
println!("trace bytes = {bytes}");
assert!(
bytes >= MIN_EXPECTED_TRACE_BYTES,
"dial9 wrote only {bytes} bytes to {} (expected >= {MIN_EXPECTED_TRACE_BYTES}); \
the segment holds a header but no events, so recording never started",
output_dir.display()
);
println!("\nOK: dial9 recorded {bytes} bytes of trace data.");
}