feat(dtmm): Implement log view

Ref: #7.
This commit is contained in:
Lucas Schwiderski 2023-02-27 16:32:29 +01:00
parent bb671c5fd2
commit 3895ab12d6
Signed by: lucas
GPG key ID: AA12679AAA6DF4D8
8 changed files with 447 additions and 310 deletions

8
Cargo.lock generated
View file

@ -2293,9 +2293,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.19"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53250a3b3fed8ff8fd988587d8925d26a83ac3845d9e03b220b37f34c2b8d6c2"
checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890"
dependencies = [
"itoa",
"libc",
@ -2313,9 +2313,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd"
[[package]]
name = "time-macros"
version = "0.2.7"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a460aeb8de6dcb0f381e1ee05f1cd56fcf5a5f6eb8187ff3d8f0b11078d38b7c"
checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36"
dependencies = [
"time-core",
]

80
crates/dtmm/src/log.rs Normal file
View file

@ -0,0 +1,80 @@
use tokio::sync::mpsc::UnboundedSender;
use tracing_error::ErrorLayer;
use tracing_subscriber::filter::FilterFn;
use tracing_subscriber::fmt;
use tracing_subscriber::fmt::format::debug_fn;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
// I currently cannot find a way to add a parameter to `dtmt_shared::create_tracing_subscriber`
// that would allow me to pass an extra `Layer` to that function. So, for now,
// its code has to be duplicated here.
pub struct ChannelWriter {
tx: UnboundedSender<String>,
}
impl ChannelWriter {
pub fn new(tx: UnboundedSender<String>) -> Self {
Self { tx }
}
}
impl std::io::Write for ChannelWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let tx = self.tx.clone();
let string = String::from_utf8_lossy(buf).to_string();
// The `send` errors when the receiving end has closed.
// But there's not much we can do at that point, so we just ignore it.
let _ = tx.send(string);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
pub fn create_tracing_subscriber(tx: UnboundedSender<String>) {
let env_layer =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) {
let fmt_layer = fmt::layer().pretty();
(Some(fmt_layer), None, None)
} else {
// Creates a layer that
// - only prints events that contain a message
// - does not print fields
// - does not print spans/targets
// - only prints time, not date
let fmt_layer = fmt::layer()
.event_format(dtmt_shared::Formatter)
.fmt_fields(debug_fn(dtmt_shared::format_field));
(
None,
Some(fmt_layer),
Some(FilterFn::new(dtmt_shared::filter)),
)
};
let channel_layer = fmt::layer()
// TODO: Re-enable and implement a formatter for the Druid widget
.with_ansi(false)
.event_format(dtmt_shared::Formatter)
.fmt_fields(debug_fn(dtmt_shared::format_field))
.with_writer(move || ChannelWriter::new(tx.clone()));
tracing_subscriber::registry()
.with(channel_layer)
.with(filter_layer)
.with(env_layer)
.with(dev_stdout_layer)
.with(prod_stdout_layer)
.with(ErrorLayer::new(fmt::format::Pretty::default()))
.init();
}

View file

@ -1,170 +1,35 @@
#![recursion_limit = "256"]
#![feature(let_chains)]
use std::fs;
use std::io::ErrorKind;
use std::path::PathBuf;
use std::sync::Arc;
use clap::command;
use clap::parser::ValueSource;
use clap::value_parser;
use clap::Arg;
use color_eyre::eyre::Context;
use color_eyre::Report;
use color_eyre::Result;
use color_eyre::{Report, Result};
use druid::AppLauncher;
use druid::ExtEventSink;
use druid::SingleUse;
use druid::Target;
use engine::delete_mod;
use engine::import_mod;
use engine::reset_mod_deployment;
use serde::Deserialize;
use serde::Serialize;
use state::ACTION_FINISH_ADD_MOD;
use state::ACTION_FINISH_DELETE_SELECTED_MOD;
use state::ACTION_FINISH_RESET_DEPLOYMENT;
use tokio::runtime::Runtime;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::RwLock;
use crate::engine::deploy_mods;
use crate::state::{AsyncAction, Delegate, State, ACTION_FINISH_DEPLOY};
use crate::state::{Delegate, State};
use crate::worker::work_thread;
mod controller;
mod engine;
mod log;
mod main_window;
mod state;
mod theme;
mod util;
mod widget;
#[derive(Clone, Debug, Serialize, Deserialize)]
struct Config {
data_dir: Option<PathBuf>,
game_dir: Option<PathBuf>,
}
fn work_thread(
event_sink: Arc<RwLock<ExtEventSink>>,
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
) -> Result<()> {
let rt = Runtime::new()?;
rt.block_on(async {
while let Some(action) = action_queue.write().await.recv().await {
let event_sink = event_sink.clone();
match action {
AsyncAction::DeployMods(state) => tokio::spawn(async move {
if let Err(err) = deploy_mods(state).await {
tracing::error!("Failed to deploy mods: {:?}", err);
}
event_sink
.write()
.await
.submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto)
.expect("failed to send command");
}),
AsyncAction::AddMod((state, info)) => tokio::spawn(async move {
match import_mod(state, info).await {
Ok(mod_info) => {
event_sink
.write()
.await
.submit_command(
ACTION_FINISH_ADD_MOD,
SingleUse::new(mod_info),
Target::Auto,
)
.expect("failed to send command");
}
Err(err) => {
tracing::error!("Failed to import mod: {:?}", err);
}
}
}),
AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move {
if let Err(err) = delete_mod(state, &info).await {
tracing::error!(
"Failed to delete mod files. \
You might want to clean up the data directory manually. \
Reason: {:?}",
err
);
}
event_sink
.write()
.await
.submit_command(
ACTION_FINISH_DELETE_SELECTED_MOD,
SingleUse::new(info),
Target::Auto,
)
.expect("failed to send command");
}),
AsyncAction::ResetDeployment(state) => tokio::spawn(async move {
if let Err(err) = reset_mod_deployment(state).await {
tracing::error!("Failed to reset mod deployment: {:?}", err);
}
event_sink
.write()
.await
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
.expect("failed to send command");
}),
};
}
});
Ok(())
}
#[cfg(not(arget_os = "windows"))]
fn get_default_config_path() -> PathBuf {
let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| {
let user = std::env::var("USER").expect("user env variable not set");
format!("/home/{user}")
});
format!("{home}/.config")
});
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
}
#[cfg(target_os = "windows")]
fn get_default_config_path() -> PathBuf {
let config_dir = std::env::var("APPDATA").expect("appdata env var not set");
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
}
#[cfg(not(arget_os = "windows"))]
fn get_default_data_dir() -> PathBuf {
let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| {
let user = std::env::var("USER").expect("user env variable not set");
format!("/home/{user}")
});
format!("{home}/.local/share")
});
PathBuf::from(data_dir).join("dtmm")
}
#[cfg(target_os = "windows")]
fn get_default_data_dir() -> PathBuf {
let data_dir = std::env::var("APPDATA").expect("appdata env var not set");
PathBuf::from(data_dir).join("dtmm")
}
mod worker;
#[tracing::instrument]
fn main() -> Result<()> {
color_eyre::install()?;
let default_config_path = get_default_config_path();
let default_config_path = util::get_default_config_path();
tracing::trace!(default_config_path = %default_config_path.display());
@ -185,78 +50,30 @@ fn main() -> Result<()> {
)
.get_matches();
dtmt_shared::create_tracing_subscriber();
let (log_tx, log_rx) = tokio::sync::mpsc::unbounded_channel();
log::create_tracing_subscriber(log_tx);
unsafe {
oodle_sys::init(matches.get_one::<String>("oodle"));
}
let config: Config = {
let path = matches
.get_one::<PathBuf>("config")
.expect("argument missing despite default");
match fs::read(path) {
Ok(data) => {
let data = String::from_utf8(data).wrap_err_with(|| {
format!("config file {} contains invalid UTF-8", path.display())
})?;
serde_sjson::from_str(&data)
.wrap_err_with(|| format!("invalid config file {}", path.display()))?
}
Err(err) if err.kind() == ErrorKind::NotFound => {
if matches.value_source("config") != Some(ValueSource::DefaultValue) {
return Err(err).wrap_err_with(|| {
format!("failed to read config file {}", path.display())
})?;
}
{
let parent = default_config_path
.parent()
.expect("a file path always has a parent directory");
fs::create_dir_all(parent).wrap_err_with(|| {
format!("failed to create directories {}", parent.display())
})?;
}
let config = Config {
data_dir: Some(get_default_data_dir()),
game_dir: None,
};
{
let data = serde_sjson::to_string(&config)
.wrap_err("failed to serialize default config value")?;
fs::write(&default_config_path, data).wrap_err_with(|| {
format!(
"failed to write default config to {}",
default_config_path.display()
)
})?;
}
config
}
Err(err) => {
return Err(err)
.wrap_err_with(|| format!("failed to read config file {}", path.display()))?;
}
}
};
let config =
util::read_config(&default_config_path, &matches).wrap_err("failed to read config file")?;
let initial_state = State::new(config);
let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
let delegate = Delegate::new(sender);
let (action_tx, action_rx) = tokio::sync::mpsc::unbounded_channel();
let delegate = Delegate::new(action_tx);
let launcher = AppLauncher::with_window(main_window::new()).delegate(delegate);
let event_sink = launcher.get_external_handle();
std::thread::spawn(move || {
let event_sink = Arc::new(RwLock::new(event_sink));
let receiver = Arc::new(RwLock::new(receiver));
let action_rx = Arc::new(RwLock::new(action_rx));
let log_rx = Arc::new(RwLock::new(log_rx));
loop {
if let Err(err) = work_thread(event_sink.clone(), receiver.clone()) {
if let Err(err) = work_thread(event_sink.clone(), action_rx.clone(), log_rx.clone()) {
tracing::error!("Work thread failed, restarting: {:?}", err);
}
}

View file

@ -1,10 +1,11 @@
use druid::im::Vector;
use druid::widget::{
Align, Button, CrossAxisAlignment, Flex, Label, List, MainAxisAlignment, Maybe, Scroll, Split,
TextBox, ViewSwitcher,
Align, Button, CrossAxisAlignment, Flex, Label, LineBreaking, List, MainAxisAlignment, Maybe,
Scroll, SizedBox, Split, TextBox, ViewSwitcher,
};
use druid::{
lens, FileDialogOptions, FileSpec, Insets, LensExt, SingleUse, Widget, WidgetExt, WindowDesc,
lens, FileDialogOptions, FileSpec, FontDescriptor, FontFamily, Insets, LensExt, SingleUse,
Widget, WidgetExt, WindowDesc,
};
use crate::state::{ModInfo, PathBufFormatter, State, View, ACTION_START_RESET_DEPLOYMENT};
@ -261,9 +262,25 @@ fn build_main() -> impl Widget<State> {
)
}
fn build_log_view() -> impl Widget<State> {
let font = FontDescriptor::new(FontFamily::MONOSPACE);
let label = Label::raw()
.with_font(font)
.with_line_break_mode(LineBreaking::WordWrap)
.lens(State::log);
SizedBox::new(label)
.expand_width()
.height(128.0)
.scroll()
.vertical()
}
fn build_window() -> impl Widget<State> {
// TODO: Add borders between the sections
Flex::column()
.must_fill_main_axis(true)
.with_child(build_top_bar())
.with_flex_child(build_main(), 1.0)
.with_child(build_log_view())
}

View file

@ -10,7 +10,7 @@ use druid::{
use dtmt_shared::ModConfig;
use tokio::sync::mpsc::UnboundedSender;
use crate::Config;
use crate::util::Config;
pub(crate) const ACTION_SELECT_MOD: Selector<usize> = Selector::new("dtmm.action.select-mod");
pub(crate) const ACTION_SELECTED_MOD_UP: Selector = Selector::new("dtmm.action.selected-mod-up");
@ -33,6 +33,8 @@ pub(crate) const ACTION_ADD_MOD: Selector<FileInfo> = Selector::new("dtmm.action
pub(crate) const ACTION_FINISH_ADD_MOD: Selector<SingleUse<ModInfo>> =
Selector::new("dtmm.action.finish-add-mod");
pub(crate) const ACTION_LOG: Selector<SingleUse<String>> = Selector::new("dtmm.action.log");
#[derive(Copy, Clone, Data, Debug, PartialEq)]
pub(crate) enum View {
Mods,
@ -154,6 +156,7 @@ pub(crate) struct State {
game_dir: Arc<PathBuf>,
data_dir: Arc<PathBuf>,
ctx: Arc<sdk::Context>,
log: Arc<String>,
}
impl State {
@ -170,8 +173,9 @@ impl State {
selected_mod_index: None,
is_deployment_in_progress: false,
is_reset_in_progress: false,
game_dir: Arc::new(config.game_dir.unwrap_or_default()),
data_dir: Arc::new(config.data_dir.unwrap_or_default()),
game_dir: Arc::new(config.game_dir().cloned().unwrap_or_default()),
data_dir: Arc::new(config.data_dir().cloned().unwrap_or_default()),
log: Arc::new(String::new()),
}
}
@ -230,6 +234,11 @@ impl State {
pub(crate) fn get_ctx(&self) -> Arc<sdk::Context> {
self.ctx.clone()
}
pub(crate) fn add_log_line(&mut self, line: String) {
let log = Arc::make_mut(&mut self.log);
log.push_str(&line);
}
}
pub(crate) struct SelectedModLens;
@ -454,6 +463,15 @@ impl AppDelegate<State> for Delegate {
}
Handled::Yes
}
cmd if cmd.is(ACTION_LOG) => {
let line = cmd
.get(ACTION_LOG)
.expect("command type matched but didn't contain the expected value");
if let Some(line) = line.take() {
state.add_log_line(line);
}
Handled::Yes
}
cmd => {
if cfg!(debug_assertions) {
tracing::warn!("Unknown command: {:?}", cmd);

117
crates/dtmm/src/util.rs Normal file
View file

@ -0,0 +1,117 @@
use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use clap::{parser::ValueSource, ArgMatches};
use color_eyre::{eyre::Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct Config {
data_dir: Option<PathBuf>,
game_dir: Option<PathBuf>,
}
impl Config {
pub fn game_dir(&self) -> Option<&PathBuf> {
self.game_dir.as_ref()
}
pub fn data_dir(&self) -> Option<&PathBuf> {
self.data_dir.as_ref()
}
}
#[cfg(not(arget_os = "windows"))]
pub fn get_default_config_path() -> PathBuf {
let config_dir = std::env::var("XDG_CONFIG_DIR").unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| {
let user = std::env::var("USER").expect("user env variable not set");
format!("/home/{user}")
});
format!("{home}/.config")
});
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
}
#[cfg(target_os = "windows")]
pub fn get_default_config_path() -> PathBuf {
let config_dir = std::env::var("APPDATA").expect("appdata env var not set");
PathBuf::from(config_dir).join("dtmm").join("dtmm.cfg")
}
#[cfg(not(arget_os = "windows"))]
pub fn get_default_data_dir() -> PathBuf {
let data_dir = std::env::var("XDG_DATA_DIR").unwrap_or_else(|_| {
let home = std::env::var("HOME").unwrap_or_else(|_| {
let user = std::env::var("USER").expect("user env variable not set");
format!("/home/{user}")
});
format!("{home}/.local/share")
});
PathBuf::from(data_dir).join("dtmm")
}
#[cfg(target_os = "windows")]
pub fn get_default_data_dir() -> PathBuf {
let data_dir = std::env::var("APPDATA").expect("appdata env var not set");
PathBuf::from(data_dir).join("dtmm")
}
pub(crate) fn read_config<P: AsRef<Path>>(
default_config_path: P,
matches: &ArgMatches,
) -> Result<Config> {
let path = matches
.get_one::<PathBuf>("config")
.expect("argument missing despite default");
let default_config_path = default_config_path.as_ref();
match fs::read(path) {
Ok(data) => {
let data = String::from_utf8(data).wrap_err_with(|| {
format!("config file {} contains invalid UTF-8", path.display())
})?;
serde_sjson::from_str(&data)
.wrap_err_with(|| format!("invalid config file {}", path.display()))
}
Err(err) if err.kind() == ErrorKind::NotFound => {
if matches.value_source("config") != Some(ValueSource::DefaultValue) {
return Err(err)
.wrap_err_with(|| format!("failed to read config file {}", path.display()))?;
}
{
let parent = default_config_path
.parent()
.expect("a file path always has a parent directory");
fs::create_dir_all(parent).wrap_err_with(|| {
format!("failed to create directories {}", parent.display())
})?;
}
let config = Config {
data_dir: Some(get_default_data_dir()),
game_dir: None,
};
{
let data = serde_sjson::to_string(&config)
.wrap_err("failed to serialize default config value")?;
fs::write(default_config_path, data).wrap_err_with(|| {
format!(
"failed to write default config to {}",
default_config_path.display()
)
})?;
}
Ok(config)
}
Err(err) => {
Err(err).wrap_err_with(|| format!("failed to read config file {}", path.display()))
}
}
}

114
crates/dtmm/src/worker.rs Normal file
View file

@ -0,0 +1,114 @@
use std::sync::Arc;
use color_eyre::Result;
use druid::{ExtEventSink, SingleUse, Target};
use tokio::runtime::Runtime;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::RwLock;
use crate::engine::*;
use crate::state::*;
async fn handle_action(
event_sink: Arc<RwLock<ExtEventSink>>,
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
) {
while let Some(action) = action_queue.write().await.recv().await {
let event_sink = event_sink.clone();
match action {
AsyncAction::DeployMods(state) => tokio::spawn(async move {
if let Err(err) = deploy_mods(state).await {
tracing::error!("Failed to deploy mods: {:?}", err);
}
event_sink
.write()
.await
.submit_command(ACTION_FINISH_DEPLOY, (), Target::Auto)
.expect("failed to send command");
}),
AsyncAction::AddMod((state, info)) => tokio::spawn(async move {
match import_mod(state, info).await {
Ok(mod_info) => {
event_sink
.write()
.await
.submit_command(
ACTION_FINISH_ADD_MOD,
SingleUse::new(mod_info),
Target::Auto,
)
.expect("failed to send command");
}
Err(err) => {
tracing::error!("Failed to import mod: {:?}", err);
}
}
}),
AsyncAction::DeleteMod((state, info)) => tokio::spawn(async move {
if let Err(err) = delete_mod(state, &info).await {
tracing::error!(
"Failed to delete mod files. \
You might want to clean up the data directory manually. \
Reason: {:?}",
err
);
}
event_sink
.write()
.await
.submit_command(
ACTION_FINISH_DELETE_SELECTED_MOD,
SingleUse::new(info),
Target::Auto,
)
.expect("failed to send command");
}),
AsyncAction::ResetDeployment(state) => tokio::spawn(async move {
if let Err(err) = reset_mod_deployment(state).await {
tracing::error!("Failed to reset mod deployment: {:?}", err);
}
event_sink
.write()
.await
.submit_command(ACTION_FINISH_RESET_DEPLOYMENT, (), Target::Auto)
.expect("failed to send command");
}),
};
}
}
async fn handle_log(
event_sink: Arc<RwLock<ExtEventSink>>,
log_queue: Arc<RwLock<UnboundedReceiver<String>>>,
) {
while let Some(line) = log_queue.write().await.recv().await {
let event_sink = event_sink.clone();
event_sink
.write()
.await
.submit_command(ACTION_LOG, SingleUse::new(line), Target::Auto)
.expect("failed to send command");
}
}
pub(crate) fn work_thread(
event_sink: Arc<RwLock<ExtEventSink>>,
action_queue: Arc<RwLock<UnboundedReceiver<AsyncAction>>>,
log_queue: Arc<RwLock<UnboundedReceiver<String>>>,
) -> Result<()> {
let rt = Runtime::new()?;
rt.block_on(async {
loop {
tokio::select! {
_ = handle_action(event_sink.clone(), action_queue.clone()) => {},
_ = handle_log(event_sink.clone(), log_queue.clone()) => {},
}
}
});
Ok(())
}

View file

@ -1,47 +1,43 @@
// Rust Analyzer cannot properly determine that `cfg!(debug_assertions)` alone does not make code
// unused. These sections should be small enough that no truly dead code slips in.
use std::fmt::Result;
#[allow(dead_code)]
mod prod {
use std::fmt::Result;
use time::format_description::FormatItem;
use time::macros::format_description;
use time::OffsetDateTime;
use tracing::field::Field;
use tracing::{Event, Metadata, Subscriber};
use tracing_error::ErrorLayer;
use tracing_subscriber::filter::FilterFn;
use tracing_subscriber::fmt::format::{debug_fn, Writer};
use tracing_subscriber::fmt::{self, FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::prelude::*;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::EnvFilter;
use time::format_description::FormatItem;
use time::macros::format_description;
use time::OffsetDateTime;
use tracing::field::Field;
use tracing::{Event, Metadata, Subscriber};
use tracing_error::ErrorLayer;
use tracing_subscriber::filter::FilterFn;
use tracing_subscriber::fmt::format::{debug_fn, Writer};
use tracing_subscriber::fmt::{FmtContext, FormatEvent, FormatFields};
use tracing_subscriber::prelude::*;
use tracing_subscriber::registry::LookupSpan;
use tracing_subscriber::EnvFilter;
pub const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]");
const TIME_FORMAT: &[FormatItem] = format_description!("[hour]:[minute]:[second]");
fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result {
pub fn format_field(w: &mut Writer<'_>, field: &Field, val: &dyn std::fmt::Debug) -> Result {
if field.name() == "message" {
write!(w, "{:?}", val)
} else {
Ok(())
}
}
}
fn filter(metadata: &Metadata<'_>) -> bool {
pub fn filter(metadata: &Metadata<'_>) -> bool {
metadata
.fields()
.iter()
.any(|field| field.name() == "message")
}
}
struct Formatter;
pub struct Formatter;
impl<S, N> FormatEvent<S, N> for Formatter
where
impl<S, N> FormatEvent<S, N> for Formatter
where
S: Subscriber + for<'a> LookupSpan<'a>,
N: for<'a> FormatFields<'a> + 'static,
{
{
fn format_event(
&self,
ctx: &FmtContext<'_, S, N>,
@ -59,55 +55,33 @@ mod prod {
writeln!(writer)
}
}
}
/// Creates a subscriber that
/// - only prints events that contain a message
/// - does not print fields
/// - does not print spans/targets
/// - only prints time, not date
pub fn create_tracing_subscriber() {
let filter_layer = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
pub fn create_tracing_subscriber() {
let env_layer =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
let fmt_layer = tracing_subscriber::fmt::layer()
let (dev_stdout_layer, prod_stdout_layer, filter_layer) = if cfg!(debug_assertions) {
let fmt_layer = fmt::layer().pretty();
(Some(fmt_layer), None, None)
} else {
// Creates a layer that
// - only prints events that contain a message
// - does not print fields
// - does not print spans/targets
// - only prints time, not date
let fmt_layer = fmt::layer()
.event_format(Formatter)
.fmt_fields(debug_fn(format_field));
tracing_subscriber::registry()
.with(FilterFn::new(filter))
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::new(
tracing_subscriber::fmt::format::Pretty::default(),
))
.init();
}
}
#[allow(dead_code)]
mod dev {
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::EnvFilter;
pub fn create_tracing_subscriber() {
let filter_layer = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::try_new("info").unwrap());
let fmt_layer = tracing_subscriber::fmt::layer().pretty();
(None, Some(fmt_layer), Some(FilterFn::new(filter)))
};
tracing_subscriber::registry()
.with(filter_layer)
.with(fmt_layer)
.with(ErrorLayer::new(
tracing_subscriber::fmt::format::Pretty::default(),
))
.with(env_layer)
.with(dev_stdout_layer)
.with(prod_stdout_layer)
.with(ErrorLayer::new(fmt::format::Pretty::default()))
.init();
}
}
#[cfg(debug_assertions)]
pub use dev::create_tracing_subscriber;
#[cfg(not(debug_assertions))]
pub use prod::create_tracing_subscriber;