feat(ffi): replace tracing's RollingFileAppender with SizeAndDateRollingWriter (#6162)
This new file writer supports both total size and age based rolling, and will take care of a whole class of edge cases that make rageshake uploading fail. * It will rotate log files when the configured time period elapses (e.g., hourly, daily). * When the total size of all log files exceeds the configured limit, the oldest files are removed. * Logs older than the configured max age are automatically removed during cleanup. * Only files matching the configured prefix and suffix are managed by any given writer instance.
This commit is contained in:
Generated
+2
@@ -3309,6 +3309,7 @@ version = "0.16.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-compat",
|
||||
"chrono",
|
||||
"console_error_panic_hook",
|
||||
"extension-trait",
|
||||
"eyeball-im",
|
||||
@@ -3331,6 +3332,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"similar-asserts",
|
||||
"tempfile",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
|
||||
@@ -45,6 +45,7 @@ experimental-element-recent-emojis = ["matrix-sdk/experimental-element-recent-em
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
chrono.workspace = true
|
||||
extension-trait = "1.0.2"
|
||||
eyeball-im.workspace = true
|
||||
futures-util.workspace = true
|
||||
@@ -127,6 +128,7 @@ paranoid-android = { version = "0.2.2", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
similar-asserts.workspace = true
|
||||
tempfile.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
uniffi = { workspace = true, features = ["build"] }
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
error::Error,
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{self, Debug},
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! A generic `ChunkIterator` that operates over a `Vec`.
|
||||
//!
|
||||
//! This type is not designed to work over FFI, but it can be embedded inside an
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::Debug,
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
// Allow UniFFI to use methods marked as `#[deprecated]`.
|
||||
#![allow(deprecated)]
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, error::Error, fmt, fmt::Display};
|
||||
|
||||
use matrix_sdk::{
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::{bail, Context};
|
||||
use matrix_sdk::IdParseError;
|
||||
use matrix_sdk_ui::timeline::TimelineEventItemId;
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) fn unwrap_or_clone_arc<T: Clone>(arc: Arc<T>) -> T {
|
||||
|
||||
@@ -30,7 +30,6 @@ mod store;
|
||||
mod sync_service;
|
||||
mod task_handle;
|
||||
mod timeline;
|
||||
mod tracing;
|
||||
mod utd;
|
||||
mod utils;
|
||||
mod widget;
|
||||
|
||||
@@ -9,8 +9,9 @@
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::ruma::LocationContent;
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct LastLocation {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use matrix_sdk_ui::notification_client::{
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use matrix_sdk::{
|
||||
|
||||
+65
-24
@@ -1,10 +1,25 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::OnceLock;
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::sync::{atomic::AtomicBool, Arc};
|
||||
|
||||
use ::tracing::info;
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing::warn;
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use ::tracing::warn;
|
||||
use tracing_appender::rolling::Rotation;
|
||||
#[cfg(feature = "sentry")]
|
||||
use tracing_core::Level;
|
||||
use tracing_core::Subscriber;
|
||||
@@ -23,9 +38,22 @@ use tracing_subscriber::{
|
||||
EnvFilter, Layer, Registry,
|
||||
};
|
||||
|
||||
use crate::error::ClientError;
|
||||
|
||||
/// Default maximum total size of all log files combined (10MB).
|
||||
const DEFAULT_MAX_TOTAL_SIZE_BYTES: u64 = 10 * 1024 * 1024;
|
||||
|
||||
/// Default maximum age of log files in seconds (1 week).
|
||||
const DEFAULT_MAX_AGE_SECONDS: u64 = 7 * 24 * 60 * 60;
|
||||
|
||||
mod rolling_writer;
|
||||
pub mod tracing;
|
||||
|
||||
use rolling_writer::SizeAndDateRollingWriter;
|
||||
|
||||
use self::tracing::LogLevel;
|
||||
#[cfg(feature = "sentry")]
|
||||
use crate::tracing::BRIDGE_SPAN_NAME;
|
||||
use crate::{error::ClientError, tracing::LogLevel};
|
||||
use self::tracing::BRIDGE_SPAN_NAME;
|
||||
|
||||
// Adjusted version of tracing_subscriber::fmt::Format
|
||||
struct EventFormatter {
|
||||
@@ -153,7 +181,7 @@ type ReloadHandle = Handle<
|
||||
Layered<EnvFilter, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
SizeAndDateRollingWriter,
|
||||
>,
|
||||
Layered<EnvFilter, Registry>,
|
||||
>;
|
||||
@@ -218,21 +246,17 @@ fn make_file_layer(
|
||||
Layered<EnvFilter, Registry, Registry>,
|
||||
FieldsFormatterForFiles,
|
||||
EventFormatter,
|
||||
RollingFileAppender,
|
||||
SizeAndDateRollingWriter,
|
||||
> {
|
||||
let mut builder = RollingFileAppender::builder()
|
||||
.rotation(Rotation::HOURLY)
|
||||
.filename_prefix(&file_configuration.file_prefix);
|
||||
|
||||
if let Some(max_files) = file_configuration.max_files {
|
||||
builder = builder.max_log_files(max_files as usize)
|
||||
}
|
||||
if let Some(file_suffix) = file_configuration.file_suffix {
|
||||
builder = builder.filename_suffix(file_suffix)
|
||||
}
|
||||
|
||||
let writer =
|
||||
builder.build(&file_configuration.path).expect("Failed to create a rolling file appender.");
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
&file_configuration.path,
|
||||
file_configuration.file_prefix,
|
||||
file_configuration.file_suffix.unwrap_or_else(|| String::from(".log")),
|
||||
Rotation::HOURLY,
|
||||
file_configuration.max_total_size_bytes.unwrap_or(DEFAULT_MAX_TOTAL_SIZE_BYTES),
|
||||
file_configuration.max_age_seconds.unwrap_or(DEFAULT_MAX_AGE_SECONDS),
|
||||
)
|
||||
.expect("Failed to create a rolling file appender.");
|
||||
|
||||
fmt::layer()
|
||||
.fmt_fields(FieldsFormatterForFiles::default())
|
||||
@@ -254,13 +278,30 @@ pub struct TracingFileConfiguration {
|
||||
file_prefix: String,
|
||||
|
||||
/// Optional suffix for the log file's names.
|
||||
///
|
||||
/// Default is ".log" if not specified.
|
||||
file_suffix: Option<String>,
|
||||
|
||||
/// Maximum number of rotated files.
|
||||
/// Maximum total size of all log files combined in bytes.
|
||||
///
|
||||
/// If not set, there's no max limit, i.e. the number of log files is
|
||||
/// unlimited.
|
||||
max_files: Option<u64>,
|
||||
/// When the total size of all log files with the configured prefix and
|
||||
/// suffix exceeds this limit, the oldest files will be removed until
|
||||
/// the total is below the limit.
|
||||
///
|
||||
/// This is useful to prevent log files from consuming too much disk space
|
||||
/// over time, even with multiple rotated files.
|
||||
///
|
||||
/// Default: 10MB (10 * 1024 * 1024 bytes) if not specified.
|
||||
max_total_size_bytes: Option<u64>,
|
||||
|
||||
/// Maximum age of log files in seconds.
|
||||
///
|
||||
/// Log files older than this age will be automatically removed during
|
||||
/// cleanup. This is checked when the writer is created and during
|
||||
/// rotation operations.
|
||||
///
|
||||
/// Default: 1 week (7 * 24 * 60 * 60 seconds) if not specified.
|
||||
max_age_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, PartialOrd)]
|
||||
@@ -558,7 +599,7 @@ impl TracingConfiguration {
|
||||
}
|
||||
|
||||
// Log the log levels 🧠.
|
||||
tracing::info!(env_filter, "Logging has been set up");
|
||||
info!(env_filter, "Logging has been set up");
|
||||
|
||||
logging_ctx
|
||||
}
|
||||
@@ -0,0 +1,923 @@
|
||||
// Copyright 2026 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{
|
||||
fs::{self, File, OpenOptions},
|
||||
io::{self, Write},
|
||||
path::{Path, PathBuf},
|
||||
sync::Mutex,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use tracing_appender::rolling::Rotation;
|
||||
|
||||
/// Custom rolling file appender that supports time-based rotation and
|
||||
/// size-based cleanup.
|
||||
///
|
||||
/// This writer automatically manages log files with the following behavior:
|
||||
///
|
||||
/// # File Naming
|
||||
///
|
||||
/// Log files are named using the pattern: `{prefix}.{timestamp}.{suffix}`
|
||||
/// where the timestamp format depends on the rotation period:
|
||||
/// - `MINUTELY`: `YYYY-MM-DD-HH-MM`
|
||||
/// - `HOURLY`: `YYYY-MM-DD-HH`
|
||||
/// - `DAILY`: `YYYY-MM-DD`
|
||||
/// - `WEEKLY` or `NEVER`: `YYYY-Www` (ISO week number, e.g., `2024-W03`)
|
||||
///
|
||||
/// # Automatic Rotation
|
||||
///
|
||||
/// Files are rotated (a new file is created) when the configured time period
|
||||
/// changes. For example, with hourly rotation, a new file is created when the
|
||||
/// hour changes. Rotation is checked:
|
||||
/// - During writer initialization (creates/opens file for current period)
|
||||
/// - Before each write operation (only rotates if time period has changed)
|
||||
///
|
||||
/// If a log file already exists for the current time period, it will be
|
||||
/// reopened and appended to rather than creating a new file.
|
||||
///
|
||||
/// # Automatic Cleanup
|
||||
///
|
||||
/// The writer performs cleanup operations during initialization and rotation:
|
||||
/// - **Size limit enforcement**: When total size of all log files exceeds
|
||||
/// `max_total_size_bytes`, the oldest files are removed until under the limit
|
||||
/// - **Age-based cleanup**: Files older than `max_age_seconds` (based on
|
||||
/// filesystem modification time) are automatically removed
|
||||
/// - **File filtering**: Only files matching both the configured prefix and
|
||||
/// suffix are managed; other files in the directory are left untouched
|
||||
///
|
||||
/// # Side Effects on Creation
|
||||
///
|
||||
/// When `new()` is called, the following side effects occur:
|
||||
/// 1. Creates the log directory if it doesn't exist (including parent
|
||||
/// directories)
|
||||
/// 2. Creates or opens a log file for the current time period (appends if
|
||||
/// exists)
|
||||
/// 3. Performs cleanup of old files based on age (by filesystem mtime)
|
||||
/// 4. Enforces the total size limit by removing oldest files if needed
|
||||
///
|
||||
/// # Thread Safety
|
||||
///
|
||||
/// This writer is safe to use from multiple threads. Internal state is
|
||||
/// protected by a mutex, ensuring that file operations and rotations are
|
||||
/// properly synchronized.
|
||||
pub(super) struct SizeAndDateRollingWriter {
|
||||
config: WriterConfig,
|
||||
state: Mutex<Option<WriterState>>,
|
||||
}
|
||||
|
||||
/// Immutable configuration for the writer - shared without locks.
|
||||
///
|
||||
/// This struct contains all configuration parameters that remain constant
|
||||
/// throughout the writer's lifetime. Since these values never change, they
|
||||
/// can be safely shared across threads without synchronization.
|
||||
struct WriterConfig {
|
||||
/// Directory where log files are created
|
||||
base_path: PathBuf,
|
||||
/// Prefix for log file names (e.g., "app" results in "app.2024-01-15.log")
|
||||
file_prefix: String,
|
||||
/// Suffix for log file names (typically ".log")
|
||||
file_suffix: String,
|
||||
/// Time period for automatic rotation (MINUTELY, HOURLY, DAILY, WEEKLY, or
|
||||
/// NEVER which is treated as WEEKLY)
|
||||
rotation: Rotation,
|
||||
/// Maximum total size in bytes of all log files before cleanup
|
||||
max_total_size_bytes: u64,
|
||||
/// Maximum age in seconds before a log file is removed during cleanup
|
||||
max_age_seconds: u64,
|
||||
}
|
||||
|
||||
/// Mutable state that requires synchronization.
|
||||
///
|
||||
/// This struct contains the current log file handle and its path. These values
|
||||
/// change when rotation occurs, so access must be protected by a mutex to
|
||||
/// ensure thread safety.
|
||||
///
|
||||
/// The state is wrapped in `Option` because it needs to be temporarily taken
|
||||
/// during rotation operations to allow mutation while holding the lock.
|
||||
struct WriterState {
|
||||
/// The currently open log file handle for writing
|
||||
current_file: File,
|
||||
/// Path to the current log file (used to identify it during cleanup)
|
||||
current_path: PathBuf,
|
||||
}
|
||||
|
||||
impl SizeAndDateRollingWriter {
|
||||
/// Creates a new rolling writer with the specified configuration.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Directory where log files will be created. Will be created if
|
||||
/// it doesn't exist.
|
||||
/// * `file_prefix` - Prefix for log file names (e.g., "app")
|
||||
/// * `file_suffix` - Suffix for log file names (e.g., ".log")
|
||||
/// * `rotation` - Time period for rotation (MINUTELY, HOURLY, DAILY,
|
||||
/// WEEKLY, or NEVER which is treated as WEEKLY)
|
||||
/// * `max_total_size_bytes` - Maximum total size of all log files. When
|
||||
/// exceeded, oldest files are removed.
|
||||
/// * `max_age_seconds` - Maximum age of log files in seconds. Files older
|
||||
/// than this (by filesystem mtime) are removed during cleanup.
|
||||
///
|
||||
/// # Side Effects
|
||||
///
|
||||
/// This method performs several file system operations in order:
|
||||
/// 1. Creates the directory at `path` if it doesn't exist
|
||||
/// 2. Creates or reopens a log file for the current time period (appends if
|
||||
/// exists)
|
||||
/// 3. Scans the directory for existing log files matching the prefix/suffix
|
||||
/// 4. Removes files older than `max_age_seconds` (by filesystem mtime)
|
||||
/// 5. Removes oldest files if total size exceeds `max_total_size_bytes`
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - The directory cannot be created
|
||||
/// - The directory cannot be read
|
||||
/// - The log file cannot be created or opened
|
||||
/// - File metadata cannot be read during cleanup
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```ignore
|
||||
/// let writer = SizeAndDateRollingWriter::new(
|
||||
/// "/var/log/myapp",
|
||||
/// "app".to_owned(),
|
||||
/// ".log".to_owned(),
|
||||
/// Rotation::HOURLY,
|
||||
/// 100 * 1024 * 1024, // 100 MB
|
||||
/// 7 * 24 * 60 * 60, // 7 days
|
||||
/// )?;
|
||||
/// ```
|
||||
pub(super) fn new(
|
||||
path: impl AsRef<Path>,
|
||||
file_prefix: String,
|
||||
file_suffix: String,
|
||||
rotation: Rotation,
|
||||
max_total_size_bytes: u64,
|
||||
max_age_seconds: u64,
|
||||
) -> io::Result<Self> {
|
||||
let base_path = path.as_ref().to_path_buf();
|
||||
fs::create_dir_all(&base_path)?;
|
||||
|
||||
let config = WriterConfig {
|
||||
base_path,
|
||||
file_prefix,
|
||||
file_suffix,
|
||||
rotation,
|
||||
max_total_size_bytes,
|
||||
max_age_seconds,
|
||||
};
|
||||
|
||||
// Create initial state with first rotation
|
||||
let mut state = None;
|
||||
Self::rotate_internal(&config, &mut state, false)?;
|
||||
|
||||
Ok(Self { config, state: Mutex::new(state) })
|
||||
}
|
||||
|
||||
/// Extract the timestamp from the current filename.
|
||||
fn extract_timestamp_from_path(config: &WriterConfig, current_path: &Path) -> Option<String> {
|
||||
let filename = current_path.file_name()?.to_str()?;
|
||||
|
||||
// Strip prefix and suffix to get the timestamp
|
||||
// Format: "prefix.timestamp.suffix"
|
||||
let without_prefix = filename.strip_prefix(&format!("{}.", config.file_prefix))?;
|
||||
let timestamp = without_prefix.strip_suffix(&config.file_suffix)?;
|
||||
|
||||
Some(timestamp.to_owned())
|
||||
}
|
||||
|
||||
/// Check if rotation is needed based on time period change.
|
||||
fn should_rotate_by_time(config: &WriterConfig, current_path: &Path) -> bool {
|
||||
let current_time = Self::format_rotation_timestamp(config);
|
||||
let last_rotation_time = Self::extract_timestamp_from_path(config, current_path);
|
||||
|
||||
// If we can't extract the timestamp, assume rotation is needed
|
||||
match last_rotation_time {
|
||||
Some(last_time) => current_time != last_time,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the current time as a timestamp string for the rotation period.
|
||||
///
|
||||
/// Returns a timestamp string based on the configured rotation period
|
||||
/// (e.g., "2024-01-15-14" for hourly rotation).
|
||||
fn format_rotation_timestamp(config: &WriterConfig) -> String {
|
||||
let now = chrono::Local::now();
|
||||
match config.rotation {
|
||||
Rotation::MINUTELY => now.format("%Y-%m-%d-%H-%M").to_string(),
|
||||
Rotation::HOURLY => now.format("%Y-%m-%d-%H").to_string(),
|
||||
Rotation::DAILY => now.format("%Y-%m-%d").to_string(),
|
||||
Rotation::WEEKLY | Rotation::NEVER => now.format("%Y-W%W").to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Rotate the log file, creating a new file with a timestamp-based name.
|
||||
///
|
||||
/// If `check_conditions` is true, rotation only happens if time or size
|
||||
/// thresholds are met. Otherwise, rotation is forced.
|
||||
///
|
||||
/// This method also handles initial state creation when called with None
|
||||
/// state.
|
||||
fn rotate_internal(
|
||||
config: &WriterConfig,
|
||||
state: &mut Option<WriterState>,
|
||||
check_conditions: bool,
|
||||
) -> io::Result<()> {
|
||||
// Check if rotation is needed (skip for uninitialized state)
|
||||
if check_conditions {
|
||||
if let Some(state) = state.as_ref() {
|
||||
if !Self::should_rotate_by_time(config, &state.current_path) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let time_str = Self::format_rotation_timestamp(config);
|
||||
|
||||
// Generate filename with timestamp
|
||||
let filename = format!("{}.{}{}", config.file_prefix, time_str, config.file_suffix);
|
||||
let new_path = config.base_path.join(filename);
|
||||
|
||||
// Open or create file in append mode
|
||||
let new_file = OpenOptions::new().create(true).append(true).open(&new_path)?;
|
||||
|
||||
let new_state = WriterState { current_file: new_file, current_path: new_path };
|
||||
|
||||
// Clean up logs older than configured max age
|
||||
Self::trim_old_logs_internal(config, &new_state)?;
|
||||
|
||||
// Enforce total size limit by removing oldest files if needed
|
||||
Self::enforce_total_size_limit_internal(config, &new_state)?;
|
||||
|
||||
*state = Some(new_state);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all log files matching our prefix and suffix, sorted by modification
|
||||
/// time.
|
||||
///
|
||||
/// Returns a vector of (path, modification_time) tuples, oldest first.
|
||||
fn get_matching_log_files(config: &WriterConfig) -> io::Result<Vec<(PathBuf, SystemTime)>> {
|
||||
let mut files: Vec<_> = fs::read_dir(&config.base_path)?
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let filename = path.file_name()?.to_str()?;
|
||||
// Only process files matching our prefix and suffix
|
||||
if filename.starts_with(&config.file_prefix)
|
||||
&& filename.ends_with(&config.file_suffix)
|
||||
{
|
||||
let metadata = fs::metadata(&path).ok()?;
|
||||
let modified = metadata.modified().ok()?;
|
||||
return Some((path, modified));
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Sort by modification time (oldest first)
|
||||
files.sort_by_key(|(_, modified)| *modified);
|
||||
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
/// Remove all log files older than the configured max age.
|
||||
///
|
||||
/// Only files matching the configured prefix and suffix are removed.
|
||||
/// Other files in the directory are left alone.
|
||||
fn trim_old_logs_internal(config: &WriterConfig, state: &WriterState) -> io::Result<()> {
|
||||
let now = SystemTime::now();
|
||||
let files = Self::get_matching_log_files(config)?;
|
||||
|
||||
for (path, modified) in files {
|
||||
// Skip the current file
|
||||
if path == state.current_path {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if file is older than max age
|
||||
if let Ok(duration) = now.duration_since(modified) {
|
||||
if duration.as_secs() > config.max_age_seconds {
|
||||
let _ = fs::remove_file(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Enforce total size limit across all log files.
|
||||
///
|
||||
/// If the total size of all matching log files exceeds
|
||||
/// max_total_size_bytes, remove the oldest files until the total is
|
||||
/// below the limit.
|
||||
fn enforce_total_size_limit_internal(
|
||||
config: &WriterConfig,
|
||||
state: &WriterState,
|
||||
) -> io::Result<()> {
|
||||
let max_total = config.max_total_size_bytes;
|
||||
|
||||
let files = Self::get_matching_log_files(config)?;
|
||||
|
||||
// Calculate total size of all log files
|
||||
let mut total_size: u64 = 0;
|
||||
for (path, _) in &files {
|
||||
if let Ok(metadata) = fs::metadata(path) {
|
||||
total_size += metadata.len();
|
||||
}
|
||||
}
|
||||
|
||||
// If under limit, nothing to do
|
||||
if total_size <= max_total {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Remove oldest files until we're under the limit
|
||||
// Files are already sorted by modification time (oldest first)
|
||||
for (path, _) in files {
|
||||
// Don't remove the current file
|
||||
if path == state.current_path {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Ok(metadata) = fs::metadata(&path) {
|
||||
let file_size = metadata.len();
|
||||
let _ = fs::remove_file(&path);
|
||||
total_size = total_size.saturating_sub(file_size);
|
||||
|
||||
// Check if we're now under the limit
|
||||
if total_size <= max_total {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for SizeAndDateRollingWriter {
|
||||
type Writer = SizeAndDateRollingWriterHandle<'a>;
|
||||
|
||||
fn make_writer(&'a self) -> Self::Writer {
|
||||
SizeAndDateRollingWriterHandle {
|
||||
config: &self.config,
|
||||
state: &self.state,
|
||||
_phantom: std::marker::PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) struct SizeAndDateRollingWriterHandle<'a> {
|
||||
config: &'a WriterConfig,
|
||||
state: &'a Mutex<Option<WriterState>>,
|
||||
_phantom: std::marker::PhantomData<&'a ()>,
|
||||
}
|
||||
|
||||
impl<'a> Write for SizeAndDateRollingWriterHandle<'a> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
|
||||
// Check if rotation is needed
|
||||
SizeAndDateRollingWriter::rotate_internal(self.config, &mut state, true)?;
|
||||
|
||||
// Write to file (state must be initialized after rotation)
|
||||
state.as_mut().unwrap().current_file.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
if let Some(s) = state.as_mut() {
|
||||
s.current_file.flush()
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SizeAndDateRollingWriter {
|
||||
/// Manually trigger log rotation if conditions are met.
|
||||
///
|
||||
/// This will rotate the current log file if the time period has changed.
|
||||
fn roll(&self) -> io::Result<()> {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
Self::rotate_internal(&self.config, &mut state, true)
|
||||
}
|
||||
|
||||
/// Manually trigger cleanup of old log files.
|
||||
///
|
||||
/// This removes all log files older than the configured max age, keeping
|
||||
/// only files that match the configured prefix and suffix.
|
||||
fn trim(&self) -> io::Result<()> {
|
||||
let state = self.state.lock().unwrap();
|
||||
if let Some(ref state) = *state {
|
||||
Self::trim_old_logs_internal(&self.config, state)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::io::Write;
|
||||
|
||||
use tempfile::tempdir;
|
||||
use tracing_subscriber::fmt::MakeWriter;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_rotation_file_naming() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"app".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check file naming pattern
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
let filename = path.file_name()?.to_str()?.to_owned();
|
||||
if filename.starts_with("app") && filename.ends_with(".log") {
|
||||
Some(filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Should have one file with proper naming
|
||||
assert_eq!(log_files.len(), 1, "Expected exactly one log file");
|
||||
|
||||
// Check that files follow the expected naming pattern
|
||||
for filename in &log_files {
|
||||
assert!(
|
||||
filename.starts_with("app."),
|
||||
"Filename should start with prefix: {}",
|
||||
filename
|
||||
);
|
||||
assert!(filename.ends_with(".log"), "Filename should end with suffix: {}", filename);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefix_suffix_filtering() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create some unrelated files
|
||||
std::fs::write(log_path.join("other.txt"), "unrelated").unwrap();
|
||||
std::fs::write(log_path.join("app.txt"), "wrong suffix").unwrap();
|
||||
std::fs::write(log_path.join("test.log"), "wrong prefix").unwrap();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"app".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"test message\n").unwrap();
|
||||
handle.flush().unwrap();
|
||||
|
||||
// Trigger manual trim
|
||||
writer.trim().unwrap();
|
||||
|
||||
// Check that unrelated files still exist
|
||||
assert!(log_path.join("other.txt").exists(), "Unrelated file should not be removed");
|
||||
assert!(log_path.join("app.txt").exists(), "File with wrong suffix should not be removed");
|
||||
assert!(log_path.join("test.log").exists(), "File with wrong prefix should not be removed");
|
||||
|
||||
// App log file should still exist
|
||||
let app_logs: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let filename = entry.file_name().to_str()?.to_owned();
|
||||
if filename.starts_with("app.") && filename.ends_with(".log") {
|
||||
Some(filename)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert_eq!(app_logs.len(), 1, "Expected one app log file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_manual_roll_and_trim() {
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"manual".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Manual roll should work
|
||||
assert!(writer.roll().is_ok(), "Manual roll should succeed");
|
||||
|
||||
// Manual trim should work
|
||||
assert!(writer.trim().is_ok(), "Manual trim should succeed");
|
||||
|
||||
// Should still have log file
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("manual") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
assert!(!log_files.is_empty(), "Should have at least one log file");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_size_limit_removes_oldest_files() {
|
||||
// This test verifies that when the total size of all log files exceeds
|
||||
// max_total_size_bytes, the oldest files are removed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Manually create several log files with different timestamps (alphabetically
|
||||
// sorted = oldest first) Total of 240 bytes
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-00.log"), "x".repeat(80)).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-01.log"), "y".repeat(80)).unwrap();
|
||||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||||
std::fs::write(log_path.join("total.2024-01-01-10-02.log"), "z".repeat(80)).unwrap();
|
||||
|
||||
let count_files = || {
|
||||
std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("total") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.count()
|
||||
};
|
||||
|
||||
assert_eq!(count_files(), 3, "Should have 3 log files");
|
||||
|
||||
// Now create a new writer with 200 byte total limit
|
||||
// Current total is 240 bytes. The writer will:
|
||||
// 1. Create a new file with current timestamp
|
||||
// 2. See total exceeds 200 bytes
|
||||
// 3. Remove oldest file(s) until under limit
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"total".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::MINUTELY,
|
||||
200, // 200 byte total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// After writer creation, we should still have 3 files:
|
||||
// - Oldest file (10-00) was removed
|
||||
// - Two middle files (10-01, 10-02) remain
|
||||
// - New current file was created
|
||||
let remaining_files = count_files();
|
||||
assert_eq!(
|
||||
remaining_files, 3,
|
||||
"Should have 3 files: 2 old files + 1 new current file, but have {}",
|
||||
remaining_files
|
||||
);
|
||||
|
||||
// Calculate total size of remaining files
|
||||
let total_size: u64 = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("total") {
|
||||
Some(std::fs::metadata(&path).ok()?.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
// Total should be 160 bytes (80 + 80 + 0 for new file)
|
||||
assert!(total_size <= 200, "Total size should be under 200 bytes, but is {}", total_size);
|
||||
|
||||
// Verify the oldest file was removed by checking filenames
|
||||
let filenames: Vec<String> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() {
|
||||
path.file_name()?.to_str().map(|s| s.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert!(
|
||||
!filenames.contains(&"total.2024-01-01-10-00.log".to_owned()),
|
||||
"Oldest file should have been removed"
|
||||
);
|
||||
assert!(
|
||||
filenames.iter().any(|f| f.starts_with("total.2024-01-01-10-01")),
|
||||
"Second file should still exist"
|
||||
);
|
||||
assert!(
|
||||
filenames.iter().any(|f| f.starts_with("total.2024-01-01-10-02")),
|
||||
"Third file should still exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_based_rotation_logic() {
|
||||
// Test that the rotation logic correctly identifies when rotation is needed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"time".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write some data
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"initial log entry\n").unwrap();
|
||||
handle.flush().unwrap();
|
||||
|
||||
// Verify initial state - should have created one file
|
||||
let initial_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("time") {
|
||||
Some(path.file_name()?.to_str()?.to_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(initial_files.len(), 1, "Should have one log file initially");
|
||||
|
||||
// Verify the file contains our data
|
||||
let file_content = std::fs::read_to_string(log_path.join(&initial_files[0])).unwrap();
|
||||
assert!(file_content.contains("initial log entry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_old_files_cleanup() {
|
||||
// Test that files older than one week are removed
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create files with timestamps more than a week old
|
||||
// We can't easily manipulate file mtimes without external crates,
|
||||
// but we can verify the cleanup logic doesn't fail
|
||||
std::fs::write(log_path.join("old.2020-01-01-10-00.log"), "old data").unwrap();
|
||||
|
||||
// Create a writer which will trigger cleanup
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"old".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// The old file should still exist because we can't manipulate mtime easily
|
||||
// But we verify that cleanup doesn't crash
|
||||
writer.trim().unwrap();
|
||||
|
||||
// Verify we can still write
|
||||
let mut handle = writer.make_writer();
|
||||
handle.write_all(b"new data").unwrap();
|
||||
handle.flush().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_operations() {
|
||||
// Test that writing works correctly across multiple writes
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
let writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"write".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::HOURLY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Write multiple times
|
||||
for i in 0..5 {
|
||||
let mut handle = writer.make_writer();
|
||||
let message = format!("Log entry {}\n", i);
|
||||
handle.write_all(message.as_bytes()).unwrap();
|
||||
handle.flush().unwrap();
|
||||
}
|
||||
|
||||
// Verify all writes went to the same file (same hour)
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("write") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(log_files.len(), 1, "Should have one log file for same time period");
|
||||
|
||||
// Verify content
|
||||
let content = std::fs::read_to_string(&log_files[0]).unwrap();
|
||||
for i in 0..5 {
|
||||
assert!(content.contains(&format!("Log entry {}", i)));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_reopening_existing_file() {
|
||||
// Test that reopening appends to existing file within same time period
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create first writer and write
|
||||
let writer1 = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"reopen".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle1 = writer1.make_writer();
|
||||
handle1.write_all(b"first write\n").unwrap();
|
||||
handle1.flush().unwrap();
|
||||
drop(writer1);
|
||||
|
||||
// Create second writer (simulating restart within same day)
|
||||
let writer2 = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"reopen".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::DAILY,
|
||||
10 * 1024 * 1024, // 10MB total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let mut handle2 = writer2.make_writer();
|
||||
handle2.write_all(b"second write\n").unwrap();
|
||||
handle2.flush().unwrap();
|
||||
|
||||
// Should still have only one file
|
||||
let log_files: Vec<_> = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("reopen") {
|
||||
Some(path)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(log_files.len(), 1, "Should have one log file");
|
||||
|
||||
// Verify both writes are in the file
|
||||
let content = std::fs::read_to_string(&log_files[0]).unwrap();
|
||||
assert!(content.contains("first write"));
|
||||
assert!(content.contains("second write"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_total_size_limit_with_multiple_old_files() {
|
||||
// Test that multiple old files are removed until under limit
|
||||
let temp_dir = tempdir().unwrap();
|
||||
let log_path = temp_dir.path();
|
||||
|
||||
// Create 5 files, each 50 bytes (total 250 bytes)
|
||||
for i in 0..5 {
|
||||
let filename = format!("multi.2024-01-01-10-0{}.log", i);
|
||||
std::fs::write(log_path.join(filename), "x".repeat(50)).unwrap();
|
||||
}
|
||||
|
||||
let count_files = || {
|
||||
std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter(|entry| {
|
||||
if let Ok(entry) = entry {
|
||||
if let Some(name) = entry.file_name().to_str() {
|
||||
return name.starts_with("multi");
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
.count()
|
||||
};
|
||||
|
||||
assert_eq!(count_files(), 5, "Should start with 5 files");
|
||||
|
||||
// Create writer with 100 byte limit (should keep only 2 old files + new
|
||||
// current)
|
||||
let _writer = SizeAndDateRollingWriter::new(
|
||||
log_path,
|
||||
"multi".to_owned(),
|
||||
".log".to_owned(),
|
||||
Rotation::MINUTELY,
|
||||
100, // 100 byte total size limit
|
||||
7 * 24 * 60 * 60, // 1 week in seconds
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Should have removed oldest files
|
||||
let remaining = count_files();
|
||||
assert!(remaining <= 3, "Should have removed multiple old files to stay under limit");
|
||||
|
||||
// Verify total size is under limit
|
||||
let total_size: u64 = std::fs::read_dir(log_path)
|
||||
.unwrap()
|
||||
.filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
let path = entry.path();
|
||||
if path.is_file() && path.file_name()?.to_str()?.starts_with("multi") {
|
||||
Some(std::fs::metadata(&path).ok()?.len())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.sum();
|
||||
|
||||
assert!(total_size <= 100, "Total size should be under 100 bytes, but is {}", total_size);
|
||||
}
|
||||
}
|
||||
+14
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(feature = "sentry")]
|
||||
use std::borrow::ToOwned;
|
||||
use std::{
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::authentication::oauth::{
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{collections::HashMap, fs, path::PathBuf, pin::pin, sync::Arc};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk::{EncryptionState, RoomState};
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk::RoomDisplayName;
|
||||
|
||||
/// Verifies the passed `String` matches the expected room alias format:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
// Copyright 2024 Mauro Romito
|
||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@@ -10,7 +9,7 @@
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::{fmt::Debug, sync::Arc};
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
use std::{fmt::Debug, mem::MaybeUninit, ptr::addr_of_mut, sync::Arc, time::Duration};
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use anyhow::Context as _;
|
||||
use matrix_sdk::{room_preview::RoomPreview as SdkRoomPreview, Client};
|
||||
use ruma::room::{JoinRuleSummary, RoomType as RumaRoomType};
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use futures_util::StreamExt;
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[cfg(feature = "sqlite")]
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use matrix_sdk_common::executor::JoinHandle;
|
||||
use tracing::debug;
|
||||
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use matrix_sdk_ui::timeline::{
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
// Copyright 2025 The Matrix.org Foundation C.I.C.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for that specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use language_tags::LanguageTag;
|
||||
|
||||
Reference in New Issue
Block a user