all lists on lists.proxmox.com
 help / color / mirror / Atom feed
From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [pdm-devel] [PATCH datacenter-manager 2/2] server: add task log, auth log and access log rotation
Date: Thu, 27 Nov 2025 16:37:08 +0100	[thread overview]
Message-ID: <20251127153708.393210-3-l.wagner@proxmox.com> (raw)
In-Reply-To: <20251127153708.393210-1-l.wagner@proxmox.com>

This adds a periodic task that runs once a day at midnight. This task
will rotate the task log archive, the auth log and also the access log
file.

Copied from PBS and adapted as needed. We don't have a place yet to
configure the 'max_days' parameter for rotating the task log archive, so
this one is always set to `None` at the moment.

Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
 server/src/bin/proxmox-datacenter-api/main.rs |   2 +-
 .../proxmox-datacenter-api/tasks/logrotate.rs | 195 ++++++++++++++++++
 .../bin/proxmox-datacenter-api/tasks/mod.rs   |   2 +
 .../bin/proxmox-datacenter-privileged-api.rs  |   5 +-
 4 files changed, 201 insertions(+), 3 deletions(-)
 create mode 100644 server/src/bin/proxmox-datacenter-api/tasks/logrotate.rs

diff --git a/server/src/bin/proxmox-datacenter-api/main.rs b/server/src/bin/proxmox-datacenter-api/main.rs
index 0e80c829..7459e68f 100644
--- a/server/src/bin/proxmox-datacenter-api/main.rs
+++ b/server/src/bin/proxmox-datacenter-api/main.rs
@@ -426,9 +426,9 @@ async fn run_task_scheduler() {
 
 async fn schedule_tasks() -> Result<(), Error> {
     // TODO: move out to own module, refactor PBS stuff for reuse & then add:
-    // - task log rotation
     // - stats (rrd) collection
     // - ...?
+    tasks::logrotate::schedule_task_log_rotate().await;
 
     Ok(())
 }
diff --git a/server/src/bin/proxmox-datacenter-api/tasks/logrotate.rs b/server/src/bin/proxmox-datacenter-api/tasks/logrotate.rs
new file mode 100644
index 00000000..6afb069b
--- /dev/null
+++ b/server/src/bin/proxmox-datacenter-api/tasks/logrotate.rs
@@ -0,0 +1,195 @@
+use anyhow::{format_err, Error};
+
+use proxmox_lang::try_block;
+use proxmox_rest_server::WorkerTask;
+use proxmox_sys::logrotate::LogRotate;
+use proxmox_time::CalendarEvent;
+
+use pdm_api_types::Authid;
+use server::jobstate::{self, Job, JobState};
+
+/// Rotate task logs, auth logs and access logs.
+///
+/// This task runs every day at midnight, except when it has never run before, then it runs
+/// immediately.
+pub async fn schedule_task_log_rotate() {
+    let worker_type = "logrotate";
+    let job_id = "access-log_and_task-archive";
+
+    // schedule daily at 00:00 like normal logrotate
+    let schedule = "00:00";
+
+    if !check_schedule(worker_type, schedule, job_id) {
+        // if we never ran the rotation, schedule instantly
+        match JobState::load(worker_type, job_id) {
+            Ok(JobState::Created { .. }) => {}
+            _ => return,
+        }
+    }
+
+    let mut job = match Job::new(worker_type, job_id) {
+        Ok(job) => job,
+        Err(_) => return, // could not get lock
+    };
+
+    if let Err(err) = WorkerTask::new_thread(
+        worker_type,
+        None,
+        Authid::root_auth_id().to_string(),
+        false,
+        move |worker| {
+            job.start(&worker.upid().to_string())?;
+            proxmox_log::info!("starting task log rotation");
+
+            let result = try_block!({
+                let max_size = 512 * 1024 - 1; // an entry has ~ 100b, so > 5000 entries/file
+                let max_files = 20; // times twenty files gives > 100000 task entries
+
+                // TODO: Make this configurable
+                let max_days = None;
+
+                let options = proxmox_product_config::default_create_options();
+
+                let has_rotated = proxmox_rest_server::rotate_task_log_archive(
+                    max_size,
+                    true,
+                    Some(max_files),
+                    max_days,
+                    Some(options),
+                )?;
+
+                if has_rotated {
+                    log::info!("task log archive was rotated");
+                } else {
+                    log::info!("task log archive was not rotated");
+                }
+
+                let max_size = 32 * 1024 * 1024 - 1;
+                let max_files = 14;
+
+                let mut logrotate = LogRotate::new(
+                    pdm_buildcfg::API_ACCESS_LOG_FN,
+                    true,
+                    Some(max_files),
+                    Some(options),
+                )?;
+
+                if logrotate.rotate(max_size)? {
+                    println!("rotated access log, telling daemons to re-open log file");
+                    proxmox_async::runtime::block_on(command_reopen_access_logfiles())?;
+                    log::info!("API access log was rotated");
+                } else {
+                    log::info!("API access log was not rotated");
+                }
+
+                let mut logrotate = LogRotate::new(
+                    pdm_buildcfg::API_AUTH_LOG_FN,
+                    true,
+                    Some(max_files),
+                    Some(options),
+                )?;
+
+                if logrotate.rotate(max_size)? {
+                    println!("rotated auth log, telling daemons to re-open log file");
+                    proxmox_async::runtime::block_on(command_reopen_auth_logfiles())?;
+                    log::info!("API authentication log was rotated");
+                } else {
+                    log::info!("API authentication log was not rotated");
+                }
+
+                if has_rotated {
+                    log::info!("cleaning up old task logs");
+                    if let Err(err) = proxmox_rest_server::cleanup_old_tasks(true) {
+                        log::warn!("could not completely cleanup old tasks: {err}");
+                    }
+                }
+
+                Ok(())
+            });
+
+            let status = worker.create_state(&result);
+
+            if let Err(err) = job.finish(status) {
+                eprintln!("could not finish job state for {worker_type}: {err}");
+            }
+
+            result
+        },
+    ) {
+        eprintln!("unable to start task log rotation: {err}");
+    }
+}
+
+async fn command_reopen_access_logfiles() -> Result<(), Error> {
+    // only care about the most recent daemon instance for each, proxy & api, as other older ones
+    // should not respond to new requests anyway, but only finish their current one and then exit.
+    let sock = proxmox_daemon::command_socket::this_path();
+    let f1 =
+        proxmox_daemon::command_socket::send_raw(sock, "{\"command\":\"api-access-log-reopen\"}\n");
+
+    let pid = proxmox_rest_server::read_pid(pdm_buildcfg::PDM_API_PID_FN)?;
+    let sock = proxmox_daemon::command_socket::path_from_pid(pid);
+    let f2 =
+        proxmox_daemon::command_socket::send_raw(sock, "{\"command\":\"api-access-log-reopen\"}\n");
+
+    match futures::join!(f1, f2) {
+        (Err(e1), Err(e2)) => Err(format_err!(
+            "reopen commands failed, proxy: {e1}; api: {e2}"
+        )),
+        (Err(e1), Ok(_)) => Err(format_err!("reopen commands failed, proxy: {e1}")),
+        (Ok(_), Err(e2)) => Err(format_err!("reopen commands failed, api: {e2}")),
+        _ => Ok(()),
+    }
+}
+
+async fn command_reopen_auth_logfiles() -> Result<(), Error> {
+    // only care about the most recent daemon instance for each, proxy & api, as other older ones
+    // should not respond to new requests anyway, but only finish their current one and then exit.
+    let sock = proxmox_daemon::command_socket::this_path();
+    let f1 =
+        proxmox_daemon::command_socket::send_raw(sock, "{\"command\":\"api-auth-log-reopen\"}\n");
+
+    let pid = proxmox_rest_server::read_pid(pdm_buildcfg::PDM_API_PID_FN)?;
+    let sock = proxmox_daemon::command_socket::path_from_pid(pid);
+    let f2 =
+        proxmox_daemon::command_socket::send_raw(sock, "{\"command\":\"api-auth-log-reopen\"}\n");
+
+    match futures::join!(f1, f2) {
+        (Err(e1), Err(e2)) => Err(format_err!(
+            "reopen commands failed, proxy: {e1}; api: {e2}"
+        )),
+        (Err(e1), Ok(_)) => Err(format_err!("reopen commands failed, proxy: {e1}")),
+        (Ok(_), Err(e2)) => Err(format_err!("reopen commands failed, api: {e2}")),
+        _ => Ok(()),
+    }
+}
+
+fn check_schedule(worker_type: &str, event_str: &str, id: &str) -> bool {
+    let event: CalendarEvent = match event_str.parse() {
+        Ok(event) => event,
+        Err(err) => {
+            eprintln!("unable to parse schedule '{event_str}' - {err}");
+            return false;
+        }
+    };
+
+    let last = match jobstate::last_run_time(worker_type, id) {
+        Ok(time) => time,
+        Err(err) => {
+            eprintln!("could not get last run time of {worker_type} {id}: {err}");
+            return false;
+        }
+    };
+
+    let next = match event.compute_next_event(last) {
+        Ok(Some(next)) => next,
+        Ok(None) => return false,
+        Err(err) => {
+            eprintln!("compute_next_event for '{event_str}' failed - {err}");
+            return false;
+        }
+    };
+
+    let now = proxmox_time::epoch_i64();
+    next <= now
+}
diff --git a/server/src/bin/proxmox-datacenter-api/tasks/mod.rs b/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
index f4d1d3a1..b6c3a70c 100644
--- a/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
+++ b/server/src/bin/proxmox-datacenter-api/tasks/mod.rs
@@ -1,3 +1,5 @@
+pub mod logrotate;
+
 pub mod remote_node_mapping;
 pub mod remote_tasks;
 pub mod remote_updates;
diff --git a/server/src/bin/proxmox-datacenter-privileged-api.rs b/server/src/bin/proxmox-datacenter-privileged-api.rs
index 14ff4bf4..9b0a037b 100644
--- a/server/src/bin/proxmox-datacenter-privileged-api.rs
+++ b/server/src/bin/proxmox-datacenter-privileged-api.rs
@@ -30,10 +30,10 @@ fn main() -> Result<(), Error> {
         .tasklog_pbs()
         .init()?;
 
-    create_directories()?;
-
     proxmox_product_config::init(pdm_config::api_user()?, pdm_config::priv_user()?);
 
+    create_directories()?;
+
     let mut args = std::env::args();
     args.next();
     for arg in args {
@@ -63,6 +63,7 @@ fn create_directories() -> Result<(), Error> {
     let api_user = pdm_config::api_user()?;
 
     pdm_config::setup::create_configdir()?;
+    server::jobstate::create_jobstate_dir()?;
 
     pdm_config::setup::mkdir_perms(
         pdm_buildcfg::PDM_RUN_DIR,
-- 
2.47.3



_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel


      parent reply	other threads:[~2025-11-27 15:36 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2025-11-27 15:37 [pdm-devel] [PATCH datacenter-manager 0/2] access/auth/task-log rotation Lukas Wagner
2025-11-27 15:37 ` [pdm-devel] [PATCH datacenter-manager 1/2] server: add jobstate module from PBS Lukas Wagner
2025-11-27 15:37 ` Lukas Wagner [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20251127153708.393210-3-l.wagner@proxmox.com \
    --to=l.wagner@proxmox.com \
    --cc=pdm-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal