1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
#![doc = include_str!("../README.md")]

use tokio::sync::OnceCell;

#[cfg(test)]
#[macro_use]
pub mod test_util;

pub mod common;
pub mod config;
pub mod grpc;
pub mod postgres;
pub mod resources;

pub use crate::config::Config;
pub use clap::Parser;

/// struct holding cli configuration options
#[derive(Parser, Debug, Clone)]
pub struct Cli {
    /// Indicates if we should initialize the database. If not found, defaults to false
    #[arg(long)]
    pub init_psql: Option<bool>,
    /// Indicates if we should rebuild the database. If not found, defaults to false
    #[arg(long)]
    pub rebuild_psql: Option<bool>,
}
impl Copy for Cli {}

/// Initialized log4rs handle
pub static LOG_HANDLE: OnceCell<Option<log4rs::Handle>> = OnceCell::const_new();
pub(crate) async fn get_log_handle() -> Option<log4rs::Handle> {
    LOG_HANDLE
        .get_or_init(|| async move {
            // Set up basic logger to make sure we can write to stdout
            let stdout = log4rs::append::console::ConsoleAppender::builder()
                .encoder(Box::new(log4rs::encode::pattern::PatternEncoder::new(
                    "{d(%Y-%m-%d %H:%M:%S)} | {I} | {h({l}):5.5} | {f}:{L} | {m}{n}",
                )))
                .build();
            let file = log4rs::append::file::FileAppender::builder()
                .encoder(Box::new(log4rs::encode::json::JsonEncoder::new()))
                .build("logs/all.log")
                .unwrap();

            match log4rs::config::Config::builder()
                .appender(log4rs::config::Appender::builder().build("stdout", Box::new(stdout)))
                .appender(log4rs::config::Appender::builder().build("file", Box::new(file)))
                .logger(
                    log4rs::config::Logger::builder()
                        .appender("file")
                        .build("backend", log::LevelFilter::Debug),
                )
                .logger(
                    log4rs::config::Logger::builder()
                        .appender("file")
                        .build("app", log::LevelFilter::Debug),
                )
                .logger(
                    log4rs::config::Logger::builder()
                        .appender("file")
                        .build("test", log::LevelFilter::Debug),
                )
                .build(
                    log4rs::config::Root::builder()
                        .appender("stdout")
                        .build(log::LevelFilter::Debug),
                ) {
                Ok(config) => log4rs::init_config(config).ok(),
                Err(_) => None,
            }
        })
        .await
        .to_owned()
}

/// Initialize a log4rs logger with provided configuration file path
pub async fn load_logger_config_from_file(config_file: &str) -> Result<(), String> {
    let log_handle = get_log_handle()
        .await
        .ok_or("(load_logger_config_from_file) Could not get the log handle.")?;
    match log4rs::config::load_config_file(config_file, Default::default()) {
        Ok(config) => {
            log_handle.set_config(config);
            Ok(())
        }
        Err(e) => Err(format!(
            "(logger) Could not parse log config file [{}]: {}.",
            config_file, e,
        )),
    }
}

/// Tokio signal handler that will wait for a user to press CTRL+C.
/// This signal handler can be used in our [`tonic::transport::Server`] method `serve_with_shutdown`.
///
/// # Examples
///
/// ## tonic
/// ```
/// use svc_storage::shutdown_signal;
/// pub async fn server() {
///     let (_, health_service) = tonic_health::server::health_reporter();
///     tonic::transport::Server::builder()
///         .add_service(health_service)
///         .serve_with_shutdown("0.0.0.0:50051".parse().unwrap(), shutdown_signal("grpc", None));
/// }
/// ```
///
/// ## using a shutdown signal channel
/// ```
/// use svc_storage::shutdown_signal;
/// pub async fn server() {
///     let (shutdown_tx, shutdown_rx) = tokio::sync::oneshot::channel::<()>();
///     let (_, health_service) = tonic_health::server::health_reporter();
///     tokio::spawn(async move {
///         tonic::transport::Server::builder()
///             .add_service(health_service)
///             .serve_with_shutdown("0.0.0.0:50051".parse().unwrap(), shutdown_signal("grpc", Some(shutdown_rx)))
///             .await;
///     });
///
///     // Send server the shutdown request
///     shutdown_tx.send(()).expect("Could not stop server.");
/// }
/// ```
pub async fn shutdown_signal(
    server: &str,
    shutdown_rx: Option<tokio::sync::oneshot::Receiver<()>>,
) {
    match shutdown_rx {
        Some(receiver) => receiver
            .await
            .expect("(shutdown_signal) expect tokio signal oneshot Receiver"),
        None => tokio::signal::ctrl_c()
            .await
            .expect("(shutdown_signal) expect tokio signal ctrl-c"),
    }

    log::warn!("(shutdown_signal) server shutdown for [{}].", server);
}

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_load_logger_config_from_file() {
        get_log_handle().await;
        ut_info!("(test_config_from_env) Start.");

        let result = load_logger_config_from_file("/usr/src/app/log4rs.yaml").await;
        ut_debug!("(test_config_from_env) {:?}", result);
        assert!(result.is_ok());

        // This message should be written to file
        ut_error!("(test_config_from_env) Testing log config from file. This should be written to the tests.log file.");

        ut_info!("(test_config_from_env) Success.");
    }
}