Skip to content

fix: bound tenant metrics on successful control paths - #806

Merged
srstack merged 5 commits into
mainfrom
fix/metrics-cardinality-p0
Jul 27, 2026
Merged

fix: bound tenant metrics on successful control paths#806
srstack merged 5 commits into
mainfrom
fix/metrics-cardinality-p0

Conversation

@srstack

@srstack srstack commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • aggregate tenant HTTP transport bytes by direction only
  • retain tenant attribution for provisioning failures and control-plane 4xx/5xx, while aggregating successful control-plane requests
  • add metric contract tests and shared-smoke metric-shape guards

Validation

  • go test ./pkg/metrics ./pkg/server -count=1
  • go test ./... -run ^ -count=1
  • bash -n e2e/shared-smoke-test/cases/metrics-consistency.sh
  • make lint
  • make build-server
  • git diff --check

Summary by CodeRabbit

  • Metrics
    • Reduced metric label cardinality across tenant request, HTTP byte, and business-event series.
    • Tenant/org labels are now limited to applicable data-plane and failure scenarios; control-plane success requests are aggregated without tenant labels.
    • Removed the tenant HTTP transport bytes metric from exported output and dashboard documentation.
  • Bug Fixes
    • Added/strengthened consistency checks to prevent unexpected high-cardinality or tenant-labeled series.
  • Documentation
    • Updated Grafana tenant-usage dashboard and tenant metrics contract.
  • Tests
    • Updated and added assertions in unit, server, auth, and e2e smoke tests to match the new metric semantics.

Copilot AI review requested due to automatic review settings July 27, 2026 07:15
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7bf3e95b-c0a1-4297-867d-6314415664e5

📥 Commits

Reviewing files that changed from the base of the PR and between 025988f and d682053.

📒 Files selected for processing (8)
  • docs/grafana/README.md
  • docs/grafana/drive9-tenant-usage-dashboard.json
  • e2e/shared-smoke-test/cases/metrics-consistency.sh
  • pkg/metrics/operations.go
  • pkg/metrics/operations_test.go
  • pkg/server/instrumentation.go
  • pkg/server/server_test.go
  • pkg/tenant/pool_test.go
💤 Files with no reviewable changes (3)
  • e2e/shared-smoke-test/cases/metrics-consistency.sh
  • docs/grafana/drive9-tenant-usage-dashboard.json
  • pkg/server/instrumentation.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/metrics/operations_test.go

📝 Walkthrough

Walkthrough

Tenant request and business-event metrics now conditionally emit tenant labels. Tenant HTTP byte metrics are removed from recording, instrumentation, tests, and dashboard documentation. Metric contract assertions and smoke checks are updated for aggregate-only control-plane successes.

Changes

Tenant metrics cardinality

Layer / File(s) Summary
Request and event attribution rules
pkg/metrics/operations.go, pkg/metrics/operations_test.go, pkg/server/auth_test.go, pkg/tenant/pool_test.go
Request labels are limited by surface and status class; business-event labels use result-aware failure attribution; related assertions are updated.
HTTP byte metric removal
pkg/metrics/operations.go, pkg/server/instrumentation.go, pkg/metrics/operations_test.go, pkg/server/server_test.go
Tenant HTTP byte instruments and APIs are removed, along with server emission and corresponding expectations.
Metric contract and validation updates
docs/grafana/README.md, docs/grafana/drive9-tenant-usage-dashboard.json, e2e/shared-smoke-test/cases/metrics-consistency.sh
Documentation and the dashboard remove HTTP transport bytes, while smoke checks reject tenant-labeled control-plane 2xx series.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: mornyx, qiffang

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the core change of aggregating successful control-plane tenant metrics to reduce cardinality.
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.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/metrics-cardinality-p0

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.

Copilot AI 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.

Pull request overview

This PR tightens the Prometheus metric label cardinality for tenant-related metrics by aggregating successful control-plane request series (and HTTP transport bytes) while preserving tenant/org attribution for data-plane traffic and error paths.

Changes:

  • Make drive9_tenant_requests_total conditionally include tenant_id/tidbcloud_org_id based on (surface, status_class) to bound high-cardinality series.
  • Aggregate drive9_tenant_http_bytes_total by direction only (no tenant/org labels).
  • Update metric contract tests and add an e2e shared-smoke guard to ensure metric shape stays bounded.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
pkg/server/instrumentation.go Switch tenant HTTP byte recording to the new aggregate-only bytes API.
pkg/server/auth_test.go Adjust status-route metric assertions to expect aggregated (non-tenant-labeled) success series.
pkg/metrics/operations.go Implement conditional tenant attribution for request counters; aggregate HTTP byte metric by direction only; narrow business-event tenant labeling to failures.
pkg/metrics/operations_test.go Update metric contract tests to reflect new label/attribution rules for requests, events, and bytes.
e2e/shared-smoke-test/cases/metrics-consistency.sh Add metric-shape guards (bounded series count for HTTP bytes; no tenant labels on control-plane 2xx request series).

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

Comment thread pkg/metrics/operations_test.go
Comment thread pkg/metrics/operations.go

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 025988f0c4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread pkg/metrics/operations.go Outdated
Comment thread pkg/metrics/operations.go Outdated
Comment thread pkg/metrics/operations.go

@srstack srstack left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

LGTM.

Verified locally on 2bd0a80d in an isolated worktree: go build ./... clean, go vet ./pkg/... clean, and pkg/metrics + pkg/server pass with -count=1. Net −12 lines (+96/−108), changes are tightly scoped with no unrelated refactoring.

This implements the three P0 items from the cardinality analysis, and the alert side needs no changes at all — which I verified rather than assumed.

Measured baseline this addresses

From dev-tke-bj (cls-9rse7rff, context verified, env=dev, 3 pods), scraping :9009/metrics directly since that cluster has no vmagent: 363,033 lines per pod, under a 10k-tenant load test.

The key finding was that surface="fs" had exactly 10,000 distinct tenants — matching the load test precisely — while tenant_requests_total had 31,655. The extra 21,655 were all surface="provision", and cross-checking confirmed the leak boundary was clean: tenant_storage_bytes and fs_events_rows were strict subsets of the fs set (empty difference), while tenant_http_bytes_total had 21,655 tenants outside it.

The reason this matters beyond the current numbers: tenant_count{status="active"} is 574,071, and only 21,655 have gone through the HTTP provision path so far. Provisioning is a one-shot lifecycle event, but the series it left behind persisted forever on whichever pod served the request — the WorkMetricsCleanup path added in #802 only fires on deleting/deleted transitions, so active tenants never get cleaned. This was the only genuinely unbounded metric in the system, on track for ~26× current cardinality.

The three changes

1. tenant_http_bytes_total removed entirely (−63,310)

Right call over just dropping the tenant label. It had zero alert consumers, and its only dashboard panel was already broken: HTTP Transport Bytes grouped and filtered by surface, which #802 had removed, so it had been rendering empty since then. With tenant_file_bytes_total confirmed as the billing metric — its 7 call sites all record logical file bytes on fs/upload read/write paths, independent of whether data lands in DB or S3 — the transport-layer counter had no remaining purpose. Declaration, both Record* wrappers, the private helper, both call sites, the dead panel, and both README lines all go together. No orphaned code.

2. tenant_provision attribution split by result (−~42,500)

Successful provisioning (ok/accepted/shared_pool) drops the tenant labels; failures keep them. This reuses isOperationFailureResult from #802, which already matches error and cluster_error, with quota_error matching via the _error suffix rule.

This is the design point I'd flag as the most important one in the PR. My first suggestion was to drop tenant_provision from the whitelist outright and rely on tenant_operation_failures_total for attribution — that was wrong, and I checked before asserting it here: provisioning failures are reported only through metricEvent(ctx, "tenant_provision", "result", ...), and tenant_operation_failures_total is populated exclusively by RecordTenantOperation*, which the provision path never calls (its only instrumentation is a single setRequestMetricTenant at server.go:5589). Dropping the whitelist entry would have left Drive9TenantProvisionFailures with no backing metric. Splitting by result keeps per-tenant failure attribution while bounding the unbounded success path — consistent with the "failures only carry tenant" philosophy #802 established.

3. Control-plane surfaces split by status class (−~23,500)

Data-plane surfaces (fs, upload, object_store, sql, events, journal, git_workspace) always carry tenant labels; control-plane surfaces only on 4xx/5xx. Measured impact is negligible on the savings — 21,569 of provision's 21,655 series were 2xx, and all 1,969 status series were — while preserving per-tenant attribution for control-plane errors.

Alert compatibility: verified, zero runbooks changes

I checked each affected rule against the new label shapes:

  • Drive9TenantProvisionFailures selects result=~"error|cluster_error" with tenant_id!="" — lands exactly on the branch isOperationFailureResult matches, so per-tenant attribution survives.
  • Drive9TenantSchemaInitFailures selects event="tenant_schema_init" with tenant_id!="" — unconditionally whitelisted.
  • Drive9TenantServerErrorRateHigh reads through the drive9_tenant_requests:rate5m:* recording rule, whose surface=~"fs|upload|object_store|sql|events|journal|git_workspace" filter is identical to the new tenantAttributedSurface data-plane list.

That last one is worth noting: the runbooks side had already narrowed to these seven surfaces, so the whitelist here is aligned with an existing contract rather than newly invented.

The self-caught regression in 575c4a8c

Worth calling out. The first version wrote:

return event == "tenant_provision" && isOperationFailureResult(result)

which silently dropped auth, tenant_schema_init, and tenant_status from the whitelist — meaning Drive9TenantSchemaInitFailures would have gone false-green. Exactly the failure class this whole effort has been fighting. Catching and reverting to the switch form before review is the right instinct.

Cardinality guard

Adding the assertion to e2e/shared-smoke-test/cases/metrics-consistency.sh — control-plane 2xx series must not carry tenant_id, fail if they do — is more valuable than a unit test here, because it validates real /metrics output. This is the gate whose absence let the original problem through.

Minor, non-blocking

docs/grafana/README.md still documents drive9_tenant_requests_total as carrying result unconditionally, and drive9_tenant_file_bytes_total as carrying surface/action. Both were removed in #802, so the doc drift predates this PR — but since it already edits this file, correcting those two lines would be a cheap win. The $surface template variable in the usage dashboard is also worth a look now that the panel using it is gone.

Remaining work from the analysis (out of scope here)

  • P1 — shared_db_pool_*: 5,698 physical pools produce ~39,886 series across four metrics. soft_max/hard_max are configuration constants and spending_limit is per-org (only 2 distinct orgs measured), so exporting them per-org instead of per-pool saves ~17,000; shared_db_pool_total is always 1 and its presence semantics are already covered by shared_db_pool_tenants (another ~5,700). Also worth dropping the redundant db_pool_id label since db_pool_uuid is globally unique — it doesn't reduce series count but stops identity churn on every pool create/delete from accumulating within the retention window. status_age at 95 series (non-active only) is correctly designed; leave it.
  • Billing metrics stay as-is: tenant_file_bytes_total, tenant_storage_bytes, tenant_media_files, tenant_video_files — 90,000 series, all strict subsets of the fs tenant set with no leakage. Legitimate cost.
  • Worth a separate look: tenant_operation_failures_total covers 10,000 tenants, of which action="write", status_class="4xx" accounts for 9,552 — near-total. Could be expected quota rejection or something the load test surfaced. That's an observability question, not a cardinality one.

Also confirmed clean and needing no action: service_gauge is down to 51 series per pod (46 from mutation_replay), so #802's Set(0)Delete conversion fully worked — earlier high readings were pre-#802 residue. db_pool_connections has zero distinct tenant_id, as intended.

Nothing blocking.

@srstack
srstack merged commit ff122a9 into main Jul 27, 2026
3 checks passed
@srstack
srstack deleted the fix/metrics-cardinality-p0 branch July 27, 2026 11:39
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.

3 participants