log4rs icon indicating copy to clipboard operation
log4rs copied to clipboard

An appender to roll file on load then delegate to another appender

Open OvermindDL1 opened this issue 4 years ago • 3 comments

Would it be possible to add an option to RollingFileAppender to call the roller upon initial load before any logging is done? I attempted to make an OnLaunchTrigger but that lets one log message leak through so I end up getting a file with that single log, or I get that single new log entry at the end of the old log file. Optimally I'm looking to roll the logs on every load of the application based on the configuration of the roller before any logs are logged (of course the logger is initialized before any logging is done).

OvermindDL1 avatar Apr 30 '21 22:04 OvermindDL1

Actually I may have just had a really simple idea to just delegate to another appender after performing a roll... So here's an appender that takes a path, roll, and appender options, it roll's the path using the roller (if the path exists) and then delegates (rather constructs and then returns) the other specified appender, so it works with any appender. Feel free to take and incorporate (and write tests for) this is so wanted as this is the basic missing feature I've been wanting (and so I bet others have as well):

//! This is an appender that rolls a file on launch and then delegates to another appender

use log4rs::append::rolling_file::policy::compound::roll::Roll;
use log4rs::append::Append;
use log4rs::config::{Deserialize, Deserializers};
use serde_value::Value;
use std::collections::BTreeMap;
use std::path::Path;

#[serde(deny_unknown_fields)]
#[derive(Clone, Eq, PartialEq, Hash, Debug, serde::Deserialize)]
pub struct LaunchRollFileAppenderConfig {
	appender: Appender,
	path: String,
	launch_roller: Roller,
}

#[derive(Clone, Eq, PartialEq, Hash, Debug)]
struct Appender {
	kind: String,
	config: Value,
}

impl<'de> serde::Deserialize<'de> for Appender {
	fn deserialize<D>(d: D) -> Result<Appender, D::Error>
	where
		D: serde::Deserializer<'de>,
	{
		let mut map = BTreeMap::<Value, Value>::deserialize(d)?;

		let kind = match map.remove(&Value::String("kind".to_owned())) {
			Some(kind) => kind.deserialize_into().map_err(|e| e.to_error())?,
			None => return Err(serde::de::Error::missing_field("kind")),
		};

		Ok(Appender {
			kind,
			config: Value::Map(map),
		})
	}
}

#[derive(Clone, Eq, PartialEq, Hash, Debug)]
struct Roller {
	kind: String,
	config: Value,
}

impl<'de> serde::Deserialize<'de> for Roller {
	fn deserialize<D>(d: D) -> Result<Roller, D::Error>
	where
		D: serde::Deserializer<'de>,
	{
		let mut map = BTreeMap::<Value, Value>::deserialize(d)?;

		let kind = match map.remove(&Value::String("kind".to_owned())) {
			Some(kind) => kind.deserialize_into().map_err(|e| e.to_error())?,
			None => return Err(serde::de::Error::missing_field("kind")),
		};

		Ok(Roller {
			kind,
			config: Value::Map(map),
		})
	}
}

#[derive(Clone, Eq, PartialEq, Hash, Debug)]
struct Policy {
	kind: String,
	config: Value,
}

impl<'de> serde::Deserialize<'de> for Policy {
	fn deserialize<D>(d: D) -> Result<Policy, D::Error>
	where
		D: serde::Deserializer<'de>,
	{
		let mut map = BTreeMap::<Value, Value>::deserialize(d)?;

		let kind = match map.remove(&Value::String("kind".to_owned())) {
			Some(kind) => kind.deserialize_into().map_err(|e| e.to_error())?,
			None => "compound".to_owned(),
		};

		Ok(Policy {
			kind,
			config: Value::Map(map),
		})
	}
}

#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Default)]
pub struct RollFileOnLaunchAppenderDeserializer;

impl Deserialize for RollFileOnLaunchAppenderDeserializer {
	type Trait = dyn Append;

	type Config = LaunchRollFileAppenderConfig;

	fn deserialize(
		&self,
		config: LaunchRollFileAppenderConfig,
		deserializers: &Deserializers,
	) -> anyhow::Result<Box<dyn Append>> {
		let path = Path::new(&config.path);
		if path.exists() && path.is_file() {
			let launch_roller: Box<dyn Roll> = deserializers
				.deserialize(&config.launch_roller.kind, config.launch_roller.config)?;
			launch_roller.roll(path)?;
		}

		let appender = deserializers.deserialize(&config.appender.kind, config.appender.config)?;

		Ok(appender)
	}
}

If I get time to put in an appropriate amount of work then I will submit a PR myself, but otherwise feel free to take and improve this before then. :-)

OvermindDL1 avatar Apr 30 '21 23:04 OvermindDL1

@OvermindDL1 I've implemented something like this in my dayjob since we use log4rs there. At the time it seemed like a very niche feature, can we get a show of hands who wants to see this?

estk avatar Jul 13 '21 15:07 estk

Hard part is getting hands from people that would like to have this, but don't currently watch the repo, that was me up until I got fed up of not having it and instead manually trying to read the configuration and move the file myself before log4rs got loaded up, which was harder than I preferred. ^.^;

I've actually ended up making a few new appender's since I've discovered how easy it is to make them. Probably be a good idea in the docs to implement a simple appender to show people.

OvermindDL1 avatar Jul 26 '21 22:07 OvermindDL1