Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

14 implement logger initialization logic #15

Merged
merged 21 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
148 changes: 147 additions & 1 deletion backend/src/config.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,162 @@
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)?;
std::fs::OpenOptions::new()
.create(true)
.write(true)
.open(log_file)
.change_context(LoggerError::CantWrite)?;
ikopylov marked this conversation as resolved.
Show resolved Hide resolved

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("Can't write logs in file")]
CantWrite,
#[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
Loading