Skip to content

Rework Ceph collector as a Prometheus complement - #23357

Draft
ktsaou wants to merge 1 commit into
netdata:masterfrom
ktsaou:ceph-collector-improvements
Draft

Rework Ceph collector as a Prometheus complement#23357
ktsaou wants to merge 1 commit into
netdata:masterfrom
ktsaou:ceph-collector-improvements

Conversation

@ktsaou

@ktsaou ktsaou commented Aug 1, 2026

Copy link
Copy Markdown
Member

Summary

Rework the native Ceph Dashboard collector as a complement to Ceph's Prometheus producers rather than a second overlapping continuous telemetry path.

  • keep only a lightweight Dashboard API/authentication/active-MGR availability probe enabled by default
  • add independent controls for every legacy metric feature and suppress unused Dashboard requests
  • add bounded on-demand Functions for health RCA, OSDs, pools, daemons, RGW multisite, and explicit RGW quota targets
  • support secure standby-to-active MGR discovery without losing API paths or forwarding credentials to untrusted origins
  • correct OSD pagination, rate semantics, capacity/pool arithmetic, missing-value handling, and dynamic chart lifecycle
  • add source-derived contract fixtures for Reef 18.2.8, Squid 19.2.5, and Tentacle 20.2.2
  • document distributed deployment, retention, sovereignty, Prometheus/Grafana/Zabbix coexistence, and RGW telemetry boundaries

Why

The existing collector predates the current Ceph telemetry architecture. It continuously polls Dashboard endpoints for data now exposed more directly by the MGR Prometheus module and per-node ceph-exporter, while also containing correctness and compatibility problems on current Ceph releases.

Ceph's exporters refresh their own caches independently of scraper cadence. Matching Netdata scrape intervals to those configured producer periods avoids duplicate or skipped cached samples; it does not change producer refresh work. Dashboard REST requests are different because each request performs work in the active MGR, so overlapping or potentially expensive native features are now opt-in.

User-visible changes

  • Existing jobs without a collect: section now enable only collect.dashboard_api_status.
  • All 16 existing cluster, OSD, and pool metric features default to false and remain individually re-enableable with their shipped chart identities preserved.
  • Health, OSD, pool, and daemon Functions are enabled by default and bounded to 500 rows.
  • RGW multisite and quota Functions are disabled by default and bounded to 100 rows; quota collection requires explicit user, bucket, or account targets.
  • RGW request rates, method latency, HTTP status codes, and client attribution are deliberately not fabricated from Dashboard data; those require RGW request/access logs.

Distributed deployment model

  • scrape host-local ceph-exporter once on each Ceph node
  • scrape the active MGR Prometheus endpoint once per cluster
  • run one native Dashboard collector job per cluster on a designated Child or on-prem Parent
  • stream Children to on-prem Parents when centralized retention and data-sovereignty control are required

Validation

  • go test -race ./plugin/go.d/collector/ceph/... -count=1
  • go vet ./plugin/go.d/collector/ceph/...
  • go test ./plugin/go.d/collector/... -count=1
  • integration metadata/docs generation and taxonomy checks
  • 44 taxonomy unit tests
  • Function responses validated against FUNCTION_UI_SCHEMA.json
  • JSON Schema/default parity, formatting, diff, and sensitive-data checks
  • authenticated live Squid smoke against both the active Dashboard and a real standby 303 redirect: 141 metrics across 77 enabled charts in both cases
  • live unauthenticated active/standby behavior checked on Reef, Squid, and Tentacle; authenticated Reef/Tentacle smoke was not attempted after SSH host-key verification failed, and host trust was not bypassed

Related work

Complements #23330. This change does not modify the Prometheus profiles from that pull request.


Summary by cubic

Reworked the ceph go.d collector to complement Ceph’s Prometheus producers. Default is a lightweight Dashboard API/auth/active‑MGR status probe with on-demand Functions and hardened active‑MGR discovery, plus OSD/pool correctness fixes.

  • New Features

    • Legacy charts are opt‑in; only ceph.dashboard_api_status is enabled by default.
    • Added Functions: health, OSDs, pools, daemons (enabled, bounded), and RGW multisite/quotas (opt‑in, bounded, explicit targets).
    • Secure standby→active MGR discovery via allowed_redirect_origins; no credential forwarding to unknown hosts.
    • Preserved chart IDs; added taxonomy/docs and a new health alert for Dashboard API availability.
  • Migration

    • Existing jobs without collect: now publish only dashboard_api_status; enable needed legacy charts under collect.*.
    • Run one Dashboard collector job per cluster; scrape ceph-exporter on each node and the mgr Prometheus endpoint once.
    • Set allowed_redirect_origins for HA clusters and supply bearer_token_file or username/password.
    • Use Functions for RCA and RGW data; set explicit RGW quota targets to collect quotas.

Written for commit fee3d22. Summary will update on new commits.

Review in cubic

@github-actions github-actions Bot added area/docs area/health area/collectors Everything related to data collection collectors/go.d area/metadata Integrations metadata area/go labels Aug 1, 2026
@sonarqubecloud

sonarqubecloud Bot commented Aug 1, 2026

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot 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.

8 issues found across 31 files

Confidence score: 3/5

  • In src/go/plugin/go.d/collector/ceph/collect_osd.go, pagination can run indefinitely when X-Total-Count is missing and pages repeat, which can stall the OSD run and block later collection cycles for the job — add a hard pagination bound and/or repeated-page detection to guarantee termination.
  • In src/go/plugin/go.d/collector/ceph/collect_pools.go, pool identity/state reuse can apply the wrong synthetic overflow/full chart set to a pool named other (or vice versa), leading to misleading pool metrics over time — track role transitions explicitly or replace entity charts when the role changes.
  • In src/go/plugin/go.d/collector/ceph/charts.go, a startup-time Dashboard outage can skip creating ceph.dashboard_api_status, so the API-unavailable alert cannot trigger until a later successful probe — register the status chart up front with a default unavailable state.
  • In src/go/plugin/go.d/collector/ceph/cephfunc/router.go and src/go/plugin/go.d/collector/ceph/func_deps.go, function exposure/snapshot consistency can drift (disabled RGW methods still shown, OSD inventory can mix across paginated changes), causing confusing 404s and inconsistent function outputs — gate published methods by config and preserve a stable pagination snapshot/total across a run.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/go/plugin/go.d/collector/ceph/collect_pools.go">

<violation number="1" location="src/go/plugin/go.d/collector/ceph/collect_pools.go:81">
P2: A pool named `other` can reuse a prior synthetic overflow state (or vice versa) across polls, leaving it with the wrong chart set. Track overflow/full state or replace the existing entity charts when that role changes; otherwise a newly selected real `other` emits dimensions with no charts, while a new overflow may retain full charts.</violation>
</file>

<file name="src/go/plugin/go.d/collector/ceph/charts.go">

<violation number="1" location="src/go/plugin/go.d/collector/ceph/charts.go:54">
P2: A Dashboard outage on a fresh collector startup produces no `ceph.dashboard_api_status` chart, so the default API-unavailable alert cannot fire until one successful identity probe has already occurred. Consider registering an unlabeled/status chart before the identity probe, or otherwise creating it on the failure path without consuming the later FSID-labeled cluster-chart initialization.</violation>
</file>

<file name="src/go/plugin/go.d/collector/ceph/metadata.yaml">

<violation number="1" location="src/go/plugin/go.d/collector/ceph/metadata.yaml:844">
P3: The Function UI columns document OSD and RGW quota `utilization` with `unit: percent`, but the runtime `cephfunc` schema registers these columns with unit `%`. Keep the metadata and the emitted Function schema consistent so the documented column type/unit matches what operators actually see in the Function table.</violation>
</file>

<file name="src/go/plugin/go.d/collector/ceph/collect_osd.go">

<violation number="1" location="src/go/plugin/go.d/collector/ceph/collect_osd.go:116">
P2: A Dashboard response that omits `X-Total-Count` and ignores pagination can make an OSD collection run forever, preventing subsequent collection cycles. Consider detecting repeated page identities (or otherwise bounding headerless pagination) and return an error before continuing.</violation>
</file>

<file name="src/go/plugin/go.d/collector/ceph/cephfunc/router.go">

<violation number="1" location="src/go/plugin/go.d/collector/ceph/cephfunc/router.go:28">
P2: Disabled RGW Functions remain published in the Function UI and fail only after invocation with 404. Bind each method's availability to the job configuration (or avoid publishing disabled methods) so opt-in methods are not advertised before enabled.</violation>
</file>

<file name="src/go/plugin/go.d/collector/ceph/testdata/v20.2.2/dashboard_contract.json">

<violation number="1" location="src/go/plugin/go.d/collector/ceph/testdata/v20.2.2/dashboard_contract.json:244">
P3: The RGW account fixture's fields don't line up with what the collector extracts for an account quota row: the fixture provides `id`/`name`, but lookupRGWQuota reads `account_id`/`account` for the Account attribution and `stats` for usage. As a result the contract test's account target never exercises those fields (Account comes out empty, no usage), so the fixture doesn't validate the account contract it's meant to guard. Consider aligning the fixture's key names/usage block with the fields the parser reads, or the mismatch stays invisible to the test.</violation>
</file>

<file name="src/go/plugin/go.d/collector/ceph/func_deps.go">

<violation number="1" location="src/go/plugin/go.d/collector/ceph/func_deps.go:163">
P2: OSD Function results can represent a mixed inventory snapshot when OSD membership changes during pagination: a later `X-Total-Count` overwrites the earlier value and rows may have been skipped or shifted. Preserve the first advertised total and reject/retry when a later page differs, matching `fetchAllOSDs`.</violation>
</file>

<file name="src/go/plugin/go.d/collector/ceph/client.go">

<violation number="1" location="src/go/plugin/go.d/collector/ceph/client.go:1">
P3: The set of headers that the Ceph client forbids (authorization, cookie, host) is now enforced in two separate places: newCephClient in client.go and validateConfig in init.go. Keeping the same rule duplicated in two layers makes it easy for the two copies to drift (e.g., adding a new managed header in one place but not the other), and one of them is functionally redundant since newCephClient is always constructed after validation. Consider keeping the single source of truth in validateConfig (or in the client) and having the other path rely on it.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Agent as Netdata Agent Runtime
    participant Coll as Ceph Collector
    participant Client as Ceph API Client
    participant Standby as Standby MGR Dashboard
    participant Active as Active MGR Dashboard
    participant Router as Ceph Function Router
    participant UI as Netdata UI - Function Caller

    Note over Agent, UI: Ceph Prometheus producers and per-node ceph-exporter remain responsible for continuous telemetry. This Dashboard job is complementary and opt-in by default.

    Agent->>Coll: Periodic Collect/Check
    Coll->>Client: Ensure cluster identity (GET /api/health/get_cluster_fsid)
    Client->>Client: Cached active MGR base + JWT
    opt No cached active base - NEW secure discovery
        Client->>Standby: Discovery probe (no Authorization/Cookie)
        Standby-->>Client: 303 redirect to active MGR
        Client->>Client: Validate origin in allowed_redirect_origins
        alt Untrusted origin or not_follow_redirects
            Client-->>Coll: Fail without forwarding credentials
        else Trusted origin
            Client->>Active: Discovery probe (no credentials)
            Active-->>Client: 401
            Client->>Active: POST /api/auth (JSON username/password)
            Active-->>Client: JWT
        end
    end
    Client->>Active: GET FSID with Bearer JWT
    Active-->>Client: fsid
    Client-->>Coll: active MGR base + fsid
    Coll->>Coll: Publish dashboard_api_status chart with fsid label

    opt Optional legacy features enabled via collect.*
        Coll->>Client: Feature-gated Dashboard requests (health/minimal, paginated OSDs, pool stats)
        Client->>Active: Bearer JWT requests
        Active-->>Client: Response data
        Client-->>Coll: Metric sections
    end

    UI->>Agent: Invoke on-demand Function
    Agent->>Router: Handle(method, params)
    Router->>Router: Check method enabled, limit, timeout
    alt Disabled method (RGW functions default off)
        Router-->>UI: 404 NotFound
    else Enabled method
        Router->>Client: Bounded Dashboard queries for requested rows
        Client->>Active: Bearer JWT
        Active-->>Client: Row data
        Router->>Router: Sort, cap, truncate to limit
        Router-->>UI: Table Function response
    end

    Note over Client, Active: Runtime failover: 401 invalidates JWT (relogin once). Redirect invalidates active base and rediscovers.
Loading

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

seen := make(map[string]bool, len(selected))
for _, sample := range selected {
seen[sample.key] = true
if _, ok := c.seenPools[sample.key]; !ok {

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.

P2: A pool named other can reuse a prior synthetic overflow state (or vice versa) across polls, leaving it with the wrong chart set. Track overflow/full state or replace the existing entity charts when that role changes; otherwise a newly selected real other emits dimensions with no charts, while a new overflow may retain full charts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/go/plugin/go.d/collector/ceph/collect_pools.go, line 81:

<comment>A pool named `other` can reuse a prior synthetic overflow state (or vice versa) across polls, leaving it with the wrong chart set. Track overflow/full state or replace the existing entity charts when that role changes; otherwise a newly selected real `other` emits dimensions with no charts, while a new overflow may retain full charts.</comment>

<file context>
@@ -3,56 +3,157 @@
+	seen := make(map[string]bool, len(selected))
+	for _, sample := range selected {
+		seen[sample.key] = true
+		if _, ok := c.seenPools[sample.key]; !ok {
+			c.seenPools[sample.key] = &entityState{}
+			c.addPoolCharts(sample.key, !sample.overflow)
</file context>

)

var clusterCharts = collectorapi.Charts{
dashboardAPIStatusChart.Copy(),

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.

P2: A Dashboard outage on a fresh collector startup produces no ceph.dashboard_api_status chart, so the default API-unavailable alert cannot fire until one successful identity probe has already occurred. Consider registering an unlabeled/status chart before the identity probe, or otherwise creating it on the failure path without consuming the later FSID-labeled cluster-chart initialization.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/go/plugin/go.d/collector/ceph/charts.go, line 54:

<comment>A Dashboard outage on a fresh collector startup produces no `ceph.dashboard_api_status` chart, so the default API-unavailable alert cannot fire until one successful identity probe has already occurred. Consider registering an unlabeled/status chart before the identity probe, or otherwise creating it on the failure path without consuming the later FSID-labeled cluster-chart initialization.</comment>

<file context>
@@ -48,6 +51,7 @@ const (
 )
 
 var clusterCharts = collectorapi.Charts{
+	dashboardAPIStatusChart.Copy(),
 	clusterStatusChart.Copy(),
 	clusterHostsCountChart.Copy(),
</file context>

func (c *Collector) fetchAllOSDs(ctx context.Context) ([]apiOsdResponse, error) {
var all []apiOsdResponse
total := -1
for offset := 0; ; offset += osdPageSize {

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.

P2: A Dashboard response that omits X-Total-Count and ignores pagination can make an OSD collection run forever, preventing subsequent collection cycles. Consider detecting repeated page identities (or otherwise bounding headerless pagination) and return an error before continuing.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/go/plugin/go.d/collector/ceph/collect_osd.go, line 116:

<comment>A Dashboard response that omits `X-Total-Count` and ignores pagination can make an OSD collection run forever, preventing subsequent collection cycles. Consider detecting repeated page identities (or otherwise bounding headerless pagination) and return an error before continuing.</comment>

<file context>
@@ -3,64 +3,285 @@
+func (c *Collector) fetchAllOSDs(ctx context.Context) ([]apiOsdResponse, error) {
+	var all []apiOsdResponse
+	total := -1
+	for offset := 0; ; offset += osdPageSize {
+		query := url.Values{
+			"offset": {strconv.Itoa(offset)},
</file context>

return &router{deps: deps, config: config}
}

func Methods() []funcapi.FunctionConfig {

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.

P2: Disabled RGW Functions remain published in the Function UI and fail only after invocation with 404. Bind each method's availability to the job configuration (or avoid publishing disabled methods) so opt-in methods are not advertised before enabled.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/go/plugin/go.d/collector/ceph/cephfunc/router.go, line 28:

<comment>Disabled RGW Functions remain published in the Function UI and fail only after invocation with 404. Bind each method's availability to the job configuration (or avoid publishing disabled methods) so opt-in methods are not advertised before enabled.</comment>

<file context>
@@ -0,0 +1,428 @@
+	return &router{deps: deps, config: config}
+}
+
+func Methods() []funcapi.FunctionConfig {
+	return []funcapi.FunctionConfig{
+		{ID: MethodHealth, Name: "Ceph Health", UpdateEvery: 10, Help: "Detailed Ceph health checks and RCA messages."},
</file context>

if err != nil || parsed < 0 {
return cephfunc.OSDResult{}, errors.New("OSD inventory returned an invalid X-Total-Count")
}
total = parsed

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.

P2: OSD Function results can represent a mixed inventory snapshot when OSD membership changes during pagination: a later X-Total-Count overwrites the earlier value and rows may have been skipped or shifted. Preserve the first advertised total and reject/retry when a later page differs, matching fetchAllOSDs.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/go/plugin/go.d/collector/ceph/func_deps.go, line 163:

<comment>OSD Function results can represent a mixed inventory snapshot when OSD membership changes during pagination: a later `X-Total-Count` overwrites the earlier value and rows may have been skipped or shifted. Preserve the first advertised total and reject/retry when a later page differs, matching `fetchAllOSDs`.</comment>

<file context>
@@ -0,0 +1,962 @@
+			if err != nil || parsed < 0 {
+				return cephfunc.OSDResult{}, errors.New("OSD inventory returned an invalid X-Total-Count")
+			}
+			total = parsed
+		}
+		pageLength := len(page)
</file context>

- {name: total_bytes, type: integer, unit: bytes, visibility: hidden, description: Total OSD capacity.}
- {name: used_bytes, type: integer, unit: bytes, description: Used OSD capacity.}
- {name: available_bytes, type: integer, unit: bytes, description: Available OSD capacity.}
- {name: utilization, type: float, unit: percent, description: OSD utilization.}

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.

P3: The Function UI columns document OSD and RGW quota utilization with unit: percent, but the runtime cephfunc schema registers these columns with unit %. Keep the metadata and the emitted Function schema consistent so the documented column type/unit matches what operators actually see in the Function table.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/go/plugin/go.d/collector/ceph/metadata.yaml, line 844:

<comment>The Function UI columns document OSD and RGW quota `utilization` with `unit: percent`, but the runtime `cephfunc` schema registers these columns with unit `%`. Keep the metadata and the emitted Function schema consistent so the documented column type/unit matches what operators actually see in the Function table.</comment>

<file context>
@@ -211,218 +595,394 @@ modules:
+              - {name: total_bytes, type: integer, unit: bytes, visibility: hidden, description: Total OSD capacity.}
+              - {name: used_bytes, type: integer, unit: bytes, description: Used OSD capacity.}
+              - {name: available_bytes, type: integer, unit: bytes, description: Available OSD capacity.}
+              - {name: utilization, type: float, unit: percent, description: OSD utilization.}
+              - {name: read_bytes_per_sec, type: float, unit: bytes/s, visibility: hidden, description: Current read rate.}
+              - {name: write_bytes_per_sec, type: float, unit: bytes/s, visibility: hidden, description: Current write rate.}
</file context>

"max_objects": 100,
"max_size": 1000
},
"id": "RGW00000000000000001",

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.

P3: The RGW account fixture's fields don't line up with what the collector extracts for an account quota row: the fixture provides id/name, but lookupRGWQuota reads account_id/account for the Account attribution and stats for usage. As a result the contract test's account target never exercises those fields (Account comes out empty, no usage), so the fixture doesn't validate the account contract it's meant to guard. Consider aligning the fixture's key names/usage block with the fields the parser reads, or the mismatch stays invisible to the test.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/go/plugin/go.d/collector/ceph/testdata/v20.2.2/dashboard_contract.json, line 244:

<comment>The RGW account fixture's fields don't line up with what the collector extracts for an account quota row: the fixture provides `id`/`name`, but lookupRGWQuota reads `account_id`/`account` for the Account attribution and `stats` for usage. As a result the contract test's account target never exercises those fields (Account comes out empty, no usage), so the fixture doesn't validate the account contract it's meant to guard. Consider aligning the fixture's key names/usage block with the fields the parser reads, or the mismatch stays invisible to the test.</comment>

<file context>
@@ -0,0 +1,348 @@
+        "max_objects": 100,
+        "max_size": 1000
+      },
+      "id": "RGW00000000000000001",
+      "name": "account-a",
+      "tenant": "tenant"
</file context>

@@ -0,0 +1,528 @@
// SPDX-License-Identifier: GPL-3.0-or-later

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.

P3: The set of headers that the Ceph client forbids (authorization, cookie, host) is now enforced in two separate places: newCephClient in client.go and validateConfig in init.go. Keeping the same rule duplicated in two layers makes it easy for the two copies to drift (e.g., adding a new managed header in one place but not the other), and one of them is functionally redundant since newCephClient is always constructed after validation. Consider keeping the single source of truth in validateConfig (or in the client) and having the other path rely on it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/go/plugin/go.d/collector/ceph/client.go, line 1:

<comment>The set of headers that the Ceph client forbids (authorization, cookie, host) is now enforced in two separate places: newCephClient in client.go and validateConfig in init.go. Keeping the same rule duplicated in two layers makes it easy for the two copies to drift (e.g., adding a new managed header in one place but not the other), and one of them is functionally redundant since newCephClient is always constructed after validation. Consider keeping the single source of truth in validateConfig (or in the client) and having the other path rely on it.</comment>

<file context>
@@ -0,0 +1,528 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+package ceph
</file context>

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

Labels

area/collectors Everything related to data collection area/docs area/go area/health area/metadata Integrations metadata collectors/go.d

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant