Skip to content

Commit

Permalink
14 implement logger initialization logic (#15)
Browse files Browse the repository at this point in the history
* add backend

* post review fixes pt.1

* post review fixes pt.2

* remove wrappers

* post review fixes (WIP)

* change build flow + error handling

* log4rs add (WIP)

* add loggers

* clean up

* remove dep

* remove stderr target

* delete stderr target + unify trace-level

* delete redundant lock file

* clean up

* logger init rework

* post review fixes + error_stack errors

* adjust fixes

* replace write check with is_empty check

* Update backend/src/config.rs

* Update backend/src/config.rs
  • Loading branch information
archeoss authored Dec 22, 2023
1 parent c2b74ef commit d46cd94
Show file tree
Hide file tree
Showing 9 changed files with 238 additions and 33 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Bob Management GUI changelog
## [Unreleased]

#### Added

- Initial project structure, backend only (#9)
- Initial project stricture, frontend (#10)
- Dockerfile and Docker-Compose to simplify deployment (#5)
- CI/CD configuration (#11)
- Logger Initialization (#14)
2 changes: 2 additions & 0 deletions backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ tower-http = { version = "0.4", features = ["cors", "fs"] }

## Logging
tracing = "0.1"
file-rotate = "0.7"
tracing-appender = "0.2"
tracing-subscriber = "0.3"

## Error Handling
Expand Down
144 changes: 143 additions & 1 deletion backend/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,158 @@
use cli::Config;
use crate::prelude::*;
use cli::{Config, LoggerConfig};
use file_rotate::{suffix::AppendTimestamp, ContentLimit, FileRotate};
use thiserror::Error;
use tower_http::cors::CorsLayer;
use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
use tracing_subscriber::{filter::LevelFilter, prelude::*, util::SubscriberInitExt};

#[allow(clippy::module_name_repetitions)]
pub trait ConfigExt {
/// Return either very permissive [`CORS`](`CorsLayer`) configuration
/// or empty one based on `cors_allow_all` field
fn get_cors_configuration(&self) -> CorsLayer;
}

pub trait LoggerExt {
/// Initialize logger.
///
/// Returns [`WorkerGuard`]s for off-thread writers.
/// Should not be dropped.
///
/// # Errors
///
/// Function returns error if `init_file_rotate` fails
fn init_logger(&self) -> Result<Vec<WorkerGuard>, LoggerError>;

/// Returns [`std:io::Write`] object that rotates files on write
///
/// # Errors
///
/// Function returns error if `log_file` is not specified
fn init_file_rotate(&self) -> Result<FileRotate<AppendTimestamp>, LoggerError>;

/// Returns non-blocking file writer
///
/// Also returns [`WorkerGuard`] for off-thread writing.
/// Should not be dropped.
///
/// # Errors
///
/// This function will return an error if the file logger configuration is empty, file logging
/// is disabled or logs filename is not specified
fn non_blocking_file_writer(&self) -> Result<(NonBlocking, WorkerGuard), LoggerError>;

/// Returns non-blocking stdout writer
///
/// Also returns [`WorkerGuard`] for off-thread writing.
/// Should not be dropped.
///
/// # Errors
///
/// This function will return an error if the stdout logger configuration is empty or stdout logging
/// is disabled
fn non_blocking_stdout_writer(&self) -> Result<(NonBlocking, WorkerGuard), LoggerError>;
}

impl ConfigExt for Config {
fn get_cors_configuration(&self) -> CorsLayer {
self.cors_allow_all
.then_some(CorsLayer::very_permissive())
.unwrap_or_default()
}
}

impl LoggerExt for LoggerConfig {
fn init_logger(&self) -> Result<Vec<WorkerGuard>, LoggerError> {
let mut guards = Vec::with_capacity(2);

let file_writer = disable_on_error(self.non_blocking_file_writer())?;
let stdout_writer = disable_on_error(self.non_blocking_stdout_writer())?;

let mut layers_iter =
[file_writer, stdout_writer]
.into_iter()
.flatten()
.map(|(writer, guard)| {
guards.push(guard);
tracing_subscriber::fmt::layer()
.with_writer(writer)
.with_filter(LevelFilter::from_level(self.trace_level))
});

if let Some(first_layer) = layers_iter.next() {
tracing_subscriber::registry()
.with(layers_iter.fold(first_layer.boxed(), |layer, next_layer| {
layer.and_then(next_layer).boxed()
}))
.init();
};

Ok(guards)
}

fn init_file_rotate(&self) -> Result<FileRotate<AppendTimestamp>, LoggerError> {
let config = self.file.as_ref().ok_or(LoggerError::EmptyConfig)?;
let log_file = config.log_file.as_ref().ok_or(LoggerError::NoFileName)?;
if log_file.as_os_str().is_empty() {
return Err(LoggerError::NoFileName.into());
}

Ok(FileRotate::new(
log_file,
AppendTimestamp::default(file_rotate::suffix::FileLimit::MaxFiles(config.log_amount)),
ContentLimit::BytesSurpassed(config.log_size),
file_rotate::compression::Compression::OnRotate(1),
None,
))
}

fn non_blocking_file_writer(&self) -> Result<(NonBlocking, WorkerGuard), LoggerError> {
self.file.as_ref().map_or_else(
|| Err(LoggerError::EmptyConfig.into()),
|config| {
if config.enabled {
Ok(tracing_appender::non_blocking(self.init_file_rotate()?))
} else {
Err(LoggerError::NotEnabled.into())
}
},
)
}

fn non_blocking_stdout_writer(&self) -> Result<(NonBlocking, WorkerGuard), LoggerError> {
self.stdout.as_ref().map_or_else(
|| Err(LoggerError::EmptyConfig.into()),
|config| {
if config.enabled {
Ok(tracing_appender::non_blocking(std::io::stdout()))
} else {
Err(LoggerError::NotEnabled.into())
}
},
)
}
}

#[derive(Debug, Error)]
pub enum LoggerError {
#[error("Empty logger configuration")]
EmptyConfig,
#[error("No filename specified")]
NoFileName,
#[error("This logger is not enabled")]
NotEnabled,
}

/// Consume some errors to produce empty logger
fn disable_on_error(
logger: Result<(NonBlocking, WorkerGuard), LoggerError>,
) -> Result<Option<(NonBlocking, WorkerGuard)>, LoggerError> {
Ok(match logger {
Ok(writer) => Some(writer),
Err(e) => match e.current_context() {
LoggerError::NotEnabled | LoggerError::EmptyConfig => None,
_ => return Err(e),
},
})
}
1 change: 0 additions & 1 deletion backend/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
use axum::{routing::get, Router};

use utoipa::OpenApi;

pub mod config;
pub mod connector;
pub mod error;
Expand Down
19 changes: 8 additions & 11 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
#![allow(clippy::multiple_crate_versions)]
#![allow(
clippy::multiple_crate_versions,
clippy::unwrap_used,
clippy::expect_used
)]

use axum::Router;
use bob_management::{
config::ConfigExt,
config::{ConfigExt, LoggerExt},
prelude::*,
root,
router::{ApiV1, ApiVersion, NoApi, RouterApiExt},
Expand All @@ -12,23 +16,21 @@ use bob_management::{
use cli::Parser;
use error_stack::{Result, ResultExt};
use hyper::Method;
use std::{env, path::PathBuf};
use std::env;
use tower::ServiceBuilder;
use tower_http::{cors::CorsLayer, services::ServeDir};
use tracing::Level;

const FRONTEND_FOLDER: &str = "frontend";

#[tokio::main]
#[allow(clippy::unwrap_used, clippy::expect_used)]
async fn main() -> Result<(), AppError> {
let config = cli::Config::try_from(cli::Args::parse())
.change_context(AppError::InitializationError)
.attach_printable("Couldn't get config file.")?;

let logger = &config.logger;

init_tracer(&logger.log_file, logger.trace_level);
let _guard = logger.init_logger().unwrap();
tracing::info!("Logger: {logger:?}");

let cors: CorsLayer = config.get_cors_configuration();
Expand All @@ -50,11 +52,6 @@ async fn main() -> Result<(), AppError> {
Ok(())
}

fn init_tracer(_log_file: &Option<PathBuf>, trace_level: Level) {
let subscriber = tracing_subscriber::fmt().with_max_level(trace_level);
subscriber.init();
}

#[allow(clippy::unwrap_used, clippy::expect_used)]
fn router(cors: CorsLayer) -> Router {
let mut frontend = env::current_exe().expect("Couldn't get current executable path.");
Expand Down
95 changes: 78 additions & 17 deletions cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,53 @@ pub struct Config {
}

/// Logger Configuration passed on initialization
#[allow(clippy::module_name_repetitions)]
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LoggerConfig {
/// [Stub] File to save logs
pub log_file: Option<PathBuf>,

/// [Stub] Number of log files
#[serde(default = "LoggerConfig::default_log_amount")]
pub log_amount: usize,
/// Rolling file logger config
#[serde(default)]
pub file: Option<FileLogger>,

/// [Stub] Max size of a single log file, in bytes
#[serde(default = "LoggerConfig::default_log_size")]
pub log_size: u64,
/// Stdout logger config
#[serde(default)]
pub stdout: Option<StdoutLogger>,

/// Tracing Level
#[serde(default = "LoggerConfig::tracing_default")]
#[serde(default = "LoggerConfig::level_default")]
#[serde_as(as = "DisplayFromStr")]
pub trace_level: tracing::Level,
}

/// File Logger Configuration for writing logs to files
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct FileLogger {
/// Enable log output to file
pub enabled: bool,

/// File to save logs
pub log_file: Option<PathBuf>,

/// Number of log files
#[serde(default = "FileLogger::default_log_amount")]
pub log_amount: usize,

/// Max size of a single log file, in bytes
#[serde(default = "FileLogger::default_log_size")]
pub log_size: usize,
}

/// Stdout Logger Configuration for printing logs into stdout
#[serde_as]
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct StdoutLogger {
/// Enable log output to stdout
pub enabled: bool,
}

impl Default for Config {
fn default() -> Self {
Self {
Expand All @@ -59,41 +84,77 @@ impl Default for Config {
}
}

impl Default for LoggerConfig {
fn default() -> Self {
Self {
file: None,
stdout: None,
trace_level: Self::level_default(),
}
}
}

impl Config {
#[must_use]
pub const fn default_cors() -> bool {
false
}

#[must_use]
pub const fn default_timeout() -> Duration {
Duration::from_millis(5000)
}
}

impl LoggerConfig {
pub const fn tracing_default() -> tracing::Level {
tracing::Level::INFO
#[must_use]
pub const fn level_default() -> tracing::Level {
tracing::Level::TRACE
}
}

impl FileLogger {
#[must_use]
pub const fn default_enabled() -> bool {
false
}

#[must_use]
pub const fn default_log_amount() -> usize {
5
}

pub const fn default_log_size() -> u64 {
10u64.pow(6)
#[must_use]
pub const fn default_log_size() -> usize {
10usize.pow(6)
}
}

impl Default for LoggerConfig {
impl StdoutLogger {
#[must_use]
pub const fn default_enabled() -> bool {
false
}
}

impl Default for FileLogger {
fn default() -> Self {
Self {
log_file: None,
log_amount: Self::default_log_amount(),
log_size: Self::default_log_size(),
trace_level: Self::tracing_default(),
enabled: Self::default_enabled(),
}
}
}

impl Default for StdoutLogger {
fn default() -> Self {
Self {
enabled: Self::default_enabled(),
}
}
}
pub trait FromFile {
/// Parses the file spcified in `path`
///
Expand Down
2 changes: 1 addition & 1 deletion cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ mod config;

pub use clap::Parser;
pub use cli::Args;
pub use config::{Config, FromFile, LoggerConfig};
pub use config::{Config, FileLogger, FromFile, LoggerConfig, StdoutLogger};
Loading

0 comments on commit d46cd94

Please sign in to comment.