Add logging functionality to the dev panel

This commit is contained in:
Greg Burri 2025-04-21 16:52:14 +02:00
parent c8dd82a5e0
commit 812ba9dc3b
6 changed files with 141 additions and 38 deletions

View file

@ -187,6 +187,12 @@ body {
} }
} }
#dev-panel {
.log-line-odd {
background-color: consts.$color-1;
}
}
form { form {
display: grid; display: grid;
grid-template-columns: auto 1fr; grid-template-columns: auto 1fr;

View file

@ -3,6 +3,7 @@ use askama::Template;
use crate::{ use crate::{
Context, Context,
data::{db, model}, data::{db, model},
log::Log,
translation::{self, Sentence, Tr}, translation::{self, Sentence, Tr},
}; };
@ -58,6 +59,8 @@ pub struct HomeTemplate {
pub struct DevPanelTemplate { pub struct DevPanelTemplate {
pub context: Context, pub context: Context,
pub recipes: Recipes, pub recipes: Recipes,
pub log: Log,
pub current_log_file: String,
} }
#[derive(Template)] #[derive(Template)]

View file

@ -1,5 +1,12 @@
use std::{fs, path::Path}; use std::{
fs::{self, File},
io::{BufRead, BufReader},
path::{Path, PathBuf},
sync::Arc,
};
use itertools::Itertools;
use tracing::{Level, event};
use tracing_appender::{ use tracing_appender::{
non_blocking::WorkerGuard, non_blocking::WorkerGuard,
rolling::{RollingFileAppender, Rotation}, rolling::{RollingFileAppender, Rotation},
@ -22,38 +29,82 @@ const TRACING_LEVEL: tracing::Level = tracing::Level::INFO;
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
const TRACING_DISPLAY_THREAD: bool = false; const TRACING_DISPLAY_THREAD: bool = false;
pub fn init<P>(directory: P) -> WorkerGuard #[derive(Clone)]
where pub struct Log {
P: AsRef<Path>, guard: Arc<WorkerGuard>,
{ directory: PathBuf,
if !directory.as_ref().exists() { }
fs::DirBuilder::new().create(&directory).unwrap();
// TODO: Remove all 'unwrap'.
impl Log {
pub fn new<P>(directory: P) -> Self
where
P: AsRef<Path>,
{
if !directory.as_ref().exists() {
fs::DirBuilder::new().create(&directory).unwrap();
}
let file_appender = RollingFileAppender::builder()
.rotation(Rotation::DAILY)
.filename_prefix(consts::BASE_LOG_FILENAME)
.filename_suffix("log")
.build(&directory)
.expect("Initializing rolling file appender failed");
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender);
let layer_file = tracing_subscriber::fmt::layer()
.with_writer(non_blocking.with_max_level(TRACING_LEVEL))
.with_ansi(false)
.with_thread_ids(TRACING_DISPLAY_THREAD)
.with_thread_names(TRACING_DISPLAY_THREAD);
let layer_stdout = tracing_subscriber::fmt::layer()
.with_writer(std::io::stdout.with_max_level(TRACING_LEVEL))
.with_thread_ids(TRACING_DISPLAY_THREAD)
.with_thread_names(TRACING_DISPLAY_THREAD);
tracing_subscriber::Registry::default()
.with(layer_file)
.with(layer_stdout)
.init();
Log {
guard: Arc::new(guard),
directory: directory.as_ref().to_path_buf(),
}
} }
let file_appender = RollingFileAppender::builder() pub fn file_names(&self) -> std::io::Result<Vec<String>> {
.rotation(Rotation::DAILY) Ok(self
.filename_prefix(consts::BASE_LOG_FILENAME) .directory
.filename_suffix("log") .read_dir()?
.build(directory) .map(|entry| {
.expect("Initializing rolling file appender failed"); entry
.unwrap()
.path()
.file_name()
.unwrap()
.to_str()
.unwrap()
.to_string()
})
.sorted()
.collect())
}
let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); pub fn read_content(&self, filename: &str) -> std::io::Result<Vec<String>> {
let filepath = self.directory.join(filename);
let layer_file = tracing_subscriber::fmt::layer() if filepath.is_file() {
.with_writer(non_blocking.with_max_level(TRACING_LEVEL)) let file = File::open(filepath)?;
.with_ansi(false) // tracing::event!(Level::ERROR, "file: {:?}", file);
.with_thread_ids(TRACING_DISPLAY_THREAD) Ok(BufReader::new(file)
.with_thread_names(TRACING_DISPLAY_THREAD); .lines()
.map(|l| l.unwrap_or_default())
let layer_stdout = tracing_subscriber::fmt::layer() .collect())
.with_writer(std::io::stdout.with_max_level(TRACING_LEVEL)) } else {
.with_thread_ids(TRACING_DISPLAY_THREAD) Ok(vec![])
.with_thread_names(TRACING_DISPLAY_THREAD); }
}
tracing_subscriber::Registry::default()
.with(layer_file)
.with(layer_stdout)
.init();
guard
} }

View file

@ -26,6 +26,7 @@ use tower_http::{
use tracing::{Level, event}; use tracing::{Level, event};
use data::{backup, db, model}; use data::{backup, db, model};
use log::Log;
use translation::Tr; use translation::Tr;
mod config; mod config;
@ -45,6 +46,7 @@ mod utils;
struct AppState { struct AppState {
config: Config, config: Config,
db_connection: db::Connection, db_connection: db::Connection,
log: Log,
} }
impl FromRef<AppState> for Config { impl FromRef<AppState> for Config {
@ -59,6 +61,12 @@ impl FromRef<AppState> for db::Connection {
} }
} }
impl FromRef<AppState> for Log {
fn from_ref(app_state: &AppState) -> Log {
app_state.log.clone()
}
}
impl axum::response::IntoResponse for db::DBError { impl axum::response::IntoResponse for db::DBError {
fn into_response(self) -> Response { fn into_response(self) -> Response {
ron_utils::ron_error(StatusCode::INTERNAL_SERVER_ERROR, &self.to_string()).into_response() ron_utils::ron_error(StatusCode::INTERNAL_SERVER_ERROR, &self.to_string()).into_response()
@ -103,7 +111,7 @@ impl Context {
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
let config = config::load(); let config = config::load();
let _guard = log::init(&config.logs_directory); let log = Log::new(&config.logs_directory);
event!(Level::INFO, "Configuration: {:?}", config); event!(Level::INFO, "Configuration: {:?}", config);
@ -133,6 +141,7 @@ async fn main() {
let state = AppState { let state = AppState {
config, config,
db_connection, db_connection,
log,
}; };
let ron_api_routes = Router::new() let ron_api_routes = Router::new()

View file

@ -1,13 +1,14 @@
use askama::Template; use askama::Template;
use axum::{ use axum::{
body, debug_handler, body, debug_handler,
extract::{Extension, Request, State}, extract::{Extension, Query, Request, State},
http::{StatusCode, header}, http::{StatusCode, header},
middleware::Next, middleware::Next,
response::{Html, IntoResponse, Response}, response::{Html, IntoResponse, Response},
}; };
use serde::Deserialize;
use crate::{Context, Result, consts, data::db, html_templates::*, ron_utils}; use crate::{AppState, Context, Result, consts, data::db, html_templates::*, log::Log, ron_utils};
pub mod fragments; pub mod fragments;
pub mod recipe; pub mod recipe;
@ -62,10 +63,18 @@ pub async fn home_page(
///// DEV_PANEL ///// ///// DEV_PANEL /////
#[debug_handler] #[derive(Deserialize)]
pub struct LogFile {
#[serde(default)]
pub log_file: String,
}
#[debug_handler(state = AppState)]
pub async fn dev_panel( pub async fn dev_panel(
State(connection): State<db::Connection>, State(connection): State<db::Connection>,
State(log): State<Log>,
Extension(context): Extension<Context>, Extension(context): Extension<Context>,
log_file: Query<LogFile>,
) -> Result<Response> { ) -> Result<Response> {
if context.user.is_some() && context.user.as_ref().unwrap().is_admin { if context.user.is_some() && context.user.as_ref().unwrap().is_admin {
Ok(Html( Ok(Html(
@ -73,6 +82,8 @@ pub async fn dev_panel(
recipes: Recipes::new(connection, &context.user, context.tr.current_lang_code()) recipes: Recipes::new(connection, &context.user, context.tr.current_lang_code())
.await?, .await?,
context, context,
log,
current_log_file: log_file.log_file.clone(),
} }
.render()?, .render()?,
) )

View file

@ -2,13 +2,36 @@
{% block content %} {% block content %}
<div class="content" id="dev_panel"> <div class="content" id="dev-panel">
<input type="button" class="button" id="test-toast" value="Test toast"> <input type="button" class="button" id="test-toast" value="Test toast">
<input type="button" class="button" id="test-modal-dialog" value="Test modal"> <input type="button" class="button" id="test-modal-dialog" value="Test modal">
<div type="log">
<ul class="log-files">
{% for f in log.file_names().unwrap() %}
<li class="log-file"><a href="/dev_panel?log_file={{ f }}">{{ f }}</a></li>
{% endfor %}
</ul>
<div class="current-log-file">
{% match log.read_content(current_log_file) %}
{% when Ok(lines) %}
{% for l in lines %}
<div class="
{% if loop.index0 % 2 == 0 %}
log-line-even
{% else %}
log-line-odd
{% endif %}
" >{{ l }}</div>
{% endfor %}
{% when Err(err) %}
Error reading log: {{ err }}
{% endmatch %}
</div>
</div>
</div> </div>
<div id="hidden-templates"> <div id="hidden-templates">
<div class="modal-test-message"> <div class="modal-test-message">
This is a message. This is a message.
</div> </div>