pub mod cli;
pub mod ingest;
pub mod report_serve;
pub mod server;
pub mod simulator;
pub mod storage;
mod trace_shape;
pub use report_serve::report_serve_router;
pub use server::metrics::attach_request_metrics;
use std::path::PathBuf;
pub(crate) fn resolve_dev_ui_dir(dev: bool) -> anyhow::Result<Option<PathBuf>> {
if !dev {
return Ok(None);
}
let candidates = [
PathBuf::from("ui/dist"),
PathBuf::from("dial9-viewer/ui/dist"),
];
let Some(dir) = candidates.into_iter().find(|p| p.exists()) else {
anyhow::bail!(
"--dev: could not find ui/dist/ directory. Run from the dial9-viewer/ or repo root directory."
);
};
if !dir.join("index.html").exists() {
tracing::warn!(
path = %dir.display(),
"ui/dist has no built UI - run `npm run build` or `npm run dev:embedded` \
in dial9-viewer/ui first (UI work requires Node, see ui/README.md)"
);
}
tracing::info!(path = %dir.display(), "dev mode: serving UI from disk");
Ok(Some(dir))
}
async fn detect_bucket_region(bucket: &str) -> Option<String> {
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
let client = aws_sdk_s3::Client::new(&config);
server::region_from_head_bucket(&client, bucket).await
}
#[derive(Debug, Clone)]
pub struct ViewerConfig {
pub bucket: Option<String>,
pub prefix: Option<String>,
pub local_dir: Option<PathBuf>,
pub dev: bool,
pub agg: bool,
pub agg_source_dir: Option<PathBuf>,
pub agg_output_dir: Option<PathBuf>,
pub agg_output_bucket: Option<String>,
pub agg_output_prefix: String,
pub agg_segment_secs: i64,
pub enable_upload: bool,
}
impl Default for ViewerConfig {
fn default() -> Self {
Self {
bucket: None,
prefix: None,
local_dir: None,
dev: false,
agg: false,
agg_source_dir: None,
agg_output_dir: None,
agg_output_bucket: None,
agg_output_prefix: "flamegraph-data".to_string(),
agg_segment_secs: crate::ingest::aggregate::DEFAULT_SEGMENT_DURATION_SECS,
enable_upload: false,
}
}
}
pub(crate) async fn s3_backend_for(bucket: &str) -> storage::S3Backend {
if let Some(region) = detect_bucket_region(bucket).await {
tracing::info!(%region, %bucket, "detected bucket region");
let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(aws_sdk_s3::config::Region::new(region))
.load()
.await;
storage::S3Backend::from_client(aws_sdk_s3::Client::new(&config))
} else {
tracing::warn!(%bucket, "could not detect bucket region, using default");
storage::S3Backend::from_env().await
}
}
pub fn init_tracing(local: bool) {
let env_filter = || {
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "dial9_viewer=info".parse().unwrap())
};
if local {
tracing_subscriber::fmt()
.with_env_filter(env_filter())
.init();
} else {
tracing_subscriber::fmt()
.json()
.with_env_filter(env_filter())
.init();
}
}
pub async fn build_app(
ViewerConfig {
bucket,
prefix,
local_dir,
dev,
agg,
agg_source_dir,
agg_output_dir,
agg_output_bucket,
agg_output_prefix,
agg_segment_secs,
enable_upload,
}: ViewerConfig,
) -> anyhow::Result<axum::Router> {
use crate::ingest::aggregate::AggContext;
use crate::server::AggOutput;
let agg_output = if let Some(out_bucket) = &agg_output_bucket {
let backend = std::sync::Arc::new(s3_backend_for(out_bucket).await);
AggOutput::s3(out_bucket.clone(), backend).with_prefix(agg_output_prefix.clone())
} else {
AggOutput::temporary().with_prefix(agg_output_prefix.clone())
};
tracing::info!(
output = %agg_output.location(),
"aggregate output destination (writes go here, never the source)"
);
let agg = if let Some(src_dir) = &agg_source_dir {
let src_dir = std::fs::canonicalize(src_dir)?;
let out_dir = agg_output_dir.unwrap_or_else(|| src_dir.join("flamegraph-data"));
std::fs::create_dir_all(&out_dir)?;
let out_dir = std::fs::canonicalize(&out_dir)?;
tracing::info!(
source = %src_dir.display(),
output = %out_dir.display(),
"demand-driven aggregation enabled (local)"
);
Some(AggContext {
source: std::sync::Arc::new(storage::LocalBackend::new(&src_dir)),
output: std::sync::Arc::new(storage::LocalBackend::new(&out_dir)),
source_bucket: "local".to_string(),
source_is_local: true,
output_bucket: "local".to_string(),
output_prefix: ".".to_string(),
source_prefixes: vec![String::new()],
segment_duration_secs: agg_segment_secs,
})
} else if agg {
let Some(src_bucket) = bucket.clone() else {
anyhow::bail!("--agg requires --bucket (the S3 source of raw traces)");
};
let source = std::sync::Arc::new(s3_backend_for(&src_bucket).await);
tracing::info!(
source_bucket = %src_bucket,
output = %agg_output.location(),
output_prefix = %agg_output.prefix(),
"demand-driven aggregation enabled (S3 source)"
);
Some(AggContext {
source,
output: agg_output.backend(),
output_bucket: agg_output.output_bucket_for(&src_bucket),
source_bucket: src_bucket,
source_is_local: false,
output_prefix: agg_output.prefix().to_string(),
source_prefixes: vec![prefix.clone().unwrap_or_default()],
segment_duration_secs: agg_segment_secs,
})
} else {
None
};
let dev_ui_dir = resolve_dev_ui_dir(dev)?;
let (mut app_state, source_is_s3) = if let Some(agg) = &agg {
let source_is_s3 = !agg.source_is_local;
let state = server::AppState::new(
std::sync::Arc::clone(&agg.source),
Some(agg.source_bucket.clone()),
prefix.clone(),
)
.with_agg(agg.clone());
(state, source_is_s3)
} else if let Some(dir) = &local_dir {
let dir = std::fs::canonicalize(dir)?;
tracing::info!(path = %dir.display(), "serving traces from local directory");
let backend = storage::LocalBackend::new(&dir);
let state = server::AppState::new(
std::sync::Arc::new(backend),
Some("local".into()),
prefix.clone(),
);
(state, false)
} else if let Some(bucket_name) = &bucket {
if let Some(region) = detect_bucket_region(bucket_name).await {
tracing::info!(%region, bucket = %bucket_name, "detected bucket region");
let config = aws_config::defaults(aws_config::BehaviorVersion::latest())
.region(aws_sdk_s3::config::Region::new(region))
.load()
.await;
let client = aws_sdk_s3::Client::new(&config);
let backend = storage::S3Backend::from_client(client);
let state =
server::AppState::new(std::sync::Arc::new(backend), bucket.clone(), prefix.clone());
(state, true)
} else {
tracing::warn!(bucket = %bucket_name, "could not detect bucket region, using default");
let backend = storage::S3Backend::from_env().await;
let state =
server::AppState::new(std::sync::Arc::new(backend), bucket.clone(), prefix.clone());
(state, true)
}
} else {
let backend = storage::S3Backend::from_env().await;
let state =
server::AppState::new(std::sync::Arc::new(backend), bucket.clone(), prefix.clone());
(state, true)
};
app_state = app_state
.with_byo_creds(source_is_s3)
.with_agg_output(agg_output)
.with_agg_segment_secs(agg_segment_secs);
if source_is_s3 {
let assumer = server::credentials::StsRoleAssumer::from_env().await;
app_state = app_state.with_role_assumer(std::sync::Arc::new(assumer));
}
if let Some(d) = dev_ui_dir {
app_state = app_state.with_dev_ui_dir(d);
}
if enable_upload {
tracing::info!(
"trace-upload feature enabled (POST /api/upload); no auth — trusted network only"
);
app_state = app_state.with_uploads(server::UploadLimits::default());
}
let app = server::router(app_state);
Ok(app)
}
pub async fn shutdown_signal() {
tokio::signal::ctrl_c()
.await
.expect("failed to install CTRL+C handler");
tracing::info!("shutting down");
}