1#![doc = include_str!("../docs/cli-reference.md")]
2#![warn(clippy::all, clippy::pedantic)]
3#![allow(clippy::missing_errors_doc)]
4
5pub mod cmds;
10pub mod fmt;
11
12use clap::{Args, Parser, Subcommand};
13use ordinary_config::OrdinaryConfig;
14use std::fmt::Display;
15use std::fs::DirEntry;
16use std::path::Path;
17
18use crate::fmt::StdioLogFmt;
19use ordinary_monitor::LOG_FILE_FORMAT;
20use ordinary_monitor::tracing::logger::OrdinaryLogger;
21use tracing::level_filters::LevelFilter;
22
23#[derive(Clone, Debug)]
24pub enum LogLevel {
25 Error,
26 Warn,
27 Info,
28 Debug,
29 Trace,
30}
31
32impl LogLevel {
33 fn to_level_filter(&self) -> LevelFilter {
34 match self {
35 Self::Error => LevelFilter::ERROR,
36 Self::Warn => LevelFilter::WARN,
37 Self::Info => LevelFilter::INFO,
38 Self::Debug => LevelFilter::DEBUG,
39 Self::Trace => LevelFilter::TRACE,
40 }
41 }
42}
43
44impl clap::ValueEnum for LogLevel {
45 fn value_variants<'a>() -> &'a [Self] {
46 &[
47 Self::Error,
48 Self::Warn,
49 Self::Info,
50 Self::Debug,
51 Self::Trace,
52 ]
53 }
54
55 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
56 match self {
57 Self::Error => Some(clap::builder::PossibleValue::new("error")),
58 Self::Warn => Some(clap::builder::PossibleValue::new("warn")),
59 Self::Info => Some(clap::builder::PossibleValue::new("info")),
60 Self::Debug => Some(clap::builder::PossibleValue::new("debug")),
61 Self::Trace => Some(clap::builder::PossibleValue::new("trace")),
62 }
63 }
64}
65
66impl Display for LogLevel {
67 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68 let str = match self {
69 Self::Error => String::from("error"),
70 Self::Warn => String::from("warn"),
71 Self::Info => String::from("info"),
72 Self::Debug => String::from("debug"),
73 Self::Trace => String::from("trace"),
74 };
75 write!(f, "{str}")
76 }
77}
78
79#[derive(Clone, Debug)]
80pub enum LogFileRotation {
81 Day,
82 Hour,
83 Minute,
84 Never,
85}
86
87impl clap::ValueEnum for LogFileRotation {
88 fn value_variants<'a>() -> &'a [Self] {
89 &[Self::Day, Self::Hour, Self::Minute, Self::Never]
90 }
91
92 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
93 match self {
94 Self::Day => Some(clap::builder::PossibleValue::new("day")),
95 Self::Hour => Some(clap::builder::PossibleValue::new("hour")),
96 Self::Minute => Some(clap::builder::PossibleValue::new("minute")),
97 Self::Never => Some(clap::builder::PossibleValue::new("never")),
98 }
99 }
100}
101
102impl Display for LogFileRotation {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 let str = match self {
105 Self::Day => String::from("day"),
106 Self::Hour => String::from("hour"),
107 Self::Minute => String::from("minute"),
108 Self::Never => String::from("never"),
109 };
110 write!(f, "{str}")
111 }
112}
113
114#[derive(Clone, Debug)]
115pub enum ProvisionMode {
116 Localhost,
117 Staging,
118 Production,
119}
120
121impl clap::ValueEnum for ProvisionMode {
122 fn value_variants<'a>() -> &'a [Self] {
123 &[Self::Staging, Self::Production, Self::Localhost]
124 }
125
126 fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
127 match self {
128 Self::Localhost => Some(clap::builder::PossibleValue::new("localhost")),
129 Self::Staging => Some(clap::builder::PossibleValue::new("staging")),
130 Self::Production => Some(clap::builder::PossibleValue::new("production")),
131 }
132 }
133}
134
135impl Display for ProvisionMode {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 let str = match self {
138 Self::Localhost => String::from("localhost"),
139 Self::Staging => String::from("staging"),
140 Self::Production => String::from("production"),
141 };
142 write!(f, "{str}")
143 }
144}
145
146#[allow(clippy::struct_excessive_bools)]
147#[derive(Parser, Debug)]
148#[command(version, about, long_about = None)]
149#[command(propagate_version = true)]
150pub struct Cli {
151 #[command(subcommand)]
152 pub commands: Commands,
153
154 #[command(flatten)]
155 pub global_args: GlobalArgs,
156}
157
158#[allow(clippy::struct_excessive_bools)]
159#[derive(Debug, Args, Clone)]
160pub struct GlobalArgs {
161 #[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())]
163 pub data_dir: String,
164
165 #[arg(long, global = true, default_value_t = false)]
167 pub stored_logs: bool,
168
169 #[arg(long, global = true, default_value_t = false)]
171 pub stdio_logs: bool,
172
173 #[arg(long, global = true, default_value_t = StdioLogFmt::Json)]
175 pub stdio_logs_fmt: StdioLogFmt,
176
177 #[arg(long, global = true, default_value_t = false)]
179 pub journald_logs: bool,
180
181 #[arg(long, global = true, default_value_t = LogLevel::Info)]
184 pub log_level: LogLevel,
185
186 #[arg(long, global = true, default_value_t = false)]
188 pub log_sizes: bool,
189
190 #[arg(long, global = true, default_value_t = false)]
192 pub stdio_logs_timing: bool,
193}
194
195#[derive(Subcommand, Debug)]
196pub enum Commands {
197 Init {
199 #[command(flatten)]
200 api_init: ApiInit,
201
202 #[arg(long)]
204 api_domain: String,
205
206 #[arg(long, default_value_t = String::from("password"))]
208 password: String,
209
210 #[arg(long, default_value_t = false)]
212 mfa_stored: bool,
213
214 #[arg(long, value_delimiter = ',', num_args = 1..)]
216 api_contacts: Vec<String>,
217
218 #[arg(long, value_delimiter = ',', num_args = 1..)]
223 app_domains: Vec<String>,
224
225 #[arg(long, value_delimiter = ',', num_args = 0..)]
231 privileged_domains: Option<Vec<String>>,
232 },
233 Api {
235 #[command(flatten)]
236 api_init: ApiInit,
237
238 #[command(flatten)]
239 app_api: AppApi,
240
241 #[arg(long, default_value_t = false)]
243 dedicated_ports: bool,
244
245 #[arg(long, default_value_t = false)]
247 swagger: bool,
248 },
249 App {
251 #[command(flatten)]
252 app_api: AppApi,
253
254 #[arg(short, long, default_value = ".")]
256 project: String,
257
258 #[arg(long)]
260 domain_override: Option<String>,
261 },
262}
263
264#[allow(clippy::struct_excessive_bools)]
265#[derive(Debug, Args)]
266pub struct AppApi {
267 #[arg(long, default_value_t = ProvisionMode::Localhost)]
269 pub provision: ProvisionMode,
270
271 #[arg(long)]
273 pub port: Option<u16>,
274
275 #[arg(long)]
278 pub redirect_port: Option<u16>,
279
280 #[arg(long, default_value_t = false)]
282 pub insecure: bool,
283
284 #[arg(long, default_value_t = false)]
286 pub insecure_cookies: bool,
287
288 #[arg(long, default_value_t = 72)]
290 pub log_ttl_hours: u16,
291
292 #[arg(long, default_value_t = 10_000_000)]
294 pub log_rotation_file_size: u64,
295
296 #[arg(long, default_value_t = 60)]
299 pub log_rotation_mins: u16,
300
301 #[arg(long, default_value_t = false)]
303 pub log_headers: bool,
304
305 #[arg(long, default_value_t = false)]
307 pub log_ips: bool,
308
309 #[arg(long, default_value_t = String::from("none"))]
312 pub redacted_header_hash: String,
313}
314
315#[derive(Debug, Args)]
316pub struct ApiInit {
317 #[arg(long, default_value_t = String::from("staging"))]
319 pub environment: String,
320
321 #[arg(long)]
323 pub storage_size: usize,
324}
325
326fn traverse(dir: &Path, cb: &dyn Fn(&DirEntry)) -> std::io::Result<()> {
327 if dir.is_dir() {
328 for entry in std::fs::read_dir(dir)? {
329 let entry = entry?;
330 let path = entry.path();
331 if path.is_dir() {
332 traverse(&path, cb)?;
333 } else {
334 cb(&entry);
335 }
336 }
337 }
338 Ok(())
339}
340
341#[allow(clippy::too_many_lines)]
342pub fn setup(cli: &Cli) -> anyhow::Result<Option<OrdinaryLogger>> {
343 let logs_dir = match &cli.commands {
344 Commands::App { project, .. } => {
345 let config = OrdinaryConfig::get(project)?;
346
347 Path::new(&cli.global_args.data_dir)
348 .join("apps")
349 .join(config.domain)
350 .join("logs")
351 }
352 Commands::Init { api_init, .. } | Commands::Api { api_init, .. } => {
353 Path::new(&cli.global_args.data_dir)
354 .join("environments")
355 .join(&api_init.environment)
356 .join("logs")
357 }
358 };
359
360 std::fs::create_dir_all(&logs_dir)?;
361
362 let log_level_str = cli.global_args.log_level.to_string();
363
364 let directives = [
365 ("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), ];
383
384 let mut directives_string = format!("ordinaryd={}", &log_level_str);
385
386 for (lib, lvl) in directives {
387 directives_string = format!("{directives_string},{lib}={lvl}");
388 }
389
390 let filter = tracing_subscriber::EnvFilter::builder()
391 .with_default_directive(cli.global_args.log_level.to_level_filter().into())
392 .parse(directives_string)?;
393
394 let logger = if cli.global_args.stored_logs || cli.global_args.stdio_logs {
395 let mut args = None;
396
397 if let Commands::Api { app_api, .. } = &cli.commands {
398 args = Some(app_api);
399 }
400
401 if let Commands::App { app_api, .. } = &cli.commands {
402 args = Some(app_api);
403 }
404
405 if let Some(AppApi {
406 log_ttl_hours,
407 log_rotation_file_size,
408 log_rotation_mins,
409 ..
410 }) = args
411 {
412 Some(OrdinaryLogger::new(
413 cli.global_args.stored_logs,
414 cli.global_args.stdio_logs,
415 &cli.global_args.stdio_logs_fmt.to_string(),
416 cli.global_args.journald_logs,
417 &logs_dir,
418 filter,
419 *log_ttl_hours,
420 *log_rotation_mins,
421 usize::try_from(*log_rotation_file_size)?,
422 LOG_FILE_FORMAT,
423 cli.global_args.log_sizes,
424 cli.global_args.stdio_logs_timing,
425 )?)
426 } else {
427 None
428 }
429 } else {
430 None
431 };
432
433 std::panic::set_hook(Box::new(|info| {
434 if let Some(msg) = info.payload_as_str()
435 && let Some(loc) = info.location()
436 {
437 tracing::error!(%loc, msg, "panic");
438 } else if let Some(loc) = info.location() {
439 tracing::error!(%loc, "panic");
440 }
441 }));
442
443 Ok(logger)
444}
445
446#[allow(clippy::too_many_lines, clippy::missing_panics_doc)]
447pub async fn run(cli: &Cli, logger: Option<OrdinaryLogger>) -> anyhow::Result<()> {
448 match &cli.commands {
449 Commands::App {
450 app_api,
451 project,
452 domain_override,
453 } => {
454 cmds::app::run(
455 project,
456 domain_override,
457 &cli.global_args.data_dir,
458 cli.global_args.log_sizes,
459 app_api.insecure,
460 app_api.insecure_cookies,
461 app_api.log_headers,
462 app_api.log_ips,
463 app_api.port,
464 app_api.redirect_port,
465 &app_api.provision,
466 )
467 .await?;
468 }
469 Commands::Init {
470 api_init,
471 api_domain,
472 password,
473 mfa_stored,
474 api_contacts,
475 app_domains,
476 privileged_domains,
477 } => {
478 cmds::init::run(
479 &api_init.environment,
480 api_domain,
481 password,
482 &cli.global_args.data_dir,
483 api_init.storage_size,
484 *mfa_stored,
485 api_contacts,
486 app_domains,
487 privileged_domains,
488 logger,
489 )
490 .await?;
491 }
492 Commands::Api {
493 api_init,
494 app_api,
495 dedicated_ports,
496 swagger,
497 ..
498 } => {
499 cmds::api::run(
500 &api_init.environment,
501 &cli.global_args.data_dir,
502 api_init.storage_size,
503 cli.global_args.log_sizes,
504 app_api.insecure,
505 app_api.insecure_cookies,
506 app_api.log_headers,
507 app_api.log_ips,
508 app_api.port,
509 app_api.redirect_port,
510 &app_api.provision,
511 cli.global_args.stored_logs,
512 logger,
513 &app_api.redacted_header_hash,
514 *dedicated_ports,
515 *swagger,
516 )
517 .await?;
518 }
519 }
520
521 Ok(())
522}