diff --git a/po/POTFILES b/po/POTFILES
index 4e446aaf40..fa769a8a95 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -122,6 +122,7 @@ src/locking/lock_driver_lockd.c
src/locking/lock_driver_sanlock.c
src/locking/lock_manager.c
src/locking/sanlock_helper.c
+src/logging/log_cleaner.c
src/logging/log_daemon.c
src/logging/log_daemon_dispatch.c
src/logging/log_handler.c
diff --git a/src/logging/log_cleaner.c b/src/logging/log_cleaner.c
new file mode 100644
index 0000000000..bb8f719f1b
--- /dev/null
+++ b/src/logging/log_cleaner.c
@@ -0,0 +1,269 @@
+/*
+ * log_cleaner.c: cleans obsolete log files
+ *
+ * Copyright (C) 2022 Virtuozzo International GmbH
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; If not, see
+ * .
+ */
+
+#include
+
+#include
+
+#include "log_cleaner.h"
+#include "log_handler.h"
+
+#include "virerror.h"
+#include "virobject.h"
+#include "virfile.h"
+#include "viralloc.h"
+#include "virlog.h"
+#include "virrotatingfile.h"
+#include "virstring.h"
+
+#define VIR_FROM_THIS VIR_FROM_LOGGING
+
+VIR_LOG_INIT("logging.log_cleaner");
+
+/* Cleanup log root (/var/log/libvirt) and all subfolders (e.g. /var/log/libvirt/qemu) */
+#define CLEANER_LOG_DEPTH 1
+#define CLEANER_LOG_TIMEOUT_MS (24 * 3600 * 1000) /* One day */
+#define MAX_TIME ((time_t) G_MAXINT64)
+
+static GRegex *log_regex;
+
+typedef struct _virLogCleanerChain virLogCleanerChain;
+struct _virLogCleanerChain {
+ int rotated_max_index;
+ time_t last_modified;
+};
+
+typedef struct _virLogCleanerData virLogCleanerData;
+struct _virLogCleanerData {
+ virLogHandler *handler;
+ time_t oldest_to_keep;
+ GHashTable *chains;
+};
+
+static char *
+virLogCleanerParseFilename(const char *path,
+ int *rotated_index)
+{
+ g_autoptr(GMatchInfo) matchInfo = NULL;
+ g_autofree char *rotated_index_str = NULL;
+ g_autofree char *clear_path = NULL;
+ char *chain_prefix = NULL;
+
+ clear_path = realpath(path, NULL);
+ if (!clear_path) {
+ VIR_WARN("Failed to resolve path %s: %s", path, g_strerror(errno));
+ return NULL;
+ }
+
+ if (!g_regex_match(log_regex, path, 0, &matchInfo))
+ return NULL;
+
+ chain_prefix = g_match_info_fetch(matchInfo, 1);
+ if (!rotated_index)
+ return chain_prefix;
+
+ *rotated_index = 0;
+ rotated_index_str = g_match_info_fetch(matchInfo, 3);
+
+ if (!rotated_index_str)
+ return chain_prefix;
+
+ if (virStrToLong_i(rotated_index_str, NULL, 10, rotated_index) < 0) {
+ virReportError(VIR_ERR_INTERNAL_ERROR,
+ _("Failed to parse rotated index from '%s'"),
+ rotated_index_str);
+ return NULL;
+ }
+ return chain_prefix;
+}
+
+static void
+virLogCleanerDeleteFile(const char *path)
+{
+ if (unlink(path) < 0 && errno != ENOENT)
+ VIR_WARN("Unable to delete %s: %s", path, g_strerror(errno));
+}
+
+static void
+virLogCleanerProcessFile(virLogCleanerData *data,
+ const char *path,
+ struct stat *sb)
+{
+ int rotated_index = 0;
+ g_autofree char *chain_prefix = NULL;
+ virLogCleanerChain *chain;
+
+ if (!S_ISREG(sb->st_mode))
+ return;
+
+ chain_prefix = virLogCleanerParseFilename(path, &rotated_index);
+
+ if (!chain_prefix)
+ return;
+
+ if (rotated_index > data->handler->config->max_backups) {
+ virLogCleanerDeleteFile(path);
+ return;
+ }
+
+ chain = g_hash_table_lookup(data->chains, chain_prefix);
+
+ if (!chain) {
+ chain = g_new0(virLogCleanerChain, 1);
+ g_hash_table_insert(data->chains, g_steal_pointer(&chain_prefix), chain);
+ }
+
+ chain->last_modified = MAX(chain->last_modified, sb->st_mtime);
+ chain->rotated_max_index = MAX(chain->rotated_max_index,
+ rotated_index);
+}
+
+static GHashTable *
+virLogCleanerCreateTable(virLogHandler *handler)
+{
+ /* HashTable: (const char*) chain_prefix -> (virLogCleanerChain*) chain */
+ GHashTable *chains = g_hash_table_new_full(g_str_hash, g_str_equal,
+ g_free, g_free);
+ size_t i;
+ virLogHandlerLogFile *file;
+ char *chain_prefix;
+ virLogCleanerChain *chain;
+ VIR_LOCK_GUARD lock = virObjectLockGuard(handler);
+
+ for (i = 0; i < handler->nfiles; i++) {
+ file = handler->files[i];
+ chain_prefix = virLogCleanerParseFilename(virRotatingFileWriterGetPath(file->file),
+ NULL);
+ if (!chain_prefix)
+ continue;
+
+ chain = g_new0(virLogCleanerChain, 1);
+ chain->last_modified = MAX_TIME; /* Here we set MAX_TIME to the currently
+ * opened files to prevent its deletion. */
+ g_hash_table_insert(chains, chain_prefix, chain);
+ }
+
+ return chains;
+}
+
+static void
+virLogCleanerProcessFolder(virLogCleanerData *data,
+ const char *path,
+ int depth_left)
+{
+ DIR *dir;
+ struct dirent *entry;
+ struct stat sb;
+
+ if (virDirOpenIfExists(&dir, path) < 0)
+ return;
+
+ while (virDirRead(dir, &entry, path) > 0) {
+ g_autofree char *newpath = g_strdup_printf("%s/%s", path, entry->d_name);
+
+ if (stat(newpath, &sb) < 0) {
+ VIR_WARN("Unable to stat %s: %s", newpath, g_strerror(errno));
+ continue;
+ }
+
+ if (S_ISDIR(sb.st_mode)) {
+ if (depth_left > 0)
+ virLogCleanerProcessFolder(data, newpath, depth_left - 1);
+ continue;
+ }
+
+ virLogCleanerProcessFile(data, newpath, &sb);
+ }
+
+ virDirClose(dir);
+}
+
+static void
+virLogCleanerChainCB(gpointer key,
+ gpointer value,
+ gpointer user_data)
+{
+ char *chain_prefix = key;
+ virLogCleanerChain *chain = value;
+ virLogCleanerData *data = user_data;
+ g_autofree char *path = NULL;
+ size_t i;
+
+ if (chain->last_modified > data->oldest_to_keep)
+ return;
+
+ path = g_strdup_printf("%s.log", chain_prefix);
+ virLogCleanerDeleteFile(path);
+
+ for (i = 0; i <= chain->rotated_max_index; i++) {
+ g_autofree char *rotated_path = g_strdup_printf("%s.%zu", path, i);
+
+ virLogCleanerDeleteFile(rotated_path);
+ }
+}
+
+static void
+virLogCleanerTimer(int timer G_GNUC_UNUSED, void *opaque)
+{
+ virLogHandler *handler = opaque;
+ virLogCleanerData data = {
+ .handler = handler,
+ .oldest_to_keep = time(NULL) - 3600 * 24 * handler->config->max_age_days,
+ .chains = virLogCleanerCreateTable(handler),
+ };
+
+ /* First prepare the hashmap of chains to delete */
+ virLogCleanerProcessFolder(&data,
+ handler->config->log_root,
+ CLEANER_LOG_DEPTH);
+ g_hash_table_foreach(data.chains, virLogCleanerChainCB, &data);
+ g_hash_table_destroy(data.chains);
+}
+
+int
+virLogCleanerInit(virLogHandler *handler)
+{
+ if (handler->config->max_age_days <= 0)
+ return 0;
+
+ log_regex = g_regex_new("^(.*)\\.log(\\.(\\d+))?$", 0, 0, NULL);
+ if (!log_regex) {
+ virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+ _("Unable to compile regex"));
+ return -1;
+ }
+
+ handler->cleanup_log_timer = virEventAddTimeout(CLEANER_LOG_TIMEOUT_MS,
+ virLogCleanerTimer,
+ handler, NULL);
+ return handler->cleanup_log_timer;
+}
+
+void
+virLogCleanerShutdown(virLogHandler *handler)
+{
+ if (handler->cleanup_log_timer != -1) {
+ virEventRemoveTimeout(handler->cleanup_log_timer);
+ handler->cleanup_log_timer = -1;
+ }
+
+ g_regex_unref(log_regex);
+ log_regex = NULL;
+}
diff --git a/src/logging/log_cleaner.h b/src/logging/log_cleaner.h
new file mode 100644
index 0000000000..1a9a94b68d
--- /dev/null
+++ b/src/logging/log_cleaner.h
@@ -0,0 +1,29 @@
+/*
+ * log_cleaner.h: cleans obsolete log files
+ *
+ * Copyright (C) 2022 Virtuozzo International GmbH
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; If not, see
+ * .
+ */
+
+#pragma once
+
+#include "log_handler.h"
+
+int
+virLogCleanerInit(virLogHandler *handler);
+
+void
+virLogCleanerShutdown(virLogHandler *handler);
diff --git a/src/logging/log_handler.h b/src/logging/log_handler.h
index d473a19fc6..97dad27eda 100644
--- a/src/logging/log_handler.h
+++ b/src/logging/log_handler.h
@@ -48,6 +48,8 @@ struct _virLogHandler {
bool privileged;
virLogDaemonConfig *config;
+ int cleanup_log_timer;
+
virLogHandlerLogFile **files;
size_t nfiles;
diff --git a/src/logging/meson.build b/src/logging/meson.build
index 7066f16fad..fa6af50fa6 100644
--- a/src/logging/meson.build
+++ b/src/logging/meson.build
@@ -30,6 +30,7 @@ log_daemon_sources = files(
'log_daemon_config.c',
'log_daemon_dispatch.c',
'log_handler.c',
+ 'log_cleaner.c',
)
if conf.has('WITH_REMOTE')