feat(telemetry): report process memory and process and host uptime
This commit is contained in:
16 files changed
+311
-8
No files matched your search
Generated
+24
@@ -1977,6 +1977,27 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "procfs"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "25485360a54d6861439d60facef26de713b1e126bf015ec8f98239467a2b82f7"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"procfs-core",
|
||||||
|
"rustix",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "procfs-core"
|
||||||
|
version = "0.18.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e6401bf7b6af22f78b563665d15a22e9aef27775b79b149a66ca022468a4e405"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"hex",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prost"
|
name = "prost"
|
||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
@@ -3671,6 +3692,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"wakey",
|
"wakey",
|
||||||
"wakey-core",
|
"wakey-core",
|
||||||
|
"wakey-linux",
|
||||||
"wakey-vt100",
|
"wakey-vt100",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3696,6 +3718,7 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
"wakey-agent",
|
"wakey-agent",
|
||||||
"wakey-core",
|
"wakey-core",
|
||||||
|
"wakey-linux",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3733,6 +3756,7 @@ dependencies = [
|
|||||||
"lda-ipjs",
|
"lda-ipjs",
|
||||||
"macaddr",
|
"macaddr",
|
||||||
"nix 0.31.2",
|
"nix 0.31.2",
|
||||||
|
"procfs",
|
||||||
"pty-process",
|
"pty-process",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|||||||
+2
-1
@@ -55,7 +55,8 @@ or current fleet size. Metric labels should remain bounded; no per-device labels
|
|||||||
|
|
||||||
## Process And Host Visibility
|
## Process And Host Visibility
|
||||||
|
|
||||||
- [ ] Add shared process measurements for agent and CC: start time/uptime, resident memory, CPU time, and open file descriptors where supported. Keep version as resource metadata rather than repeating it on every sample.
|
- [x] Add Linux process age/RSS and host uptime gauges for agent and CC through opt-in OTLP, using the existing collection interval. Core owns OS-independent instruments; wakey-linux supplies the optional procfs reader. Missing data omits observations; no added labels, config fields, health routes or polling tasks. Collector coverage verifies units and values; router/live dashboard validation pending.
|
||||||
|
- [ ] Extend process measurements with CPU time and open file descriptors where supported, plus start-time/version metadata if needed. Keep version as resource metadata rather than repeating it on every sample.
|
||||||
- [ ] Add a small host summary: hostname, OS/kernel, CPU count, host uptime, load, and available/total memory. Distinguish host uptime from process uptime so router reboots and agent restarts are visible separately. Sources must tolerate missing Linux files; not every agent host is a router.
|
- [ ] Add a small host summary: hostname, OS/kernel, CPU count, host uptime, load, and available/total memory. Distinguish host uptime from process uptime so router reboots and agent restarts are visible separately. Sources must tolerate missing Linux files; not every agent host is a router.
|
||||||
- [ ] Expose changing measurements through OTLP on a modest sampling interval; show selected process/host details on Fleet Health and agent details. Keep host information behind the protected operator interface, not public health endpoints. Avoid expensive reads on every request and unbounded labels.
|
- [ ] Expose changing measurements through OTLP on a modest sampling interval; show selected process/host details on Fleet Health and agent details. Keep host information behind the protected operator interface, not public health endpoints. Avoid expensive reads on every request and unbounded labels.
|
||||||
- [ ] Keep `/healthz` as cheap liveness; consider a separate bounded `/readyz` database-access check for CC. Collector outages and disconnected fleet agents must not make a functioning CC unready.
|
- [ ] Keep `/healthz` as cheap liveness; consider a separate bounded `/readyz` database-access check for CC. Collector outages and disconnected fleet agents must not make a functioning CC unready.
|
||||||
|
|||||||
@@ -26,6 +26,41 @@ that includes recent activity and allow at least two exports for rate queries.
|
|||||||
Counter resets on restart are handled by `rate` and `increase`. Apply those
|
Counter resets on restart are handled by `rate` and `increase`. Apply those
|
||||||
functions **before** summing across instances so individual resets remain visible.
|
functions **before** summing across instances so individual resets remain visible.
|
||||||
|
|
||||||
|
## Process And Host
|
||||||
|
|
||||||
|
Discover the new gauges first (these are Linux-only):
|
||||||
|
|
||||||
|
```promql
|
||||||
|
count by (__name__, job, instance) ({__name__=~"wakey_(process|host)_.*"})
|
||||||
|
```
|
||||||
|
|
||||||
|
Process age and host uptime, in seconds:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
wakey_process_uptime_seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
```promql
|
||||||
|
wakey_host_uptime_seconds
|
||||||
|
```
|
||||||
|
|
||||||
|
Set Grafana's unit to a duration. Compare the two: process age resetting while
|
||||||
|
host uptime continues suggests a daemon restart; both resetting suggests a host
|
||||||
|
restart or a changed namespace/view. Missing samples are not a zero uptime or
|
||||||
|
proof of a restart. These are gauges: do not use `rate` or `increase` on them.
|
||||||
|
|
||||||
|
Resident process memory, in bytes:
|
||||||
|
|
||||||
|
```promql
|
||||||
|
wakey_process_memory_resident_bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
Set the unit to **bytes (IEC)**. Keep `job` and `instance` in the legend to compare
|
||||||
|
agent and CC separately. RSS includes shared/file-backed pages and is not heap
|
||||||
|
allocation alone. A missing series can mean disabled telemetry, an unsupported
|
||||||
|
platform, unavailable procfs, or export trouble. No new health endpoint or host
|
||||||
|
inventory is provided here; do not sum host uptime from multiple daemons.
|
||||||
|
|
||||||
## Operations
|
## Operations
|
||||||
|
|
||||||
Completed attempts per second, including errors and timeouts:
|
Completed attempts per second, including errors and timeouts:
|
||||||
|
|||||||
+25
-1
@@ -157,6 +157,9 @@ queries, rates, averages, percentiles, and Grafana units.
|
|||||||
| `wakey.stored.devices` | CC gauge of retained device rows, labeled by `agent_id` |
|
| `wakey.stored.devices` | CC gauge of retained device rows, labeled by `agent_id` |
|
||||||
| `wakey.stored.endpoints` | CC gauge of retained endpoint rows, including inactive entries, labeled by `agent_id` |
|
| `wakey.stored.endpoints` | CC gauge of retained endpoint rows, including inactive entries, labeled by `agent_id` |
|
||||||
| `wakey.fleet.devices` | CC gauge of unfiltered grouped identities, labeled by `identity` (`saved` or `unsaved`) |
|
| `wakey.fleet.devices` | CC gauge of unfiltered grouped identities, labeled by `identity` (`saved` or `unsaved`) |
|
||||||
|
| `wakey.process.uptime` | Linux gauge: seconds since this process's kernel-reported start, including suspend |
|
||||||
|
| `wakey.process.memory.resident` | Linux gauge: approximate resident memory in bytes, from VmRSS |
|
||||||
|
| `wakey.host.uptime` | Linux gauge: uptime in seconds reported by the process's procfs view, including suspend |
|
||||||
| `wakey.terminal.active` | Agent PTY workers currently alive |
|
| `wakey.terminal.active` | Agent PTY workers currently alive |
|
||||||
| `wakey.terminal.input.bytes` | Input bytes received by the terminal relay |
|
| `wakey.terminal.input.bytes` | Input bytes received by the terminal relay |
|
||||||
| `wakey.terminal.output.bytes` | Output bytes read from the PTY, including while detached |
|
| `wakey.terminal.output.bytes` | Output bytes read from the PTY, including while detached |
|
||||||
@@ -164,9 +167,30 @@ queries, rates, averages, percentiles, and Grafana units.
|
|||||||
|
|
||||||
Metric labels do not include request/session IDs, MACs, or IPs. New spans use
|
Metric labels do not include request/session IDs, MACs, or IPs. New spans use
|
||||||
`skip_all` and explicitly select safe fields; terminal bytes and credentials are
|
`skip_all` and explicitly select safe fields; terminal bytes and credentials are
|
||||||
not attached. Profiles, host CPU/RSS metrics, connected-agent gauges, and export-loss
|
not attached. Profiles, host CPU/total-memory metrics, connected-agent gauges, and export-loss
|
||||||
dashboards are not part of this initial slice.
|
dashboards are not part of this initial slice.
|
||||||
|
|
||||||
|
Process and host gauges are registered automatically for Linux agent/CC daemons
|
||||||
|
when OTLP is enabled. `wakey-core` owns the OS-independent instruments and
|
||||||
|
`ProcessMetricsSource` interface; `wakey-linux` supplies the procfs reader via
|
||||||
|
its optional `telemetry` feature. Each daemon attaches that reader to its
|
||||||
|
telemetry guard, so core has no Linux dependency or filesystem reads.
|
||||||
|
Their small procfs reads run during the existing 30-second
|
||||||
|
metric collection and final shutdown collection; no independent poller or task
|
||||||
|
is started. Non-Linux builds omit these gauges. Missing/unreadable procfs data
|
||||||
|
omits the affected observation instead of reporting zero or failing the daemon.
|
||||||
|
No hostname, PID, command line or environment values are exported by this slice.
|
||||||
|
Service/instance resource attributes identify each daemon; two daemons on the
|
||||||
|
same host can report the same host uptime, so do not sum it across processes.
|
||||||
|
|
||||||
|
RSS includes anonymous, file-backed and shared resident pages, not just heap,
|
||||||
|
and is an approximate kernel measurement. Process age uses procfs start ticks
|
||||||
|
and uptime, not wall-clock subtraction or telemetry initialization time.
|
||||||
|
Container/time namespaces can change the visible host clock; this is the
|
||||||
|
process's Linux view, not an assertion about a physical machine's boot time.
|
||||||
|
The Linux parser is [procfs](https://docs.rs/procfs/0.18.0/procfs/), with optional
|
||||||
|
default features disabled. Public `/healthz` remains unchanged.
|
||||||
|
|
||||||
Current-state fleet gauges sample independently every 30 seconds, with bounded
|
Current-state fleet gauges sample independently every 30 seconds, with bounded
|
||||||
revision retries to avoid mixed-state counts. Enrolled empty agents report zero;
|
revision retries to avoid mixed-state counts. Enrolled empty agents report zero;
|
||||||
removed labels disappear when no enrollment or retained rows remain. Failed samples
|
removed labels disappear when no enrollment or retained rows remain. Failed samples
|
||||||
|
|||||||
@@ -53,6 +53,9 @@ Use netlink only when:
|
|||||||
|
|
||||||
Today that mainly applies to `neighbor::nl`.
|
Today that mainly applies to `neighbor::nl`.
|
||||||
|
|
||||||
|
`wakey-linux` explicitly uses netlink for neighbor and address inventory on its
|
||||||
|
Unix-gated path, without automatic JSON fallback.
|
||||||
|
|
||||||
## Relationship to wakey
|
## Relationship to wakey
|
||||||
|
|
||||||
`wakey` is the product.
|
`wakey` is the product.
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! rtnetlink-based neighbor table query. One syscall, filter in userspace.
|
//! rtnetlink-based neighbor query: dump links and neighbors, then filter in userspace.
|
||||||
#![cfg(unix)]
|
#![cfg(unix)]
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
|
|||||||
@@ -37,3 +37,6 @@ url = "2"
|
|||||||
vt100 = { package = "wakey-vt100", path = "../vendor/vt100", registry = "gitea", version = "=0.16.2-wakey.2" }
|
vt100 = { package = "wakey-vt100", path = "../vendor/vt100", registry = "gitea", version = "=0.16.2-wakey.2" }
|
||||||
wakey = { path = "..", registry = "gitea", version = "0" }
|
wakey = { path = "..", registry = "gitea", version = "0" }
|
||||||
wakey-core = { path = "../wakey-core", registry = "gitea", version = "0", features = ["telemetry"] }
|
wakey-core = { path = "../wakey-core", registry = "gitea", version = "0", features = ["telemetry"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
wakey-linux = { path = "../wakey-linux", registry = "gitea", version = "0", features = ["telemetry"] }
|
||||||
@@ -6,7 +6,7 @@ pub fn init(
|
|||||||
config: &TelemetryConfig,
|
config: &TelemetryConfig,
|
||||||
agent_id: Option<&str>,
|
agent_id: Option<&str>,
|
||||||
) -> Result<TelemetryGuard> {
|
) -> Result<TelemetryGuard> {
|
||||||
wakey_core::telemetry::init(
|
let guard = wakey_core::telemetry::init(
|
||||||
config,
|
config,
|
||||||
"wakey-agent",
|
"wakey-agent",
|
||||||
match verbose {
|
match verbose {
|
||||||
@@ -15,5 +15,8 @@ pub fn init(
|
|||||||
_ => "wakey_agent=trace,wakey=debug,wakey_core=info,opentelemetry=warn",
|
_ => "wakey_agent=trace,wakey=debug,wakey_core=info,opentelemetry=warn",
|
||||||
},
|
},
|
||||||
agent_id,
|
agent_id,
|
||||||
)
|
)?;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let guard = guard.with_process_metrics(wakey_linux::telemetry::ProcfsProcessMetrics);
|
||||||
|
Ok(guard)
|
||||||
}
|
}
|
||||||
@@ -37,3 +37,6 @@ tracing = { workspace = true }
|
|||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
wakey-agent = { path = "../wakey-agent", registry = "gitea", version = "0" }
|
wakey-agent = { path = "../wakey-agent", registry = "gitea", version = "0" }
|
||||||
wakey-core = { path = "../wakey-core", registry = "gitea", version = "0", features = ["telemetry"] }
|
wakey-core = { path = "../wakey-core", registry = "gitea", version = "0", features = ["telemetry"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
wakey-linux = { path = "../wakey-linux", registry = "gitea", version = "0", features = ["telemetry"] }
|
||||||
@@ -3,7 +3,7 @@ use anyhow::Result;
|
|||||||
pub use wakey_core::telemetry::TelemetryGuard;
|
pub use wakey_core::telemetry::TelemetryGuard;
|
||||||
|
|
||||||
pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<TelemetryGuard> {
|
pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<TelemetryGuard> {
|
||||||
wakey_core::telemetry::init(
|
let guard = wakey_core::telemetry::init(
|
||||||
telemetry,
|
telemetry,
|
||||||
"wakey-control-plane",
|
"wakey-control-plane",
|
||||||
match verbose {
|
match verbose {
|
||||||
@@ -12,5 +12,8 @@ pub fn init(verbose: u8, telemetry: &TelemetryConfig) -> Result<TelemetryGuard>
|
|||||||
_ => "wakey_control_plane=trace,wakey_core=info,opentelemetry=warn",
|
_ => "wakey_control_plane=trace,wakey_core=info,opentelemetry=warn",
|
||||||
},
|
},
|
||||||
None,
|
None,
|
||||||
)
|
)?;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
let guard = guard.with_process_metrics(wakey_linux::telemetry::ProcfsProcessMetrics);
|
||||||
|
Ok(guard)
|
||||||
}
|
}
|
||||||
@@ -43,9 +43,23 @@ pub struct TelemetryGuard {
|
|||||||
traces: Option<SdkTracerProvider>,
|
traces: Option<SdkTracerProvider>,
|
||||||
logs: Option<SdkLoggerProvider>,
|
logs: Option<SdkLoggerProvider>,
|
||||||
metrics: Option<SdkMeterProvider>,
|
metrics: Option<SdkMeterProvider>,
|
||||||
|
process: Option<process::ProcessMetrics>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TelemetryGuard {
|
impl TelemetryGuard {
|
||||||
|
/// Attach platform measurements only when OTLP metrics are enabled. Core
|
||||||
|
/// owns instruments; the supplied reader owns all OS-specific collection.
|
||||||
|
pub fn with_process_metrics(mut self, source: impl ProcessMetricsSource) -> Self {
|
||||||
|
if let Some(metrics) = &self.metrics {
|
||||||
|
use opentelemetry::metrics::MeterProvider as _;
|
||||||
|
self.process = Some(process::ProcessMetrics::new(
|
||||||
|
&metrics.meter("wakey"),
|
||||||
|
source,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn shutdown(self) {
|
pub fn shutdown(self) {
|
||||||
// Export failures must not turn an otherwise successful daemon exit into a failure.
|
// Export failures must not turn an otherwise successful daemon exit into a failure.
|
||||||
if let Some(provider) = self.metrics {
|
if let Some(provider) = self.metrics {
|
||||||
@@ -198,6 +212,7 @@ pub fn init(
|
|||||||
traces: Some(traces),
|
traces: Some(traces),
|
||||||
logs: Some(logs),
|
logs: Some(logs),
|
||||||
metrics: Some(metrics),
|
metrics: Some(metrics),
|
||||||
|
process: None,
|
||||||
};
|
};
|
||||||
} else if config.json_logs {
|
} else if config.json_logs {
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
@@ -292,7 +307,9 @@ fn local_timer() -> fmt::time::OffsetTime<time::format_description::well_known::
|
|||||||
// therefore does no per-PTY-read allocations or instrument registration.
|
// therefore does no per-PTY-read allocations or instrument registration.
|
||||||
static METRICS: OnceLock<Instruments> = OnceLock::new();
|
static METRICS: OnceLock<Instruments> = OnceLock::new();
|
||||||
mod fleet;
|
mod fleet;
|
||||||
|
mod process;
|
||||||
pub use fleet::{AgentDeviceCounts, FleetCounts, FleetMetrics};
|
pub use fleet::{AgentDeviceCounts, FleetCounts, FleetMetrics};
|
||||||
|
pub use process::ProcessMetricsSource;
|
||||||
struct Instruments {
|
struct Instruments {
|
||||||
operations: opentelemetry::metrics::Counter<u64>,
|
operations: opentelemetry::metrics::Counter<u64>,
|
||||||
duration: opentelemetry::metrics::Histogram<f64>,
|
duration: opentelemetry::metrics::Histogram<f64>,
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
//! OS-independent process/host instruments. Callers supply a platform reader.
|
||||||
|
//! Failed reads omit a sample: zero must not masquerade as a restart or freed RAM.
|
||||||
|
|
||||||
|
use opentelemetry::metrics::{Meter, ObservableGauge};
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
/// Values are seconds/bytes, or None when unsupported/unavailable. Readers run
|
||||||
|
/// on the metric collection thread: keep work small and local, never perform
|
||||||
|
/// network requests or emit exporter-directed logs from these callbacks.
|
||||||
|
pub trait ProcessMetricsSource: Send + Sync + 'static {
|
||||||
|
fn process_uptime_seconds(&self) -> Option<f64>;
|
||||||
|
fn resident_memory_bytes(&self) -> Option<u64>;
|
||||||
|
fn host_uptime_seconds(&self) -> Option<f64>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) struct ProcessMetrics {
|
||||||
|
_uptime: ObservableGauge<f64>,
|
||||||
|
_resident: ObservableGauge<u64>,
|
||||||
|
_host_uptime: ObservableGauge<f64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProcessMetrics {
|
||||||
|
pub(super) fn new(meter: &Meter, source: impl ProcessMetricsSource) -> Self {
|
||||||
|
let source = Arc::new(source);
|
||||||
|
let uptime = source.clone();
|
||||||
|
let resident = source.clone();
|
||||||
|
Self {
|
||||||
|
_uptime: meter
|
||||||
|
.f64_observable_gauge("wakey.process.uptime")
|
||||||
|
.with_unit("s")
|
||||||
|
.with_description("Process age reported by the platform reader")
|
||||||
|
.with_callback(move |observer| {
|
||||||
|
if let Some(seconds) = uptime.process_uptime_seconds()
|
||||||
|
&& seconds.is_finite()
|
||||||
|
&& seconds >= 0.0
|
||||||
|
{
|
||||||
|
observer.observe(seconds, &[]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
_resident: meter
|
||||||
|
.u64_observable_gauge("wakey.process.memory.resident")
|
||||||
|
.with_unit("By")
|
||||||
|
.with_description("Resident process memory reported by the platform reader")
|
||||||
|
.with_callback(move |observer| {
|
||||||
|
if let Some(bytes) = resident.resident_memory_bytes() {
|
||||||
|
observer.observe(bytes, &[]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
_host_uptime: meter
|
||||||
|
.f64_observable_gauge("wakey.host.uptime")
|
||||||
|
.with_unit("s")
|
||||||
|
.with_description("Host uptime reported by the platform reader")
|
||||||
|
.with_callback(move |observer| {
|
||||||
|
if let Some(seconds) = source.host_uptime_seconds()
|
||||||
|
&& seconds.is_finite()
|
||||||
|
&& seconds >= 0.0
|
||||||
|
{
|
||||||
|
observer.observe(seconds, &[]);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -197,6 +197,20 @@ async fn exports_all_signals_and_preserves_remote_command_parent() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
let server = tokio::spawn(server);
|
let server = tokio::spawn(server);
|
||||||
|
// The exporter contract is OS-independent; Linux procfs collection is
|
||||||
|
// tested in wakey-linux rather than imported into core.
|
||||||
|
struct ProcessSample;
|
||||||
|
impl telemetry::ProcessMetricsSource for ProcessSample {
|
||||||
|
fn process_uptime_seconds(&self) -> Option<f64> {
|
||||||
|
Some(120.5)
|
||||||
|
}
|
||||||
|
fn resident_memory_bytes(&self) -> Option<u64> {
|
||||||
|
Some(32 * 1024 * 1024)
|
||||||
|
}
|
||||||
|
fn host_uptime_seconds(&self) -> Option<f64> {
|
||||||
|
Some(10_000.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
let guard = telemetry::init(
|
let guard = telemetry::init(
|
||||||
&TelemetryConfig {
|
&TelemetryConfig {
|
||||||
otlp_endpoint: Some(endpoint),
|
otlp_endpoint: Some(endpoint),
|
||||||
@@ -211,7 +225,8 @@ async fn exports_all_signals_and_preserves_remote_command_parent() {
|
|||||||
"info",
|
"info",
|
||||||
Some("test-agent"),
|
Some("test-agent"),
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap()
|
||||||
|
.with_process_metrics(ProcessSample);
|
||||||
|
|
||||||
let fleet_metrics = telemetry::FleetMetrics::new().unwrap();
|
let fleet_metrics = telemetry::FleetMetrics::new().unwrap();
|
||||||
fleet_metrics.replace(Some(telemetry::FleetCounts {
|
fleet_metrics.replace(Some(telemetry::FleetCounts {
|
||||||
@@ -344,6 +359,44 @@ async fn exports_all_signals_and_preserves_remote_command_parent() {
|
|||||||
assert!(names.contains(&name), "missing {name}");
|
assert!(names.contains(&name), "missing {name}");
|
||||||
}
|
}
|
||||||
let latest = metrics.last().unwrap();
|
let latest = metrics.last().unwrap();
|
||||||
|
{
|
||||||
|
for (name, unit, expected) in [
|
||||||
|
(
|
||||||
|
"wakey.process.uptime",
|
||||||
|
"s",
|
||||||
|
number_data_point::Value::AsDouble(120.5),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wakey.process.memory.resident",
|
||||||
|
"By",
|
||||||
|
number_data_point::Value::AsInt(32 * 1024 * 1024),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"wakey.host.uptime",
|
||||||
|
"s",
|
||||||
|
number_data_point::Value::AsDouble(10_000.0),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
let metric = latest
|
||||||
|
.resource_metrics
|
||||||
|
.iter()
|
||||||
|
.flat_map(|resource| &resource.scope_metrics)
|
||||||
|
.flat_map(|scope| &scope.metrics)
|
||||||
|
.find(|metric| metric.name == name)
|
||||||
|
.unwrap_or_else(|| panic!("missing {name}"));
|
||||||
|
assert_eq!(metric.unit, unit);
|
||||||
|
let Some(metric::Data::Gauge(gauge)) = &metric.data else {
|
||||||
|
panic!("{name} must be a gauge");
|
||||||
|
};
|
||||||
|
assert_eq!(gauge.data_points.len(), 1);
|
||||||
|
let point = &gauge.data_points[0];
|
||||||
|
assert!(
|
||||||
|
point.attributes.is_empty(),
|
||||||
|
"process metrics need no PID/host/address labels"
|
||||||
|
);
|
||||||
|
assert_eq!(point.value, Some(expected));
|
||||||
|
}
|
||||||
|
}
|
||||||
for (name, label, expected) in [
|
for (name, label, expected) in [
|
||||||
(
|
(
|
||||||
"wakey.stored.devices",
|
"wakey.stored.devices",
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ wakey-core = { path = "../wakey-core", registry = "gitea", version = "0"}
|
|||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = "3"
|
serial_test = "3"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
telemetry = ["wakey-core/telemetry", "dep:procfs"]
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
procfs = { version = "0.18", default-features = false, optional = true }
|
||||||
|
|
||||||
[dependencies.lda-ipjs]
|
[dependencies.lda-ipjs]
|
||||||
path = "../ipjs"
|
path = "../ipjs"
|
||||||
registry = "gitea"
|
registry = "gitea"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
pub mod devices;
|
pub mod devices;
|
||||||
pub mod dhcp;
|
pub mod dhcp;
|
||||||
pub mod observations;
|
pub mod observations;
|
||||||
|
#[cfg(all(target_os = "linux", feature = "telemetry"))]
|
||||||
|
pub mod telemetry;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod wake;
|
pub mod wake;
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
//! Linux procfs reader for core's OS-independent process/host instruments.
|
||||||
|
use procfs::Current;
|
||||||
|
use wakey_core::telemetry::ProcessMetricsSource;
|
||||||
|
|
||||||
|
/// Reads only this process and the visible host clock; never scans other PIDs.
|
||||||
|
/// Constructing the reader does no I/O, so disabled OTLP remains local-only.
|
||||||
|
pub struct ProcfsProcessMetrics;
|
||||||
|
|
||||||
|
impl ProcessMetricsSource for ProcfsProcessMetrics {
|
||||||
|
fn process_uptime_seconds(&self) -> Option<f64> {
|
||||||
|
process_uptime()
|
||||||
|
}
|
||||||
|
fn resident_memory_bytes(&self) -> Option<u64> {
|
||||||
|
resident_bytes()
|
||||||
|
}
|
||||||
|
fn host_uptime_seconds(&self) -> Option<f64> {
|
||||||
|
host_uptime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_uptime() -> Option<f64> {
|
||||||
|
let seconds = procfs::Uptime::current().ok()?.uptime;
|
||||||
|
(seconds.is_finite() && seconds >= 0.0).then_some(seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process_uptime() -> Option<f64> {
|
||||||
|
let stat = procfs::process::Process::myself().ok()?.stat().ok()?;
|
||||||
|
let ticks = procfs::ticks_per_second();
|
||||||
|
if ticks == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
// Both values use the kernel's boot-time clock, not wall time or telemetry
|
||||||
|
// initialization time. This also avoids parsing process names in stat ourselves.
|
||||||
|
let age = host_uptime()? - stat.starttime as f64 / ticks as f64;
|
||||||
|
(age.is_finite() && age >= 0.0).then_some(age)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resident_bytes() -> Option<u64> {
|
||||||
|
let status = procfs::process::Process::myself().ok()?.status().ok()?;
|
||||||
|
// procfs documents VmRSS in KiB even though the Linux file spells it "kB".
|
||||||
|
// It includes anonymous, file-backed and shared resident pages, not heap alone.
|
||||||
|
status.vmrss?.checked_mul(1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_process_and_visible_host_can_be_sampled() {
|
||||||
|
let source = ProcfsProcessMetrics;
|
||||||
|
let age = source
|
||||||
|
.process_uptime_seconds()
|
||||||
|
.expect("process start/uptime available");
|
||||||
|
let host = source.host_uptime_seconds().expect("host uptime available");
|
||||||
|
assert!(age.is_finite() && age >= 0.0);
|
||||||
|
assert!(host.is_finite() && host >= age);
|
||||||
|
assert!(source.resident_memory_bytes().expect("VmRSS available") > 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user