public inbox for pdm-devel@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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal