mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-26 15:00:48 +01:00
* feat(ec): add encode_ts_ns to EC shard metadata and the shard read RPC EcShardConfig and VolumeEcShardReadRequest gain an int64 encode_ts_ns (encode time in unix nanos). It rides in .vif and the read request so a read can be scoped to the encode run that produced the index. * fix(ec): stamp each encode and reject cross-run shard reads Generate stamps EncodeTsNs into the volume's .vif. Reads carry it to the shard's owning volume (resolved together via FindEcVolumeWithShard, so a multi-disk server validates the disk that actually serves the bytes) and reject a shard from a different encode run, recovering from parity. A zero on either side (pre-upgrade volume) skips the guard. * fix(ec): stamp the encode identity on the worker-generated .vif The worker-local encode path now writes EncodeTsNs (and the resolved EC ratio) into the .vif, so the read guard is not silently off for volumes encoded by the maintenance worker. * fix(ec): wipe stale EC artifacts before re-encoding VolumeEcShardsGenerate evicts any in-memory EcVolume for the volume and removes its on-disk shard/index/sidecar files before writing fresh ones, so a retried encode never builds on a partial prior run and the unlink frees the inodes instead of leaving open fds serving old bytes. * fix(ec): unmount EC shards across all disks UnmountEcShards walked only the first disk holding the shard, leaving a duplicate copy mounted on a sibling disk (split-disk reconciled volumes) still serving and heartbeating. Traverse every disk and emit one deletion delta per disk. * fix(ec): delete orphan shards without a local .ecx deleteEcShardIdsForEachLocation gated shard-file removal on a local .ecx, so it could not clean an orphan .ecNN left by a failed copy on a disk with no index. Delete the requested shard files unconditionally; the index-file (.ecx/.ecj/.vif) routing stays gated as before. * fix(ec): clear stale EC shards cluster-wide before re-encoding ec.encode unmounts and deletes EC shards for the target volumes on every node before regenerating: fatal for the shards the topology reports (mounted leftovers), best-effort for the rest (a sweep that catches unmounted failed-copy orphans). A down node is a no-op. * fix(ec): don't nil EC fds on close so reads can't race eviction A reader resolves an EcVolume/shard under the lock then reads after it is released, so an eviction that nils ecxFile/ecdFile would race that read and panic. Close the fds without nilling the fields: the field is now write-once (no data race) and a concurrent read hits a closed fd, getting a clean error that the caller recovers from parity. * fix(ec): wipe stale EC artifacts on every disk and surface failures The pre-encode wipe only deleted beside the source volume, so a stale shard on a sibling disk survived and could be mounted against the new index at reconcile. Sweep every disk. Removal also ignored os.Remove errors, reporting a failed cleanup as success and letting a stale shard join the next generation; surface the first real failure (treating already-gone as success) from removeStaleEcArtifacts and the shard delete. * fix(ec): log when a local shard is skipped for a different encode run The cross-run guard returned errShardNotLocal, indistinguishable in logs from a genuinely-absent shard. Add a V(1) line naming both EncodeTsNs so operators can tell "wrong encode generation" from "shard not here". * fix(ec): surface metadata removal failures in the shard delete path deleteEcShardIdsForEachLocation still dropped os.Remove errors on the .ecx/.ecj/.vif/sidecar cleanup. A surviving stale .ecx is the orphan-index condition this path prevents, so route those through removeFileIfExists and return the first real failure instead of reporting cleanup as success. * fix(ec): fail orphan cleanup when a reachable node's delete fails The pre-encode orphan sweep swallowed every error for unreported (node, volume) pairs. That is only safe for an unreachable node, which cannot receive this encode's new generation. A reachable node whose delete genuinely failed (permission/IO) keeps an orphan shard that a later copy re-stamps with the new run's volume-level .vif identity, so the read guard would accept stale data. Surface those; stay best-effort only for unreachable nodes (gRPC Unavailable / no status). * fix(ec): guard ecjFile under its lock in the EC delete path EcVolume.Close nils ecjFile under ecjFileAccessLock; a delete that resolved its .ecx lookup before a concurrent eviction (the generate-time UnloadEcVolume) could then reach the journal append with a nil fd. Bail with a clear "volume closed" error under the lock instead. * fix(ec): reject an unstamped shard when the caller has an encode identity The read guard required both identities nonzero, so a current (stamped) caller accepted a holder with identity 0 and could be served a stale pre-upgrade shard. Reject when the caller is stamped and the holder differs (including unstamped); stay lenient only when the caller itself has no identity (pre-upgrade reader). A skipped shard recovers from parity. * fix(ec): full-teardown delete so cluster cleanup wipes a whole generation The pre-encode cluster sweep deleted only the listed canonical shards on remote nodes, leaving index/sidecar (and, on builds with versioned generations, those too) behind. Add a full_teardown flag to VolumeEcShardsDelete that evicts the volume and wipes every EC artifact for it on every disk via removeStaleEcArtifacts; the shell and worker pre-encode cleanup paths set it. Other delete callers (balance/decode/repair) are unchanged. * fix(ec): take ecjFileAccessLock before the nil-check in Sync and Close Sync and Close read ev.ecjFile before acquiring ecjFileAccessLock while Close nils it under the lock, a data race on the field. Take the lock first, then nil-check inside, in both. * fix(ec): acknowledge full_teardown so a pre-upgrade server can't fake success An old volume server silently ignores full_teardown and returns success for an ordinary delete, so the caller wrongly believes the generation was wiped and copies a fresh gen-0 onto an unwiped node. Echo full_teardown_done in the response; the worker destination cleanup fails when it is absent, and the shell cluster sweep fails for a reported (mounted) leftover while staying best-effort for an unreported node. encode_ts_ns stays an accepted transient (an old server just skips the new read guard, no regression). * fix(ec): fail the pre-encode sweep for any reachable node that can't ack teardown A reachable pre-upgrade server ignores full_teardown and returns success without wiping an orphan, which a later copy then folds into the new generation. Treat a missing full_teardown_done ack as fatal for every reachable node (best-effort only for a gRPC-unreachable one), not just for topology-reported pairs. * fix(ec): return the served shard identity and validate it client-side The encode identity was only enforced server-side, so a pre-upgrade server ignored the request field and served bytes unchecked. Echo the served shard's EncodeTsNs on every read response chunk and have the client reject a mismatch (including 0 from an old server), so the guard holds regardless of server version; a rejected read recovers from parity. * fix(ec): reject a short/empty remote shard read instead of serving zeros doReadRemoteEcShardInterval accepted an immediate EOF or a short stream and returned success with a partly zero-filled, unvalidated buffer (the server stamps the identity only on chunks that carry bytes). A non-deleted interval must arrive whole: require n == len(buf), exempting the is_deleted short-circuit (n=0), matching readLocalEcShardInterval's local check. A short read now fails so the caller recovers from parity. * test(ec): fake volume server echoes the full_teardown acknowledgement The worker now fails a teardown delete that isn't acknowledged (so a pre-upgrade server can't silently skip the wipe). The fake server's no-op VolumeEcShardsDelete returned an empty response, which the worker read as a skipped teardown and aborted the encode. Echo full_teardown_done. * feat(ec): mirror the encode-run identity guard + full_teardown into the Rust volume server The Go volume server stamps an encode-run identity (encode_ts_ns) into the .vif and rejects a read served from a shard of a different run; full_teardown wipes a whole generation and acknowledges it. The Rust volume server had none of it. Mirror the shared logic: load encode_ts_ns from the .vif onto the EcVolume, stamp it on every read response, and reject a request/response mismatch on both the server and the distributed-read client (recovering from parity); handle full_teardown by evicting the volume and wiping every EC artifact on each disk, echoing full_teardown_done so the caller can detect a server that ignored it. * fix(ec): remove a stale .vif on full teardown of a shard-only node A shard copy installs shards + .ecx before .vif, so an interrupted copy after a teardown could mount the new files under the previous run's identity / version / shard ratio / dat_file_size carried by the surviving .vif. Remove .vif during full teardown, gated on .idx absence so a source-volume holder keeps its live .vif. In Rust this lives in a teardown-only helper so the reconcile / load- fallback paths (which share the base removal) still preserve .vif. * fix(ec): treat a missing teardown ack as fatal, not as an unreachable node isNodeUnreachable returned true for any non-gRPC-status error, so a reachable pre-upgrade server's missing full_teardown_done ack (a plain error) was classified unreachable and the unreported pair was silently skipped. Classify only a real codes.Unavailable as unreachable, and wrap the missing ack in a sentinel the sweep treats as fatal regardless. A genuinely down node still surfaces as Unavailable from the RPC and stays best-effort. * fix(ec): reject a short shard read in the local EC needle reader read_ec_shard_needle ignored the byte count from shard.read_at and appended the whole pre-sized buffer, so a truncated shard's zero-filled tail passed the later length check and parsed as garbage. Require n == buf.len() per interval, erroring on a short read like the local interval reader already does. * fix(ec): probe reachability before skipping a node that returns Unavailable The pre-encode sweep skipped any node whose teardown delete returned codes.Unavailable, but a reachable volume server in maintenance mode also returns that code for the maintenance-gated delete, so its stale EC files were left behind on a node that can still receive the new generation. Confirm with a non-maintenance-gated empty-target Ping: skip only when the node fails the probe too (genuinely unreachable). * fix(ec): use try_exists for the teardown .vif .idx guard The teardown-only .vif removal gated on Path::exists(), which returns false on a permission/IO stat error, so a stat failure on a present .idx would read as a shard-only node and delete the live source volume's .vif. Gate on try_exists() == Ok(false) instead, preserving the sidecar on any stat error. * fix(ec): only skip a sweep node when a Ping confirms it is transport-down The pre-encode sweep skipped a node whenever its teardown delete and a liveness Ping both failed, but it treated ANY Ping error as down — an application-level Internal/ResourceExhausted, or Unimplemented from a pre-Ping server, left a reachable node's stale generation in place. Classify the Ping tri-state and skip only when it transport-fails with codes.Unavailable; a reachable or inconclusive node stays fatal. * fix(ec): exclude sweep-skipped nodes from the encode's rebalance The pre-encode sweep skips a genuinely-down node best-effort, but the rebalance then recollected the current topology — a node that recovered between the two could become a copy target and receive the new generation while still holding its stale, never-cleared shards. Have the sweep return the skipped set and exclude those nodes from the rebalance for this encode, so a node we could not clean cannot receive the new generation. Standalone ec.balance is unaffected. * fix(ec): re-sweep recovered nodes before generation so they aren't stranded A node skipped as down by the pre-encode sweep is excluded from the rebalance, but it can recover and become the generation host — mounting all shards locally, then being excluded from distribution. Union-only verification accepts all shards on one node and deletes the originals: a single point of failure. Re-sweep the skipped nodes just before generation; one whose teardown now succeeds leaves the skipped set and rebalances normally, while a node still down stays skipped. * fix(ec): abort the encode if a selected source is still skipped after re-sweep The re-sweep un-skips a recovered node, but the source was selected before it and a node can stay down through the re-sweep then recover just in time to be the generation host — mounting all shards locally while still excluded from the rebalance, which union-only verification accepts before deleting the originals. Abort the encode when a selected source remains skipped after the re-sweep. * fix(ec): batch delete returns retriable 503 when a volume became EC mid-batch If a volume is not EC at the batch-delete classification but is encoded to EC and its .dat deleted before the regular-volume mutation, the mutation returns an exact "not found" that the filer chunk-GC treats as completed, dropping the delete. Recheck EC presence under the mutation lock and return a retriable 503 with the "try again" token so the filer requeues it onto the EC path. * fix(ec): recheck EC state before the regular batch-delete mutation ec.encode mounts EC shards (copied from the .dat) before deleting the originals, so a volume can be EC while its .dat still exists. The batch delete only rechecked EC after a NotFound, so a successful regular-volume delete in that window wrote a tombstone to the soon-removed .dat — the delete was lost and the needle resurrected from the pre-tombstone shards. Recheck has_ec_volume under the write lock before delete_volume_needle and return a retriable 503 so the filer requeues onto the EC path. * fix(volume): make the metrics push test independent of test order test_push_metrics_once asserted the pushed body contains the request-counter family without ever touching the counter — a CounterVec with no children emits nothing, so the assertion only held when another test had already created a labelset in the shared registry. Create one in the test itself.
493 lines
19 KiB
Rust
493 lines
19 KiB
Rust
//! Prometheus metrics for the volume server.
|
|
//!
|
|
//! Mirrors the Go SeaweedFS volume server metrics.
|
|
|
|
use prometheus::{
|
|
self, Encoder, GaugeVec, HistogramOpts, HistogramVec, IntCounterVec, IntGauge, IntGaugeVec,
|
|
Opts, Registry, TextEncoder,
|
|
};
|
|
use std::sync::Once;
|
|
|
|
use crate::version;
|
|
|
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct PushGatewayConfig {
|
|
pub address: String,
|
|
pub interval_seconds: u32,
|
|
}
|
|
|
|
lazy_static::lazy_static! {
|
|
pub static ref REGISTRY: Registry = Registry::new();
|
|
|
|
// ---- Request metrics (Go: VolumeServerRequestCounter, VolumeServerRequestHistogram) ----
|
|
|
|
/// Request counter with labels `type` (HTTP method) and `code` (HTTP status).
|
|
pub static ref REQUEST_COUNTER: IntCounterVec = IntCounterVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_request_total", "Volume server requests"),
|
|
&["type", "code"],
|
|
).expect("metric can be created");
|
|
|
|
/// Request duration histogram with label `type` (HTTP method).
|
|
pub static ref REQUEST_DURATION: HistogramVec = HistogramVec::new(
|
|
HistogramOpts::new(
|
|
"SeaweedFS_volumeServer_request_seconds",
|
|
"Volume server request duration in seconds",
|
|
).buckets(exponential_buckets(0.0001, 2.0, 24)),
|
|
&["type"],
|
|
).expect("metric can be created");
|
|
|
|
// ---- Handler counters (Go: VolumeServerHandlerCounter) ----
|
|
|
|
/// Handler-level operation counter with label `type`.
|
|
pub static ref HANDLER_COUNTER: IntCounterVec = IntCounterVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_handler_total", "Volume server handler counters"),
|
|
&["type"],
|
|
).expect("metric can be created");
|
|
|
|
// ---- Vacuuming metrics (Go: VolumeServerVacuuming*) ----
|
|
|
|
/// Vacuuming compact counter with label `success` (true/false).
|
|
pub static ref VACUUMING_COMPACT_COUNTER: IntCounterVec = IntCounterVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_vacuuming_compact_count", "Counter of volume vacuuming Compact counter"),
|
|
&["success"],
|
|
).expect("metric can be created");
|
|
|
|
/// Vacuuming commit counter with label `success` (true/false).
|
|
pub static ref VACUUMING_COMMIT_COUNTER: IntCounterVec = IntCounterVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_vacuuming_commit_count", "Counter of volume vacuuming commit counter"),
|
|
&["success"],
|
|
).expect("metric can be created");
|
|
|
|
/// Vacuuming duration histogram with label `type` (compact/commit).
|
|
pub static ref VACUUMING_HISTOGRAM: HistogramVec = HistogramVec::new(
|
|
HistogramOpts::new(
|
|
"SeaweedFS_volumeServer_vacuuming_seconds",
|
|
"Volume vacuuming duration in seconds",
|
|
).buckets(exponential_buckets(0.0001, 2.0, 24)),
|
|
&["type"],
|
|
).expect("metric can be created");
|
|
|
|
// ---- Volume gauges (Go: VolumeServerVolumeGauge, VolumeServerReadOnlyVolumeGauge) ----
|
|
|
|
/// Volumes per collection and type (volume/ec_shards).
|
|
pub static ref VOLUME_GAUGE: GaugeVec = GaugeVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_volumes", "Number of volumes"),
|
|
&["collection", "type"],
|
|
).expect("metric can be created");
|
|
|
|
/// Read-only volumes per collection and type.
|
|
pub static ref READ_ONLY_VOLUME_GAUGE: GaugeVec = GaugeVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_read_only_volumes", "Number of read-only volumes."),
|
|
&["collection", "type"],
|
|
).expect("metric can be created");
|
|
|
|
/// Maximum number of volumes this server can hold.
|
|
pub static ref MAX_VOLUMES: IntGauge = IntGauge::new(
|
|
"SeaweedFS_volumeServer_max_volumes",
|
|
"Maximum number of volumes",
|
|
).expect("metric can be created");
|
|
|
|
// ---- Disk size gauges (Go: VolumeServerDiskSizeGauge) ----
|
|
|
|
/// Actual disk size used by volumes per collection and type (normal/deleted_bytes/ec).
|
|
pub static ref DISK_SIZE_GAUGE: GaugeVec = GaugeVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_total_disk_size", "Actual disk size used by volumes"),
|
|
&["collection", "type"],
|
|
).expect("metric can be created");
|
|
|
|
// ---- Resource gauges (Go: VolumeServerResourceGauge) ----
|
|
|
|
/// Disk resource usage per directory and type (all/used/free/avail).
|
|
pub static ref RESOURCE_GAUGE: GaugeVec = GaugeVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_resource", "Server resource usage"),
|
|
&["name", "type"],
|
|
).expect("metric can be created");
|
|
|
|
// ---- In-flight gauges (Go: VolumeServerInFlightRequestsGauge, InFlightDownload/UploadSize) ----
|
|
|
|
/// In-flight requests per HTTP method.
|
|
pub static ref INFLIGHT_REQUESTS_GAUGE: IntGaugeVec = IntGaugeVec::new(
|
|
Opts::new("SeaweedFS_volumeServer_in_flight_requests", "Current number of in-flight requests being handled by volume server."),
|
|
&["type"],
|
|
).expect("metric can be created");
|
|
|
|
/// Concurrent download limit in bytes.
|
|
pub static ref CONCURRENT_DOWNLOAD_LIMIT: IntGauge = IntGauge::new(
|
|
"SeaweedFS_volumeServer_concurrent_download_limit",
|
|
"Limit for total concurrent download size in bytes",
|
|
).expect("metric can be created");
|
|
|
|
/// Concurrent upload limit in bytes.
|
|
pub static ref CONCURRENT_UPLOAD_LIMIT: IntGauge = IntGauge::new(
|
|
"SeaweedFS_volumeServer_concurrent_upload_limit",
|
|
"Limit for total concurrent upload size in bytes",
|
|
).expect("metric can be created");
|
|
|
|
/// Current in-flight download bytes.
|
|
pub static ref INFLIGHT_DOWNLOAD_SIZE: IntGauge = IntGauge::new(
|
|
"SeaweedFS_volumeServer_in_flight_download_size",
|
|
"In flight total download size.",
|
|
).expect("metric can be created");
|
|
|
|
/// Current in-flight upload bytes.
|
|
pub static ref INFLIGHT_UPLOAD_SIZE: IntGauge = IntGauge::new(
|
|
"SeaweedFS_volumeServer_in_flight_upload_size",
|
|
"In flight total upload size.",
|
|
).expect("metric can be created");
|
|
|
|
/// Upload error counter by HTTP status code. Code "0" = transport error (no response).
|
|
pub static ref UPLOAD_ERROR_COUNTER: IntCounterVec = IntCounterVec::new(
|
|
Opts::new("SeaweedFS_upload_error_total",
|
|
"Counter of upload errors by HTTP status code. Code 0 means transport error (no response received)."),
|
|
&["code"],
|
|
).expect("metric can be created");
|
|
|
|
// ---- Scrubbing metrics (Go: VolumeServerScrub*) ----
|
|
|
|
/// Last scrub execution time, as seconds since UNIX epoch, with label `mode`.
|
|
pub static ref SCRUB_LAST_TIME_SECONDS: GaugeVec = GaugeVec::new(
|
|
Opts::new(
|
|
"SeaweedFS_volumeServer_scrub_last_time_seconds",
|
|
"Last scrub execution time, as seconds since UNIX epoch.",
|
|
),
|
|
&["mode"],
|
|
).expect("metric can be created");
|
|
|
|
/// Counter of overall volumes with issues detected during scrubbing, with label `mode`.
|
|
pub static ref SCRUB_VOLUME_FAILURES: IntCounterVec = IntCounterVec::new(
|
|
Opts::new(
|
|
"SeaweedFS_volumeServer_scrub_volume_failures",
|
|
"Counter of overall volumes with issues detected during scrubbing.",
|
|
),
|
|
&["mode"],
|
|
).expect("metric can be created");
|
|
|
|
/// Counter of overall EC shards with issues detected during scrubbing, with label `mode`.
|
|
pub static ref SCRUB_SHARD_FAILURES: IntCounterVec = IntCounterVec::new(
|
|
Opts::new(
|
|
"SeaweedFS_volumeServer_scrub_shard_failures",
|
|
"Counter of overall EC shards with issues detected during scrubbing.",
|
|
),
|
|
&["mode"],
|
|
).expect("metric can be created");
|
|
|
|
// ---- Legacy aliases for backward compat with existing code ----
|
|
|
|
/// Total number of volumes on this server (flat gauge).
|
|
pub static ref VOLUMES_TOTAL: IntGauge = IntGauge::new(
|
|
"volume_server_volumes_total",
|
|
"Total number of volumes",
|
|
).expect("metric can be created");
|
|
|
|
/// Disk size in bytes per directory.
|
|
pub static ref DISK_SIZE_BYTES: IntGaugeVec = IntGaugeVec::new(
|
|
Opts::new("volume_server_disk_size_bytes", "Disk size in bytes"),
|
|
&["dir"],
|
|
).expect("metric can be created");
|
|
|
|
/// Disk free bytes per directory.
|
|
pub static ref DISK_FREE_BYTES: IntGaugeVec = IntGaugeVec::new(
|
|
Opts::new("volume_server_disk_free_bytes", "Disk free space in bytes"),
|
|
&["dir"],
|
|
).expect("metric can be created");
|
|
|
|
/// Current number of in-flight requests (flat gauge).
|
|
pub static ref INFLIGHT_REQUESTS: IntGauge = IntGauge::new(
|
|
"volume_server_inflight_requests",
|
|
"Current number of in-flight requests",
|
|
).expect("metric can be created");
|
|
|
|
/// Total number of files stored across all volumes.
|
|
pub static ref VOLUME_FILE_COUNT: IntGauge = IntGauge::new(
|
|
"volume_server_volume_file_count",
|
|
"Total number of files stored across all volumes",
|
|
).expect("metric can be created");
|
|
|
|
// ---- Build info (Go: BuildInfo) ----
|
|
|
|
/// Build information gauge, always set to 1. Matches Go:
|
|
/// Namespace="SeaweedFS", Subsystem="build", Name="info",
|
|
/// labels: version, commit, sizelimit, goos, goarch.
|
|
pub static ref BUILD_INFO: GaugeVec = GaugeVec::new(
|
|
Opts::new("SeaweedFS_build_info", "A metric with a constant '1' value labeled by version, commit, sizelimit, goos, and goarch from which SeaweedFS was built."),
|
|
&["version", "commit", "sizelimit", "goos", "goarch"],
|
|
).expect("metric can be created");
|
|
}
|
|
|
|
/// Generate exponential bucket boundaries for histograms.
|
|
fn exponential_buckets(start: f64, factor: f64, count: usize) -> Vec<f64> {
|
|
let mut buckets = Vec::with_capacity(count);
|
|
let mut val = start;
|
|
for _ in 0..count {
|
|
buckets.push(val);
|
|
val *= factor;
|
|
}
|
|
buckets
|
|
}
|
|
|
|
// Handler counter type constants (matches Go's metrics_names.go).
|
|
pub const WRITE_TO_LOCAL_DISK: &str = "writeToLocalDisk";
|
|
pub const WRITE_TO_REPLICAS: &str = "writeToReplicas";
|
|
pub const DOWNLOAD_LIMIT_COND: &str = "downloadLimitCondition";
|
|
pub const UPLOAD_LIMIT_COND: &str = "uploadLimitCondition";
|
|
pub const READ_PROXY_REQ: &str = "readProxyRequest";
|
|
pub const READ_REDIRECT_REQ: &str = "readRedirectRequest";
|
|
pub const EMPTY_READ_PROXY_LOC: &str = "emptyReadProxyLocaction";
|
|
pub const FAILED_READ_PROXY_REQ: &str = "failedReadProxyRequest";
|
|
|
|
// Error metric name constants.
|
|
pub const ERROR_SIZE_MISMATCH_OFFSET_SIZE: &str = "errorSizeMismatchOffsetSize";
|
|
pub const ERROR_SIZE_MISMATCH: &str = "errorSizeMismatch";
|
|
pub const ERROR_CRC: &str = "errorCRC";
|
|
pub const ERROR_INDEX_OUT_OF_RANGE: &str = "errorIndexOutOfRange";
|
|
pub const ERROR_GET_NOT_FOUND: &str = "errorGetNotFound";
|
|
pub const ERROR_GET_INTERNAL: &str = "errorGetInternal";
|
|
pub const ERROR_WRITE_TO_LOCAL_DISK: &str = "errorWriteToLocalDisk";
|
|
pub const ERROR_UNMARSHAL_PAIRS: &str = "errorUnmarshalPairs";
|
|
pub const ERROR_WRITE_TO_REPLICAS: &str = "errorWriteToReplicas";
|
|
|
|
// Go volume heartbeat metric label values.
|
|
pub const READ_ONLY_LABEL_IS_READ_ONLY: &str = "IsReadOnly";
|
|
pub const READ_ONLY_LABEL_NO_WRITE_OR_DELETE: &str = "noWriteOrDelete";
|
|
pub const READ_ONLY_LABEL_NO_WRITE_CAN_DELETE: &str = "noWriteCanDelete";
|
|
pub const READ_ONLY_LABEL_IS_DISK_SPACE_LOW: &str = "isDiskSpaceLow";
|
|
pub const DISK_SIZE_LABEL_NORMAL: &str = "normal";
|
|
pub const DISK_SIZE_LABEL_DELETED_BYTES: &str = "deleted_bytes";
|
|
pub const DISK_SIZE_LABEL_EC: &str = "ec";
|
|
|
|
static REGISTER_METRICS: Once = Once::new();
|
|
|
|
/// Register all metrics with the custom registry.
|
|
/// Call this once at startup.
|
|
pub fn register_metrics() {
|
|
REGISTER_METRICS.call_once(|| {
|
|
let metrics: Vec<Box<dyn prometheus::core::Collector>> = vec![
|
|
// New Go-compatible metrics
|
|
Box::new(REQUEST_COUNTER.clone()),
|
|
Box::new(REQUEST_DURATION.clone()),
|
|
Box::new(HANDLER_COUNTER.clone()),
|
|
Box::new(VACUUMING_COMPACT_COUNTER.clone()),
|
|
Box::new(VACUUMING_COMMIT_COUNTER.clone()),
|
|
Box::new(VACUUMING_HISTOGRAM.clone()),
|
|
Box::new(VOLUME_GAUGE.clone()),
|
|
Box::new(READ_ONLY_VOLUME_GAUGE.clone()),
|
|
Box::new(MAX_VOLUMES.clone()),
|
|
Box::new(DISK_SIZE_GAUGE.clone()),
|
|
Box::new(RESOURCE_GAUGE.clone()),
|
|
Box::new(INFLIGHT_REQUESTS_GAUGE.clone()),
|
|
Box::new(CONCURRENT_DOWNLOAD_LIMIT.clone()),
|
|
Box::new(CONCURRENT_UPLOAD_LIMIT.clone()),
|
|
Box::new(INFLIGHT_DOWNLOAD_SIZE.clone()),
|
|
Box::new(INFLIGHT_UPLOAD_SIZE.clone()),
|
|
Box::new(UPLOAD_ERROR_COUNTER.clone()),
|
|
Box::new(SCRUB_LAST_TIME_SECONDS.clone()),
|
|
Box::new(SCRUB_VOLUME_FAILURES.clone()),
|
|
Box::new(SCRUB_SHARD_FAILURES.clone()),
|
|
// Legacy metrics
|
|
Box::new(VOLUMES_TOTAL.clone()),
|
|
Box::new(DISK_SIZE_BYTES.clone()),
|
|
Box::new(DISK_FREE_BYTES.clone()),
|
|
Box::new(INFLIGHT_REQUESTS.clone()),
|
|
Box::new(VOLUME_FILE_COUNT.clone()),
|
|
// Build info
|
|
Box::new(BUILD_INFO.clone()),
|
|
];
|
|
for m in metrics {
|
|
REGISTRY.register(m).expect("metric registered");
|
|
}
|
|
|
|
// Set build info gauge to 1 with version/commit/sizelimit/os/arch labels (matches Go).
|
|
BUILD_INFO
|
|
.with_label_values(&[
|
|
version::version(),
|
|
version::commit(),
|
|
version::size_limit(),
|
|
std::env::consts::OS,
|
|
std::env::consts::ARCH,
|
|
])
|
|
.set(1.0);
|
|
});
|
|
}
|
|
|
|
/// Gather all metrics and encode them in Prometheus text exposition format.
|
|
pub fn gather_metrics() -> String {
|
|
let encoder = TextEncoder::new();
|
|
let metric_families = REGISTRY.gather();
|
|
let mut buffer = Vec::new();
|
|
encoder
|
|
.encode(&metric_families, &mut buffer)
|
|
.expect("encoding metrics");
|
|
String::from_utf8(buffer).expect("metrics are valid UTF-8")
|
|
}
|
|
|
|
pub fn delete_collection_metrics(collection: &str) {
|
|
// Mirrors Go's DeletePartialMatch(prometheus.Labels{"collection": collection})
|
|
// which removes ALL metric entries matching the collection label, regardless
|
|
// of other label values (like "type"). We gather the metric families to discover
|
|
// all type values dynamically, matching Go's partial-match behavior.
|
|
delete_partial_match_collection(&VOLUME_GAUGE, collection);
|
|
delete_partial_match_collection(&READ_ONLY_VOLUME_GAUGE, collection);
|
|
delete_partial_match_collection(&DISK_SIZE_GAUGE, collection);
|
|
}
|
|
|
|
/// Remove all metric entries from a GaugeVec where the "collection" label matches.
|
|
/// This emulates Go's `DeletePartialMatch(prometheus.Labels{"collection": collection})`.
|
|
fn delete_partial_match_collection(gauge: &GaugeVec, collection: &str) {
|
|
use prometheus::core::Collector;
|
|
let families = gauge.collect();
|
|
for family in &families {
|
|
for metric in family.get_metric() {
|
|
let labels = metric.get_label();
|
|
let mut matches_collection = false;
|
|
let mut type_value = None;
|
|
for label in labels {
|
|
if label.get_name() == "collection" && label.get_value() == collection {
|
|
matches_collection = true;
|
|
}
|
|
if label.get_name() == "type" {
|
|
type_value = Some(label.get_value().to_string());
|
|
}
|
|
}
|
|
if matches_collection {
|
|
if let Some(ref tv) = type_value {
|
|
let _ = gauge.remove_label_values(&[collection, tv]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn build_pushgateway_url(address: &str, job: &str, instance: &str) -> String {
|
|
let base = if address.starts_with("http://") || address.starts_with("https://") {
|
|
address.to_string()
|
|
} else {
|
|
format!("http://{}", address)
|
|
};
|
|
let base = base.trim_end_matches('/');
|
|
format!("{}/metrics/job/{}/instance/{}", base, job, instance)
|
|
}
|
|
|
|
pub async fn push_metrics_once(
|
|
client: &reqwest::Client,
|
|
address: &str,
|
|
job: &str,
|
|
instance: &str,
|
|
) -> Result<(), String> {
|
|
let url = build_pushgateway_url(address, job, instance);
|
|
let response = client
|
|
.put(&url)
|
|
.header(
|
|
reqwest::header::CONTENT_TYPE,
|
|
"text/plain; version=0.0.4; charset=utf-8",
|
|
)
|
|
.body(gather_metrics())
|
|
.send()
|
|
.await
|
|
.map_err(|e| format!("push metrics request failed: {}", e))?;
|
|
|
|
if response.status().is_success() {
|
|
Ok(())
|
|
} else {
|
|
Err(format!(
|
|
"push metrics failed with status {}",
|
|
response.status()
|
|
))
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use axum::{routing::put, Router};
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
#[test]
|
|
fn test_gather_metrics_returns_text() {
|
|
register_metrics();
|
|
REQUEST_COUNTER.with_label_values(&["GET", "200"]).inc();
|
|
let output = gather_metrics();
|
|
assert!(output.contains("SeaweedFS_volumeServer_request_total"));
|
|
}
|
|
|
|
#[test]
|
|
fn test_build_pushgateway_url() {
|
|
assert_eq!(
|
|
build_pushgateway_url("localhost:9091", "volumeServer", "test-instance"),
|
|
"http://localhost:9091/metrics/job/volumeServer/instance/test-instance"
|
|
);
|
|
assert_eq!(
|
|
build_pushgateway_url("https://push.example", "volumeServer", "node-a"),
|
|
"https://push.example/metrics/job/volumeServer/instance/node-a"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_push_metrics_once() {
|
|
register_metrics();
|
|
// A CounterVec with no children emits nothing, so create a labelset
|
|
// instead of depending on another test having touched the counter
|
|
// first (test order is nondeterministic).
|
|
REQUEST_COUNTER.with_label_values(&["GET", "200"]).inc();
|
|
|
|
let captured = Arc::new(Mutex::new(None::<String>));
|
|
let captured_clone = captured.clone();
|
|
|
|
let app = Router::new().route(
|
|
"/metrics/job/volumeServer/instance/test-instance",
|
|
put(move |body: String| {
|
|
let captured = captured_clone.clone();
|
|
async move {
|
|
*captured.lock().unwrap() = Some(body);
|
|
"ok"
|
|
}
|
|
}),
|
|
);
|
|
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
|
let addr = listener.local_addr().unwrap();
|
|
let server = tokio::spawn(async move {
|
|
axum::serve(listener, app).await.unwrap();
|
|
});
|
|
|
|
let client = reqwest::Client::new();
|
|
push_metrics_once(
|
|
&client,
|
|
&format!("127.0.0.1:{}", addr.port()),
|
|
"volumeServer",
|
|
"test-instance",
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
let body = captured.lock().unwrap().clone().unwrap();
|
|
assert!(body.contains("SeaweedFS_volumeServer_request_total"));
|
|
|
|
server.abort();
|
|
}
|
|
|
|
#[test]
|
|
fn test_delete_collection_metrics_removes_collection_labelsets() {
|
|
register_metrics();
|
|
|
|
VOLUME_GAUGE.with_label_values(&["pics", "volume"]).set(2.0);
|
|
VOLUME_GAUGE.with_label_values(&["pics", "ec_shards"]).set(3.0);
|
|
READ_ONLY_VOLUME_GAUGE
|
|
.with_label_values(&["pics", "volume"])
|
|
.set(1.0);
|
|
DISK_SIZE_GAUGE
|
|
.with_label_values(&["pics", "normal"])
|
|
.set(10.0);
|
|
DISK_SIZE_GAUGE
|
|
.with_label_values(&["pics", "deleted_bytes"])
|
|
.set(4.0);
|
|
|
|
delete_collection_metrics("pics");
|
|
|
|
let output = gather_metrics();
|
|
assert!(!output.contains("collection=\"pics\",type=\"volume\""));
|
|
assert!(!output.contains("collection=\"pics\",type=\"ec_shards\""));
|
|
assert!(!output.contains("collection=\"pics\",type=\"normal\""));
|
|
assert!(!output.contains("collection=\"pics\",type=\"deleted_bytes\""));
|
|
}
|
|
}
|