Files
rustfs/crates/obs
houseme 00536da80c refactor(obs): make dial9 telemetry opt-in and actually record events (#4663)
* refactor(obs): make dial9 telemetry opt-in and actually record events

The dial9 Tokio-runtime profiler was disabled by default, yet every build
paid for it, and enabling it produced trace files with no events in them.

Recorded empty traces
---------------------
`build_traced_runtime` called `TracedRuntime::builder()...build(..)`, but dial9
only starts recording in `build_and_start*`. `build` still returns a live guard
whose `is_enabled()` reports true, and still creates and seals segment files —
they just contain a header and no events. It also skipped `with_trace_path`, so
the background worker driving the segment pipeline was never spawned.

Measured on the new smoke example: 310 bytes of bare segment header, against
5640 bytes for the same workload once recording actually starts.

Switch to `with_trace_path(..).build_and_start(..)`.

Cost was unconditional
----------------------
`--cfg tokio_unstable` was a global `[build] rustflags` entry and `rustfs-obs`
depended on `dial9-tokio-telemetry` unconditionally, so all builds depended on
Tokio's non-semver API. Worse, an environment `RUSTFLAGS` replaces (never
appends to) the config-file value, so any caller exporting their own RUSTFLAGS
silently dropped the flag — the long comment in build.yml was a scar from that.

dial9 is now an opt-in feature (`dial9`, plus `dial9-s3` and `dial9-taskdump`),
the global rustflag is gone, and `crates/obs/build.rs` fails the compile if the
feature is on without the flag. Telemetry builds go through `make build-profiling`.

Metrics that could not lie
--------------------------
`rustfs_dial9_{events_total,bytes_written_total,rotations_total,cpu_overhead_percent}`
were hard-coded to zero — a Counter pinned at 0 reads as "nothing happened".
Removed. `rustfs_dial9_enabled` was sourced from the environment, so it read 1
even when the traced runtime failed and the process fell back to a standard
runtime; it is replaced by `rustfs_dial9_supported` (compile-time),
`rustfs_dial9_configured` (intent) and `rustfs_dial9_active_sessions` (reality).

No `writer_healthy` gauge is exported: dial9's `RotatingWriter` can enter its
`Finished` state and stop writing, but exposes no way to observe that, so the
gauge could only ever be hard-coded to 1. Documented as a known gap instead.

Final events were lost
----------------------
The `TelemetryGuard` lived in a `static OnceLock`, which is never dropped, so
buffered events were never flushed at exit. `build_tokio_runtime` now returns
the guard and `run_process` drops it before any exit path.

Also
----
- `disk_usage_bytes` was a `read_dir` + per-file `stat` on the metrics
  collection path. It is now sampled by a background task into an atomic.
- `SAMPLING_RATE`/`S3_BUCKET`/`S3_PREFIX` were parsed, warned about, and
  discarded. S3 upload is now wired to dial9's `with_s3_uploader` behind
  `dial9-s3`; `SAMPLING_RATE` has no upstream equivalent and is removed.
- Wire `with_task_dumps` (async backtraces of stalled tasks), configurable via
  `RUSTFS_RUNTIME_DIAL9_TASK_DUMP_{ENABLED,IDLE_THRESHOLD_MS}`.
- Split `telemetry/dial9.rs` into `config`/`state`/`enabled`/`disabled`; the
  stub keeps the public API identical so callers need no `#[cfg]`.
- Drop four print-only examples and the manual test bin that exercised the
  removed `init_session` scaffolding.

Verified: cargo check/clippy/test across default, `dial9`, and `dial9-s3`;
build.rs correctly rejects `dial9` without `--cfg tokio_unstable`;
`make pre-commit` passes.

Co-Authored-By: heihutu <heihutu@gmail.com>

* docs(obs): document dial9 as an on-demand profiler

scripts/run.sh advertised a `SAMPLING_RATE` knob that was never passed to dial9,
and claimed "CPU overhead < 5% (with sampling rate 1.0)" and "lower values reduce
CPU overhead" on the strength of it. The knob is gone; the guidance built on it
had to go too.

Replace it with what is actually true: dial9 needs a `make build-profiling`
binary, its disk budget evicts oldest-first (so a high poll rate can overwrite
the incident you are chasing), and it cannot be toggled without a restart.

Add docs/operations/dial9-runtime-profiling.md covering the build variants, an
investigation walkthrough, the configuration table, how to read the three
supported/configured/active_sessions gauges against each other, and the upstream
gap that makes writer death only indirectly observable.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(obs): add a dial9 smoke example that proves events are recorded

The bug this guards against is invisible to every existing signal: with
`build` instead of `build_and_start`, dial9 creates the trace file, seals
segments, and reports `TelemetryGuard::is_enabled() == true` — it simply
records no events. Only the segment's byte count tells the two apart.

Measured on this workload: 5640 bytes when recording, 310 bytes (a bare
segment header) when not. The example asserts >= 2048 bytes, and was verified
to fail with the `build` call restored.

Also correct the comment on the `is_enabled` check in `finish_traced_runtime`.
It claimed to catch "recording silently off"; it does not. It only rejects the
inert guard a lenient config yields after a build failure. Recording is
guaranteed by `build_and_start`, not by that check.

Co-Authored-By: heihutu <heihutu@gmail.com>

* test(rustfs): accept Unsupported runtime telemetry capability

A binary built without the `dial9` feature now reports the runtime-telemetry
capability as `Unsupported` rather than `Disabled`. The distinction matters to
operators: `Disabled` implies the capability can be switched on by setting an
environment variable, which is not true here — telemetry needs a rebuild.

Widen the assertion and pin the new semantics: when `dial9::is_supported()` is
false, the state must be exactly `Unsupported`.

Co-Authored-By: heihutu <heihutu@gmail.com>

* fix(obs): drop the dial9-s3 feature, its TLS stack is vulnerable

CI's Dependency Review and `cargo deny` both reject the branch: dial9's
`worker-s3` feature depends on aws-sdk-s3-transfer-manager 0.1.3, which pins
aws-smithy-http-client onto hyper-rustls 0.24 and rustls-webpki 0.101.7. That
webpki carries RUSTSEC-2026-0098, -0099 and -0104.

0.1.3 is the latest release of the transfer manager, and 1.2.0 the latest of the
smithy client, so there is nothing to upgrade to. Cargo's feature unification can
add features but cannot drop a transitive dependency, so it cannot be worked
around from here either — the rest of the workspace already resolves to the safe
rustls-webpki 0.103 / hyper-rustls 0.27.

Remove the `dial9-s3` feature and the `with_s3_uploader` wiring. The two S3
environment variables stay parsed and warned about, now naming the real reason
rather than a missing build feature. Trace segments are collected from the output
directory instead. Tracked as D9-14 in rustfs/backlog#1157.

With this, Cargo.lock is byte-identical to main: the PR no longer touches the
dependency graph at all.

Also correct the `dial9-taskdump` documentation. It claimed the feature "compiles
to a no-op elsewhere"; in fact `tokio/taskdump` raises a `compile_error!` on any
target other than linux/{aarch64,x86,x86_64}. Verified by trying to build it on
macOS, which is how the claim was found to be wrong.

Co-Authored-By: heihutu <heihutu@gmail.com>

---------

Co-authored-by: heihutu <heihutu@gmail.com>
2026-07-10 10:52:48 +00:00
..

rustfs-obs

Observability library for RustFS providing structured JSON logging, distributed tracing, metrics via OpenTelemetry, and continuous profiling via Pyroscope.


Features

Feature Description
Structured logging JSON-formatted logs via tracing-subscriber
Rolling-file logging Daily / hourly rotation with automatic cleanup and high-precision timestamps
Distributed tracing OTLP/HTTP export to Jaeger, Tempo, or any OTel collector
Metrics OTLP/HTTP export, bridged from the metrics crate facade
Continuous Profiling CPU/Memory profiling export to Pyroscope
Log cleanup Background task: size limits, zstd/gzip compression, retention policies
GPU metrics (optional) Enable with the gpu feature flag

Quick Start

# Cargo.toml
[dependencies]
rustfs-obs = { version = "0.0.5" }

# GPU metrics support
rustfs-obs = { version = "0.0.5", features = ["gpu"] }
use rustfs_obs::init_obs;

#[tokio::main]
async fn main() {
    // Build config from environment variables, then initialise all backends.
    let _guard = init_obs(None).await.expect("failed to initialise observability");

    tracing::info!("RustFS started");

    // _guard is dropped here — all providers are flushed and shut down.
}

Keep _guard alive for the lifetime of your application. Dropping it triggers an ordered shutdown of every OpenTelemetry provider.


Initialisation

With an explicit OTLP endpoint

use rustfs_obs::init_obs;

let _guard = init_obs(Some("http://otel-collector:4318".to_string()))
.await
.expect("observability init failed");

With a custom config struct

use rustfs_obs::{AppConfig, OtelConfig, init_obs_with_config};

let config = AppConfig::new_with_endpoint(Some("http://localhost:4318".to_string()));
let _guard = init_obs_with_config( & config.observability)
.await
.expect("observability init failed");

Routing Logic

The library selects a backend automatically based on configuration:

1. Any OTLP endpoint set?
   └─ YES → Full OTLP/HTTP pipeline (traces + metrics + logs)
            + Profiling (Pyroscope) only if:
              - RUSTFS_OBS_PROFILING_EXPORT_ENABLED=true (explicit opt-in, default: false)

2. RUSTFS_OBS_LOG_DIRECTORY set to a non-empty path?
   └─ YES → Rolling-file JSON logging
            + Stdout mirror enabled if:
              - RUSTFS_OBS_LOG_STDOUT_ENABLED=true (explicit), OR
              - RUSTFS_OBS_ENVIRONMENT != "production" (automatic)

3. Default → Stdout-only JSON logging (all signals)

Key Points:

  • When no log directory is configured, logs automatically go to stdout only (perfect for development)
  • When a log directory is set, logs go to rolling files in that directory
  • In non-production environments, stdout is automatically mirrored alongside file logging for visibility
  • In production mode, you must explicitly set RUSTFS_OBS_LOG_STDOUT_ENABLED=true to see stdout in addition to files

Environment Variables

All configuration is read from environment variables at startup.

OTLP / Export

Variable Default Description
RUSTFS_OBS_ENDPOINT (empty) Root OTLP/HTTP endpoint, e.g. http://otel-collector:4318
RUSTFS_OBS_TRACE_ENDPOINT (empty) Dedicated trace endpoint (overrides root + /v1/traces)
RUSTFS_OBS_METRIC_ENDPOINT (empty) Dedicated metrics endpoint
RUSTFS_OBS_LOG_ENDPOINT (empty) Dedicated log endpoint
RUSTFS_OBS_PROFILING_ENDPOINT (empty) Dedicated profiling endpoint (e.g. Pyroscope)
RUSTFS_OBS_TRACES_EXPORT_ENABLED true Toggle trace export
RUSTFS_OBS_METRICS_EXPORT_ENABLED true Toggle metrics export
RUSTFS_OBS_LOGS_EXPORT_ENABLED true Toggle OTLP log export
RUSTFS_OBS_PROFILING_EXPORT_ENABLED false Toggle profiling export
RUSTFS_OBS_USE_STDOUT false Mirror all signals to stdout alongside OTLP
RUSTFS_OBS_SAMPLE_RATIO 0.1 Trace sampling ratio 0.01.0
RUSTFS_OBS_METER_INTERVAL 15 Metrics export interval (seconds)

Service identity

Variable Default Description
RUSTFS_OBS_SERVICE_NAME rustfs OTel service.name
RUSTFS_OBS_SERVICE_VERSION (crate version) OTel service.version
RUSTFS_OBS_ENVIRONMENT development Deployment environment (production, development, …)

Local logging

Variable Default Description
RUSTFS_OBS_LOGGER_LEVEL info Log level; RUST_LOG syntax supported
RUSTFS_OBS_LOG_STDOUT_ENABLED false When file logging is active, also mirror to stdout
RUSTFS_OBS_LOG_DIRECTORY (empty) Directory for rolling log files. When empty, logs go to stdout only
RUSTFS_OBS_LOG_FILENAME rustfs.log Base filename for rolling logs. Rotated archives include a high-precision timestamp and counter. With the default RUSTFS_OBS_LOG_MATCH_MODE=suffix, names look like <timestamp>-<counter>.rustfs.log (e.g., 20231027103001.123456-0.rustfs.log); with prefix, they look like rustfs.log.<timestamp>-<counter> (e.g., rustfs.log.20231027103001.123456-0).
RUSTFS_OBS_LOG_ROTATION_TIME hourly Rotation granularity: minutely, hourly, or daily
RUSTFS_OBS_LOG_KEEP_FILES 30 Number of rolling files to keep (also used by cleaner)
RUSTFS_OBS_LOG_MATCH_MODE suffix File matching mode: prefix or suffix

Log cleanup

Variable Default Description
RUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES 2147483648 Hard cap on total log directory size (2 GiB)
RUSTFS_OBS_LOG_MAX_SINGLE_FILE_SIZE_BYTES 0 Per-file size cap; 0 = unlimited
RUSTFS_OBS_LOG_COMPRESS_OLD_FILES true Compress files before deleting
RUSTFS_OBS_LOG_GZIP_COMPRESSION_LEVEL 6 Gzip level 1 (fastest) 9 (best)
RUSTFS_OBS_LOG_COMPRESSION_ALGORITHM zstd Compression codec: zstd or gzip
RUSTFS_OBS_LOG_PARALLEL_COMPRESS true Enable work-stealing parallel compression
RUSTFS_OBS_LOG_PARALLEL_WORKERS 6 Number of cleaner worker threads
RUSTFS_OBS_LOG_ZSTD_COMPRESSION_LEVEL 8 Zstd level 1 (fastest) 21 (best ratio)
RUSTFS_OBS_LOG_ZSTD_FALLBACK_TO_GZIP true Fallback to gzip when zstd compression fails
RUSTFS_OBS_LOG_ZSTD_WORKERS 1 zstdmt worker threads per compression task
RUSTFS_OBS_LOG_COMPRESSED_FILE_RETENTION_DAYS 30 Delete .gz / .zst archives older than N days; 0 = keep forever
RUSTFS_OBS_LOG_EXCLUDE_PATTERNS (empty) Comma-separated glob patterns to never clean up
RUSTFS_OBS_LOG_DELETE_EMPTY_FILES true Remove zero-byte files
RUSTFS_OBS_LOG_MIN_FILE_AGE_SECONDS 3600 Minimum file age (seconds) before cleanup
RUSTFS_OBS_LOG_CLEANUP_INTERVAL_SECONDS 1800 How often the cleanup task runs (0.5 hours)
RUSTFS_OBS_LOG_DRY_RUN false Report deletions without actually removing files

Cleaner & Rotation Metrics

The log rotation and cleanup pipeline emits these metrics (via the metrics facade):

Metric Type Description
rustfs_log_cleaner_deleted_files_total counter Number of files deleted per cleanup pass
rustfs_log_cleaner_freed_bytes_total counter Bytes reclaimed by deletion
rustfs_log_cleaner_compress_duration_seconds histogram Compression stage duration
rustfs_log_cleaner_steal_success_rate gauge Work-stealing success ratio in parallel mode
rustfs_log_cleaner_runs_total counter Successful cleanup loop runs
rustfs_log_cleaner_run_failures_total counter Failed or panicked cleanup loop runs
rustfs_log_cleaner_rotation_total counter Successful file rotations
rustfs_log_cleaner_rotation_failures_total counter Failed file rotations
rustfs_log_cleaner_rotation_duration_seconds histogram Rotation latency
rustfs_log_cleaner_active_file_size_bytes gauge Current active log file size

These metrics cover compression, cleanup, and file rotation end-to-end.

Metric Semantics

  • deleted_files_total and freed_bytes_total are emitted after each cleanup pass and include both normal log cleanup and expired compressed archive cleanup.
  • compress_duration_seconds measures compression stage wall-clock time for both serial and parallel modes.
  • steal_success_rate is updated by the parallel work-stealing path and remains at the last computed value.
  • rotation_* metrics are emitted by RollingAppender and include retries; a failed final rotation increments rotation_failures_total.
  • active_file_size_bytes is sampled on writes and after successful roll, so dashboards can track current active file growth.

Grafana Dashboard JSON Draft (Ready to Import)

Save this as rustfs-log-cleaner-dashboard.json, then import from Grafana UI. The canonical metric names use underscore notation, for example rustfs_log_cleaner_deleted_files_total.

The same panels are now checked in at: .docker/observability/grafana/dashboards/rustfs.json (row title: Log Cleaner).

{
  "uid": "rustfs-log-cleaner",
  "title": "RustFS Log Cleaner",
  "timezone": "browser",
  "schemaVersion": 39,
  "version": 1,
  "refresh": "10s",
  "tags": ["rustfs", "observability", "log-cleaner"],
  "time": {
    "from": "now-6h",
    "to": "now"
  },
  "panels": [
    {
      "id": 1,
      "title": "Cleanup Runs / Failures",
      "type": "timeseries",
      "targets": [
        { "refId": "A", "expr": "sum(rate(rustfs_log_cleaner_runs_total[5m]))", "legendFormat": "runs/s" },
        { "refId": "B", "expr": "sum(rate(rustfs_log_cleaner_run_failures_total[5m]))", "legendFormat": "failures/s" }
      ],
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }
    },
    {
      "id": 2,
      "title": "Freed Bytes / Deleted Files",
      "type": "timeseries",
      "targets": [
        { "refId": "A", "expr": "sum(rate(rustfs_log_cleaner_freed_bytes_total[15m]))", "legendFormat": "bytes/s" },
        { "refId": "B", "expr": "sum(rate(rustfs_log_cleaner_deleted_files_total[15m]))", "legendFormat": "files/s" }
      ],
      "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }
    },
    {
      "id": 3,
      "title": "Compression P95 Latency",
      "type": "timeseries",
      "targets": [
        {
          "refId": "A",
          "expr": "histogram_quantile(0.95, sum(rate(rustfs_log_cleaner_compress_duration_seconds_bucket[5m])) by (le))",
          "legendFormat": "p95"
        }
      ],
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }
    },
    {
      "id": 4,
      "title": "Rotation Success / Failure",
      "type": "timeseries",
      "targets": [
        { "refId": "A", "expr": "sum(rate(rustfs_log_cleaner_rotation_total[5m]))", "legendFormat": "rotation/s" },
        { "refId": "B", "expr": "sum(rate(rustfs_log_cleaner_rotation_failures_total[5m]))", "legendFormat": "rotation_failures/s" }
      ],
      "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }
    },
    {
      "id": 5,
      "title": "Steal Success Rate",
      "type": "timeseries",
      "targets": [
        { "refId": "A", "expr": "max(rustfs_log_cleaner_steal_success_rate)", "legendFormat": "ratio" }
      ],
      "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }
    },
    {
      "id": 6,
      "title": "Active File Size",
      "type": "timeseries",
      "targets": [
        { "refId": "A", "expr": "max(rustfs_log_cleaner_active_file_size_bytes)", "legendFormat": "bytes" }
      ],
      "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }
    }
  ]
}

PromQL Templates

Use these templates directly in Grafana panels/alerts.

  • Cleanup run rate
    • sum(rate(rustfs_log_cleaner_runs_total[$__rate_interval]))
  • Cleanup failure rate
    • sum(rate(rustfs_log_cleaner_run_failures_total[$__rate_interval]))
  • Cleanup failure ratio
    • sum(rate(rustfs_log_cleaner_run_failures_total[$__rate_interval])) / clamp_min(sum(rate(rustfs_log_cleaner_runs_total[$__rate_interval])), 1e-9)
  • Freed bytes throughput
    • sum(rate(rustfs_log_cleaner_freed_bytes_total[$__rate_interval]))
  • Deleted files throughput
    • sum(rate(rustfs_log_cleaner_deleted_files_total[$__rate_interval]))
  • Compression p95 latency
    • histogram_quantile(0.95, sum(rate(rustfs_log_cleaner_compress_duration_seconds_bucket[$__rate_interval])) by (le))
  • Rotation failure ratio
    • sum(rate(rustfs_log_cleaner_rotation_failures_total[$__rate_interval])) / clamp_min(sum(rate(rustfs_log_cleaner_rotation_total[$__rate_interval])), 1e-9)
  • Work-stealing efficiency (latest)
    • max(rustfs_log_cleaner_steal_success_rate)
  • Active file size (latest)
    • max(rustfs_log_cleaner_active_file_size_bytes)

Suggested Alerts

  • CleanupFailureRatioHigh: failure ratio > 0.05 for 10m.
  • CompressionLatencyP95High: p95 above your baseline SLO for 15m.
  • RotationFailuresDetected: rotation failure rate > 0 for 3 consecutive windows.
  • NoCleanupActivity: runs rate == 0 for expected active environments.

Metrics Compatibility

The project is currently in active development. Metric names and labels are updated directly when architecture evolves, and no backward-compatibility shim is maintained for old names. Use the metric names documented in this README as the current source of truth.


Examples

Stdout-only (development default)

# No RUSTFS_OBS_LOG_DIRECTORY set → stdout JSON
RUSTFS_OBS_LOGGER_LEVEL=debug ./rustfs

Rolling-file logging

export RUSTFS_OBS_LOG_DIRECTORY=/var/log/rustfs
export RUSTFS_OBS_LOGGER_LEVEL=info
export RUSTFS_OBS_LOG_KEEP_FILES=30
export RUSTFS_OBS_LOG_MAX_TOTAL_SIZE_BYTES=5368709120   # 5 GiB
./rustfs

Full OTLP pipeline (production)

export RUSTFS_OBS_ENDPOINT=http://otel-collector:4318
export RUSTFS_OBS_ENVIRONMENT=production
export RUSTFS_OBS_SAMPLE_RATIO=0.05      # 5% trace sampling
export RUSTFS_OBS_LOG_DIRECTORY=/var/log/rustfs
export RUSTFS_OBS_LOG_STDOUT_ENABLED=false
./rustfs

Separate per-signal endpoints

export RUSTFS_OBS_TRACE_ENDPOINT=http://tempo:4318/v1/traces
export RUSTFS_OBS_METRIC_ENDPOINT=http://prometheus-otel:4318/v1/metrics
export RUSTFS_OBS_LOG_ENDPOINT=http://loki-otel:4318/v1/logs
./rustfs

Dry-run cleanup audit

export RUSTFS_OBS_LOG_DIRECTORY=/var/log/rustfs
export RUSTFS_OBS_LOG_DRY_RUN=true
./rustfs
# Observe log output — no files will actually be deleted.
export RUSTFS_OBS_LOG_DIRECTORY=/var/log/rustfs
export RUSTFS_OBS_LOG_COMPRESSION_ALGORITHM=zstd
export RUSTFS_OBS_LOG_PARALLEL_COMPRESS=true
export RUSTFS_OBS_LOG_PARALLEL_WORKERS=6
export RUSTFS_OBS_LOG_ZSTD_COMPRESSION_LEVEL=8
export RUSTFS_OBS_LOG_ZSTD_FALLBACK_TO_GZIP=true
export RUSTFS_OBS_LOG_ZSTD_WORKERS=1
./rustfs

Module Structure

rustfs-obs/src/
├── lib.rs                   # Crate root; public re-exports
├── config.rs                # OtelConfig + AppConfig; env-var loading
├── error.rs                 # TelemetryError type
├── global.rs                # init_obs / init_obs_with_config entry points
│
├── telemetry/               # Backend initialisation
│   ├── mod.rs               # init_telemetry routing logic
│   ├── guard.rs             # OtelGuard RAII lifecycle manager
│   ├── filter.rs            # EnvFilter construction helpers
│   ├── resource.rs          # OTel Resource builder
│   ├── local.rs             # Stdout-only and rolling-file backends
│   ├── otel.rs              # Full OTLP/HTTP pipeline
│   └── recorder.rs          # metrics-crate → OTel bridge (Recorder)
│
├── cleaner/                 # Background log-file cleanup subsystem
│   ├── mod.rs               # LogCleaner public API + tests
│   ├── types.rs             # Shared cleaner types (match mode, compression codec, FileInfo)
│   ├── scanner.rs           # Filesystem discovery
│   ├── compress.rs          # Gzip/Zstd compression helper
│   └── core.rs              # Selection, compression, deletion logic
│
└── system/                  # Host metrics (CPU, memory, disk, GPU)
    ├── mod.rs
    ├── attributes.rs
    ├── collector.rs
    ├── metrics.rs
    └── gpu.rs               # GPU metrics (feature = "gpu")

Using LogCleaner Directly

use std::path::PathBuf;
use rustfs_obs::LogCleaner;
use rustfs_obs::types::FileMatchMode;

let cleaner = LogCleaner::builder(
PathBuf::from("/var/log/rustfs"),
"rustfs.log.".to_string(),  // file_pattern
"rustfs.log".to_string(),   // active_filename
)
.match_mode(FileMatchMode::Prefix)
.keep_files(10)
.max_total_size_bytes(2 * 1024 * 1024 * 1024) // 2 GiB
.max_single_file_size_bytes(0) // unlimited
.compress_old_files(true)
.gzip_compression_level(6)
.compressed_file_retention_days(7)
.exclude_patterns(vec!["current.log".to_string()])
.delete_empty_files(true)
.min_file_age_seconds(3600) // 1 hour
.dry_run(false)
.build();

let (deleted, freed_bytes) = cleaner.cleanup().expect("cleanup failed");
println!("Deleted {deleted} files, freed {freed_bytes} bytes");

Feature Flags

Flag Description
(default) Core logging, tracing, and metrics
gpu GPU utilisation metrics via nvml
full All features enabled
# Enable GPU monitoring
rustfs-obs = { version = "0.0.5", features = ["gpu"] }

# Enable everything
rustfs-obs = { version = "0.0.5", features = ["full"] }

License

Apache 2.0 — see LICENSE.