Skip to content

feat: RGW index pool PG probe for per-bucket SLI/SLO monitoring#52

Open
jrse wants to merge 10 commits into
cobaltcore-dev:mainfrom
jaracogmbh:feature/bucket-GET-LIST-SLI-metrics-tiered
Open

feat: RGW index pool PG probe for per-bucket SLI/SLO monitoring#52
jrse wants to merge 10 commits into
cobaltcore-dev:mainfrom
jaracogmbh:feature/bucket-GET-LIST-SLI-metrics-tiered

Conversation

@jrse

@jrse jrse commented May 27, 2026

Copy link
Copy Markdown
Contributor

RGW SLI/SLO Monitoring: Refactored per ADR-001

Problem (revised)

The original PR derived per-bucket availability from storage-layer PG health. After reviewing what AWS, GCP, Azure, and the Google SRE Workbook actually do, we identified three issues:

What Changed in This PR

L1/L2 SLI Metrics (Opslog Producer — Tenant-Level)

Metric Type Labels Description
radosgw_request_total Counter tenant, protocol, operation, status_class, region Canonical SLI: per-tenant request count by protocol/operation/status
radosgw_request_duration_seconds Histogram tenant, protocol, operation, region Request latency (le: 0.05, 0.1, 0.5, 1, 5, 30, +Inf)
radosgw_sli_stale_series_reaped_total Counter Housekeeping: count of reaped idle tenant series

Label Values

Label Values
protocol s3, swift
operation get, put, list, delete, head, multipart
status_class 2xx, 3xx, 4xx, 5xx
region e.g., qa-de-1, eu-de-2 (from --region flag / REGION env var)

L3 PG Probe Metrics (New Producer — Pool-Level Infrastructure Health)

Metric Type Labels Description
radosgw_index_probe_success Gauge pgid, pool, node, instance Per-PG probe result: 1=ok, 0=failed
radosgw_index_probe_latency_seconds Gauge pgid, pool, node, instance Per-PG RADOS round-trip latency
radosgw_index_pool_available_pgs_ratio Gauge pool, node, instance Fraction of PGs in active+clean state (0.0–1.0)
radosgw_index_pool_total_pgs Gauge pool, node, instance Total PG count in the index pool
radosgw_index_pool_covered_pgs Gauge pool, node, instance PGs covered by the probe bucket's shards
radosgw_index_probe_cycle_duration_seconds Gauge pool, node, instance Duration of last complete probe cycle
radosgw_index_probe_cycles_total Counter pool, node, instance Total probe cycles executed

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a configurable opslog bucket SLI collector with stale-series reaping, command/config wiring, tests, and docs; introduces a pg-probe producer that discovers and probes RGW index PGs with Prometheus/NATS output; and adds two shell scripts for PG coverage checks and PG risk ranking.

Changes

Bucket SLO Collector

Layer / File(s) Summary
Configuration and CLI/env wiring
pkg/commands/ctl.go, pkg/commands/producer_ops_log.go, pkg/producers/opslog/config.go
Adds config fields and command wiring for bucket SLO stale TTL and reap interval, including CLI flags and env overrides; introduces getEnvUint64 helper.
Collector structure and lifecycle
pkg/producers/opslog/prometheus_sli.go
Replaces per-series Prometheus vectors with sliCollector, storing counters and last-seen timestamps, skipping stale series during collection, reaping expired state in the background, and exposing a stale-series reaped counter.
Prometheus integration and SLI routing
pkg/producers/opslog/prometheus.go, pkg/producers/opslog/sli.go
Builds SLICollectorConfig from MetricsConfig, registers the collector with parsed durations and defaults, and routes bucket SLI observations through globalSLICollector.
Collector test helpers and behavior checks
pkg/producers/opslog/prometheus_sli_test.go, pkg/producers/opslog/metrics_test.go
Adds helpers to collect DTO counter metrics and updates tests to assert against collector state, including stale-series omission and reap behavior.
Bucket SLO documentation
pkg/producers/opslog/README.md
Documents new CLI/env flags, the stale-series reaped metric, the hot/cold/stale emission model, usage examples, and performance considerations.

PG Probe Producer and Index-PG Tools

Layer / File(s) Summary
PG probe configuration and entrypoints
pkg/producers/pgprobe/config.go, pkg/commands/producer_pg_probe.go, pkg/commands/local_producer.go, pkg/producers/config/producer.go, pkg/producers/config/producer_pgprobe.go
Introduces PGProbeConfig, adds the pg-probe command with env/flag merging and validation, registers it under local-producer and wires pg_probe into producer startup.
Bucket to PG mapping and impact model
pkg/producers/pgprobe/mapping.go
Builds bucket↔PG mappings from RGW Admin metadata or pool object scans and computes per-bucket impact records from failed PG sets.
Monitoring runtime, discovery, and probing
pkg/producers/pgprobe/pgprobe.go
Implements the monitoring loop: connect to RADOS, discover representative shard objects per PG, probe them periodically, refresh mappings, and produce per-PG probe results.
Prometheus and NATS publishing
pkg/producers/pgprobe/nats.go, pkg/producers/pgprobe/prometheus.go
Publishes probe-cycle summaries to NATS and exports per-PG, pool, and per-bucket Prometheus metrics; starts an HTTP /metrics server.
Bucket index PG coverage and impact script
scripts/check-bucket-index-pg-coverage-v4.sh
Adds a shell utility that checks bucket index shard coverage across pool PGs, probes representative shard objects, simulates failed PGs when requested, and reports affected buckets and overall status.
Index PG risk ranking script
scripts/list-index-pg-risk.sh
Adds a shell utility that maps .dir.* objects to PGs and buckets, aggregates shard placement, and prints a PG risk ranking table.
Combined monitoring documentation
changes_summary.md
Documents the pg-probe producer, the opslog collector refactor, combined alerting strategy, and example alert rules and deployment guidance.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant PGProbe
  participant RADOS
  participant Prometheus
  participant NATS

  CLI->>PGProbe: StartMonitoring(cfg)
  PGProbe->>RADOS: discoverProbeTargets()
  loop each probe interval
    PGProbe->>RADOS: stat representative shard per PG
    RADOS-->>PGProbe: ProbeResult list
    PGProbe->>Prometheus: PublishToPrometheus(results)
    PGProbe->>NATS: PublishToNATS(results)
  end
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • fa-at-pulsit
  • senolcolak

Poem

🐇 I thumped through PGs by moonlit pool,
With metrics neat and scripts as tool,
I swept stale tracks from clovered ground,
Probe pings and counters softly sound,
And tucked bright metrics where they'd be found.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The PR title accurately summarizes the primary changes: adding RGW index pool PG probing for bucket SLI/SLO monitoring. It is concise, clear, and directly reflects the main feature introduced across the changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
pkg/producers/opslog/metrics_test.go (2)

388-388: ⚡ Quick win

Avoid fixed 5ms sleeps for threshold-based assertions.

These sleeps are close to the 1ms cutoffs and can become flaky under CI scheduling jitter. Prefer assert.Eventually (or a longer bounded polling window) for deterministic timing tests.

Also applies to: 443-443, 470-470

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/producers/opslog/metrics_test.go` at line 388, Replace the brittle
time.Sleep(5 * time.Millisecond) calls in metrics_test.go with a bounded polling
assertion (e.g., use testify's assert.Eventually or a small retry loop) that
checks the expected condition within a timeout (e.g., 50-200ms) and a short poll
interval (e.g., 1-5ms); locate the three occurrences of time.Sleep(5 *
time.Millisecond) in the test file and change each to assert.Eventually(t,
func() bool { /* check the metric/state you expect after the sleep */ },
time.Millisecond*200, time.Millisecond*5) or an equivalent loop to avoid CI
scheduling flakiness.

322-326: ⚡ Quick win

Isolate globalSLICollector per test with cleanup.

These tests mutate package-global state but don’t restore it. Please snapshot and reset via t.Cleanup to avoid cross-test coupling.

Suggested pattern
+prev := globalSLICollector
+t.Cleanup(func() { globalSLICollector = prev })
 globalSLICollector = newSLICollector(SLICollectorConfig{
   StaleTTL:   24 * time.Hour,
   ColdAfter:  5 * time.Minute,
   SkipEveryN: 1,
 })

Also applies to: 351-355, 382-382, 410-410, 429-429, 459-459

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/producers/opslog/metrics_test.go` around lines 322 - 326, Tests mutate
the package-global variable globalSLICollector when calling
newSLICollector(SLICollectorConfig{...}); to prevent cross-test coupling, in
each test that assigns globalSLICollector snapshot the previous value (e.g., old
:= globalSLICollector), assign the new collector, and register a
t.Cleanup(func(){ globalSLICollector = old }) to restore it after the test;
apply this pattern to every test that changes globalSLICollector (including the
other occurrences where newSLICollector is used) and ensure nil/zero-value
restoration works correctly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/producers/opslog/prometheus_sli_test.go`:
- Line 13: The functions collectToDTO and findHistogramByLabels in sliCollector
are unused and failing golangci-lint; either remove both helper functions (and
any related unused imports or references) or add unit tests that call them
(e.g., in prometheus_sli_test.go create tests that instantiate sliCollector,
populate it with sample metrics, call collectToDTO and findHistogramByLabels,
and assert expected DTO/histogram responses) so they are exercised; pick one
approach and apply it consistently to both symbols to clear the lint error.

In `@pkg/producers/opslog/prometheus_sli.go`:
- Around line 328-333: The startup log prints the raw cfg but newSLICollector
may normalize SkipEveryN and ReapInterval, causing misleading diagnostics; after
constructing the collector (via newSLICollector) log the effective/normalized
values (e.g., use the collector's cfg or its fields such as StaleTTL, ColdAfter,
SkipEveryN, ReapInterval) instead of the original cfg so the "SLI collector
registered with tiered emission" message reflects the normalized settings;
update the log to read values from the returned collector (or its normalized
config) and move the log call to after newSLICollector returns.

In `@pkg/producers/opslog/README.md`:
- Line 259: The README line documenting BUCKET_SLO_REAP_INTERVAL uses the
inconsistent default expression `stale_ttl/4`; update that default to use the
flag-style name `stale-ttl/4` to match the rest of the docs and avoid operator
confusion (reference symbol: BUCKET_SLO_REAP_INTERVAL and the setting name
stale-ttl).

---

Nitpick comments:
In `@pkg/producers/opslog/metrics_test.go`:
- Line 388: Replace the brittle time.Sleep(5 * time.Millisecond) calls in
metrics_test.go with a bounded polling assertion (e.g., use testify's
assert.Eventually or a small retry loop) that checks the expected condition
within a timeout (e.g., 50-200ms) and a short poll interval (e.g., 1-5ms);
locate the three occurrences of time.Sleep(5 * time.Millisecond) in the test
file and change each to assert.Eventually(t, func() bool { /* check the
metric/state you expect after the sleep */ }, time.Millisecond*200,
time.Millisecond*5) or an equivalent loop to avoid CI scheduling flakiness.
- Around line 322-326: Tests mutate the package-global variable
globalSLICollector when calling newSLICollector(SLICollectorConfig{...}); to
prevent cross-test coupling, in each test that assigns globalSLICollector
snapshot the previous value (e.g., old := globalSLICollector), assign the new
collector, and register a t.Cleanup(func(){ globalSLICollector = old }) to
restore it after the test; apply this pattern to every test that changes
globalSLICollector (including the other occurrences where newSLICollector is
used) and ensure nil/zero-value restoration works correctly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 677af566-60dd-4973-9349-96e2e3ddb956

📥 Commits

Reviewing files that changed from the base of the PR and between e9585ae and d07a83d.

📒 Files selected for processing (9)
  • pkg/commands/ctl.go
  • pkg/commands/producer_ops_log.go
  • pkg/producers/opslog/README.md
  • pkg/producers/opslog/config.go
  • pkg/producers/opslog/metrics_test.go
  • pkg/producers/opslog/prometheus.go
  • pkg/producers/opslog/prometheus_sli.go
  • pkg/producers/opslog/prometheus_sli_test.go
  • pkg/producers/opslog/sli.go

Comment thread pkg/producers/opslog/prometheus_sli_test.go Outdated
Comment thread pkg/producers/opslog/prometheus_sli.go Outdated
Comment thread pkg/producers/opslog/README.md Outdated
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/check-bucket-index-pg-coverage-v4.sh`:
- Around line 106-114: The PGID lists are being written with sort -V which can
produce order incompatible with comm's expected collating order; update the
writes for POOL_PGS and USED_PGS to force a C locale so comm sees consistent
byte-collation (e.g., prefix the sort invocation with LC_ALL=C: LC_ALL=C sort -V
> "$POOL_PGS" and LC_ALL=C sort -V > "$USED_PGS"), and likewise ensure any comm
invocations run under LC_ALL=C; apply the same change to the second occurrence
at lines 150-153 referenced in the review.

In `@scripts/list-index-pg-risk.sh`:
- Around line 61-94: The per-object bucket name lookup currently runs awk
against BUCKET_ID_NAME_MAP for every object (in the loop using
bucket_name="$(awk -F '\t' -v id="$bucket_instance_id" ...
"$BUCKET_ID_NAME_MAP")"), which is inefficient; preload BUCKET_ID_NAME_MAP once
into a shell associative array (or simple map) before the while-read loop and
then replace the awk call with a direct array lookup (e.g.,
bucket_name="${BUCKET_MAP[$bucket_instance_id]:-}" or set to "<unknown>" if
empty). Initialize the map by reading BUCKET_ID_NAME_MAP (splitting on tab) and
populating BUCKET_MAP, then use bucket_name lookup inside the loop and remove
the per-iteration awk invocation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: dbbd75ea-5e9a-4dd9-bb8b-b695dc3042af

📥 Commits

Reviewing files that changed from the base of the PR and between 2d29051 and 3e3e9f6.

📒 Files selected for processing (2)
  • scripts/check-bucket-index-pg-coverage-v4.sh
  • scripts/list-index-pg-risk.sh

Comment thread scripts/check-bucket-index-pg-coverage-v4.sh Outdated
Comment thread scripts/list-index-pg-risk.sh Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/producers/opslog/prometheus.go (1)

120-125: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject non-positive bucket_slo_stale_ttl values.

time.ParseDuration accepts 0s and negative durations, and this block stores them as-is. That breaks the collector: pkg/producers/opslog/prometheus_sli.go treats anything older than StaleTTL as stale, so 0s/negative TTLs make bucket SLI series disappear from /metrics almost immediately even though TrackBucketSLO is enabled. Please keep the 24h default for d <= 0 and warn instead.

Suggested patch
 	if mc.BucketSLOStaleTTL != "" {
 		if d, err := time.ParseDuration(mc.BucketSLOStaleTTL); err == nil {
-			cfg.StaleTTL = d
+			if d > 0 {
+				cfg.StaleTTL = d
+			} else {
+				log.Warn().
+					Str("value", mc.BucketSLOStaleTTL).
+					Msg("bucket_slo_stale_ttl must be > 0, using default 24h")
+			}
 		} else {
 			log.Warn().Str("value", mc.BucketSLOStaleTTL).Msg("invalid bucket_slo_stale_ttl, using default 24h")
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/producers/opslog/prometheus.go` around lines 120 - 125, The parsed
BucketSLOStaleTTL may be zero or negative which breaks downstream logic; in the
config handling where you parse mc.BucketSLOStaleTTL (time.ParseDuration) and
set cfg.StaleTTL, reject non-positive durations by checking parsed d > 0 before
assigning; if d <= 0 or parse fails, keep the existing 24h default and emit a
warning (use the same log.Warn() path) indicating the invalid non-positive value
so callers know the default is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changes_summary.md`:
- Line 16: The wording claims the pigeonhole principle guarantees full PG
coverage with the "probe bucket" (997 shards) across 128 PGs; change the
rationale text to say coverage was "empirically verified" (or similar) instead
of invoking the pigeonhole principle, and explicitly retain the verification
step that probes all Placement Groups in the `.rgw.buckets.index` pool(s) every
15 seconds using the pre-sharded probe bucket and lightweight `rados stat`
checks; update any phrasing that references "pigeonhole" to reference empirical
verification and keep the explicit verification sentence intact.
- Around line 31-51: The markdown contains unlabeled fenced code blocks; update
each triple-backtick fence that wraps the "prysm local-producer pg-probe"
diagram and the "Tier 1: PG Probe → RADOS-level availability..." list to include
a language identifier (e.g., text or bash) so lint MD040 is satisfied. Locate
the blocks by searching for the exact block contents ("prysm local-producer
pg-probe" diagram and the "Tier 1: PG Probe..." block) and prepend the opening
``` with an appropriate identifier such as ```text for both occurrences.

In `@pkg/producers/pgprobe/mapping.go`:
- Around line 46-100: ListBuckets returns all RGW buckets so the loop in the PG
mapping code (using ListBuckets, getBucketMetadata, getObjectPG and populating
BucketPGMapping / mapping.PGToBuckets) can attach buckets from other pools; fix
by filtering buckets to the current index pool before adding them: after calling
getBucketMetadata(info) check the bucket's placement/zone/pool metadata (or
validate shard object existence in the target ioctx) and skip buckets whose
placement does not match the provided pool or whose shard objects do not exist
in that pool, then only proceed to resolve shards with getObjectPG and update
mapping.Buckets, mapping.BucketToPGs and mapping.PGToBuckets for buckets that
pass this validation.

In `@pkg/producers/pgprobe/pgprobe.go`:
- Around line 61-69: After normalizing zero defaults for cfg.Interval,
cfg.MappingRefreshInterval, and cfg.ProbeTimeoutMs, add explicit validation to
reject negative values (e.g., if cfg.Interval < 0 or cfg.MappingRefreshInterval
< 0 or cfg.ProbeTimeoutMs < 0) and return a clear error (or fail fast) instead
of allowing those values to reach time.NewTicker in the probe loop setup; apply
the same validation in the other initialization block referenced (lines 132-137)
so both probe creation paths validate and fail with a descriptive message before
starting timers.
- Around line 242-287: The discoverProbeTargets function currently selects the
probe bucket by choosing the marker with the most .dir.* shard objects; instead,
consult the configured cfg.ProbeBucket and use it to pick the correct marker.
Modify discoverProbeTargets to accept (or access) the configured probe bucket
name (cfg.ProbeBucket), map that bucket name to its marker via
groupShardsByBucket result (or derive the marker from shard object names), and
set probeBucketMarker and probeBucketObjList based on that configured marker; if
the configured bucket is missing or has no shards, return a clear error. Ensure
references to discoverProbeTargets, cfg.ProbeBucket, groupShardsByBucket,
probeBucketMarker, and probeBucketObjList are updated so discovery explicitly
resolves the configured probe bucket instead of using the largest-shard
heuristic.
- Around line 331-341: runProbes currently calls ioctx.Stat(objName) with no
timeout; wire cfg.ProbeTimeoutMs into the probe path by creating a
context.WithTimeout using time.Duration(cfg.ProbeTimeoutMs)*time.Millisecond for
each probe, then perform the Stat call in a cancellable way (e.g., run
ioctx.Stat(objName) in a goroutine and select on ctx.Done() to detect timeouts
or use any available context-aware Stat API if present), convert ctx
timeout/error into a ProbeResult error/latency, and ensure you call cancel() to
free resources; update runProbes to reference cfg.ProbeTimeoutMs and treat
context deadline exceeded as a probe timeout.

---

Outside diff comments:
In `@pkg/producers/opslog/prometheus.go`:
- Around line 120-125: The parsed BucketSLOStaleTTL may be zero or negative
which breaks downstream logic; in the config handling where you parse
mc.BucketSLOStaleTTL (time.ParseDuration) and set cfg.StaleTTL, reject
non-positive durations by checking parsed d > 0 before assigning; if d <= 0 or
parse fails, keep the existing 24h default and emit a warning (use the same
log.Warn() path) indicating the invalid non-positive value so callers know the
default is used.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 675e3e44-91c9-4075-b275-07c8366359e6

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3e9f6 and 5541437.

📒 Files selected for processing (16)
  • changes_summary.md
  • pkg/commands/local_producer.go
  • pkg/commands/producer_ops_log.go
  • pkg/commands/producer_pg_probe.go
  • pkg/producers/config/producer.go
  • pkg/producers/opslog/config.go
  • pkg/producers/opslog/metrics_test.go
  • pkg/producers/opslog/prometheus.go
  • pkg/producers/opslog/prometheus_sli.go
  • pkg/producers/opslog/prometheus_sli_test.go
  • pkg/producers/opslog/sli.go
  • pkg/producers/pgprobe/config.go
  • pkg/producers/pgprobe/mapping.go
  • pkg/producers/pgprobe/nats.go
  • pkg/producers/pgprobe/pgprobe.go
  • pkg/producers/pgprobe/prometheus.go
💤 Files with no reviewable changes (2)
  • pkg/producers/opslog/prometheus_sli_test.go
  • pkg/commands/producer_ops_log.go
✅ Files skipped from review due to trivial changes (1)
  • pkg/commands/local_producer.go
🚧 Files skipped from review as they are similar to previous changes (2)
  • pkg/producers/opslog/config.go
  • pkg/producers/opslog/sli.go

Comment thread changes_summary.md Outdated
Comment thread changes_summary.md Outdated
Comment thread pkg/producers/pgprobe/mapping.go Outdated
Comment thread pkg/producers/pgprobe/pgprobe.go Outdated
Comment thread pkg/producers/pgprobe/pgprobe.go
Comment thread pkg/producers/pgprobe/pgprobe.go
Comment thread pkg/producers/pgprobe/prometheus.go Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
pkg/producers/pgprobe/mapping.go (1)

1-15: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Gate pkg/producers/pgprobe/mapping.go (and pkg/producers/pgprobe/pgprobe.go) behind //go:build ceph.

mapping.go imports github.com/ceph/go-ceph/rados/rgw/admin but has no //go:build ceph header (same for pgprobe.go), unlike pkg/producers/config/producer_pgprobe.go, so it will compile in non-Ceph builds.

  • Major: BucketPGMapping.BucketToPGs / PGToBuckets are keyed by bucket_name only, discarding Tenant/BucketID even though BucketInfo includes them, so collisions can’t be disambiguated.
Suggested fix
+//go:build ceph
+
 // SPDX-FileCopyrightText: 2025 SAP SE or an SAP affiliate company and prysm contributors
 //
 // SPDX-License-Identifier: Apache-2.0
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/producers/pgprobe/mapping.go` around lines 1 - 15, Add a Go build tag to
gate Ceph-specific files and fix the map keying collision: add the `//go:build
ceph` (and optional `// +build ceph`) header to both
pkg/producers/pgprobe/mapping.go and pkg/producers/pgprobe/pgprobe.go so
Ceph-only imports (github.com/ceph/go-ceph/...) are excluded from non-Ceph
builds; then update BucketPGMapping.BucketToPGs and BucketPGMapping.PGToBuckets
to use a disambiguating composite key (e.g., include Tenant and/or BucketID from
BucketInfo) instead of just bucket_name so entries are unique — modify where
BucketInfo is created/used and any lookup code in methods on BucketPGMapping to
construct and parse the composite key consistently.
pkg/producers/pgprobe/nats.go (1)

54-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Populate covered_pgs explicitly and stop overloading total_pgs.

CoveredPGs is never assigned here, so every NATS event publishes covered_pgs: 0. At the same time, TotalPGs is derived from len(results), which is just the probed set available to this function. That makes the payload misleading for downstream consumers that distinguish pool size from probe coverage. At minimum set CoveredPGs to len(results); if total_pgs is intended to mean the pool-wide total, that count needs to be passed into PublishToNATS from the caller rather than inferred here.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/producers/pgprobe/nats.go` around lines 54 - 67, The ProbeEvent being
published never sets CoveredPGs and misuses TotalPGs = len(results), so set
CoveredPGs explicitly and stop overloading TotalPGs: either (quick fix) assign
CoveredPGs = len(results) in the ProbeEvent construction (keep TotalPGs as-is if
it intentionally represents probed count), or (correct fix) change the
PublishToNATS/caller contract to accept a poolTotalCount parameter, set
ProbeEvent.TotalPGs = poolTotalCount and ProbeEvent.CoveredPGs = len(results);
update all call sites of PublishToNATS accordingly.
changes_summary.md (1)

157-189: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Update this section to match the collector actually introduced in this PR.

This changelog says the opslog collector was simplified to two tiers and that --bucket-slo-cold-after / --bucket-slo-skip-every-n were removed, but the PR objective for this stack adds a hot/cold/stale collector and those exact settings. As written, operators would be told to remove live config and would miss the cold-series behavior entirely.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changes_summary.md` around lines 157 - 189, The changelog section is
incorrect for the collector introduced in this PR; update changes_summary.md so
it accurately reflects the implemented hot/cold/stale collector (not the
simplified 2-tier active/stale), restore and document the removed CLI flags and
env vars that actually exist in the PR (--bucket-slo-cold-after,
--bucket-slo-skip-every-n / BUCKET_SLO_COLD_AFTER, BUCKET_SLO_SKIP_EVERY_N),
mention the hot/cold/stale behavior and cold-series emission logic (ColdAfter,
SkipEveryN, emitCold and any related config fields like
BucketSLOColdAfter/BucketSLOSkipEveryN), and ensure the retained sections
(counter, reaper, flags) match the real code paths and defaults in the PR so
operators do not remove live config by mistake.
♻️ Duplicate comments (1)
changes_summary.md (1)

16-16: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Correct the PG coverage rationale.

“997 shards over 128 PGs” does not prove every PG is covered; it only guarantees collisions. Please reword this to empirical coverage and keep the explicit verification step below.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changes_summary.md` at line 16, Update the statement about PG coverage so it
does not claim a mathematical guarantee: replace the phrase asserting that "a
single pre-sharded 'probe bucket' (997 shards) covers all 128 PGs via the
pigeonhole principle" with wording that describes observed/empirical coverage
(e.g., "empirically covers most PGs") and explicitly note this is not a proof;
keep the subsequent explicit verification step that probes all Placement Groups
in the `.rgw.buckets.index` pool(s) every 15 seconds and retains the specific
implementation detail about using a pre-sharded probe bucket (997 shards) and
lightweight `rados stat` calls against 128 PGs for SLI derivation.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@changes_summary.md`:
- Around line 313-330: The fenced ASCII diagram blocks (e.g., the block starting
with "┌─────────────────────────────────────────────────────────────┐" and
containing "builder-base (golang:1.26-alpine)" and the similar block later
around "builder-noceph / builder-ceph") are using bare triple backticks; add a
language identifier "text" to those fenced code blocks (and the other similar
block referenced at the later location) so they become ```text ... ``` to
satisfy markdownlint MD040.

In `@Dockerfile`:
- Around line 17-18: The Dockerfile currently uses floating Alpine tags (e.g.,
FROM golang:1.26-alpine AS builder-base) which can resolve to Alpine releases
that lack ceph19-dev / librados19; pin the Alpine release used by the builder
and every runtime stage (including the runtime-ceph stage) to a known working
release (e.g., replace "alpine" with "alpine3.22" or the specific v3.21/v3.22
tag you validated) so apk package resolution is deterministic; update all FROM
lines that reference "*-alpine" or "alpine" to the chosen pinned tag and ensure
multi-stage names like builder-base and runtime-ceph keep the same aliases.
- Around line 62-76: The Alpine runtime stages are missing the root CA bundle;
update both apk installs so they include ca-certificates: add ca-certificates to
the first RUN apk add --no-cache smartmontools nvme-cli line and to the RUN apk
add --no-cache \ librados19 \ smartmontools \ nvme-cli block (the
runtime/no-ceph and runtime-ceph APK installs referenced in the Dockerfile) so
the container can validate TLS certificates for admin-url, NATS, and remote
producers.

In `@pkg/producers/config/producer_pgprobe.go`:
- Around line 31-36: Validate and clamp the probe timing settings to a safe
positive value before they are used to create tickers: ensure values read via
GetIntSetting (interval and mapping_refresh_interval from producer.Settings) are
>0 (or default to 15 and 3600 respectively) and reject or clamp negatives/zero;
adjust either where interval/mappingRefresh are assigned or immediately in
StartMonitoring before using cfg.Interval and cfg.MappingRefreshInterval with
time.NewTicker to prevent panics from negative durations.

In `@pkg/producers/pgprobe/mapping.go`:
- Around line 28-31: BucketPGMapping currently keys BucketToPGs and PGToBuckets
by bare bucket name which loses Tenant information from BucketInfo and causes
silent collisions; change the map keys to a stable, unique bucket identifier
(e.g., composite key using Tenant + ":" + BucketID or a dedicated BucketKey
string/type) everywhere referenced (BucketPGMapping.BucketToPGs,
BucketPGMapping.PGToBuckets and the code that populates/reads them) and update
all consumers to construct/parse that composite key (or switch to using the
BucketKey type) so identity includes Tenant and BucketID and no longer collides
for buckets with the same visible name.

---

Outside diff comments:
In `@changes_summary.md`:
- Around line 157-189: The changelog section is incorrect for the collector
introduced in this PR; update changes_summary.md so it accurately reflects the
implemented hot/cold/stale collector (not the simplified 2-tier active/stale),
restore and document the removed CLI flags and env vars that actually exist in
the PR (--bucket-slo-cold-after, --bucket-slo-skip-every-n /
BUCKET_SLO_COLD_AFTER, BUCKET_SLO_SKIP_EVERY_N), mention the hot/cold/stale
behavior and cold-series emission logic (ColdAfter, SkipEveryN, emitCold and any
related config fields like BucketSLOColdAfter/BucketSLOSkipEveryN), and ensure
the retained sections (counter, reaper, flags) match the real code paths and
defaults in the PR so operators do not remove live config by mistake.

In `@pkg/producers/pgprobe/mapping.go`:
- Around line 1-15: Add a Go build tag to gate Ceph-specific files and fix the
map keying collision: add the `//go:build ceph` (and optional `// +build ceph`)
header to both pkg/producers/pgprobe/mapping.go and
pkg/producers/pgprobe/pgprobe.go so Ceph-only imports
(github.com/ceph/go-ceph/...) are excluded from non-Ceph builds; then update
BucketPGMapping.BucketToPGs and BucketPGMapping.PGToBuckets to use a
disambiguating composite key (e.g., include Tenant and/or BucketID from
BucketInfo) instead of just bucket_name so entries are unique — modify where
BucketInfo is created/used and any lookup code in methods on BucketPGMapping to
construct and parse the composite key consistently.

In `@pkg/producers/pgprobe/nats.go`:
- Around line 54-67: The ProbeEvent being published never sets CoveredPGs and
misuses TotalPGs = len(results), so set CoveredPGs explicitly and stop
overloading TotalPGs: either (quick fix) assign CoveredPGs = len(results) in the
ProbeEvent construction (keep TotalPGs as-is if it intentionally represents
probed count), or (correct fix) change the PublishToNATS/caller contract to
accept a poolTotalCount parameter, set ProbeEvent.TotalPGs = poolTotalCount and
ProbeEvent.CoveredPGs = len(results); update all call sites of PublishToNATS
accordingly.

---

Duplicate comments:
In `@changes_summary.md`:
- Line 16: Update the statement about PG coverage so it does not claim a
mathematical guarantee: replace the phrase asserting that "a single pre-sharded
'probe bucket' (997 shards) covers all 128 PGs via the pigeonhole principle"
with wording that describes observed/empirical coverage (e.g., "empirically
covers most PGs") and explicitly note this is not a proof; keep the subsequent
explicit verification step that probes all Placement Groups in the
`.rgw.buckets.index` pool(s) every 15 seconds and retains the specific
implementation detail about using a pre-sharded probe bucket (997 shards) and
lightweight `rados stat` calls against 128 PGs for SLI derivation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 70bf288f-2205-4f27-8e6b-42426581fadb

📥 Commits

Reviewing files that changed from the base of the PR and between 5541437 and 86d8f7f.

📒 Files selected for processing (6)
  • Dockerfile
  • changes_summary.md
  • pkg/producers/config/producer.go
  • pkg/producers/config/producer_pgprobe.go
  • pkg/producers/pgprobe/mapping.go
  • pkg/producers/pgprobe/nats.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/producers/config/producer.go

Comment thread changes_summary.md Outdated
Comment thread Dockerfile
Comment thread Dockerfile Outdated
Comment thread pkg/producers/config/producer_pgprobe.go Outdated
Comment thread pkg/producers/pgprobe/mapping.go Outdated
@jrse jrse changed the title feat(opslog): implement tiered SLI collector with stale series reaping feat: RGW index pool PG probe for per-bucket SLI/SLO monitoring Jun 3, 2026
@senolcolak

senolcolak commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator

Hi Jan,
Thanks for thisi, the pigeonhole reasoning and cardinality math are genuinely clever work. brilliant idea

I want to be upfront before requesting changes: after checking what AWS, GCP, Azure, and the Google SRE Workbook actually do, I think we should rethink the framing. None of them derive per-bucket availability from storage-layer health, and your own Swift-vs-S3 example shows why — the metric can't catch RGW or protocol-handler bugs.

What we want to achieve: a customer-facing SLI that reflects what customers actually experience (HTTP success rate per tenant), backed by infrastructure leading indicators that page us before the SLI drops.

How to get there from your PR(my suggestions):

  1. Keep the PG probe, but reframe it as a Layer 3 leading indicator, rename bucket_sli_* → bucket_storage_health_*, drop the derived per-bucket gauges.
  2. Build the SLI from your opslog counter: add a protocol label, restore the latency histogram, aggregate per-tenant in PromQL.
  3. Add a small blackbox-exporter canary per protocol per region (~160 series) — this is the piece that catches the Swift case.
    I know this is a real scope change from what you proposed. below I attached the two files from the LLM's, after checking the other providers I decided to create a ADR, which would guide us.

analysis-industry-comparison.md
ADR-001-rgw-sli-slo-architecture.md

@jrse

jrse commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Hi Jan, Thanks for thisi, the pigeonhole reasoning and cardinality math are genuinely clever work. brilliant idea

I want to be upfront before requesting changes: after checking what AWS, GCP, Azure, and the Google SRE Workbook actually do, I think we should rethink the framing. None of them derive per-bucket availability from storage-layer health, and your own Swift-vs-S3 example shows why — the metric can't catch RGW or protocol-handler bugs.

What we want to achieve: a customer-facing SLI that reflects what customers actually experience (HTTP success rate per tenant), backed by infrastructure leading indicators that page us before the SLI drops.

How to get there from your PR(my suggestions):

  1. Keep the PG probe, but reframe it as a Layer 3 leading indicator, rename bucket_sli_* → bucket_storage_health_*, drop the derived per-bucket gauges.
  2. Build the SLI from your opslog counter: add a protocol label, restore the latency histogram, aggregate per-tenant in PromQL.
  3. Add a small blackbox-exporter canary per protocol per region (~160 series) — this is the piece that catches the Swift case.
    I know this is a real scope change from what you proposed. below I attached the two files from the LLM's, after checking the other providers I decided to create a ADR, which would guide us.

analysis-industry-comparison.md ADR-001-rgw-sli-slo-architecture.md

Hi Senol, thanks for feedback. After review i agree, using per bucket metrics for customer is not something we should do, it is much better to use the customer / tenant based approach and move the pg probe to the L3.

I changed the following.
The SLI is now:
prysm_rgw_request_total{tenant, protocol, operation, status_class, region}
Measured from actual customer HTTP requests (opslog — every request that hits RGW)

Aggregated per-tenant (no bucket label)

  • Includes protocol label so Swift-vs-S3 divergence is visible
  • Includes latency histogram (prysm_rgw_request_duration_seconds) for latency SLOs
  • Anonymous requests excluded (tenant="none" filtered out)

Stale series reaped (no traffic → no SLI contribution, matching AWS idle handling)
The L1 SLA query is exactly what AWS/GCP/Azure do:

1 - (sum by (tenant)(rate(prysm_rgw_request_total{status_class="5xx"}[5m]))
/ sum by (tenant)(rate(prysm_rgw_request_total[5m])))
"Backed by infrastructure leading indicators that page us before the SLI drops"

The PG probe is retained as a pure L3 signal:

prysm_rgw_index_probe_success{pgid, pool} — per-PG health
prysm_rgw_index_pool_available_pgs_ratio{pool} — pool-level aggregate
prysm_rgw_index_probe_latency_seconds{pgid, pool} — RADOS round-trip

These detect storage-layer degradation before it manifests as HTTP 5xx. No "SLI" in the naming, no per-bucket gauges, no conflation with the customer-facing metric.

i need to check, how to do this one :-) > 3. Add a small blackbox-exporter canary per protocol per region (~160 series) — this is the piece that catches the Swift case.

@jrse jrse force-pushed the feature/bucket-GET-LIST-SLI-metrics-tiered branch from 054f8bb to 67d92c1 Compare June 11, 2026 09:06
@jrse jrse force-pushed the feature/bucket-GET-LIST-SLI-metrics-tiered branch from 40a012e to ce82c54 Compare June 17, 2026 09:28
Implement a three-layer observability architecture for RGW object storage:

L1/L2 SLI (opslog producer):
- radosgw_request_total{tenant, protocol, operation, status_class, region}
- radosgw_request_duration_seconds histogram (7 fixed le buckets)
- Protocol detection (S3/Swift), 6 operation classes, stale series reaper
- Activated via --track-bucket-slo --region <region>

L3 PG Probe (new local-producer, requires -tags ceph):
- Probes all PGs in RGW index pools via rados stat every 15s
- radosgw_index_probe_success, radosgw_index_probe_latency_seconds
- radosgw_index_pool_available_pgs_ratio (leading indicator)
- Multi-pool support, hourly mapping refresh, pigeonhole-based coverage

Build:
- Multi-stage Dockerfile: runtime-noceph (default) and runtime-ceph
- Conditional compilation via //go:build ceph
- CI workflow for pg-probe container

Bug fix:
- Correct user/bucket label swap in radosgw_total_requests_per_bucket

Includes unit tests for SLI classification, protocol detection, status
parsing, stale reaping, and per-bucket label correctness.
@jrse jrse force-pushed the feature/bucket-GET-LIST-SLI-metrics-tiered branch from ce82c54 to ba0e7d5 Compare June 17, 2026 09:30
Signed-off-by: Jan Fabian Radon <jan.fabian.radon@sap.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors RGW SLI/SLO monitoring to (1) produce per-tenant request/latency SLIs from ops logs and (2) introduce a new Ceph build-tag–gated PG probe producer for RGW index-pool infrastructure health (L3 leading indicator), plus packaging/docs updates to support running and releasing both components.

Changes:

  • Replaces prior bucket-focused SLI tracking with a per-tenant SLI Prometheus collector (radosgw_request_total, radosgw_request_duration_seconds) including stale-series reaping and new region labeling.
  • Adds a new pg-probe local producer + config-wired producer (pg_probe) that probes representative RGW index shard objects per PG and exports pool/PG health metrics.
  • Updates CLI flags/env vars, docs/manifests, Docker multi-stage builds (with/without Ceph), and the release workflow to publish a Ceph-enabled image variant.

Reviewed changes

Copilot reviewed 23 out of 23 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pkg/producers/pgprobe/prometheus.go Defines and publishes PG probe Prometheus metrics and starts a metrics HTTP server.
pkg/producers/pgprobe/pgprobe.go Implements Ceph/RADOS connection, target discovery, and per-PG probing loop.
pkg/producers/pgprobe/config.go Introduces configuration struct for the pg-probe producer.
pkg/producers/opslog/sli.go Refactors operation classification to ADR-defined per-tenant SLI operation/protocol categories.
pkg/producers/opslog/README.md Updates ops-log producer documentation for new SLI metrics and stale-series reaping flags/env vars.
pkg/producers/opslog/prometheus.go Wires SLI collector config (region + TTL/reap settings) into Prometheus initialization.
pkg/producers/opslog/prometheus_total_requests.go Fixes parsing of per-bucket request counter key fields.
pkg/producers/opslog/prometheus_total_requests_test.go Adds regression test for per-bucket counter label parsing.
pkg/producers/opslog/prometheus_sli.go Implements custom Prometheus collector for per-tenant SLI metrics with background stale-series reaper.
pkg/producers/opslog/prometheus_sli_test.go Adds test helpers for collecting and locating emitted SLI metrics.
pkg/producers/opslog/metrics.go Routes TrackBucketSLO path to the new per-tenant SLI observer.
pkg/producers/opslog/metrics_test.go Updates SLI classification/protocol tests and adjusts SLI metric update tests.
pkg/producers/opslog/config.go Adds Region and SLI collector tuning fields to ops-log config structures.
pkg/producers/config/producer.go Adds a registry mechanism for build-tag–gated producer handlers.
pkg/producers/config/producer_pgprobe.go Registers and starts the pg_probe producer from YAML/global config (ceph build tag).
pkg/commands/producer_pg_probe.go Adds prysm local-producer pg-probe CLI command and env/flag configuration.
pkg/commands/producer_ops_log.go Adds --region and SLI stale/reap tuning flags/env merging for ops-log CLI.
ops-log-k8s-mutating-wh/README.md Updates webhook deployment examples to new env var names/sets (incl. SLI tracking).
ops-log-k8s-mutating-wh/manifest-examples/05-prysm-sidecar-secret.yaml Updates example secret with new tracking env vars.
ops-log-k8s-mutating-wh/manifest-examples/05-prysm-sidecar-configmap.yaml Updates example configmap with new tracking env vars.
docs/ops-log.md Updates documentation examples to the new env var set including SLI tracking.
Dockerfile Splits builds into no-Ceph and Ceph-enabled targets; adds runtime-ceph stage.
.github/workflows/release.yml Adds build/push + attestation for a Ceph-enabled image variant.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread pkg/producers/pgprobe/pgprobe.go Outdated
Comment on lines +133 to +141
func StartPrometheusServer(port int) {
go func() {
http.Handle("/metrics", promhttp.Handler())
log.Info().Int("port", port).Msg("starting prometheus metrics server for pg-probe")
if err := http.ListenAndServe(fmt.Sprintf(":%d", port), nil); err != nil {
log.Fatal().Err(err).Msg("prometheus metrics server failed")
}
}()
}
Comment on lines +123 to +125
if len(results) > 0 {
pgPoolAvailableRatio.With(poolLabels).Set(float64(successCount) / float64(len(results)))
}
Comment on lines +55 to +65
// LIST operations
case op == "list_bucket" || op == "list_buckets" || op == "list_objects" ||
op == "get_bucket_info" || op == "list_bucket_versions" ||
op == "list_bucket_multiparts":
return SLIOperationList, true

// Multipart operations
case op == "complete_multipart" || op == "init_multipart" ||
op == "abort_multipart" || op == "upload_part" ||
op == "list_multipart" || op == "list_bucket_multiparts":
return SLIOperationMultipart, true
Comment on lines +363 to 371
// Counter is now keyed by tenant+protocol+operation+status_class (no bucket)
beforeCounter := globalSLICollector.counterValue("tenant-slo-test", "s3", "get", "2xx")
assert.NotPanics(t, func() {
NewMetrics().Update(logEntry, config)
})
afterCounter := readCounterValue(t, sliRequestsTotal, "tenant-slo-test", "bucket-slo-test", "get", "2xx")
afterHist := readHistogramSampleCount(t, sliRequestDuration, "tenant-slo-test", "bucket-slo-test", "get")
afterCounter := globalSLICollector.counterValue("tenant-slo-test", "s3", "get", "2xx")

assert.Equal(t, beforeCounter+1, afterCounter, "SLI counter should increment")
assert.Equal(t, beforeHist+1, afterHist, "SLI histogram should record a sample")
}
Comment on lines +388 to 393
beforeCounter := globalSLICollector.counterValue("tenant-slo-zero", "s3", "get", "2xx")
NewMetrics().Update(logEntry, config)
afterCounter := readCounterValue(t, sliRequestsTotal, "tenant-slo-zero", "bucket-slo-zero", "get", "2xx")
afterHist := readHistogramSampleCount(t, sliRequestDuration, "tenant-slo-zero", "bucket-slo-zero", "get")
afterCounter := globalSLICollector.counterValue("tenant-slo-zero", "s3", "get", "2xx")

assert.Equal(t, beforeCounter+1, afterCounter, "SLI counter should increment even with zero latency")
assert.Equal(t, beforeHist+1, afterHist, "SLI histogram should record a sample even with zero latency")
}
Comment on lines +215 to +223
// List all .dir.* and .bucket.meta.* objects in the index pool
var shardObjects []string
var probeBucketMarker string

metaPrefix := ".bucket.meta." + probeBucket + ":"

iter, err := ioctx.Iter()
if err != nil {
return nil, fmt.Errorf("failed to create object iterator: %w", err)
Comment on lines +93 to +96
func PublishToPrometheus(results []ProbeResult, targets *ProbeTargets, cfg PGProbeConfig) {
successCount := 0
var totalLatency float64

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Jan Fabian Radon <jan.radon@jrse.de>

@senolcolak senolcolak left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Findings

🔴 Must Fix

1. Stale PG gauge leak — pkg/producers/pgprobe/prometheus.go

GaugeVec.With(labels).Set(1) / Set(0) accumulates label combinations forever. After a PG split or pool reconfiguration, old pgid entries linger indefinitely, polluting cardinality and misleading dashboards.

Fix: reset or delete stale labels before each cycle:

pgProbeSuccess.Reset()
pgProbeLatency.Reset()
for _, r := range results {
    // then re-populate
}

The sliCollector in opslog already solves this with a reaper — pgprobe needs the same treatment.


🟡 Should Fix

2. globalSLICollector package global causes test races — pkg/producers/opslog/prometheus_sli.go

var globalSLICollector *sliCollector is mutated directly in tests (globalSLICollector = newSLICollector(...)). Under go test -race with parallel tests this is a data race. Consider passing the collector as an explicit dependency via MetricsConfig, or at minimum add t.Cleanup to restore state after each test that mutates it.

3. getTotalPGsFromRadosmon silently returns 0 on error — pkg/producers/pgprobe/pgprobe.go

if err := conn.MonCommand(...); err != nil {
    log.Error().Err(err).Msg("failed to get pg_num")
    return 0  // caller uses this as TotalPGs
}

Returning 0 makes CoveredPGs < TotalPGs false, hiding the failure and silencing the warning that should fire. Return an error and let initPoolState propagate it.

4. Zero-latency observations silently dropped — pkg/producers/opslog/prometheus.go

if logEntry.TotalTime > 0 {   // ← drops sub-millisecond requests
    globalSLICollector.observeLatency(...)
}

TotalTime is in milliseconds, so any request completing in < 1 ms rounds to 0 and is never recorded in the histogram. Change the guard to >= 0 (or remove it entirely — observeLatency(0.0) is valid and lands in the le=0.05 bucket).


🔵 Notes

5. Credential exposure via CLI flags — pkg/commands/producer_pg_probe.go

--access-key and --secret-key are passed as flags, which means they appear in ps aux output. The getEnv fallback in producer_pgprobe.go already supports ACCESS_KEY/SECRET_KEY env vars — document this as the recommended path for production deployments.

6. Duplicate defaults — pkg/commands/producer_pg_probe.go + pkg/producers/config/producer_pgprobe.go

Defaults like Interval: 15 and ProbeTimeoutMs: 5000 are declared twice — once in init() flag defaults, once as if cfg.X <= 0 { cfg.X = default } guards. Pick one authoritative source to avoid drift.

7. No unit tests for pure pgprobe functions

groupShardsByBucket, classifyPGID (hex/PGID parsing), the JSON unmarshalling in getTotalPGsFromRadosmon, and the reap() TTL logic are all testable without RADOS. Table-driven tests for these would give good coverage at zero infrastructure cost.

8. Over-commenting in config struct — pkg/producers/pgprobe/config.go

Inline comments like // Probe interval in seconds (default: 15) restate what the field name and flag description already say. Per project conventions, comments should explain why, not what.

jrse and others added 7 commits June 25, 2026 13:42
GaugeVec.With(labels).Set() accumulates label combinations forever.
After a PG split or pool reconfiguration, old pgid entries would linger
indefinitely, polluting cardinality and misleading dashboards.

Reset pgProbeSuccess and pgProbeLatency at the start of each probe
cycle so only current PG IDs are exposed on /metrics.
- fix(pgprobe): return error from getPoolPGCount instead of silent 0
  Returning 0 hid failures and suppressed the coverage warning.
  Caller now propagates the error to initPoolState.

- fix(opslog): record sub-millisecond latencies in SLI histogram
  TotalTime=0 (sub-ms) was silently dropped by > 0 guard. Changed to
  >= 0 so 0ms requests land in the le=0.05 bucket. Only negative
  values (corrupted entries) are excluded.

- fix(opslog): prevent test race on globalSLICollector
  Added t.Cleanup to save/restore the package global in all tests that
  mutate it. Passes go test -race.

- chore(opslog): remove unused prometheus/dto imports from metrics_test
CLI flags are visible in ps aux, exposing credentials to other users
on the same host. Credentials are now exclusively sourced from
ACCESS_KEY and SECRET_KEY environment variables.

The config-file path (use-config mode) still supports access_key and
secret_key in YAML settings, which is not exposed in the process list.
Define DefaultInterval, DefaultProbeTimeoutMs, DefaultCephConfigPath,
etc. as constants in config.go. Reference them from StartMonitoring
guards, CLI flag defaults, and config-file dispatch — eliminating
three duplicate copies of the same magic numbers.
Table-driven tests for groupShardsByBucket and extractShardIndex
covering edge cases (empty input, wrong marker, non-numeric shard,
multiple buckets). Also tests default constant values and
default-application logic.

These tests require -tags ceph (librados headers) so they run in CI
Docker builds, not on developer macOS.
Field names and constants are self-documenting. Only keep comments
that explain why (env-only credentials, multi-pool rationale).

@fa-at-pulsit fa-at-pulsit left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check my few comments, few of them are blocker, imho

latency: make(map[sliLatencyKey]*sliLatencyState),
config: cfg,
counterDesc: prometheus.NewDesc(
"radosgw_request_total",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Naming mismatch between code and the ADR / your PR reply. Here the metrics are emitted as
radosgw_request_total and radosgw_request_duration_seconds (line 105), but ADR-001 and
your comment on this PR call them prysm_rgw_request_total / prysm_rgw_request_duration_seconds.


name: Release Artifacts
on:
push:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

release.yml is the only workflow in the repo and it fires solely on push: tags: v*.
That means the entire pgprobe producer (everything behind //go:build ceph:
pkg/producers/pgprobe/* and pkg/commands/producer_pg_probe.go) plus its -tags ceph tests
are compiled only during the tag/release Docker build — never on a PR. The only check on
this PR is the CodeRabbit bot; there's no go build / go test / go vet in CI at all.

So a plain compile error or a broken test in the ceph path won't surface until someone cuts
a release. Please add a PR-triggered job that runs go build -tags ceph ./... and
go test -tags ceph ./... inside a container with librados/ceph-dev headers (the same deps
the runtime-ceph Dockerfile stage installs). Ideally a non-ceph go test ./... job too.

}
pgPoolTotalPGs.With(poolLabels).Set(float64(targets.TotalPGs))
pgPoolCoveredPGs.With(poolLabels).Set(float64(targets.CoveredPGs))
probeCycleDuration.With(poolLabels).Set(totalLatency / 1000.0)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totalLatency is the sum of every per-PG probe latency, not the wall-clock duration of
the cycle. Published under a metric named ..._cycle_duration_seconds, that's misleading —
under any concurrency it has no physical meaning, and even sequentially it double-counts vs.
"how long did the cycle take."

runProbes already measures the real thing (time.Since(now)); thread that value through
and Set it here instead of the latency sum..... imho

return
}

sliOperation, ok := classifySLIOperation(logEntry.Operation)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any operation outside the six recognized classes makes classifySLIOperation return
false (sli.go:68), so observeSLI bails and the request never enters the SLI — including
its 5xx. That drops ACL/policy/admin/create-bucket and anything unmapped from both
numerator and denominator of the availability ratio.

If that's a deliberate "data-plane object ops only" SLI, fine — but it should be documented
next to the metric, because an availability figure that silently excludes whole operation
classes can hide real customer-visible errors. Alternatively, bucket everything unmatched
into an operation="other" class so the denominator stays complete.


existingOpsLogPreRunE := opsLogCmd.PreRunE
opsLogCmd.PreRunE = func(cmd *cobra.Command, args []string) error {
if opsTrackBucketSLO && !opsPromEnabled {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIP: check if tracked - is enabled via TRACK_BUCKET_SLO env or via the TrackEverything

continue
}

pgID, err := getObjectPG(conn, pool, obj)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getObjectPG issues an osd map mon command per shard, so a 997-shard probe bucket = ~997
mon round-trips per pool at startup and again on every mapping refresh (default hourly,
pgprobe.go:131). That's fine at hourly cadence, but worth a comment so it isn't a surprise
on a busy mon - and note the failure mode: a per-shard error here is warn+continue, so a
flaky mon silently shrinks CoveredPGs (and thus the available-ratio denominator) without
failing the cycle. Consider a summary warn if CoveredPGs drops materially between refreshes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants