Files
Alexey Artamonov e63b9e439c fix(monitoring): count a central request as the receiver admitted it
The limiter refuses for good any request it counts above burst, and
burst was the receiver's 4 MiB cap. A request grows on its way to the
limiter: the sampler adds a tracestate to every span it keeps, and the
limiter counts a request as the collector re-encodes it, which writes
the empty value of every attribute the wire may omit, so a valid
request of spans with empty-valued attributes is counted 1.4x. A
request the receiver admitted near its cap was therefore refused on
every retry.

The limiter now runs right after memory_limiter, so it counts the
request before sampling. The receiver caps a request at 3 MiB and
burst stays at 5 MiB, which holds 1.4x the cap; a larger burst would
also be what each pod start lets through above the rate. The rate is
counted before sampling, so the store keeps only the sampled share of
it. The texts say that a request above the cap gets a 400, which
senders do not retry, and that each collector rollout a tenant causes
lets that burst through again on every pod.

The replica bound that keeps that allowance small now applies only to
a central tenant's collector, whose render fails above ten replicas: a
collector writing to the tenant's own store has no rate, and the
replica counts it shipped with stay valid.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-10-06 09:39:04 +03:00
..

Monitoring Hub

Parameters

Common parameters

Name Description Type Value
host The hostname used to access Grafana externally (defaults to 'grafana' subdomain for the tenant host). string ""

Metrics storage configuration

Name Description Type Value
metricsStorages Configuration of metrics storage instances. []object [...]
metricsStorages[i].name Name of the storage instance. string ""
metricsStorages[i].retentionPeriod Retention period for metrics. string ""
metricsStorages[i].deduplicationInterval Deduplication interval for metrics. string ""
metricsStorages[i].storage Persistent volume size. string 10Gi
metricsStorages[i].storageClassName StorageClass used for the data. string ""
metricsStorages[i].vminsert Configuration for vminsert. object {}
metricsStorages[i].vminsert.minAllowed Minimum guaranteed resources. object {}
metricsStorages[i].vminsert.minAllowed.cpu CPU request. quantity ""
metricsStorages[i].vminsert.minAllowed.memory Memory request. quantity ""
metricsStorages[i].vminsert.maxAllowed Maximum allowed resources. object {}
metricsStorages[i].vminsert.maxAllowed.cpu CPU limit. quantity ""
metricsStorages[i].vminsert.maxAllowed.memory Memory limit. quantity ""
metricsStorages[i].vmselect Configuration for vmselect. object {}
metricsStorages[i].vmselect.minAllowed Minimum guaranteed resources. object {}
metricsStorages[i].vmselect.minAllowed.cpu CPU request. quantity ""
metricsStorages[i].vmselect.minAllowed.memory Memory request. quantity ""
metricsStorages[i].vmselect.maxAllowed Maximum allowed resources. object {}
metricsStorages[i].vmselect.maxAllowed.cpu CPU limit. quantity ""
metricsStorages[i].vmselect.maxAllowed.memory Memory limit. quantity ""
metricsStorages[i].vmstorage Configuration for vmstorage. object {}
metricsStorages[i].vmstorage.minAllowed Minimum guaranteed resources. object {}
metricsStorages[i].vmstorage.minAllowed.cpu CPU request. quantity ""
metricsStorages[i].vmstorage.minAllowed.memory Memory request. quantity ""
metricsStorages[i].vmstorage.maxAllowed Maximum allowed resources. object {}
metricsStorages[i].vmstorage.maxAllowed.cpu CPU limit. quantity ""
metricsStorages[i].vmstorage.maxAllowed.memory Memory limit. quantity ""

Logs storage configuration

Name Description Type Value
logsStorages Configuration of logs storage instances. []object [...]
logsStorages[i].name Name of the storage instance. string ""
logsStorages[i].retentionPeriod Retention period for logs. string 1
logsStorages[i].storage Persistent volume size. string 10Gi
logsStorages[i].storageClassName StorageClass used to store the data. string replicated

Traces storage configuration

Name Description Type Value
tracingStorages Configuration of traces storage instances. Empty by default (tracing off): no backend and no trace collector are rendered. Adding an entry provisions a VictoriaTraces backend and, while tracingCollector.enabled is true, deploys the OTLP collector in front of it. A tenant with shared-central tracing (the Tenant's tracingCentral) gets the collector, while tracingCollector.enabled is true, without an entry, which then exports to the shared store; entries still listed keep their store and datasource, so traces stored before the switch stay readable until the entry is removed. []object []
tracingStorages[i].name Name of the storage instance: a lowercase RFC 1123 label of at most 42 characters. Treat as immutable once a backend exists: the VTCluster/VTSingle, its Services, PVCs and Grafana datasource are all named after it, so renaming an entry deploys a new, empty backend and removes the old one, the same way changing mode does (single mode loses the old PVC, cluster mode leaves the old vtstorage PVCs Bound until the whole Monitoring release is deleted). string ""
tracingStorages[i].mode Deployment mode: "cluster" renders a VTCluster; "single" renders a single-binary VTSingle for edge/dev/small clusters. Treat as immutable once a backend exists: mode selects which Kind is rendered under the same name, so changing it on a running backend re-identifies the object rather than reconfiguring it. Switching from "single" to "cluster" prunes the VTSingle and the operator garbage-collects its PVC (stored traces are lost); switching from "cluster" to "single" (or removing the entry) leaves the vtstorage PVCs Bound and billed until the whole Monitoring release is deleted. The API cannot guard this (transition rules are not available on an array-item field), so this note is the only guard: migrate deliberately, accepting the loss. string cluster
tracingStorages[i].retentionPeriod Retention period for traces. string 14d
tracingStorages[i].storage Persistent volume size. quantity 10Gi
tracingStorages[i].storageClassName StorageClass used to store the data. string replicated
tracingStorages[i].retentionDiskUsageBytes Optional disk-usage cap as a VictoriaMetrics BytesString (e.g. "8GB" or "8GiB"), bounding disk use on top of the age-based retentionPeriod. Uses byte-suffix grammar (kb/mb/gb/tb, KiB/MiB/GiB/TiB), NOT the Kubernetes-quantity grammar of the sibling storage field, so "8Gi" is rejected by the operator. In cluster mode the cap is per vtstorage replica (replicaCount 2), so the effective total is twice the value. Unset, the store drops its oldest day once its filesystem is 80% full instead, so every store is disk-bounded. Either way VictoriaTraces keeps the last two days, so spans arriving faster than the cap allows for two days can still fill the disk. string ""
tracingCentralHost tenant-root only: accept spans from tenants that turned on shared-central tracing (the Tenant's tracingCentral) into this Monitoring's tracingStorages entry named generic, which must be in cluster mode. Every central tenant then shares that store's disk and retention with tenant-root, and nothing reserves space per tenant: once the store's disk cap is reached (retentionDiskUsageBytes, or 80% of its filesystem) the oldest spans of all of them are evicted together. Each tenant is held to the platform's monitoring.tracingCentralTenantBytesPerSecond, which by default lets one tenant write at most the cap of a default 10Gi store, counted uncompressed and before the tenant's sampling, over the two days VictoriaTraces always keeps, so raise it with the store; several tenants writing at their limit together still can fill it. That bound holds for a tenant whose collector keeps running: each collector pod starts with a full bucket, up to 5 MiB or one second of its share of the rate when that is larger, and any change to the tenant's Monitoring rolls every collector pod, so a tenant that keeps changing it writes that much more on each rollout, for up to ten replicas; closing that needs a limiter with shared state, which the collector does not run. Ignored outside tenant-root. The tenant-root namespace is inside the trust boundary of the shared store, as it is for the metrics of tenants that rely on a parent's monitoring: vmauth pins each central tenant to its own account, but workloads in tenant-root reach the store directly, and one that learns a tenant's account ID (a hash of its namespace name and UID, which its VMUsers carry) can read and write that account. Whoever tenant-root admits to its Grafana is inside it too, since tenant-root's own datasource for the store reaches vtselect without vmauth; who that is follows its Grafana's oidc mode (with System, tenant-root's own groups, which already hold the same access level in every tenant under tenant-root). The render refuses to stop hosting while central tenants remain, but removing the tenant-root Monitoring altogether is not a render and is not refused: turn tracingCentral off on the central tenants first, since until then every Monitoring render of theirs fails, their metrics, logs and alerting changes included. The refusals read each other's objects when they render, so a tenant turning tracingCentral on while tenant-root stops hosting can pass both: that tenant's collector then exports to a vmauth that is gone while every release reports Ready, until its Monitoring renders again. Recover by hosting again; turning that tenant's tracingCentral off instead ends the silent drop, and turning it on again is then refused until tenant-root hosts. bool false

Traces collector configuration

Name Description Type Value
tracingCollector OpenTelemetry Collector for trace ingestion. With shared-central tracing it refuses spans above the platform's per-tenant write rate: OTLP/HTTP senders retry them, OTLP/gRPC senders drop them, so export over HTTP. It also refuses a request larger than 3 MiB (decompressed) with a 400, which senders do not retry, so keep export batches below that. object {}
tracingCollector.enabled Deploy the OTLP collector. When disabled, applications must send OTLP straight to the traces backend (no central sampling/redaction); with shared-central tracing they have no path to it, so no new spans arrive, while the traces already stored stay readable. The collector fronts a single backend, and the limit holds with shared-central tracing too, since turning that off makes it export to the local entry again: while it is enabled, tracingStorages must hold at most one entry, and adding a second one fails the whole Monitoring render until the extra entry is removed or the collector is disabled. bool true
tracingCollector.replicas Number of collector replicas. Head sampling is trace-id-deterministic, so replicas>1 are safe and remove the single-ingest-path SPOF. With shared-central tracing each replica sends up to two requests at a time against this tenant's write limit of four per vmauth replica, so more replicas do not raise the tenant's throughput; the tenant's write rate (the platform's monitoring.tracingCentralTenantBytesPerSecond) is split across them as well, so a sender whose connection stays on one replica gets that replica's share, and each replica that starts lets through above the rate up to 5 MiB, or one second of its share when that is larger, which is why shared-central tracing allows at most 10 replicas and fails the render above that. int 1
tracingCollector.samplingPercentage Head-sampling percentage applied at the collector before export. int 10
tracingCollector.redactAttributes Span attribute keys to drop before export (the PII/secret control point, e.g. db.statement). []string []
tracingCollector.resources Resource configuration for the collector. object {}
tracingCollector.resources.requests Resource requests. object {}
tracingCollector.resources.requests.cpu CPU request. quantity 100m
tracingCollector.resources.requests.memory Memory request. quantity 256Mi
tracingCollector.resources.limits Resource limits. object {}
tracingCollector.resources.limits.cpu CPU limit. quantity 1
tracingCollector.resources.limits.memory Memory limit. quantity 1Gi

Alerta configuration

Name Description Type Value
alerta Configuration for the Alerta service. object {}
alerta.storage Persistent volume size for the database. string 10Gi
alerta.storageClassName StorageClass used for the database. string ""
alerta.resources Resource configuration. object {}
alerta.resources.requests Resource requests. object {}
alerta.resources.requests.cpu CPU request. quantity 100m
alerta.resources.requests.memory Memory request. quantity 256Mi
alerta.resources.limits Resource limits. object {}
alerta.resources.limits.cpu CPU limit. quantity 1
alerta.resources.limits.memory Memory limit. quantity 1Gi
alerta.alerts Alert routing configuration. object {}
alerta.alerts.telegram Configuration for Telegram alerts. object {}
alerta.alerts.telegram.token Telegram bot token. string ""
alerta.alerts.telegram.chatID Telegram chat ID(s), separated by commas. string ""
alerta.alerts.telegram.disabledSeverity List of severities without alerts (e.g. ["informational","warning"]). []string []
alerta.alerts.slack Configuration for Slack alerts. object {}
alerta.alerts.slack.url Configuration uri for Slack alerts. string ""
alerta.alerts.slack.disabledSeverity List of severities without alerts (e.g. ["informational","warning"]). []string []
alerta.alerts.email Configuration for email alerts. Alerts carrying the label notify_email with the string value true are emailed when their severity is in the severities list (default critical and major). A bare YAML boolean does not match. Alerts listed in alertnames are emailed unless matched by an earlier platform route. object {}
alerta.alerts.email.to Recipient address, or several separated by commas. A display name is accepted, such as Oncall <oncall@example.org>, as long as it carries no comma. Quote display names containing punctuation such as :, @, parentheses or ?. RFC 2047 encoded words are not accepted. IP domain literals accept dotted-quad IPv4 only. IPv6 literals are rejected because Go mail parsers disagree on their syntax across versions. ASCII control characters are rejected. Addresses must be literal, so Alertmanager template actions are rejected. The accepted form is deliberately narrower than RFC 5322. The local part must be a dot-atom, with no leading, trailing or doubled dot, and quoted local parts, comments and a mailto: prefix are rejected. An address outside that form fails the render, because Alertmanager parses addresses only when it sends and drops the notification with no record a tenant can read. string ""
alerta.alerts.email.from Sender address, one only. A display name is accepted. It takes the same narrower form as to, and an address outside it fails the render. string ""
alerta.alerts.email.smarthost SMTP server as host:port (e.g. "smtp.example.com:465"). A value without a port, an invalid bracketed IP address or a port with leading zeros fails the render. Port 465 uses implicit TLS, other ports use STARTTLS. string ""
alerta.alerts.email.authUsername Username for SMTP LOGIN/PLAIN authentication. The password is read from the password key of a Secret named alertmanager-email-password in the release namespace, which the platform operator creates. The name is fixed and the Secret is mounted into Alertmanager, so a missing Secret blocks the whole Alertmanager rollout, not only email delivery. The credential in it must belong to this tenant alone. smarthost is chosen by whoever writes the Monitoring, so one relay credential provisioned under the same name in every namespace can be collected by pointing smarthost at a host the tenant controls. string ""
alerta.alerts.email.repeatInterval Minimum time before a still-firing alert is emailed again, as an Alertmanager duration with units y, w, d, h, m, s, ms in descending order, each at most once, such as 5m, 12h or 1h30m. Delivery is evaluated every 5m, so shorter intervals take effect at 5m and other intervals round up to a multiple of 5m. Zero, a unit out of order and a total beyond what Alertmanager accepts fail the render. Empty keeps the 12h default. string ""
alerta.alerts.email.alertnames Alert names emailed whether or not the rule carries the notify_email label, with no severity filter. Earlier platform routes still take precedence. Alerts with event="Heartbeat", including Watchdog, go only to Alerta, and alerts with severity none, CDIDefaultStorageClassDegraded and CDINoDefaultStorageClass are blackholed and never reach email. This is the way to opt in alerts the platform ships, since their helm-owned rule definitions cannot be labelled per cluster. Entries must be plain Prometheus alert names such as KubeAPIDown, and anything else fails the render. Empty adds no extra route. []string []
alerta.alerts.email.severities Severities the notify_email route accepts (e.g. ["critical","major","warning"]). Empty keeps the default of critical and major. List every severity in use to email opted-in alerts regardless of severity. Entries must be plain severity names, and anything else fails the render. []string []

Grafana configuration

Name Description Type Value
grafana Configuration for Grafana. object {}
grafana.db Database configuration. object {}
grafana.db.size Persistent volume size for the database. string 10Gi
grafana.resources Resource configuration. object {}
grafana.resources.requests Resource requests. object {}
grafana.resources.requests.cpu CPU request. quantity 100m
grafana.resources.requests.memory Memory request. quantity 256Mi
grafana.resources.limits Resource limits. object {}
grafana.resources.limits.cpu CPU limit. quantity 1
grafana.resources.limits.memory Memory limit. quantity 1Gi

Vmagent configuration

Name Description Type Value
vmagent Configuration for VictoriaMetrics Agent. object {}
vmagent.externalLabels External labels applied to all metrics. object {}
vmagent.remoteWrite Remote write configuration. object {}

OIDC authentication

Name Description Type Value
oidc OIDC authentication for the Grafana instance. See docs/oidc-grafana.md for the operator guide. object {}
oidc.mode Identity mode. None: no OIDC, only the grafana-admin-password Secret works. System: trust the platform cozy realm via a per-instance confidential client and audience binding; zero-config default. CustomConfig: trust a tenant-supplied issuer directly (BYO); cozy is not in the path. string None
oidc.customConfig Tenant-supplied [auth.generic_oauth] payload; consumed only when mode: CustomConfig. object {}
oidc.customConfig.config Inline map of [auth.generic_oauth] keys. Written verbatim into the Grafana CR spec.config. Mutually exclusive with secretRef.name. object {}
oidc.customConfig.secretRef Reference to a Secret holding an auth.ini fragment; mounted under /etc/grafana/oidc and loaded via GF_PATHS_CUSTOM_INI. With shared-central tracing only its auth.ini key is mounted, so the fragment cannot read another key of the Secret with $__file{}. object {}
oidc.customConfig.secretRef.name Name of an existing Secret in the release namespace whose auth.ini key holds a ready-made [auth.generic_oauth] block. Mutually exclusive with customConfig.config. string ""
oidc.users Users granted access to this Grafana instance's org. Each entry is reconciled into Grafana org membership + role by a chart-owned post-install/post-upgrade Job. Works for both System and CustomConfig modes; ignored when mode: None. Users not listed here get nothing (no default org role from group mapping). []object []
oidc.users[i].email Email address matched against the email claim from the issuer. The chart pre-provisions a Grafana account with this email so the users-Job can set the org role before the operator's first login (oauth_allow_insecure_email_lookup = true glues the two together). Must match a conservative pattern: local part is [A-Za-z0-9._%+-]+, domain is [A-Za-z0-9.-]+\.[A-Za-z]{2,}. Whitespace, quoted literals, and unusual URL-reserved characters are rejected on schema level so the reconcile-Job's shell handling never has to defend against malformed input. string ""
oidc.users[i].role Grafana org role to bind: Admin, Editor, or Viewer. Applied to the release's Grafana org (Main Org.); server-level GrafanaAdmin promotion is out of scope for Phase 1. Required — the users-Job POSTs {loginOrEmail, role} to /api/orgs/1/users and PATCHes {role} to converge across re-runs; omitting the field would render role: "null" into the body and fail with HTTP 400. string {}