use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::{Result, bail};
use clap::{Parser, Subcommand};
use telelog_server::archive::Archive;
use telelog_server::{Config, TlsFiles, auth};
use telelog_sources::{DockerSource, KubeSource, Sources};
use tokio::net::TcpListener;
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(version, about)]
struct Args {
#[command(subcommand)]
command: Option<Command>,
#[arg(long, env = "TELELOG_LISTEN", default_value = "127.0.0.1:7070")]
listen: SocketAddr,
#[arg(long, env = "TELELOG_TOKEN", hide_env_values = true)]
token: Option<String>,
#[arg(long, env = "TELELOG_TLS_CERT", requires = "tls_key")]
tls_cert: Option<PathBuf>,
#[arg(long, env = "TELELOG_TLS_KEY", requires = "tls_cert")]
tls_key: Option<PathBuf>,
#[arg(long)]
allow_unauthenticated: bool,
#[arg(long, env = "TELELOG_ARCHIVE_URL")]
archive_url: Option<String>,
#[arg(long, env = "TELELOG_ARCHIVE_FLUSH_SECS", default_value_t = 60, value_parser = clap::value_parser!(u64).range(1..=3600))]
archive_flush_secs: u64,
#[arg(long, env = "TELELOG_RETENTION_DAYS", default_value_t = 90)]
retention_days: u32,
#[arg(long, env = "TELELOG_NO_DOCKER")]
no_docker: bool,
#[arg(long, env = "TELELOG_KUBERNETES")]
kubernetes: bool,
#[arg(long, env = "TELELOG_KUBE_CONTEXT")]
kube_context: Option<String>,
#[arg(long = "namespace", env = "TELELOG_NAMESPACES", value_delimiter = ',')]
namespaces: Vec<String>,
#[arg(long, env = "TELELOG_KUBE_SELECTOR")]
selector: Option<String>,
}
#[derive(Subcommand)]
enum Command {
GenToken,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.init();
let args = Args::parse();
let _ = rustls::crypto::ring::default_provider().install_default();
if let Some(Command::GenToken) = args.command {
println!("{}", auth::generate_token()?);
return Ok(());
}
let sources = connect_sources(&args).await?;
let token = args.token.filter(|t| !t.is_empty());
if token.as_ref().is_some_and(|t| t.len() < 16) {
bail!("token is too short; use at least 16 characters (`telelog-server gen-token` makes one)");
}
let tls = args
.tls_cert
.zip(args.tls_key)
.map(|(cert, key)| TlsFiles { cert, key });
if let Some(warning) =
auth::check_exposure(args.listen, token.is_some(), tls.is_some(), args.allow_unauthenticated)?
{
tracing::warn!("{warning}");
}
let archive = match &args.archive_url {
Some(url) => {
let mut archive = Archive::open(url)?;
archive.flush_every = Duration::from_secs(args.archive_flush_secs);
archive.retention_days = args.retention_days;
Some(Arc::new(archive))
}
None => None,
};
let listener = TcpListener::bind(args.listen).await?;
tracing::info!(
listen = %args.listen,
auth = if token.is_some() { "token" } else { "none" },
tls = tls.is_some(),
archive = archive.as_ref().map_or("off".to_string(), |a| a.location.clone()),
"telelog-server started"
);
let shutdown = async {
let _ = tokio::signal::ctrl_c().await;
};
telelog_server::serve(listener, Config { token, tls, archive }, sources, shutdown).await
}
async fn connect_sources(args: &Args) -> Result<Sources> {
let kubernetes = args.kubernetes || args.kube_context.is_some();
let mut sources = Sources::default();
if !args.no_docker {
match DockerSource::connect() {
Ok(docker) if kubernetes => sources.docker = Some(docker.without_kubernetes()),
Ok(docker) => sources.docker = Some(docker),
Err(e) if kubernetes => tracing::warn!("not reading Docker: {e:#}"),
Err(e) => return Err(e),
}
}
if kubernetes {
let kube = KubeSource::connect(
args.kube_context.as_deref(),
args.namespaces.clone(),
args.selector.clone(),
)
.await?;
tracing::info!(
cluster = %kube.cluster,
namespaces = %if args.namespaces.is_empty() { "all".to_string() } else { args.namespaces.join(",") },
"reading Kubernetes"
);
sources.kubernetes = Some(kube);
} else if !args.namespaces.is_empty() || args.selector.is_some() {
bail!("--namespace and --selector need --kubernetes");
}
if sources.is_empty() {
bail!("no log sources: drop --no-docker or add --kubernetes");
}
Ok(sources)
}