DEV Community

Cover image for Rust: Actix-web and Daily Logging
Be Hai Nguyen
Be Hai Nguyen

Posted on

Rust: Actix-web and Daily Logging

Currently, our actix-web learning application simply prints debug information to the console using the println! macro. In this post, we will implement proper non-blocking daily logging to files. Daily logging entails rotating to a new log file each day. Non-blocking refers to having a dedicated thread for file writing operations. We will utilise the tracing, tracing-appender, and tracing-subscriber crates for our logging implementation.

πŸ¦€ Index of the Complete Series.

πŸš€ Please note, complete code for this post can be downloaded from GitHub with:

git clone -b v0.11.0 https://github.com/behai-nguyen/rust_web_01.git
Enter fullscreen mode Exit fullscreen mode

The actix-web learning application mentioned above has been discussed in the following ten previous posts:

  1. Rust web application: MySQL server, sqlx, actix-web and tera.
  2. Rust: learning actix-web middleware 01.
  3. Rust: retrofit integration tests to an existing actix-web application.
  4. Rust: adding actix-session and actix-identity to an existing actix-web application.
  5. Rust: actix-web endpoints which accept both application/x-www-form-urlencoded and application/json content types.
  6. Rust: simple actix-web email-password login and request authentication using middleware.
  7. Rust: actix-web get SSL/HTTPS for localhost.
  8. Rust: actix-web CORS, Cookies and AJAX calls.
  9. Rust: actix-web global extractor error handlers.
  10. Rust: actix-web JSON Web Token authentication.

The code we're developing in this post is a continuation of the code from the tenth post above. πŸš€ To get the code of this tenth post, please use the following command:

git clone -b v0.10.0 https://github.com/behai-nguyen/rust_web_01.git
Enter fullscreen mode Exit fullscreen mode

-- Note the tag v0.10.0.

While this post continues from previous posts in this series, it can also be read independently. The logging module developed herein can be used in other projects without modification.


❢ For this post, we introduce a new module src/helper/app_logger.rs, and some other modules and files are updated. The project layout remains the same as in the last post. The layout chart below shows the affected files and modules:

-- Please note that files marked with β˜… are updated, and src/helper/app_logger.rs is marked with β˜†, as it is the only new module.


.
β”œβ”€β”€ .env β˜…
β”œβ”€β”€ Cargo.toml β˜…
β”œβ”€β”€ ...
β”œβ”€β”€ README.md β˜…
β”œβ”€β”€ src
β”‚ β”œβ”€β”€ auth_middleware.rs β˜…
β”‚ β”œβ”€β”€ database.rs β˜…
β”‚ β”œβ”€β”€ helper
β”‚ β”‚ β”œβ”€β”€ app_logger.rs β˜†
β”‚ β”‚ └── ...
β”‚ β”œβ”€β”€ helper.rs β˜…
β”‚ β”œβ”€β”€ main.rs β˜…
β”‚ β”œβ”€β”€ middleware.rs β˜…
β”‚ └── ...
└── tests
    β”œβ”€β”€ common.rs β˜…
    └── ...
Enter fullscreen mode Exit fullscreen mode


❷ An update to the .env file: a new entry has been added:

RUST_LOG=debug
Enter fullscreen mode Exit fullscreen mode

The value of RUST_LOG is translated into tracing::Level. Valid values include trace, debug, info, warn and error. Any other values are invalid and will default to Level::DEBUG.


❸ Updates to the Cargo.toml file: as expected, the new crates are added to the [dependencies] section.

...
[dependencies]
...
tracing = "0.1"
tracing-appender = "0.2"
tracing-subscriber = {version = "0.3", features = ["fmt", "std", "local-time", "time"]}
Enter fullscreen mode Exit fullscreen mode


❹ πŸ’₯ Issue with calculating UTC time offset on Ubuntu 22.10.

In the new code added for this post, we need to calculate the UTC time offset to obtain local time. The following code works on Windows 10:

use time::UtcOffset;

let offset = UtcOffset::current_local_offset().unwrap();
Enter fullscreen mode Exit fullscreen mode

However, on Ubuntu 22.10, it doesn't always function as expected. Sometimes, it raises the error IndeterminateOffset. The inconsistency in its behavior makes it challenging to identify a clear pattern of when it works and when it doesn't.

After extensive searching, I came across this GitHub issue, Document #293 in local-offset feature description #297. It appears that even after three years, this issue remains unresolved.

This complication adds an extra layer of difficulty in ensuring both the code and integration tests function properly. In the subsequent sections of this post, when discussing the code, we'll refer back to this issue when relevant. Please keep this in mind.


❺ The src/helper/app_logger.rs module has been designed to be easily copied into other projects, provided that the Cargo.toml file includes the required crates discussed earlier.

This module contains only a single public function, pub fn init_app_logger(utc_offset: time::UtcOffset) -> WorkerGuard, which the application calls to set up the log. Please refer to the notes and documentation within this module while reading the code.

Originally, the utc_offset: time::UtcOffset parameter was not present. However, due to the issue mentioned in πŸ’₯ Issue with calculating UTC time offset on Ubuntu 22.10, the code was refactored to include this parameter, offering a bit more flexibility.


β“΅ Setting up the daily log files.

    let log_appender = RollingFileAppender::builder()
        .rotation(Rotation::DAILY) // Daily log file.
        .filename_suffix("log") // log file names will be suffixed with `.log`
        .build("./log") // try to build an appender that stores log files in `/var/log`
        .expect("Initialising rolling file appender failed");
Enter fullscreen mode Exit fullscreen mode

To set up the daily log files, we begin by calling the pub fn builder() -> Builder function.

We specify DAILY rotation to generate daily log files. However, it's important to note that according to the documentation, the file names are appended with the current date in UTC. Since I'm in the Australian Eastern Standard Time (AEST) zone, which is 10-11 hours ahead of UTC, there were instances where my log file names were created with dates from the previous day.

To give log files the .log extension, we use the method pub fn filename_suffix(self, suffix: impl Into<String>) -> Self.

The format of the daily log file names follows the pattern YYYY-MM-DD.log, for example, 2024-03-10.log.

We then invoke the method pub fn build( &self, directory: impl AsRef<Path>) -> Result<RollingFileAppender, InitError> to specify the location of the log files within the log/ sub-directory relative to where the application is executed. For instance:

▢️<code>Windows 10:</code> F:\rust\actix_web>target\debug\learn_actix_web.exe
▢️<code>Ubuntu 22.10:</code> behai@hp-pavilion-15:~/rust/actix_web$ /home/behai/rust/actix_web/target/debug/learn_actix_web
Enter fullscreen mode Exit fullscreen mode

This results in the log files being stored at F:\rust\actix_web\log</code> and /home/behai/rust/actix_web/target/debug/learn_actix_web/log/ respectively.


β“Ά We create a non-blocking writer thread using the following code:

    let (non_blocking_appender, log_guard) = tracing_appender::non_blocking(log_appender);
Enter fullscreen mode Exit fullscreen mode

This is the documentation section for the function tracing_appender::non_blocking. For more detailed documentation, refer to the tracing_appender::non_blocking module. Please note the following:

This function returns a tuple of NonBlocking and WorkerGuard. NonBlocking implements MakeWriter which integrates with tracing_subscriber. WorkerGuard is a drop guard that is responsible for flushing any remaining logs when the program terminates.

Note that the WorkerGuard returned by non_blocking must be assigned to a binding that is not , as will result in the WorkerGuard being dropped immediately. Unintentional drops of WorkerGuard remove the guarantee that logs will be flushed during a program’s termination, in a panic or otherwise.

What this means is that we must keep log_guard alive for the application to continue logging. log_guard is an instance of the WorkerGuard struct and is also the returned value of the public function pub fn init_app_logger(utc_offset: time::UtcOffset) -> WorkerGuard. We will revisit this returned value in a later section.


β“· Next, we specify the date and time format for each log line. Each line begins with a local date and time. For instance, 2024-03-12-08:19:13:

    // Each log line starts with a local date and time token.
    // 
    // On Ubuntu 22.10, calling UtcOffset::current_local_offset().unwrap() after non_blocking()
    // causes IndeterminateOffset error!!
    // 
    // See also https://github.com/time-rs/time/pull/297.
    //
    let timer = OffsetTime::new(
        //UtcOffset::current_local_offset().unwrap(),
        utc_offset,
        format_description!("[year]-[month]-[day]-[hour]:[minute]:[second]"),
    );
Enter fullscreen mode Exit fullscreen mode

We've discussed local dates in some detail in this post.

πŸš€ Please note that this is a local date and time. In my time zone, Australian Eastern Standard Time (AEST), which is 10-11 hours ahead of UTC, the log file name for a log line that starts with 2024-03-12-08:19:13 would actually be log/2024-03-11.log.


β“Έ Next, we attempt to define the tracing::Level based on the environment variable RUST_LOG discussed previously:

    // Extracts tracing::Level from .env RUST_LOG, if there is any problem, 
    // defaults to Level::DEBUG.
    //
    let level: Level = match std::env::var_os("RUST_LOG") {
        None => Level::DEBUG,

        Some(text) => {
            match Level::from_str(text.to_str().unwrap()) {
                Ok(val) => val,
                Err(_) => Level::DEBUG
            }
        }
    };
Enter fullscreen mode Exit fullscreen mode

πŸ’₯ I initially assumed that having RUST_LOG defined in the environment file .env would suffice. However, it turns out that we need to explicitly set it in the code.


β“Ή We then proceed to β€œcreate a subscriber”, I hope I'm using the correct terminology:

    let subscriber = tracing_subscriber::registry()
        .with(
            Layer::new()
                .with_timer(timer)
                .with_ansi(false)
                .with_writer(non_blocking_appender.with_max_level(level)
                    .and(std::io::stdout.with_max_level(level)))
        );
Enter fullscreen mode Exit fullscreen mode

The function tracing_subscriber::registry() returns a tracing_subscriber::registry::Registry struct. This struct implements the trait tracing_subscriber::layer::SubscriberExt. The method fn with<L>(self, layer: L) -> Layered<L, Self> from this trait returns a tracing_subscriber::layer::Layered struct, which is a:

A Subscriber composed of a Subscriber wrapped by one or more Layers.

We create the new Layer using tracing_subscriber::fmt::Layer implementation.

Note that non_blocking_appender is an instance of tracing_appender::non_blocking::NonBlocking struct. This struct implements the trait tracing_subscriber::fmt::writer::MakeWriterExt, where the method fn with_max_level(self, level: Level) -> WithMaxLevel<Self> is defined.

πŸš€ .and(std::io::stdout.with_max_level(level)) means that anything logged to the log file will also be printed to the console.


β“Ί Next, the new Subscriber is set as the global default for the duration of the entire program:

    // tracing::subscriber::set_global_default(subscriber) can only be called once. 
    // Subsequent calls raise SetGlobalDefaultError, ignore these errors.
    //
    // There are integeration test methods which call this init_app_logger(...) repeatedly!!
    //
    match tracing::subscriber::set_global_default(subscriber) {
        Err(err) => tracing::error!("Logger set_global_default, ignored: {}", err),
        _ => (),
    }
Enter fullscreen mode Exit fullscreen mode

The documentation for the function tracing::subscriber::set_global_default states:

Can only be set once; subsequent attempts to set the global default will fail. Returns whether the initialization was successful.

Since some integration test methods call the pub fn init_app_logger(utc_offset: time::UtcOffset) -> WorkerGuard more than once, we catch potential errors and ignore them.


β“» Finally, pub fn init_app_logger(utc_offset: time::UtcOffset) -> WorkerGuard returns log_guard, as discussed above.


❻ Updates to the src/main.rs module.


β“΅ Coming back to pub fn init_app_logger(utc_offset: time::UtcOffset) -> WorkerGuard, specifically regarding the returned value discussed previously, I read and understood the quoted documentation, and I believe the code was correct. However, it did not write to log files as expected. I sought help. As per my help request post, I initially called init_app_logger in the src/lib.rs module's pub async fn run(listener: TcpListener) -> Result<Server, std::io::Error>. Consequently, as soon as run went of scope, the returned WorkerGuard was dropped, and the writer thread terminated.


Simply moved it to src/main.rs's async fn main() -> Result<(), std::io::Error>, fixed this problem:

    // Call this to load RUST_LOG.
    dotenv().ok(); 

    // Calling UtcOffset::current_local_offset().unwrap() here works in Ubuntu 22.10, i.e.,
    // it does not raise the IndeterminateOffset error.
    //
    // TO_DO. But this does not guarantee that it will always work! 
    //
    let _guards = init_app_logger(UtcOffset::current_local_offset().unwrap());
Enter fullscreen mode Exit fullscreen mode

Please note the call UtcOffset::current_local_offset().unwrap(). This is due to the problem discussed in the section πŸ’₯ Issue with calculating UTC time offset on Ubuntu 22.10.


β“Ά The function pub fn init_app_logger(utc_offset: time::UtcOffset) -> WorkerGuard requires the environment variable RUST_LOG as discussed previously. That's why dotenv().ok() is called in async fn main() -> Result<(), std::io::Error>.

Recall that dotenv().ok() is also called in the src/lib.rs module's pub async fn run(listener: TcpListener) -> Result<Server, std::io::Error> to load other environment variables. This setup might seem clunky, but I haven't found a better solution yet!


❼ Updating integration tests. We want integration tests to be able to log as well. These updates are made solely in the tests/common.rs module.

The function pub async fn spawn_app() -> TestApp in tests/common.rs calls the src/lib.rs module's function pub async fn run(listener: TcpListener) -> Result<Server, std::io::Error> to create application server instances.

This means that spawn_app() must be refactored to call pub fn init_app_logger(utc_offset: time::UtcOffset) -> WorkerGuard and somehow keep the writer thread alive after spawn_app() goes out of scope. We manage this by:

β“΅ Update TestApp struct by adding pub guard: WorkerGuard.

β“Ά Update the function pub async fn spawn_app() -> TestApp with additional calls:

    // To load RUST_LOG from .env file.
    dotenv().ok(); 

    /*
    On Ubuntu 22.10, calling UtcOffset's offset methods causes IndeterminateOffset error!!

    See also https://github.com/time-rs/time/pull/297

    ...
    */

    // TO_DO: 11 is the current number of hours the Australian Eastern Standard Time (AEST)
    // is ahead of UTC. This value need to be worked out dynamically -- if it is at all 
    // possible on Linux!!
    // 
    let guard = init_app_logger(UtcOffset::from_hms(11, 0, 0).unwrap());
Enter fullscreen mode Exit fullscreen mode


Note the call UtcOffset::from_hms(11, 0, 0).unwrap(). This is due to the problem discussed in section πŸ’₯ Issue with calculating UTC time offset on Ubuntu 22.10:

-- πŸ‘Ž Unlike src/main.rs, where UtcOffset::current_local_offset().unwrap() works, calling it here consistently results in the IndeterminateOffset error! UtcOffset::from_hms(11, 0, 0).unwrap() works, but again, this is not a guarantee it will keep working.

πŸ‘Ž The value 11 is hardcoded. Presently, the Australian Eastern Standard Time (AEST) zone is 11 hours ahead of UTC. To get the AEST date and time, we need to offset UTC by 11 hours. However, 11 is not a constant value; due to daylight savings, in Southern Hemisphere winters, it changes to 10 hours (I think). This means that this code will no longer be correct.


❽ We've reached the conclusion of this post. I'd like to mention that the ecosystem surrounding tracing and logging is incredibly vast. While this post only scratches the surface, it provides a complete working example nonetheless. We can build upon this foundation as needed.

The UTC offset issue on Ubuntu 22.10, as described, must be addressed definitively. However, that task is for another day.

I'm not entirely satisfied with the numerous debug loggings from other crates. These can be filtered and removed, but that's a topic for another post, perhaps.

I hope you find the information useful. Thank you for reading. And stay safe, as always.

✿✿✿

Feature image source:

πŸ¦€ Index of the Complete Series.

Top comments (2)

Collapse
 
behainguyen profile image
Be Hai Nguyen

In tests/common.rs, function spawn_app(), time::OffsetDateTime::now_local() also raises IndeterminateOffset error:

    let offset = time::OffsetDateTime::now_local().unwrap().offset()
        .as_hms();
    // let guard = init_app_logger(UtcOffset::from_hms(11, 0, 0).unwrap());
    let guard = init_app_logger(UtcOffset::from_hms(offset.0, offset.1, offset.2).unwrap());
Enter fullscreen mode Exit fullscreen mode
Collapse
 
behainguyen profile image
Be Hai Nguyen

Note, the following also raises IndeterminateOffset error:

    let now_local = time::OffsetDateTime::now_local();
    let now_offset = now_local.unwrap();
    let timer = OffsetTime::new(
        now_offset.offset(),
        format_description!("[year]-[month]-[day]-[hour]:[minute]:[second]"),
    );
Enter fullscreen mode Exit fullscreen mode

now_local is in error.