From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH datacenter-manager 21/26] metric collection: collect PDM host metrics in a new collection task
Date: Thu, 12 Mar 2026 14:52:22 +0100 [thread overview]
Message-ID: <20260312135229.420729-22-l.wagner@proxmox.com> (raw)
In-Reply-To: <20260312135229.420729-1-l.wagner@proxmox.com>
Then whole architecture is pretty similar to the remote metric
collection. We introduce a task that fetches host metrics and sends them
via a channel to the RRD task, which is responsible for persisting them
in the RRD database.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
Cargo.toml | 2 +
debian/control | 1 +
server/Cargo.toml | 2 +
.../local_collection_task.rs | 199 ++++++++++++++++++
server/src/metric_collection/mod.rs | 21 +-
server/src/metric_collection/rrd_task.rs | 185 ++++++++++++++++
6 files changed, 405 insertions(+), 5 deletions(-)
create mode 100644 server/src/metric_collection/local_collection_task.rs
diff --git a/Cargo.toml b/Cargo.toml
index 1adb8a0a..91741ea1 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -39,6 +39,7 @@ proxmox-auth-api = "1.0.5"
proxmox-base64 = "1"
proxmox-client = "1"
proxmox-daemon = "1"
+proxmox-disks = "0.1"
proxmox-docgen = "1"
proxmox-http = { version = "1.0.4", features = [ "client", "http-helpers", "websocket" ] } # see below
proxmox-human-byte = "1"
@@ -47,6 +48,7 @@ proxmox-ldap = { version = "1.1", features = ["sync"] }
proxmox-lang = "1.1"
proxmox-log = "1"
proxmox-login = "1.0.2"
+proxmox-procfs = "0.1"
proxmox-rest-server = "1"
# some use "cli", some use "cli" and "server", pbs-config uses nothing
proxmox-router = { version = "3.0.0", default-features = false }
diff --git a/debian/control b/debian/control
index 4ddc9efc..c61e8795 100644
--- a/debian/control
+++ b/debian/control
@@ -52,6 +52,7 @@ Build-Depends: debhelper-compat (= 13),
librust-proxmox-config-digest-1+default-dev,
librust-proxmox-config-digest-1+openssl-dev,
librust-proxmox-daemon-1+default-dev,
+ librust-proxmox-disks-0.1+default-dev,
librust-proxmox-dns-api-1+default-dev,
librust-proxmox-dns-api-1+impl-dev,
librust-proxmox-docgen-1+default-dev,
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 6969549f..65170864 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -40,6 +40,7 @@ proxmox-async.workspace = true
proxmox-auth-api = { workspace = true, features = [ "api", "ticket", "pam-authenticator", "password-authenticator" ] }
proxmox-base64.workspace = true
proxmox-daemon.workspace = true
+proxmox-disks.workspace = true
proxmox-docgen.workspace = true
proxmox-http = { workspace = true, features = [ "client-trait", "proxmox-async" ] } # pbs-client doesn't use these
proxmox-lang.workspace = true
@@ -47,6 +48,7 @@ proxmox-ldap.workspace = true
proxmox-log.workspace = true
proxmox-login.workspace = true
proxmox-openid.workspace = true
+proxmox-procfs.workspace = true
proxmox-rest-server = { workspace = true, features = [ "templates" ] }
proxmox-router = { workspace = true, features = [ "cli", "server"] }
proxmox-rrd.workspace = true
diff --git a/server/src/metric_collection/local_collection_task.rs b/server/src/metric_collection/local_collection_task.rs
new file mode 100644
index 00000000..a70b3d96
--- /dev/null
+++ b/server/src/metric_collection/local_collection_task.rs
@@ -0,0 +1,199 @@
+use std::sync::Mutex;
+use std::time::Instant;
+use std::{collections::HashMap, time::Duration};
+
+use anyhow::{Context, Error};
+use tokio::{sync::mpsc::Sender, time::MissedTickBehavior};
+
+use proxmox_disks::DiskManage;
+use proxmox_log::{debug, error};
+use proxmox_network_api::IpLink;
+use proxmox_procfs::pressure::{PressureData, Resource};
+use proxmox_sys::fs;
+use proxmox_sys::linux::procfs;
+
+use super::rrd_task::RrdStoreRequest;
+
+const HOST_METRIC_COLLECTION_INTERVAL: Duration = Duration::from_secs(10);
+
+/// Task which periodically collects metrics from the PDM host and stores
+/// them in the local metrics database.
+pub(super) struct LocalMetricCollectionTask {
+ metric_data_tx: Sender<RrdStoreRequest>,
+}
+
+impl LocalMetricCollectionTask {
+ /// Create a new metric collection task.
+ pub(super) fn new(metric_data_tx: Sender<RrdStoreRequest>) -> Self {
+ Self { metric_data_tx }
+ }
+
+ /// Run the metric collection task.
+ ///
+ /// This function never returns.
+ pub(super) async fn run(&mut self) {
+ let mut timer = tokio::time::interval(HOST_METRIC_COLLECTION_INTERVAL);
+ timer.set_missed_tick_behavior(MissedTickBehavior::Skip);
+
+ loop {
+ timer.tick().await;
+ self.handle_tick().await;
+ }
+ }
+
+ /// Handle a timer tick.
+ async fn handle_tick(&mut self) {
+ let stats = match tokio::task::spawn_blocking(collect_host_metrics).await {
+ Ok(stats) => stats,
+ Err(err) => {
+ error!("join error while collecting host stats: {err}");
+ return;
+ }
+ };
+
+ let _ = self
+ .metric_data_tx
+ .send(RrdStoreRequest::Host {
+ timestamp: proxmox_time::epoch_i64(),
+ metrics: Box::new(stats),
+ })
+ .await;
+ }
+}
+
+/// Container type for various metrics of a PDM host.
+pub(super) struct PdmHostMetrics {
+ /// CPU statistics from `/proc/stat`.
+ pub proc: Option<procfs::ProcFsStat>,
+ /// Memory statistics from `/proc/meminfo`.
+ pub meminfo: Option<procfs::ProcFsMemInfo>,
+ /// System load stats from `/proc/loadavg`.
+ pub load: Option<procfs::Loadavg>,
+ /// Aggregated network device traffic for all physical NICs.
+ pub netstats: Option<NetDevStats>,
+ /// Block device stats for the root disk.
+ pub root_blockdev_stat: Option<proxmox_disks::BlockDevStat>,
+ /// File system usage for the root disk.
+ pub root_filesystem_info: Option<fs::FileSystemInformation>,
+ /// CPU pressure stall information for the host.
+ pub cpu_pressure: Option<PressureData>,
+ /// CPU pressure stall information for the host.
+ pub memory_pressure: Option<PressureData>,
+ /// IO pressure stall information for the host.
+ pub io_pressure: Option<PressureData>,
+}
+
+/// Aggregated network device traffic for all physical NICs.
+pub(super) struct NetDevStats {
+ /// Aggregate inbound traffic over all physical NICs in bytes.
+ pub netin: u64,
+ /// Aggregate outbound traffic over all physical NICs in bytes.
+ pub netout: u64,
+}
+
+fn collect_host_metrics() -> PdmHostMetrics {
+ let proc = procfs::read_proc_stat()
+ .inspect_err(|err| error!("failed to read '/proc/stat': {err:#}"))
+ .ok();
+
+ let meminfo = procfs::read_meminfo()
+ .inspect_err(|err| error!("failed to read '/proc/meminfo': {err:#}"))
+ .ok();
+
+ let cpu_pressure = PressureData::read_system(Resource::Cpu)
+ .inspect_err(|err| error!("failed to read CPU pressure stall information: {err:#}"))
+ .ok();
+
+ let memory_pressure = PressureData::read_system(Resource::Memory)
+ .inspect_err(|err| error!("failed to read memory pressure stall information: {err:#}"))
+ .ok();
+
+ let io_pressure = PressureData::read_system(Resource::Io)
+ .inspect_err(|err| error!("failed to read IO pressure stall information: {err:#}"))
+ .ok();
+
+ let load = procfs::read_loadavg()
+ .inspect_err(|err| error!("failed to read '/proc/loadavg': {err:#}"))
+ .ok();
+
+ let root_blockdev_stat = DiskManage::new()
+ .blockdev_stat_for_path("/")
+ .inspect_err(|err| error!("failed to collect blockdev statistics for '/': {err:#}"))
+ .ok();
+
+ let root_filesystem_info = proxmox_sys::fs::fs_info("/")
+ .inspect_err(|err| {
+ error!("failed to query filesystem usage for '/': {err:#}");
+ })
+ .ok();
+
+ let netstats = collect_netdev_metrics()
+ .inspect_err(|err| {
+ error!("failed to collect network device statistics: {err:#}");
+ })
+ .ok();
+
+ PdmHostMetrics {
+ proc,
+ meminfo,
+ load,
+ netstats,
+ root_blockdev_stat,
+ root_filesystem_info,
+ cpu_pressure,
+ memory_pressure,
+ io_pressure,
+ }
+}
+
+struct NetdevCacheEntry {
+ interfaces: HashMap<String, IpLink>,
+ timestamp: Instant,
+}
+
+const NETWORK_INTERFACE_CACHE_MAX_AGE: Duration = Duration::from_secs(300);
+static NETWORK_INTERFACE_CACHE: Mutex<Option<NetdevCacheEntry>> = Mutex::new(None);
+
+fn collect_netdev_metrics() -> Result<NetDevStats, Error> {
+ let net_devs = procfs::read_proc_net_dev()?;
+
+ let mut cache = NETWORK_INTERFACE_CACHE.lock().unwrap();
+
+ let now = Instant::now();
+
+ let needs_refresh = match cache.as_ref() {
+ Some(entry) => now.duration_since(entry.timestamp) > NETWORK_INTERFACE_CACHE_MAX_AGE,
+ None => true,
+ };
+
+ if needs_refresh {
+ cache.replace({
+ debug!("updating cached network devices");
+
+ let interfaces = proxmox_network_api::get_network_interfaces()
+ .context("failed to enumerate network devices")?;
+
+ NetdevCacheEntry {
+ interfaces,
+ timestamp: now,
+ }
+ });
+ }
+
+ // unwrap: at this point we *know* that the Option is Some
+ let ip_links = cache.as_ref().unwrap();
+
+ let mut netin = 0;
+ let mut netout = 0;
+
+ for net_dev in net_devs {
+ if let Some(ip_link) = ip_links.interfaces.get(&net_dev.device) {
+ if ip_link.is_physical() {
+ netin += net_dev.receive;
+ netout += net_dev.send;
+ }
+ }
+ }
+
+ Ok(NetDevStats { netin, netout })
+}
diff --git a/server/src/metric_collection/mod.rs b/server/src/metric_collection/mod.rs
index 3cd58148..8a945fab 100644
--- a/server/src/metric_collection/mod.rs
+++ b/server/src/metric_collection/mod.rs
@@ -10,6 +10,7 @@ use tokio::sync::oneshot;
use pdm_api_types::RemoteMetricCollectionStatus;
use pdm_buildcfg::PDM_STATE_DIR_M;
+mod local_collection_task;
mod remote_collection_task;
pub mod rrd_cache;
mod rrd_task;
@@ -19,6 +20,8 @@ pub mod top_entities;
use remote_collection_task::{ControlMsg, RemoteMetricCollectionTask};
use rrd_cache::RrdCache;
+use crate::metric_collection::local_collection_task::LocalMetricCollectionTask;
+
const RRD_CACHE_BASEDIR: &str = concat!(PDM_STATE_DIR_M!(), "/rrdb");
static CONTROL_MESSAGE_TX: OnceLock<Sender<ControlMsg>> = OnceLock::new();
@@ -39,14 +42,22 @@ pub fn init() -> Result<(), Error> {
pub fn start_task() -> Result<(), Error> {
let (metric_data_tx, metric_data_rx) = mpsc::channel(128);
+ let cache = rrd_cache::get_cache();
+ tokio::spawn(async move {
+ let rrd_task_future = pin!(rrd_task::store_in_rrd_task(cache, metric_data_rx));
+ let abort_future = pin!(proxmox_daemon::shutdown_future());
+ futures::future::select(rrd_task_future, abort_future).await;
+ });
+
let (trigger_collection_tx, trigger_collection_rx) = mpsc::channel(128);
if CONTROL_MESSAGE_TX.set(trigger_collection_tx).is_err() {
bail!("control message sender already set");
}
+ let metric_data_tx_clone = metric_data_tx.clone();
tokio::spawn(async move {
let metric_collection_task_future = pin!(async move {
- match RemoteMetricCollectionTask::new(metric_data_tx, trigger_collection_rx) {
+ match RemoteMetricCollectionTask::new(metric_data_tx_clone, trigger_collection_rx) {
Ok(mut task) => task.run().await,
Err(err) => log::error!("could not start metric collection task: {err}"),
}
@@ -56,12 +67,12 @@ pub fn start_task() -> Result<(), Error> {
futures::future::select(metric_collection_task_future, abort_future).await;
});
- let cache = rrd_cache::get_cache();
-
tokio::spawn(async move {
- let rrd_task_future = pin!(rrd_task::store_in_rrd_task(cache, metric_data_rx));
+ let metric_collection_task_future =
+ pin!(async move { LocalMetricCollectionTask::new(metric_data_tx).run().await });
+
let abort_future = pin!(proxmox_daemon::shutdown_future());
- futures::future::select(rrd_task_future, abort_future).await;
+ futures::future::select(metric_collection_task_future, abort_future).await;
});
Ok(())
diff --git a/server/src/metric_collection/rrd_task.rs b/server/src/metric_collection/rrd_task.rs
index 29137858..4cf18679 100644
--- a/server/src/metric_collection/rrd_task.rs
+++ b/server/src/metric_collection/rrd_task.rs
@@ -8,6 +8,7 @@ use proxmox_rrd::rrd::DataSourceType;
use pbs_api_types::{MetricDataPoint, MetricDataType, Metrics};
use pve_api_types::{ClusterMetrics, ClusterMetricsData, ClusterMetricsDataType};
+use super::local_collection_task::PdmHostMetrics;
use super::rrd_cache::RrdCache;
/// Store request for the RRD task.
@@ -45,6 +46,16 @@ pub(super) enum RrdStoreRequest {
/// Statistics.
stats: CollectionStats,
},
+ /// Store PDM host metrics.
+ Host {
+ /// Timestamp at which the metrics were collected (UNIX epoch).
+ timestamp: i64,
+
+ /// Metric data for this PDM host.
+ // Boxed to avoid a clippy warning regarding large size differences between
+ // enum variants.
+ metrics: Box<PdmHostMetrics>,
+ },
}
/// Result for a [`RrdStoreRequest`].
@@ -117,6 +128,9 @@ pub(super) async fn store_in_rrd_task(
RrdStoreRequest::CollectionStats { timestamp, stats } => {
store_stats(&cache_clone, &stats, timestamp)
}
+ RrdStoreRequest::Host { timestamp, metrics } => {
+ store_pdm_host_metrics(&cache_clone, timestamp, &metrics)
+ }
};
})
.await;
@@ -194,6 +208,177 @@ fn store_stats(cache: &RrdCache, stats: &CollectionStats, timestamp: i64) {
);
}
+fn store_pdm_host_metrics(cache: &RrdCache, timestamp: i64, metrics: &PdmHostMetrics) {
+ if let Some(proc) = &metrics.proc {
+ cache.update_value(
+ "nodes/localhost/cpu-current",
+ proc.cpu,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/cpu-iowait",
+ proc.iowait_percent,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ }
+
+ if let Some(load) = &metrics.load {
+ cache.update_value(
+ "nodes/localhost/cpu-avg1",
+ load.0,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/cpu-avg5",
+ load.1,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/cpu-avg15",
+ load.2,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ }
+
+ if let Some(cpu_pressure) = &metrics.cpu_pressure {
+ cache.update_value(
+ "nodes/localhost/cpu-pressure-some-avg10",
+ cpu_pressure.some.average_10,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+
+ // NOTE: On a system level, 'full' CPU pressure is undefined and reported as 0,
+ // so it does not make sense to store it.
+ // https://docs.kernel.org/accounting/psi.html#pressure-interface
+ }
+
+ if let Some(meminfo) = &metrics.meminfo {
+ cache.update_value(
+ "nodes/localhost/mem-total",
+ meminfo.memtotal as f64,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/mem-used",
+ meminfo.memused as f64,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/swap-total",
+ meminfo.swaptotal as f64,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/swap-used",
+ meminfo.swapused as f64,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ }
+
+ if let Some(memory_pressure) = &metrics.memory_pressure {
+ cache.update_value(
+ "nodes/localhost/mem-pressure-some-avg10",
+ memory_pressure.some.average_10,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/mem-pressure-full-avg10",
+ memory_pressure.full.average_10,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ }
+
+ if let Some(netstats) = &metrics.netstats {
+ cache.update_value(
+ "nodes/localhost/net-in",
+ netstats.netin as f64,
+ timestamp,
+ DataSourceType::Derive,
+ );
+ cache.update_value(
+ "nodes/localhost/net-out",
+ netstats.netout as f64,
+ timestamp,
+ DataSourceType::Derive,
+ );
+ }
+
+ if let Some(disk) = &metrics.root_filesystem_info {
+ cache.update_value(
+ "nodes/localhost/disk-total",
+ disk.total as f64,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/disk-used",
+ disk.used as f64,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ }
+
+ if let Some(stat) = &metrics.root_blockdev_stat {
+ cache.update_value(
+ "nodes/localhost/disk-read-iops",
+ stat.read_ios as f64,
+ timestamp,
+ DataSourceType::Derive,
+ );
+ cache.update_value(
+ "nodes/localhost/disk-write-iops",
+ stat.write_ios as f64,
+ timestamp,
+ DataSourceType::Derive,
+ );
+ cache.update_value(
+ "nodes/localhost/disk-read",
+ (stat.read_sectors * 512) as f64,
+ timestamp,
+ DataSourceType::Derive,
+ );
+ cache.update_value(
+ "nodes/localhost/disk-write",
+ (stat.write_sectors * 512) as f64,
+ timestamp,
+ DataSourceType::Derive,
+ );
+ cache.update_value(
+ "nodes/localhost/disk-io-ticks",
+ (stat.io_ticks as f64) / 1000.0,
+ timestamp,
+ DataSourceType::Derive,
+ );
+ }
+
+ if let Some(io_pressure) = &metrics.io_pressure {
+ cache.update_value(
+ "nodes/localhost/io-pressure-some-avg10",
+ io_pressure.some.average_10,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ cache.update_value(
+ "nodes/localhost/io-pressure-full-avg10",
+ io_pressure.full.average_10,
+ timestamp,
+ DataSourceType::Gauge,
+ );
+ }
+}
+
#[cfg(test)]
mod tests {
use proxmox_rrd_api_types::{RrdMode, RrdTimeframe};
--
2.47.3
next prev parent reply other threads:[~2026-03-12 13:53 UTC|newest]
Thread overview: 31+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-12 13:52 [PATCH datacenter-manager/proxmox{,-backup,-yew-comp} 00/26] metric collection for the PDM host Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 01/26] sys: procfs: don't read from sysfs during unit tests Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 02/26] parallel-handler: import code from Proxmox Backup Server Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 03/26] parallel-handler: introduce custom error type Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 04/26] parallel-handler: add documentation Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 05/26] parallel-handler: add simple unit-test suite Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 06/26] disks: import from Proxmox Backup Server Lukas Wagner
2026-03-16 13:13 ` Arthur Bied-Charreton
2026-03-12 13:52 ` [PATCH proxmox 07/26] disks: fix typo in `initialize_gpt_disk` Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 08/26] disks: add parts of gather_disk_stats from PBS Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 09/26] disks: gate api macro behind 'api-types' feature Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 10/26] disks: clippy: collapse if-let chains where possible Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox 11/26] procfs: add helpers for querying pressure stall information Lukas Wagner
2026-03-16 13:25 ` Arthur Bied-Charreton
2026-03-12 13:52 ` [PATCH proxmox 12/26] time: use u64 parse helper from nom Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-backup 13/26] tools: move ParallelHandler to new proxmox-parallel-handler crate Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-backup 14/26] tools: replace disks module with proxmox-disks Lukas Wagner
2026-03-16 13:27 ` Arthur Bied-Charreton
2026-03-12 13:52 ` [PATCH proxmox-backup 15/26] metric collection: use blockdev_stat_for_path from proxmox_disks Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-yew-comp 16/26] node status panel: add `children` property Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-yew-comp 17/26] RRDGrid: fix size observer by attaching node reference to rendered container Lukas Wagner
2026-03-12 13:52 ` [PATCH proxmox-yew-comp 18/26] RRDGrid: add padding and increase gap between elements Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 19/26] metric collection: clarify naming for remote metric collection Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 20/26] metric collection: fix minor typo in error message Lukas Wagner
2026-03-12 13:52 ` Lukas Wagner [this message]
2026-03-12 13:52 ` [PATCH datacenter-manager 22/26] api: fix /nodes/localhost/rrddata endpoint Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 23/26] pdm: node rrd data: rename 'total-time' to 'metric-collection-total-time' Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 24/26] pdm-api-types: add PDM host metric fields Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 25/26] ui: node status: add RRD graphs for PDM host metrics Lukas Wagner
2026-03-12 13:52 ` [PATCH datacenter-manager 26/26] ui: lxc/qemu/node: use RRD value render helpers Lukas Wagner
2026-03-16 13:42 ` [PATCH datacenter-manager/proxmox{,-backup,-yew-comp} 00/26] metric collection for the PDM host Arthur Bied-Charreton
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=20260312135229.420729-22-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