Task · GCI-0022

Surface Loki retention overrides and per-stream retention as an operational finding

Description

Grafana Cloud now lets a stack admin request a Loki retention change from the stack itself, through the Databases Configuration app. The request opens a pull request against Grafana’s own deployment repository and engineering approves it. Retention above the plan floor is a separate billing dimension, and a per-stream selector can multiply the retained volume of one high-cardinality stream without moving the global figure at all. Nothing in this platform can see any of it today.

Live-verified routes, all read-only GETs

Measured 2026-09-11 against a 286-stack organisation and a five-stack control organisation.

Route Identity What it answers
grafanacloud_logs_instance_limits{limit_name="retention_period"} on grafanacloud-usage none beyond the write stack EFFECTIVE global retention per stack, nanoseconds
grafanacloud_logs_instance_limits{limit_name="max_query_lookback"} on the same datasource none beyond the write stack effective query lookback per stack, nanoseconds
GET {stack}/api/datasources/proxy/uid/grafanacloud-logs/config/tenant/v1/limits stack reader, datasource proxy full effective tenant limits as YAML, including retention_period, retention_stream and otlp_config
GET {stack}/api/plugins/grafana-dbcfg-app/resources/v1/lokiconfigretentions stack reader, plugin resource route the self-serve CHANGE REQUEST record, with author, status and the approving pull-request number
GET {stack}/api/plugins/grafana-dbcfg-app/resources/v1/settings stack reader logs_tenant_id and logs_gcom_cluster_id for the stack, useful for joining
GET grafana.com/api/hosted-{logs,metrics,traces,profiles}/<instanceId> org-realm reader an overrides map of limits EXPLICITLY set for that tenant, for all four signals

The change-request record, shape frozen from a live sibling resource

lokiconfigretentions is a Kubernetes-style list. An empty items array is the common case. The item shape was frozen from a live lokiconfigotlpconfigs item, the same CRD family on the same route, because no reachable stack carries a retention item:

metadata: name=<loki tenant id>, namespace=stacks-<stack id>, uid, generation,
          creationTimestamp, annotations["grafana.com/updateTimestamp"]
spec:     author, message, request_timestamp, <the limit payload>
status:   status: applied|pending|rejected, observed_generation, processed_timestamp,
          pr_info: { number: <int>, status: merged|... }

The retention item’s payload key is NOT verified. Treat every spec key other than the four shared ones as opaque: record the raw key and value, never guess a schema for it.

Five traps, every one a case where the wrong call returns HTTP 200

The zero-credential detector, and its measured yield

max_query_lookback != retention_period is a strong proxy for a per-stream retention override, and it is visible from the grafanacloud-usage datasource with no collector code and no credential.

The 4-stack gap between the 286 stacks reporting logs limits and the 282 reporting retention is a real denominator gap, so every figure carries its measured-stack denominator.

Privacy and cardinality

Deliberately out of scope

Changing retention, proposing a retention change, or touching a change request. This is a report. The platform’s HTTP client refuses every method but GET and that property stays load-bearing.

Acceptance Criteria

Definition of Done

Implementation Plan

Build and integrate the zero-credential Operations panel tier first; then integrate the read-only Loki dataplane and change-request collector tier, generic expected-policy mechanism, dashboard wiring, documentation, focused evidence, security review and final gate.

Implementation Notes

SCOPE QUESTION SETTLED 2026-09-11, AND THE PROXY CAP IS LIFTED. A throwaway org-realm access policy carrying logs:read and nothing else was minted in region us on a control organisation, probed, and deleted; both DELETEs returned 204 and a follow-up list confirmed no residual policy.

Route, Loki dataplane, basic auth user = hlInstanceId logs:read only
GET {lokiUrl}/config/tenant/v1/limits 200, full effective limits including retention_stream
GET {lokiUrl}/loki/api/v1/config/limits/applied 401 authentication error: invalid scope requested
GET {lokiUrl}/loki/api/v1/labels 200
GET {lokiUrl}/loki/api/v1/query_range 200, and it returned a real log line

Four consequences.

  1. Build on GET {lokiUrl}/config/tenant/v1/limits. It is the non-deprecated route, it reaches the effective retention_stream value, and it needs NO new scope, NO reader action, NO Grafana service account, NO datasource proxy and NO re-provisioning. The org CAP the collector already carries is enough. /loki/config/tenant/v1/limits is a 404; the path has no /loki prefix.
  2. Do not use the datasource proxy for this. GET {stackUrl}/api/datasources/proxy/uid/grafanacloud-logs/config/tenant/v1/limits returns the same body, but it needs datasources:query at datasources:uid:grafanacloud-logs, which is the production-log-data grant collector/sources/usage_insights.py names as unacceptable. The dataplane route makes that trade unnecessary. Never grant it for this feature.
  3. The published documentation is right about the deprecated endpoint and wrong about the new one. Grafana’s self-serve API page says this endpoint family needs “logs write permissions”. That is true of /loki/api/v1/config/limits/applied, which 401s under logs:read with invalid scope requested, and false of config/tenant/v1/limits. Do not infer one route’s scope from another’s documentation.
  4. logs:read is a FULL Loki read scope, not a label-only scope. query_range under that scope returned customer log content. The collector’s read-only property comes from its HTTP client refusing non-GET methods plus its code only ever calling label endpoints - query_range is itself a GET, so the method restriction does not bound it. CAPABILITIES.md lists logs:read against two label routes under a “verified route” heading, which reads as a boundary and is not one. Tracked separately; it is a documentation-precision defect, not a code defect.

DECISION on point 4 above, 2026-09-11 by Rob: logs:read stays. The label inventory requires it, no narrower Grafana Cloud scope reaches Loki label names and values, planned log analytics will require log reads outright, and deployments run only against organisations that have explicitly consented to that access. Recorded as a standing decision in doc-0002 and as GCI-0023’s subject.

So point 4 is a documentation task about a declared and consented capability, not a risk to mitigate. Nothing in this task narrows a scope, and nothing here authorises the collector to read log content - that remains an implementation restraint enforced by review, and a future analytics feature that changes it is its own task with its own review.

Tracker reconciliation 2026-09-11: acceptance criteria 3, 9 and 11 were corrected to the frozen Wave 1 route. The pre-probe text named a stack reader and datasource proxy and prohibited every reader action; the settled contract uses the Loki dataplane with the existing org CAP, forbids the datasource proxy, and authorises exactly one generic Databases Configuration app-access permission.

Final Summary

Delivered the usage-only retention panels, Loki effective-limit and Databases Configuration reads, three retention views, bounded metrics, generic expected-policy evaluation, Loki change-event emission, documentation and security review. Final gate at b6cf849614054894e2bea49d0154d958fc016d7b passed: 1468 tests, 2 skipped, 7232 subtests, 232.8 MiB peak RSS, both Terraform roots valid, identifier history clean and shipped-text scan clean. Live Grafana rendering was not commissioned and remains unproven.

View the source file on GitHub