1
0
Fork 0
forked from wry/wry

Add clean-logs-older-than option

This commit is contained in:
khyperia 2026-03-27 07:44:34 +01:00 committed by Julian Orth
parent 4c7d108e09
commit 880c98ecfb
17 changed files with 360 additions and 10 deletions

View file

@ -111,6 +111,32 @@ instead:
~$ jay set-log-level debug ~$ jay set-log-level debug
``` ```
## Log File Cleanup
Jay creates a new log file each time it starts. Over time, old log files can
accumulate. To automatically delete old log files on startup, use the
`clean-logs-older-than` option:
```toml
clean-logs-older-than.days = 7
```
The table accepts `weeks` and `days` fields. At least one must be specified.
They can be combined and accept fractional values:
```toml
[clean-logs-older-than]
weeks = 2
days = 3
```
Log files belonging to other running Jay instances (e.g. on another VT) are
never deleted, even if they are older than the specified age.
> [!NOTE]
> This setting only takes effect at compositor startup. It cannot be triggered
> by reloading the configuration.
## Focus Follows Mouse ## Focus Follows Mouse
When enabled, moving the pointer over a window automatically gives it keyboard When enabled, moving the pointer over a window automatically gives it keyboard

View file

@ -266,6 +266,9 @@ log-level = "debug"
> The `log-level` config setting is read at startup and cannot be changed by > The `log-level` config setting is read at startup and cannot be changed by
> reloading the configuration. Use `jay set-log-level` for runtime changes. > reloading the configuration. Use `jay set-log-level` for runtime changes.
To automatically clean up old log files, see
[Log File Cleanup](configuration/misc.md#log-file-cleanup).
## Performance issues ## Performance issues
If you experience dropped frames, stuttering, or high latency, try the If you experience dropped frames, stuttering, or high latency, try the

View file

@ -60,7 +60,7 @@ use {
atomic::{AtomicBool, Ordering::Relaxed}, atomic::{AtomicBool, Ordering::Relaxed},
}, },
task::{Context, Poll, Waker}, task::{Context, Poll, Waker},
time::Duration, time::{Duration, SystemTime},
}, },
}; };
@ -776,6 +776,10 @@ impl ConfigClient {
self.send(&ClientMessage::SetLogLevel { level }) self.send(&ClientMessage::SetLogLevel { level })
} }
pub fn clean_logs_older_than(&self, time: SystemTime) {
self.send(&ClientMessage::CleanLogsOlderThan { time })
}
pub fn unset_env(&self, key: &str) { pub fn unset_env(&self, key: &str) {
self.send(&ClientMessage::UnsetEnv { key }); self.send(&ClientMessage::UnsetEnv { key });
} }

View file

@ -21,7 +21,7 @@ use {
xwayland::XScalingMode, xwayland::XScalingMode,
}, },
serde::{Deserialize, Serialize}, serde::{Deserialize, Serialize},
std::time::Duration, std::time::{Duration, SystemTime},
}; };
#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)]
@ -861,6 +861,9 @@ pub enum ClientMessage<'a> {
SeatWarpMouseToFocus { SeatWarpMouseToFocus {
seat: Seat, seat: Seat,
}, },
CleanLogsOlderThan {
time: SystemTime,
},
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View file

@ -3,7 +3,10 @@
//! Note that you can use the `log` crate for logging. All invocations of `log::info` etc. //! Note that you can use the `log` crate for logging. All invocations of `log::info` etc.
//! automatically log into the compositors log. //! automatically log into the compositors log.
use serde::{Deserialize, Serialize}; use {
serde::{Deserialize, Serialize},
std::time::SystemTime,
};
/// The log level of the compositor or a log message. /// The log level of the compositor or a log message.
#[derive(Serialize, Deserialize, Copy, Clone, Debug)] #[derive(Serialize, Deserialize, Copy, Clone, Debug)]
@ -19,3 +22,13 @@ pub enum LogLevel {
pub fn set_log_level(level: LogLevel) { pub fn set_log_level(level: LogLevel) {
get!().set_log_level(level); get!().set_log_level(level);
} }
/// If this function is called during startup, Jay's log files before `time` are deleted.
///
/// The current log file is never deleted, nor are any other logfiles of active Jay instances (e.g.
/// on another VT), even if `time` is in the future.
///
/// Calling this function after startup has no effect.
pub fn clean_logs_older_than(time: SystemTime) {
get!().clean_logs_older_than(time);
}

View file

@ -397,6 +397,7 @@ fn start_compositor2(
egg_state: Default::default(), egg_state: Default::default(),
control_centers: Default::default(), control_centers: Default::default(),
virtual_outputs: Default::default(), virtual_outputs: Default::default(),
clean_logs_older_than: Default::default(),
}); });
state.tracker.register(ClientId::from_raw(0)); state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state); create_dummy_output(&state);
@ -457,6 +458,7 @@ async fn start_compositor3(state: Rc<State>, test_future: Option<TestFuture>) {
if state.create_default_seat.get() && state.globals.seats.is_empty() { if state.create_default_seat.get() && state.globals.seats.is_empty() {
state.create_seat(DEFAULT_SEAT_NAME); state.create_seat(DEFAULT_SEAT_NAME);
} }
state.perform_clean_logs_older_than();
state.update_ei_acceptor(); state.update_ei_acceptor();
let _geh = start_global_event_handlers(&state); let _geh = start_global_event_handlers(&state);

View file

@ -86,7 +86,7 @@ use {
hash::Hash, hash::Hash,
ops::Deref, ops::Deref,
rc::{Rc, Weak}, rc::{Rc, Weak},
time::Duration, time::{Duration, SystemTime},
}, },
thiserror::Error, thiserror::Error,
uapi::{OwnedFd, c, fcntl_dupfd_cloexec}, uapi::{OwnedFd, c, fcntl_dupfd_cloexec},
@ -1856,6 +1856,10 @@ impl ConfigProxyHandler {
self.state.set_log_level(level.into()); self.state.set_log_level(level.into());
} }
fn handle_clean_logs_older_than(&self, time: SystemTime) {
self.state.clean_logs_older_than.set(Some(time));
}
fn handle_grab(&self, kb: InputDevice, grab: bool) -> Result<(), CphError> { fn handle_grab(&self, kb: InputDevice, grab: bool) -> Result<(), CphError> {
let kb = self.get_kb(kb)?; let kb = self.get_kb(kb)?;
kb.grab(grab); kb.grab(grab);
@ -3376,6 +3380,7 @@ impl ConfigProxyHandler {
ClientMessage::GetConnectorByName { name } => self.handle_get_connector_by_name(name), ClientMessage::GetConnectorByName { name } => self.handle_get_connector_by_name(name),
ClientMessage::CreateVirtualOutput { name } => self.handle_create_virtual_output(name), ClientMessage::CreateVirtualOutput { name } => self.handle_create_virtual_output(name),
ClientMessage::RemoveVirtualOutput { name } => self.handle_remove_virtual_output(name), ClientMessage::RemoveVirtualOutput { name } => self.handle_remove_virtual_output(name),
ClientMessage::CleanLogsOlderThan { time } => self.handle_clean_logs_older_than(time),
} }
Ok(()) Ok(())
} }

View file

@ -4,7 +4,7 @@ use {
utils::{atomic_enum::AtomicEnum, errorfmt::ErrorFmt, oserror::OsError}, utils::{atomic_enum::AtomicEnum, errorfmt::ErrorFmt, oserror::OsError},
}, },
backtrace::Backtrace, backtrace::Backtrace,
bstr::BString, bstr::{BStr, BString, ByteSlice},
log::{LevelFilter, Log, Metadata, Record}, log::{LevelFilter, Log, Metadata, Record},
parking_lot::Mutex, parking_lot::Mutex,
std::{ std::{
@ -17,9 +17,11 @@ use {
Arc, Arc,
atomic::{AtomicI32, AtomicU32, Ordering::Relaxed}, atomic::{AtomicI32, AtomicU32, Ordering::Relaxed},
}, },
thread,
time::SystemTime, time::SystemTime,
}, },
uapi::{Errno, Fd, OwnedFd, Ustring, c, format_ustr}, thiserror::Error,
uapi::{AsUstr, Dirent, Errno, Fd, OwnedFd, Ustring, c, format_ustr},
}; };
thread_local! { thread_local! {
@ -80,6 +82,17 @@ impl Logger {
log::set_max_level(filter); log::set_max_level(filter);
} }
pub fn clean_logs_older_than(&self, time: SystemTime) {
let time_formatted = humantime::format_rfc3339_millis(time);
log::info!("Cleaning unused log files older than {}", time_formatted);
let path = self.path();
thread::spawn(move || {
if let Err(e) = clean_logs_older_than(path.as_bstr(), time) {
log::error!("Could not clean log files: {}", ErrorFmt(e));
}
});
}
pub fn level(&self) -> LogLevel { pub fn level(&self) -> LogLevel {
self.level.load(Relaxed) self.level.load(Relaxed)
} }
@ -105,6 +118,7 @@ impl Logger {
pub fn open_log_file(ty: &str) -> (Ustring, OwnedFd) { pub fn open_log_file(ty: &str) -> (Ustring, OwnedFd) {
let log_dir = create_log_dir(ty); let log_dir = create_log_dir(ty);
let mut flock_fail_count = 0;
for i in 0.. { for i in 0.. {
let file_name = format_ustr!( let file_name = format_ustr!(
"{}/{ty}-{}-{}.txt", "{}/{ty}-{}-{}.txt",
@ -117,7 +131,22 @@ pub fn open_log_file(ty: &str) -> (Ustring, OwnedFd) {
c::O_CREAT | c::O_EXCL | c::O_CLOEXEC | c::O_WRONLY, c::O_CREAT | c::O_EXCL | c::O_CLOEXEC | c::O_WRONLY,
0o644, 0o644,
) { ) {
Ok(f) => return (file_name, f), Ok(f) => {
if let Err(e) = uapi::flock(f.raw(), c::LOCK_EX | c::LOCK_NB) {
log::warn!("Unable to flock just-opened logfile: {}", ErrorFmt(e));
flock_fail_count += 1;
if flock_fail_count > 10 {
log::error!(concat!(
"Failed to flock just-opened logfile more than 10 times in a row. ",
"Not flocking the logfile, if the cleanup routine later succeeds to ",
"flock this logfile, it will be deleted even if it is still in use."
));
} else {
continue;
}
}
return (file_name, f);
}
Err(Errno(c::EEXIST)) => {} Err(Errno(c::EEXIST)) => {}
Err(e) => { Err(e) => {
let e: OsError = e.into(); let e: OsError = e.into();
@ -209,3 +238,73 @@ impl Log for LogWrapper {
// nothing // nothing
} }
} }
#[derive(Debug, Error)]
enum CleanLogsError {
#[error("Log path has no parent")]
NoParent,
#[error("Could not open the log directory")]
OpenDir(#[source] OsError),
#[error("Could not enumerate directory entry")]
ReadDir(#[source] OsError),
#[error("Could not open the log file")]
OpenFile(#[source] OsError),
#[error("Could not stat the log file")]
Stat(#[source] OsError),
#[error("Could not unlink the log file")]
Unlink(#[source] OsError),
}
fn clean_logs_older_than(current_log_path: &BStr, time: SystemTime) -> Result<(), CleanLogsError> {
let current_log_path = current_log_path.to_path_lossy();
let parent = current_log_path.parent().ok_or(CleanLogsError::NoParent)?;
let mut dir = uapi::opendir(parent)
.map_err(Into::into)
.map_err(CleanLogsError::OpenDir)?;
let parent = uapi::open(parent, c::O_PATH | c::O_CLOEXEC | c::O_DIRECTORY, 0)
.map_err(Into::into)
.map_err(CleanLogsError::OpenDir)?;
let time = time
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as c::time_t;
while let Some(entry) = uapi::readdir(&mut dir) {
let entry = entry.map_err(Into::into).map_err(CleanLogsError::ReadDir)?;
if let Err(err) = process_entry(parent.raw(), &entry, time) {
log::error!(
"Could not clean log file {}: {}",
entry.name().as_ustr().display(),
ErrorFmt(err),
);
}
}
fn process_entry(
parent: c::c_int,
entry: &Dirent,
time: c::time_t,
) -> Result<(), CleanLogsError> {
if entry.d_type != c::DT_REG {
return Ok(());
}
let name = entry.name();
let file = uapi::openat(parent, name, c::O_RDONLY | c::O_CLOEXEC, 0)
.map_err(Into::into)
.map_err(CleanLogsError::OpenFile)?;
let stat = uapi::fstat(*file)
.map_err(Into::into)
.map_err(CleanLogsError::Stat)?;
if stat.st_mtime >= time {
return Ok(());
}
if uapi::flock(file.raw(), c::LOCK_EX | c::LOCK_NB).is_err() {
log::info!("Preserving file still in use: {}", name.as_ustr().display());
return Ok(());
}
uapi::unlinkat(parent, name, 0)
.map_err(Into::into)
.map_err(CleanLogsError::Unlink)?;
log::info!("Deleted {}", name.as_ustr().display());
Ok(())
}
Ok(())
}

View file

@ -150,7 +150,7 @@ use {
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
rc::{Rc, Weak}, rc::{Rc, Weak},
sync::Arc, sync::Arc,
time::Duration, time::{Duration, SystemTime},
}, },
thiserror::Error, thiserror::Error,
uapi::{OwnedFd, c}, uapi::{OwnedFd, c},
@ -305,6 +305,7 @@ pub struct State {
pub egg_state: EggState, pub egg_state: EggState,
pub control_centers: ControlCenters, pub control_centers: ControlCenters,
pub virtual_outputs: VirtualOutputs, pub virtual_outputs: VirtualOutputs,
pub clean_logs_older_than: Cell<Option<SystemTime>>,
} }
// impl Drop for State { // impl Drop for State {
@ -1760,6 +1761,14 @@ impl State {
} }
} }
pub fn perform_clean_logs_older_than(&self) {
if let Some(time) = self.clean_logs_older_than.get()
&& let Some(logger) = &self.logger
{
logger.clean_logs_older_than(time);
}
}
fn colors_changed(&self) { fn colors_changed(&self) {
struct V; struct V;
impl NodeVisitorBase for V { impl NodeVisitorBase for V {

View file

@ -523,6 +523,7 @@ pub struct Config {
pub keymaps: Vec<ConfigKeymap>, pub keymaps: Vec<ConfigKeymap>,
pub auto_reload: Option<bool>, pub auto_reload: Option<bool>,
pub log_level: Option<LogLevel>, pub log_level: Option<LogLevel>,
pub clean_logs_older_than: Option<Duration>,
pub theme: Theme, pub theme: Theme,
pub egui: Egui, pub egui: Egui,
pub gfx_api: Option<GfxApi>, pub gfx_api: Option<GfxApi>,

View file

@ -9,6 +9,7 @@ use {
pub mod action; pub mod action;
mod actions; mod actions;
mod capabilities; mod capabilities;
mod clean_logs_older_than;
mod client_match; mod client_match;
mod client_rule; mod client_rule;
mod color; mod color;

View file

@ -0,0 +1,57 @@
use {
crate::{
config::{
context::Context,
extractor::{Extractor, ExtractorError, fltorint, opt},
parser::{DataType, ParseResult, Parser, UnexpectedDataType},
},
toml::{
toml_span::{DespanExt, Span, Spanned, SpannedExt},
toml_value::Value,
},
},
indexmap::IndexMap,
std::time::{Duration, TryFromFloatSecsError},
thiserror::Error,
};
#[derive(Debug, Error)]
pub enum CleanLogsOlderThanParserError {
#[error(transparent)]
Expected(#[from] UnexpectedDataType),
#[error(transparent)]
Extract(#[from] ExtractorError),
#[error("At least one of the `weeks` or `days` fields must be specified")]
WeeksOrDays,
#[error("Duration is invalid")]
InvalidDuration(#[source] TryFromFloatSecsError),
}
pub struct CleanLogsOlderThanParser<'a>(pub &'a Context<'a>);
impl Parser for CleanLogsOlderThanParser<'_> {
type Value = Duration;
type Error = CleanLogsOlderThanParserError;
const EXPECTED: &'static [DataType] = &[DataType::Table];
fn parse_table(
&mut self,
span: Span,
table: &IndexMap<Spanned<String>, Spanned<Value>>,
) -> ParseResult<Self> {
let mut ext = Extractor::new(self.0, span, table);
let (weeks, days) = ext.extract((opt(fltorint("weeks")), opt(fltorint("days"))))?;
if weeks.is_none() && days.is_none() {
return Err(CleanLogsOlderThanParserError::WeeksOrDays.spanned(span));
}
const SECONDS_IN_WEEK: f64 = 604800.0;
const SECONDS_IN_DAY: f64 = 86400.0;
let duration = Duration::try_from_secs_f64(
weeks.despan().unwrap_or_default() * SECONDS_IN_WEEK
+ days.despan().unwrap_or_default() * SECONDS_IN_DAY,
)
.map_err(CleanLogsOlderThanParserError::InvalidDuration)
.map_err(|e| e.spanned(span))?;
Ok(duration)
}
}

View file

@ -8,6 +8,7 @@ use {
parsers::{ parsers::{
action::ActionParser, action::ActionParser,
actions::ActionsParser, actions::ActionsParser,
clean_logs_older_than::CleanLogsOlderThanParser,
client_rule::ClientRulesParser, client_rule::ClientRulesParser,
color_management::ColorManagementParser, color_management::ColorManagementParser,
connector::ConnectorsParser, connector::ConnectorsParser,
@ -152,6 +153,7 @@ impl Parser for ConfigParser<'_> {
show_titles, show_titles,
fallback_output_mode_val, fallback_output_mode_val,
egui_val, egui_val,
clean_logs_older_than_val,
), ),
) = ext.extract(( ) = ext.extract((
( (
@ -211,6 +213,7 @@ impl Parser for ConfigParser<'_> {
recover(opt(bol("show-titles"))), recover(opt(bol("show-titles"))),
opt(val("fallback-output-mode")), opt(val("fallback-output-mode")),
opt(val("egui")), opt(val("egui")),
opt(val("clean-logs-older-than")),
), ),
))?; ))?;
let mut keymap = None; let mut keymap = None;
@ -307,6 +310,17 @@ impl Parser for ConfigParser<'_> {
} }
} }
} }
let mut clean_logs_older_than = None;
if let Some(value) = clean_logs_older_than_val {
match value.parse(&mut CleanLogsOlderThanParser(self.0)) {
Ok(v) => {
clean_logs_older_than = Some(v);
}
Err(e) => {
log::warn!("Could not parse clean-logs-older-than: {}", self.0.error(e));
}
}
}
let mut theme = Theme::default(); let mut theme = Theme::default();
if let Some(value) = theme_val { if let Some(value) = theme_val {
match value.parse(&mut ThemeParser(self.0)) { match value.parse(&mut ThemeParser(self.0)) {
@ -567,6 +581,7 @@ impl Parser for ConfigParser<'_> {
keymaps, keymaps,
auto_reload: auto_reload.despan(), auto_reload: auto_reload.despan(),
log_level, log_level,
clean_logs_older_than,
theme, theme,
egui, egui,
gfx_api, gfx_api,

View file

@ -35,7 +35,7 @@ use {
io::Async, io::Async,
is_reload, is_reload,
keyboard::Keymap, keyboard::Keymap,
logging::set_log_level, logging::{clean_logs_older_than, set_log_level},
on_devices_enumerated, on_idle, on_unload, open_control_center, quit, reload, on_devices_enumerated, on_idle, on_unload, open_control_center, quit, reload,
set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled, set_color_management_enabled, set_default_workspace_capture, set_explicit_sync_enabled,
set_float_above_fullscreen, set_idle, set_idle_grace_period, set_float_above_fullscreen, set_idle, set_idle_grace_period,
@ -67,7 +67,7 @@ use {
os::{fd::AsRawFd, unix::ffi::OsStrExt}, os::{fd::AsRawFd, unix::ffi::OsStrExt},
path::{Path, PathBuf}, path::{Path, PathBuf},
rc::Rc, rc::Rc,
time::Duration, time::{Duration, SystemTime, UNIX_EPOCH},
}, },
uapi::{ uapi::{
Errno, Errno,
@ -1494,6 +1494,11 @@ fn load_config(initial_load: bool, auto_reload: bool, persistent: &Rc<Persistent
if let Some(level) = config.log_level { if let Some(level) = config.log_level {
set_log_level(level); set_log_level(level);
} }
if let Some(duration) = config.clean_logs_older_than {
let now = SystemTime::now();
let in_the_past = now.checked_sub(duration).unwrap_or(UNIX_EPOCH);
clean_logs_older_than(in_the_past);
}
if let Some(idle) = config.idle { if let Some(idle) = config.idle {
set_idle(Some(idle)); set_idle(Some(idle));
} }

View file

@ -645,6 +645,23 @@
} }
] ]
}, },
"CleanLogsOlderThan": {
"description": "The definition of how old logfiles need to be for them to be automatically deleted.\n\nOmitted values are set to 0. At least one of `weeks` or `days` must be specified.\n\n- Example:\n\n ```toml\n clean-logs-older-than.weeks = 2\n ```\n",
"type": "object",
"properties": {
"weeks": {
"type": "number",
"description": "The number of weeks.",
"minimum": 0.0
},
"days": {
"type": "number",
"description": "The number of days.",
"minimum": 0.0
}
},
"required": []
},
"ClickMethod": { "ClickMethod": {
"type": "string", "type": "string",
"description": "The click method to apply to an input device.\n\nSee the libinput documentation for more details.\n", "description": "The click method to apply to an input device.\n\nSee the libinput documentation for more details.\n",
@ -964,6 +981,10 @@
"description": "Sets the log level of the compositor.\n\nThis setting cannot be changed by re-loading the configuration. Use\n`jay set-log-level` instead.\n\n- Example:\n\n ```toml\n log-level = \"debug\"\n ```\n", "description": "Sets the log level of the compositor.\n\nThis setting cannot be changed by re-loading the configuration. Use\n`jay set-log-level` instead.\n\n- Example:\n\n ```toml\n log-level = \"debug\"\n ```\n",
"$ref": "#/$defs/LogLevel" "$ref": "#/$defs/LogLevel"
}, },
"clean-logs-older-than": {
"description": "If specified on startup, deletes Jay's log files older than the specified time period.\n\nPossible keys in the table are `days` and `weeks`.\n\n- Example:\n\n ```toml\n clean-logs-older-than.days = 7\n ```\n",
"$ref": "#/$defs/CleanLogsOlderThan"
},
"theme": { "theme": {
"description": "Sets the theme of the compositor.\n", "description": "Sets the theme of the compositor.\n",
"$ref": "#/$defs/Theme" "$ref": "#/$defs/Theme"

View file

@ -960,6 +960,40 @@ The string should have one of the following values:
The brightness in cd/m^2. The brightness in cd/m^2.
<a name="types-CleanLogsOlderThan"></a>
### `CleanLogsOlderThan`
The definition of how old logfiles need to be for them to be automatically deleted.
Omitted values are set to 0. At least one of `weeks` or `days` must be specified.
- Example:
```toml
clean-logs-older-than.weeks = 2
```
Values of this type should be tables.
The table has the following fields:
- `weeks` (optional):
The number of weeks.
The value of this field should be a number.
The numbers should be greater than or equal to 0.
- `days` (optional):
The number of days.
The value of this field should be a number.
The numbers should be greater than or equal to 0.
<a name="types-ClickMethod"></a> <a name="types-ClickMethod"></a>
### `ClickMethod` ### `ClickMethod`
@ -1836,6 +1870,20 @@ The table has the following fields:
The value of this field should be a [LogLevel](#types-LogLevel). The value of this field should be a [LogLevel](#types-LogLevel).
- `clean-logs-older-than` (optional):
If specified on startup, deletes Jay's log files older than the specified time period.
Possible keys in the table are `days` and `weeks`.
- Example:
```toml
clean-logs-older-than.days = 7
```
The value of this field should be a [CleanLogsOlderThan](#types-CleanLogsOlderThan).
- `theme` (optional): - `theme` (optional):
Sets the theme of the compositor. Sets the theme of the compositor.

View file

@ -2680,6 +2680,19 @@ Config:
```toml ```toml
log-level = "debug" log-level = "debug"
``` ```
clean-logs-older-than:
ref: CleanLogsOlderThan
required: false
description: |
If specified on startup, deletes Jay's log files older than the specified time period.
Possible keys in the table are `days` and `weeks`.
- Example:
```toml
clean-logs-older-than.days = 7
```
theme: theme:
ref: Theme ref: Theme
required: false required: false
@ -3218,6 +3231,31 @@ GracePeriod:
required: false required: false
CleanLogsOlderThan:
kind: table
description: |
The definition of how old logfiles need to be for them to be automatically deleted.
Omitted values are set to 0. At least one of `weeks` or `days` must be specified.
- Example:
```toml
clean-logs-older-than.weeks = 2
```
fields:
weeks:
description: The number of weeks.
kind: number
minimum: 0
required: false
days:
description: The number of days.
kind: number
minimum: 0
required: false
RepeatRate: RepeatRate:
kind: table kind: table
description: | description: |