#![doc = include_str!("../README.md")]
#![doc = include_str!("../docs/cli-reference.md")]
#![warn(clippy::all, clippy::pedantic)]
#![allow(clippy::missing_errors_doc)]
pub mod cmds;
pub mod fmt;
use clap::{Args, Parser, Subcommand};
use ordinary_config::OrdinaryConfig;
use std::fmt::Display;
use std::fs::DirEntry;
use std::path::Path;
use crate::fmt::StdioLogFmt;
use ordinary_monitor::LOG_FILE_FORMAT;
use ordinary_monitor::tracing::logger::OrdinaryLogger;
use tracing::level_filters::LevelFilter;
#[derive(Clone, Debug)]
pub enum LogLevel {
Error,
Warn,
Info,
Debug,
Trace,
}
impl LogLevel {
fn to_level_filter(&self) -> LevelFilter {
match self {
Self::Error => LevelFilter::ERROR,
Self::Warn => LevelFilter::WARN,
Self::Info => LevelFilter::INFO,
Self::Debug => LevelFilter::DEBUG,
Self::Trace => LevelFilter::TRACE,
}
}
}
impl clap::ValueEnum for LogLevel {
fn value_variants<'a>() -> &'a [Self] {
&[
Self::Error,
Self::Warn,
Self::Info,
Self::Debug,
Self::Trace,
]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
match self {
Self::Error => Some(clap::builder::PossibleValue::new("error")),
Self::Warn => Some(clap::builder::PossibleValue::new("warn")),
Self::Info => Some(clap::builder::PossibleValue::new("info")),
Self::Debug => Some(clap::builder::PossibleValue::new("debug")),
Self::Trace => Some(clap::builder::PossibleValue::new("trace")),
}
}
}
impl Display for LogLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
Self::Error => String::from("error"),
Self::Warn => String::from("warn"),
Self::Info => String::from("info"),
Self::Debug => String::from("debug"),
Self::Trace => String::from("trace"),
};
write!(f, "{str}")
}
}
#[derive(Clone, Debug)]
pub enum LogFileRotation {
Day,
Hour,
Minute,
Never,
}
impl clap::ValueEnum for LogFileRotation {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Day, Self::Hour, Self::Minute, Self::Never]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
match self {
Self::Day => Some(clap::builder::PossibleValue::new("day")),
Self::Hour => Some(clap::builder::PossibleValue::new("hour")),
Self::Minute => Some(clap::builder::PossibleValue::new("minute")),
Self::Never => Some(clap::builder::PossibleValue::new("never")),
}
}
}
impl Display for LogFileRotation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
Self::Day => String::from("day"),
Self::Hour => String::from("hour"),
Self::Minute => String::from("minute"),
Self::Never => String::from("never"),
};
write!(f, "{str}")
}
}
#[derive(Clone, Debug)]
pub enum ProvisionMode {
Localhost,
Staging,
Production,
}
impl clap::ValueEnum for ProvisionMode {
fn value_variants<'a>() -> &'a [Self] {
&[Self::Staging, Self::Production, Self::Localhost]
}
fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
match self {
Self::Localhost => Some(clap::builder::PossibleValue::new("localhost")),
Self::Staging => Some(clap::builder::PossibleValue::new("staging")),
Self::Production => Some(clap::builder::PossibleValue::new("production")),
}
}
}
impl Display for ProvisionMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let str = match self {
Self::Localhost => String::from("localhost"),
Self::Staging => String::from("staging"),
Self::Production => String::from("production"),
};
write!(f, "{str}")
}
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub commands: Commands,
#[command(flatten)]
pub global_args: GlobalArgs,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Args, Clone)]
pub struct GlobalArgs {
#[arg(long, global = true, default_value_t = std::env::home_dir().expect("failed to get home dir").join(".ordinary").to_str().expect("failed to convert to str").to_string())]
pub data_dir: String,
#[arg(long, global = true, default_value_t = false)]
pub stored_logs: bool,
#[arg(long, global = true, default_value_t = false)]
pub stdio_logs: bool,
#[arg(long, global = true, default_value_t = StdioLogFmt::Json)]
pub stdio_logs_fmt: StdioLogFmt,
#[arg(long, global = true, default_value_t = false)]
pub journald_logs: bool,
#[arg(long, global = true, default_value_t = LogLevel::Info)]
pub log_level: LogLevel,
#[arg(long, global = true, default_value_t = false)]
pub log_sizes: bool,
#[arg(long, global = true, default_value_t = false)]
pub stdio_logs_timing: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Init {
#[command(flatten)]
api_init: ApiInit,
#[arg(long)]
api_domain: String,
#[arg(long, default_value_t = String::from("password"))]
password: String,
#[arg(long, default_value_t = false)]
mfa_stored: bool,
#[arg(long, value_delimiter = ',', num_args = 1..)]
api_contacts: Vec<String>,
#[arg(long, value_delimiter = ',', num_args = 1..)]
app_domains: Vec<String>,
#[arg(long, value_delimiter = ',', num_args = 0..)]
privileged_domains: Option<Vec<String>>,
},
Api {
#[command(flatten)]
api_init: ApiInit,
#[command(flatten)]
app_api: AppApi,
#[arg(long, default_value_t = false)]
dedicated_ports: bool,
#[arg(long, default_value_t = false)]
openapi: bool,
#[arg(long, default_value_t = false)]
swagger: bool,
},
App {
#[command(flatten)]
app_api: AppApi,
#[arg(short, long, default_value = ".")]
project: String,
#[arg(long)]
domain_override: Option<String>,
},
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Args)]
pub struct AppApi {
#[arg(long, default_value_t = ProvisionMode::Localhost)]
pub provision: ProvisionMode,
#[arg(long)]
pub port: Option<u16>,
#[arg(long)]
pub redirect_port: Option<u16>,
#[arg(long, default_value_t = false)]
pub insecure: bool,
#[arg(long, default_value_t = false)]
pub insecure_cookies: bool,
#[arg(long, default_value_t = 72)]
pub log_ttl_hours: u16,
#[arg(long, default_value_t = 10_000_000)]
pub log_rotation_file_size: u64,
#[arg(long, default_value_t = 60)]
pub log_rotation_mins: u16,
#[arg(long, default_value_t = false)]
pub log_headers: bool,
#[arg(long, default_value_t = false)]
pub log_ips: bool,
#[arg(long, default_value_t = String::from("none"))]
pub redacted_header_hash: String,
#[arg(long, default_value_t = false)]
pub danger_dns_no_verify: bool,
}
#[derive(Debug, Args)]
pub struct ApiInit {
#[arg(long, default_value_t = String::from("staging"))]
pub environment: String,
#[arg(long)]
pub storage_size: usize,
}
fn traverse(dir: &Path, cb: &dyn Fn(&DirEntry)) -> std::io::Result<()> {
if dir.is_dir() {
for entry in std::fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
traverse(&path, cb)?;
} else {
cb(&entry);
}
}
}
Ok(())
}
#[allow(clippy::too_many_lines)]
pub fn setup(cli: &Cli) -> anyhow::Result<Option<OrdinaryLogger>> {
let logs_dir = match &cli.commands {
Commands::App { project, .. } => {
let config = OrdinaryConfig::get(project)?;
Path::new(&cli.global_args.data_dir)
.join("apps")
.join(config.domain)
.join("logs")
}
Commands::Init { api_init, .. } | Commands::Api { api_init, .. } => {
Path::new(&cli.global_args.data_dir)
.join("environments")
.join(&api_init.environment)
.join("logs")
}
};
std::fs::create_dir_all(&logs_dir)?;
let log_level_str = cli.global_args.log_level.to_string();
let directives = [
("ordinary_config", &log_level_str), ("ordinary_studio", &log_level_str), ("ordinary_doctor", &log_level_str), ("ordinary_build", &log_level_str), ("ordinary_modify", &log_level_str), ("ordinary_utils", &log_level_str), ("ordinary_auth", &log_level_str), ("ordinary_api", &log_level_str), ("ordinary_app", &log_level_str), ("ordinary_template", &log_level_str), ("ordinary_action", &log_level_str), ("ordinary_integration", &log_level_str), ("ordinary_storage", &log_level_str), ("ordinary_monitor", &log_level_str), ("tower_http", &log_level_str), ("axum::rejection", &"trace".into()), ("axum::serve", &log_level_str), ];
let mut directives_string = format!("ordinaryd={}", &log_level_str);
for (lib, lvl) in directives {
directives_string = format!("{directives_string},{lib}={lvl}");
}
let filter = tracing_subscriber::EnvFilter::builder()
.with_default_directive(cli.global_args.log_level.to_level_filter().into())
.parse(directives_string)?;
let logger = if cli.global_args.stored_logs || cli.global_args.stdio_logs {
let mut args = None;
if let Commands::Api { app_api, .. } = &cli.commands {
args = Some(app_api);
}
if let Commands::App { app_api, .. } = &cli.commands {
args = Some(app_api);
}
if let Some(AppApi {
log_ttl_hours,
log_rotation_file_size,
log_rotation_mins,
..
}) = args
{
Some(OrdinaryLogger::new(
cli.global_args.stored_logs,
cli.global_args.stdio_logs,
&cli.global_args.stdio_logs_fmt.to_string(),
cli.global_args.journald_logs,
&logs_dir,
filter,
*log_ttl_hours,
*log_rotation_mins,
usize::try_from(*log_rotation_file_size)?,
LOG_FILE_FORMAT,
cli.global_args.log_sizes,
cli.global_args.stdio_logs_timing,
)?)
} else {
None
}
} else {
None
};
std::panic::set_hook(Box::new(|info| {
if let Some(msg) = info.payload_as_str()
&& let Some(loc) = info.location()
{
tracing::error!(%loc, msg, "panic");
} else if let Some(loc) = info.location() {
tracing::error!(%loc, "panic");
}
}));
Ok(logger)
}
#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
pub async fn run(cli: &Cli, logger: Option<OrdinaryLogger>) -> anyhow::Result<()> {
match &cli.commands {
Commands::App {
app_api,
project,
domain_override,
} => {
cmds::app::run(
project,
domain_override,
&cli.global_args.data_dir,
cli.global_args.log_sizes,
app_api.insecure,
app_api.insecure_cookies,
app_api.log_headers,
app_api.log_ips,
app_api.port,
app_api.redirect_port,
&app_api.provision,
app_api.danger_dns_no_verify,
)
.await?;
}
Commands::Init {
api_init,
api_domain,
password,
mfa_stored,
api_contacts,
app_domains,
privileged_domains,
} => {
cmds::init::run(
&api_init.environment,
api_domain,
password,
&cli.global_args.data_dir,
api_init.storage_size,
*mfa_stored,
api_contacts,
app_domains,
privileged_domains,
logger,
)
.await?;
}
Commands::Api {
api_init,
app_api,
dedicated_ports,
openapi,
swagger,
..
} => {
cmds::api::run(
&api_init.environment,
&cli.global_args.data_dir,
api_init.storage_size,
cli.global_args.log_sizes,
app_api.insecure,
app_api.insecure_cookies,
app_api.log_headers,
app_api.log_ips,
app_api.port,
app_api.redirect_port,
&app_api.provision,
cli.global_args.stored_logs,
logger,
&app_api.redacted_header_hash,
*dedicated_ports,
*openapi,
*swagger,
app_api.danger_dns_no_verify,
)
.await?;
}
}
Ok(())
}