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