Configuration reference
This page documents the chart values you’re most likely to set. Every value has
a sensible default; override with --set key=value or a values file
(-f my-values.yaml). Defaults shown are the chart defaults.
Images and registry
Section titled “Images and registry”The core is offline-first: every image is {registry}/{repository}:{tag}. Point
registry at a private mirror and no step reaches the public internet.
| Value | Default | Notes |
|---|---|---|
registry |
ghcr.io/ultrvlt |
Prefix applied to all first-party images. |
imageTag |
"" |
Global tag override. Empty → the chart’s appVersion (the release tag). A component’s own image.tag still wins. |
imagePullPolicy |
IfNotPresent |
|
imagePullSecrets |
[] |
Names of pre-created pull secrets (private mirror). |
ClickHouse
Section titled “ClickHouse”Provisioned as a ClickHouseInstallation (CHI) managed by the bundled operator.
One replica by default; see Replicated storage for two or
three.
| Value | Default | Notes |
|---|---|---|
clickhouse.database |
ultrvlt |
|
clickhouse.user |
ultrvlt |
|
clickhouse.password |
"" |
Empty → a random password is generated on first install and kept stable across upgrades. |
clickhouse.leastPrivilege |
true |
The ingest gateway and the query API each connect to the database as their own user, with generated passwords kept in the same Secret. The ingest user can only add telemetry; the query API’s user can read and write only its own settings, with no rights to drop data or to read files or URLs from inside the database. clickhouse.user remains the administrator, used only for schema changes, backups, restores and retention changes. false → every component connects as clickhouse.user. |
clickhouse.persistence.size |
50Gi |
Size the PVC for your retention — see Sizing and capacity. |
clickhouse.persistence.storageClass |
(cluster default) | |
clickhouse.resources |
1 CPU / 2Gi req, 4Gi limit | |
clickhouse-operator.enabled |
true |
Set false if a cluster-wide ClickHouse operator already exists. |
Replicated storage
Section titled “Replicated storage”Two or three storage replicas, each holding all the data. Losing a node, a pod or a replica’s disk then loses nothing, and the portal keeps answering from the replicas that remain. Off by default. It adds three small coordination (Keeper) pods, which keep the replicas in step.
| Value | Default | Notes |
|---|---|---|
clickhouse.replication.enabled |
false |
Turn on replicated storage. |
clickhouse.replicasCount |
1 |
2 or 3 with replication. Above 1 without it is refused, since each replica would hold different data. |
clickhouse.replication.migrate |
false |
Convert an existing install’s data (see below). Not needed for a new install. |
clickhouse.replication.keeper.replicas |
3 |
Coordination pods. Three keep working with one lost. |
clickhouse.replication.keeper.persistence.size |
2Gi |
|
clickhouse.replication.keeper.resources |
50m / 128Mi req, 512Mi limit |
Replicas and coordination pods prefer separate nodes, so give the core at least as many nodes as storage replicas. Each node must be able to run the whole core by itself (see High availability).
A new install needs only the first two values:
clickhouse: replicasCount: 2 replication: enabled: trueAn existing install keeps its data in an unreplicated database, and converting it is a deliberate step:
- Take a backup (Settings → Backups → Back up now), and wait for it to finish.
- Upgrade with
replication.enabled: trueandreplicasCount: 2. The new pods start, and the upgrade stops at the schema step with a message asking for step 3. Nothing has changed: the portal and ingest keep working from the first replica. - Upgrade again with
replication.migrate: trueas well as the values from step 2. (If you upgrade with--reuse-values, Helm reuses the values of the last successful upgrade, which is the one before step 2, so pass all three.) The data moves into a replicated database in place, which takes seconds and no extra disk. The other replicas then copy it from the first; on a large install that copy takes a while, and Core status shows the replicas catching up. Telemetry arriving meanwhile is kept.
The old database stays as ultrvlt_pre_replication, sharing its files with
the new one, until you remove it. Once the replicas are in step and new data
looks right, drop it:
kubectl exec -n <namespace> chi-<release>-ultrvlt-0-0-0 -c clickhouse -- \ clickhouse-client -q "DROP DATABASE ultrvlt_pre_replication"To roll back instead, while it still exists (data received since the migration is lost):
# on every storage replica (chi-<release>-ultrvlt-0-0-0, -0-1-0, ...):clickhouse-client -q "DROP DATABASE ultrvlt SYNC"# then on the first one only:clickhouse-client -q "RENAME DATABASE ultrvlt_pre_replication TO ultrvlt"…and upgrade with replication.enabled: false and replicasCount: 1.
migrate can stay set afterwards: it only acts on an unreplicated database.
Apart from that rollback, turning replication off again is not supported.
Adding or removing a replica is a change to replicasCount (between 2
and 3). A new replica copies the data from the others; a removed one is
forgotten by the cluster once its pod has stopped.
If a replica was down for an upgrade, it catches up when it returns. A replica that has never been part of the cluster (for example one added while it couldn’t start) shows on Core status; run the upgrade again once it’s up.
Schema and retention
Section titled “Schema and retention”| Value | Default | Notes |
|---|---|---|
schemaInit.enabled |
true |
Post-install/upgrade Job that applies the DDL (idempotent). |
schemaInit.auditRetentionDays |
365 |
Retention for the control-plane audit trail; re-tuned on each upgrade. |
Query protection
Section titled “Query protection”Limits on what a single portal query may use, so one very wide search can’t slow ingest or everyone else. Off by default; recommended for any shared production install — see Sizing and capacity.
| Value | Default | Description |
|---|---|---|
queryApi.queryLimits.maxExecutionTime |
"" (off) |
Stop a query after this long, e.g. 60s. |
queryApi.queryLimits.maxMemoryUsage |
0 (off) |
Bytes one query may use, e.g. 1073741824 (1 GiB, about a quarter of the default storage memory limit). |
queryApi.queryLimits.maxRowsToRead |
0 (off) |
Rows one query may scan. |
queryApi.rateLimit.rps |
0 (off) |
Sustained API requests per second, per user. |
queryApi.rateLimit.burst |
0 (= rps) |
Short bursts allowed above rps — a dashboard load fires many requests at once, so allow ~100. |
A query that hits a limit fails with an error naming the limit, instead of degrading the whole system.
Ingest buffer
Section titled “Ingest buffer”If storage can’t take writes for a while — restarting, being upgraded, or down — the ingest gateway keeps what it receives on disk and writes it, oldest first, as soon as storage is back. Nothing arriving during the outage is lost, up to the buffer’s limits. On by default.
| Value | Default | Description |
|---|---|---|
ingestGateway.spool.enabled |
true |
Buffer refused writes on disk. Off: they’re retried from memory for 5 minutes, then dropped. |
ingestGateway.spool.maxSizeMB |
1024 |
Buffer size per gateway replica. Past it, the oldest data is dropped. |
ingestGateway.spool.maxAge |
24h |
Data older than this is dropped instead of written. |
ingestGateway.spool.existingClaim |
"" |
A PersistentVolumeClaim you create to hold the buffer (one gateway replica only). |
By default the buffer lives on the pod’s own disk, so it survives the gateway
restarting but not its pod being deleted or moved to another node. To keep it
through that as well, create a claim (ReadWriteOnce is enough) and name it in
existingClaim.
As a rough guide, 1 GiB holds about an hour of 1,000 records per second. While the buffer holds anything, and whenever it has had to drop data, it shows on Core status.
Ingest quotas
Section titled “Ingest quotas”A per-site ceiling on how much telemetry the core accepts, so one site — a log storm, a misconfigured exporter — can’t crowd out every other site. Off by default.
| Value | Default | Description |
|---|---|---|
ingestGateway.quota.recordsPerSecond |
0 (off) |
Records per second each site may send — spans, log lines and metric points together. |
ingestGateway.quota.burstSeconds |
10 |
How far a site may run ahead of its rate, in seconds of that rate. |
ingestGateway.quota.perSite |
{} |
Per-site overrides, e.g. {edge-plant-7: 2000}. 0 exempts a site from the default. |
A site over its quota is told to slow down rather than having data silently discarded:
- A remote site keeps the refused telemetry in its on-disk buffer and resends it when the core accepts it again — it arrives late, not lost.
- Sensors sending straight to the core (no remote-site connector) drop what is refused and log how much, once a minute.
- Heartbeats are never throttled, so node counts, metering and “sensors up” stay correct while a site is over quota.
Throttled sites appear on Core status (under System), and the
ingest.throttled_records metric — by site and signal — can drive a
metric alert. The limit applies per ingest
gateway replica: with three replicas, a site can reach up to three times the
rate. For sizing a quota, see Sizing and capacity.
Synthetic monitors
Section titled “Synthetic monitors”The checker that runs synthetic monitors from this cluster. On by default; with no monitors defined it does nothing.
| Value | Default | Description |
|---|---|---|
prober.enabled |
true |
Run the checker. |
prober.location |
core |
The location name this cluster’s checks report as. |
prober.otlpEndpoint |
"" |
Where results go. Empty means this cluster’s ingest gateway; with remote-site mTLS on, point it at a local site connector, as for the sensor. |
prober.assignmentsEndpoint |
"" |
With remote-site mTLS on, also the local site connector: the checker then asks for its monitors through it and runs as that site. |
prober.secretsSecret |
"" |
An existing Secret whose keys monitors may use as header values. |
prober.egress.denyCIDRs |
[] |
Extra address ranges checks may not reach. |
prober.egress.allowCIDRs |
[] |
Ranges to re-permit, overriding the built-in blocks (loopback, link-local and metadata, multicast) and denyCIDRs. |
prober.networkPolicy.enabled |
false |
Restrict the checker’s network access to DNS, the core’s own services and egressCIDRs. Needs a CNI that enforces NetworkPolicy. |
prober.networkPolicy.egressCIDRs |
[] |
Where checks may go when the NetworkPolicy is on. |
The checker holds no database credentials and no Kubernetes API access.
Span sampling
Section titled “Span sampling”Stores a share of ordinary requests instead of every one, to cut trace storage on busy clusters. Off by default — every span is kept.
| Value | Default | Description |
|---|---|---|
ingestGateway.spanSampling.keepOneIn |
1 (off) |
Keep one in this many ordinary requests, e.g. 10 stores about 10%. |
ingestGateway.spanSampling.slowMs |
500 |
Requests at least this slow are always kept. |
ingestGateway.spanSampling.rules |
[] |
Overrides for a service, a site, or both, each with its own keepOneIn and optional slowMs. The most specific rule wins. |
What is always kept, whatever the rate:
- Errors — failed requests, HTTP 5xx responses, database errors and failed DNS lookups. Error counts stay exact.
- Slow requests — at or over
slowMs, so the latency tail isn’t thinned. - Connection data and browser (RUM) events, which network, cost and RUM views add up rather than count.
Request rates, error rates, latency percentiles, SLOs and alerts stay exact. Before leaving a request out, the core counts it in a per-minute summary of every request it received, and the service pages, dashboards, alerts and SLOs read that summary (percentiles to within about 1%; latency SLO thresholds such as 100 ms, 250 ms or 1.5 s — two significant figures — are counted exactly). Sampling changes what you can open, not what you measure: the trace list shows the requests that were kept, so a given fast, successful request may not be there to open.
A few drill-down views — DNS names, database queries, dependencies and the error breakdown — count the stored requests, each standing for the ones it replaced. These are estimates, close for busy endpoints (on a test of 40,000 requests sampled one in ten, within 2% for counts and 3% for p95/p99) and rougher for rarely called ones.
The choice is made per trace, so every span of a multi-service trace is kept or dropped together — also with several gateway replicas. Rates that divide each other (say 5 and 10) keep a trace whole across services sampled at different rates.
ingestGateway: spanSampling: keepOneIn: 10 # ~10% of ordinary requests slowMs: 500 rules: - service: checkout # keep every checkout request keepOneIn: 1 - site: edge-plant-7 # a busy site, sampled harder keepOneIn: 20 slowMs: 1000Core status shows, per site, how many spans arrived in the last hour and how many were left out.
queryApi.corsOrigins (default []) lists browser origins allowed to call the
API from a different origin with a user’s session. Leave it empty when users
reach the portal this chart serves — only set it if you host your own front end
elsewhere.
Authentication
Section titled “Authentication”Human authentication for the portal and read API. This gates browser access (distinct from the machine mTLS trust domain used by remote sites).
| Value | Default | Notes |
|---|---|---|
queryApi.auth.mode |
local |
none | local | oidc. none ships the API open — not recommended off a trusted network. |
queryApi.auth.sessionTtl |
12h |
|
queryApi.auth.sessionSecret |
"" |
Cookie signing key. Empty → auto-generated on install and kept stable; every replica shares it. Set it explicitly if you render the chart without cluster access (helm template, Argo CD), or each render rotates it and signs everyone out. |
queryApi.auth.existingSecret |
"" |
BYO Secret with session-secret, admin-username, and admin-password or admin-password-hash. |
queryApi.auth.admin.username |
admin |
|
queryApi.auth.admin.password |
"" |
Plaintext, hashed at boot. Provide this or passwordHash. |
queryApi.auth.admin.passwordHash |
"" |
Pre-computed argon2id PHC (preferred — no plaintext at rest). Set hashed: true to use it. |
In local mode the admin credential is required (fail-closed). In oidc
mode it’s optional and stays available as a break-glass login.
OIDC single sign-on (auth.mode: oidc)
Section titled “OIDC single sign-on (auth.mode: oidc)”Authorization Code + PKCE against a discovery-capable IdP. Each user’s groups
and/or email address map to viewer / editor / admin, and sign-in can be
limited to your email domains.
| Value | Notes |
|---|---|
queryApi.auth.oidc.issuer |
IdP base URL serving /.well-known/openid-configuration. |
queryApi.auth.oidc.clientId / clientSecret |
Client secret may instead come from existingSecret key oidc-client-secret. |
queryApi.auth.oidc.redirectUrl |
e.g. https://ultrvlt.example.com/api/v1/auth/oidc/callback. |
queryApi.auth.oidc.scopes |
Requested scopes. Empty → openid profile email groups. Set openid profile email for an IdP that has no groups scope (e.g. Google). |
queryApi.auth.oidc.groupsClaim |
ID-token claim holding the user’s groups. Empty → groups. |
queryApi.auth.oidc.adminGroups / editorGroups / viewerGroups |
Group → role mapping. |
queryApi.auth.oidc.adminEmails / editorEmails / viewerEmails |
Email → role mapping, for an IdP that doesn’t send groups. Only a verified email matches. |
queryApi.auth.oidc.defaultRole |
Role for users matching no group or email; empty → deny. |
queryApi.auth.oidc.allowedDomains |
Only admit users whose verified email is in one of these domains. Empty → no restriction. |
Admins can also grant roles by email from the portal’s Access page, with no redeploy — handy for adding a teammate. Those grants sit alongside the ones in config (which the portal shows but can’t change), take effect at the user’s next sign-in, and are recorded in the audit log.
A user gets the most privileged role that any of their groups, their email, or
an Access-page grant gives them. Email-based roles and allowedDomains both rely on the identity
provider marking the email as verified (email_verified); a provider that
doesn’t is denied rather than trusted. Domains match exactly, so list each
subdomain you want to admit.
Google Workspace
Section titled “Google Workspace”Google sends no group membership at sign-in, so assign roles by email (or give
everyone a defaultRole), and restrict sign-in to your Workspace domain:
queryApi: auth: mode: oidc oidc: issuer: https://accounts.google.com clientId: <OAuth client ID> clientSecret: <OAuth client secret> redirectUrl: https://ultrvlt.example.com/api/v1/auth/oidc/callback scopes: "openid profile email" allowedDomains: [example.com] adminEmails: [alice@example.com] editorEmails: [bob@example.com, carol@example.com] defaultRole: viewerCreate the OAuth client in Google Cloud Console as a Web application with
the redirect URL above, and set its consent screen’s user type to Internal.
With Google, allowedDomains also requires the account to belong to your
Workspace organization, so a personal Google account registered with a work
address is refused.
AI summaries
Section titled “AI summaries”Connect a language model to add a written summary to each investigation. Off by default; nothing is sent anywhere until you set a provider and a model.
| Value | Default | Notes |
|---|---|---|
queryApi.ai.provider |
"" |
openai (any OpenAI-compatible API: vLLM, Ollama, LM Studio, Azure OpenAI, OpenAI), anthropic, gemini (with an API key), or vertex (Gemini on Google Cloud, no key; see below). |
queryApi.ai.model |
"" |
The model name the provider expects, e.g. llama3.1:8b, claude-sonnet-4-5, gemini-3.5-flash. |
queryApi.ai.baseUrl |
"" |
Where to send requests, e.g. http://ollama.ai.svc:11434/v1. Empty: the provider’s public API. |
queryApi.ai.apiKey.existingSecret |
"" |
A Secret holding the API key (key api-key). Not needed for most self-hosted servers. |
queryApi.ai.maxOutputTokens |
1024 |
The longest summary the model may write. |
queryApi.ai.gcpProject |
"" |
vertex only: the Google Cloud project. Empty: the project of the credentials. |
queryApi.ai.gcpLocation |
"" |
vertex only: the region, e.g. europe-west4. Empty: global. |
queryApi.ai.redactLogs |
false |
Leave log lines out of what is sent. Their counts are still included. Consider it for a model outside your network. Applies to AI agents too. |
Gemini on Google Cloud (vertex) needs no API key. On GKE it uses the
query API pod’s Workload Identity: bind the pod’s Kubernetes service account
(queryApi.serviceAccount) to a Google service account that has the
Vertex AI User role (roles/aiplatform.user) in the project.
Only an investigation’s evidence report is sent, never raw telemetry, and each summary is recorded in the audit log. A model in your own environment keeps the whole loop inside your network; small models (7–8B parameters) are enough, because the analysis is already done.
AI agents (MCP)
Section titled “AI agents (MCP)”Let your team’s AI agents, such as Claude Code, Cursor or Copilot, look up telemetry read-only, with an API token. See AI agents.
| Value | Default | Notes |
|---|---|---|
queryApi.mcp.enabled |
false |
Serve the MCP endpoint at /api/v1/mcp. What agents look up goes to the models they use, so it’s off until you turn it on. |
Licensing and connected mode
Section titled “Licensing and connected mode”| Value | Default | Notes |
|---|---|---|
queryApi.license.existingSecret |
"" |
Name of a Secret with key valid.lic. |
queryApi.license.content |
"" |
Inline .lic (chart creates the Secret). |
queryApi.connected.enabled |
false |
Automatic license renewal (recommended). Enable with a portalUrl + tokenSecret. Leave off for a fully air-gapped install. |
queryApi.connected.portalUrl |
"" |
The license endpoint you were given. |
queryApi.connected.tokenSecret |
"" |
Secret name with key token. |
queryApi.connected.callhomeInterval |
6h |
Connected mode is the recommended way to run — it keeps your license current
for you and sends usage metadata only, never telemetry. Turn it on by setting
connected.enabled, portalUrl, and tokenSecret. For offline environments,
leave it off and manage the .lic yourself — see Licensing & connected
mode.
Leave both license fields empty for an unlicensed boot (core runs UI-locked).
Notification secrets
Section titled “Notification secrets”Alert notification channels store only a secret_key in the database — never a
secret value. The core resolves ULTRVLT_NOTIFY_SECRET_<key> from the Secret
below. Keys must be valid env-var suffixes ([A-Za-z0-9_]); that same string is
what you type into a channel’s secret_key field.
| Value | Default | Notes |
|---|---|---|
queryApi.notify.secrets |
{} |
Inline map, e.g. { slack_hook: "https://hooks.slack.com/..." } (chart renders a Secret). |
queryApi.notify.existingSecret |
"" |
BYO Secret; its keys become ULTRVLT_NOTIFY_SECRET_<key>. |
queryApi.notify.allowPrivateTargets |
true |
Whether webhook and email targets may be private (RFC 1918 / ULA) addresses, such as an internal chat bridge. Set false if every target is on the internet. |
Webhook/email targets that need no secret work without any of this. Whatever
the setting, notifications are never sent to loopback, link-local or
cloud-metadata addresses, and webhook URLs must be http:// or https://.
Portal ingress
Section titled “Portal ingress”| Value | Default | Notes |
|---|---|---|
portalUi.ingress.enabled |
false |
|
portalUi.ingress.className |
"" |
|
portalUi.ingress.host |
ultrvlt.local |
|
portalUi.ingress.annotations |
{} |
e.g. ALB/ACM annotations. |
portalUi.ingress.tls |
[] |
e.g. [{secretName: ultrvlt-tls, hosts: [ultrvlt.example.com]}]. |
Serve the portal over HTTPS — the session cookie is Secure.
If the portal sits behind a load balancer or ingress controller, list its
address range in queryApi.trustedProxies (for example ["10.0.0.0/8"], or
single addresses). Otherwise every user appears to come from the proxy’s
address — the audit log records the proxy, and failed-login lockouts and rate
limits treat all users as one. With the range listed, the user’s own address is
read from X-Forwarded-For, skipping only your proxies, so a client can’t
choose its own. Leave it empty when users reach the portal directly.
Remote-site enrollment
Section titled “Remote-site enrollment”Opt-in. Enabling enrollment lets one core take in telemetry from other clusters (sites), each authenticated by a certificate the core itself issues. It stands up the enrollment authority, turns on the portal’s Add site wizard, and switches the ingest endpoint to mutual TLS. For the full operator walkthrough, see Connecting remote sites.
| Value | Default | Notes |
|---|---|---|
enrollment.enabled |
false |
Turns on the enrollment authority + join-token service, enables the Add site wizard, and puts the ingest endpoint into mutual-TLS mode. |
enrollment.certTtlHours |
2160 (90d) |
Lifetime of the site certificates the core issues; sites auto-renew well before expiry. |
enrollment.adminKey |
"" |
Key protecting the enrollment admin API. Empty → generated on first install and kept stable. |
enrollment.existingSecret |
"" |
BYO Secret with key admin-key instead of the generated one. |
enrollment.persistence.size |
1Gi |
Holds the enrollment authority’s key material and issued-token record — keep it. |
Publishing the ingest endpoint to sites
Section titled “Publishing the ingest endpoint to sites”The ingest endpoint is in-cluster by default. To let remote sites reach it, publish it on a connection-level (L4) load balancer — the core still terminates the mutually-authenticated session itself, so the balancer only forwards it.
| Value | Default | Notes |
|---|---|---|
ingestGateway.wan.enabled |
false |
Publish the ingest endpoint on an external load balancer for remote sites. |
ingestGateway.wan.annotations |
{} |
Provider load-balancer annotations (e.g. an AWS NLB — see the deploy guide). |
ingestGateway.wan.sourceRanges |
[] |
CIDRs allowed to reach it — lock this to your sites’ egress addresses. Empty → open (mutual TLS still gates ingest, but scope the network too). |
ingestGateway.wan.port |
(ingest port) | Override the published port if you need to. |
ingestGateway.mtls.serverNames |
ingest-gateway |
The name sites verify on the core’s certificate. Keep in sync with queryApi.enroll.coreServerName. |
Connection facts for the wizard
Section titled “Connection facts for the wizard”Set these to the public addresses remote sites dial; the Add site wizard bakes them into the ready-to-run connector command it generates, so operators don’t have to assemble them by hand.
| Value | Default | Notes |
|---|---|---|
queryApi.enroll.publicUrl |
"" |
Public URL a site uses to enroll, e.g. https://enroll.ultrvlt.example.com. |
queryApi.enroll.corePublicEndpoint |
"" |
Public host:port of the ingest endpoint, e.g. ingest.ultrvlt.example.com:4317. |
queryApi.enroll.coreServerName |
ingest-gateway |
Name the site verifies on the core’s certificate; must match ingestGateway.mtls.serverNames. |
Data sources
Section titled “Data sources”eBPF sensor
Section titled “eBPF sensor”Off by default; only works on BTF-enabled Linux kernels. Validate the node kernel first (see Deploy to EKS).
| Value | Default | Notes |
|---|---|---|
sensor.enabled |
false |
|
sensor.siteId / sensor.clusterId |
default |
Tag captured data. |
sensor.privileged |
false |
false runs the sensor with only what it needs: Linux capabilities BPF, PERFMON, SYS_PTRACE, SYS_ADMIN and SYSLOG (to name kernel functions in CPU profiles), every other capability dropped, the runtime’s default seccomp profile, and AppArmor unconfined (so it can read other processes’ details to capture their encrypted traffic). It captures the same as a privileged container. true runs it privileged — use that on SELinux-enforcing hosts such as Bottlerocket or OpenShift, or if the sensor logs attach_failed or /proc access denied warnings. |
sensor.profiling.enabled |
true |
Continuous CPU profiling: every CPU on the node is sampled at a low fixed rate and each workload’s stacks are kept for 7 days as flame graphs (APM → Profiles). Costs well under 1% of a CPU and around 60 MiB of the sensor’s memory. Python processes are named at the Python level by reading their interpreter’s frames, which is why the sensor reads other processes’ memory (SYS_PTRACE). |
sensor.profiling.hz |
19 |
Samples per second per CPU. Higher gives more detail for short windows, at proportionally more cost. |
sensor.scrubSqlLiterals |
true |
Replaces the values in captured Postgres and MySQL queries, and in database error messages, with ? — WHERE email = ? rather than the address itself. false stores statements exactly as sent, readable by anyone who can view traces. Same value in the remote-site chart. |
sensor.tolerations |
[{operator: Exists}] |
Runs on every node, including control-plane. |
Cluster-state collector
Section titled “Cluster-state collector”| Value | Default | Notes |
|---|---|---|
clusterState.enabled |
false |
Singleton Deployment; reads k8s object state for the Infrastructure pages (no eBPF). |
Log collector
Section titled “Log collector”Opt-in OpenTelemetry Collector DaemonSet that forwards pod logs as OTLP. Not a first-party image — pinned explicitly.
| Value | Default | Notes |
|---|---|---|
logCollector.enabled |
false |
|
logCollector.image.tag |
0.111.0 |
Pinned to a statically-linked contrib build; re-verify on bumps. |
logCollector.siteId / clusterId |
default |
Keep in sync with the sensor’s. |
logCollector.k8sAttributes |
true |
Resolve service.name from the k8s workload so log service matches span service. |
logCollector.excludeLogPaths |
[] |
Extra pod-log globs to drop. |
Ingestion integrations
Section titled “Ingestion integrations”Opt-in receivers that let existing telemetry tools deliver into the core with no custom glue. Both are off by default and are pure receivers, so they work connected and air-gapped. When remote-site enrollment is on, each reuses the WAN server certificate and requires a client certificate, so an external sender is authenticated just like an enrolled site.
| Value | Default | Notes |
|---|---|---|
ingestGateway.promRemoteWrite.enabled |
false |
Prometheus remote-write endpoint (POST /api/v1/write) into the metrics path. See Prometheus remote-write. |
ingestGateway.promRemoteWrite.port |
19291 |
Port Prometheus sends to. |
ingestGateway.otlpHttp.enabled |
false |
OTLP/HTTP endpoints (/v1/logs, /v1/metrics, /v1/traces) for senders that speak OTLP/HTTP rather than gRPC — e.g. Fluent Bit forwarding logs. See Fluent Bit & Fluentd. |
ingestGateway.otlpHttp.port |
4318 |
Conventional OTLP/HTTP port. |
Browser monitoring (RUM)
Section titled “Browser monitoring (RUM)”Browser telemetry and session replay arrive on the OTLP/HTTP listener, so
ingestGateway.otlpHttp.enabled must be on. Because beacons come from untrusted
browsers over the public internet, the browser-facing path is gated by an origin
allowlist and optional abuse controls; server-to-server telemetry (no Origin
header) is unaffected. See Browser monitoring.
| Value | Default | Notes |
|---|---|---|
ingestGateway.rum.allowOrigins |
[] |
Web origins permitted to send browser telemetry (CORS). Empty → browser ingest is blocked. |
ingestGateway.rum.ingestKey |
"" |
Shared key the SDK presents on every browser upload (init({ ingestKey })). A throttling credential, not a strong secret; authenticates but binds no site; empty → not required. |
ingestGateway.rum.siteKeys |
[] |
Per-site ingest keys — a list of { site, key }. A browser presenting a listed key has its data tagged with that key’s site server-side, overriding the page’s self-asserted siteId, so the UI site scope separates data reliably (spoof-resistant, like an mTLS site). Each key must be unique. |
ingestGateway.rum.ratePerSec |
0 |
Per-origin rate limit on browser uploads. 0 → disabled. |
ingestGateway.rum.rateBurst |
0 |
Burst allowance; 0 → defaults to ratePerSec. |
Session replay needs a blob store for the recordings. Cloud object stores are the
recommended multi-pod path; the file:// backend is single-node/dev and needs a
ReadWriteMany volume shared by both the gateway and the read API.
| Value | Default | Notes |
|---|---|---|
blobStore.enabled |
false |
Turns on the session-replay chunk store. |
blobStore.url |
file:///var/lib/ultrvlt/rum-blobs |
Storage URL; the scheme picks the backend (file://, s3://, gs://, azblob://). |
blobStore.retentionDays |
30 |
Daily GC removes recordings older than this. Keep ≥ how long sessions stay queryable. 0 → keep forever. |
blobStore.persistence |
(off) | PVC for the file:// backend only; mountPath must equal the file:// URL’s path and the class must support ReadWriteMany. |
Running RUM in production (cloud storage + a public ingress)
Section titled “Running RUM in production (cloud storage + a public ingress)”For a multi-pod install, back replay on a cloud object store and give the pods a cloud identity to reach it. The gateway is the writer and query-api is the reader; the browser never gets storage credentials. On AWS that’s an S3 bucket plus IRSA — annotate each ServiceAccount with an IAM role ARN.
| Value | Default | Notes |
|---|---|---|
ingestGateway.serviceAccount.create |
false |
Create a ServiceAccount for the gateway (the replay writer). |
ingestGateway.serviceAccount.name |
"" |
Empty + create → <release>-ingest-gateway. |
ingestGateway.serviceAccount.annotations |
{} |
e.g. eks.amazonaws.com/role-arn: <writer-role> for IRSA. |
queryApi.serviceAccount.create |
false |
Create a ServiceAccount for query-api (the replay reader). |
queryApi.serviceAccount.name |
"" |
Empty + create → <release>-query-api. |
queryApi.serviceAccount.annotations |
{} |
e.g. eks.amazonaws.com/role-arn: <reader-role> (read-only is enough). |
Browser beacons need a public HTTPS route to the OTLP/HTTP listener. Keep it a
separate surface from the portal — RUM ingest is internet-open, guarded by the
gateway’s own origin allowlist + per-site keys + rate limit, whereas the portal is
an admin console you’ll usually restrict. Requires ingestGateway.otlpHttp.enabled.
| Value | Default | Notes |
|---|---|---|
ingestGateway.ingress.enabled |
false |
Browser-facing HTTPS Ingress to the gateway :4318. |
ingestGateway.ingress.className |
"" |
e.g. alb or nginx. |
ingestGateway.ingress.host |
rum.ultrvlt.local |
A dedicated hostname for ingest. |
ingestGateway.ingress.annotations |
{} |
e.g. ALB/ACM annotations (terminate TLS, redirect HTTP→HTTPS). |
ingestGateway.ingress.tls |
[] |
For in-cluster TLS (nginx + cert-manager). Omit when the LB terminates TLS with ACM. |
ingestGateway.ingress.paths |
[{path: /v1/, pathType: Prefix}] |
Routed to the gateway; the default prefix covers /v1/traces, /v1/metrics, /v1/logs, /v1/rum/replay. |
On AWS, the bucket + both IRSA roles can be provisioned with the reusable
Terraform module under deploy/aws/rum/ — its outputs are the role ARNs and the
s3://… URL you set above. Keep blobStore.retentionDays ≤ the bucket’s lifecycle
expiry so S3 never removes a recording the app still expects to serve.
Day-2 operations
Section titled “Day-2 operations”Backups
Section titled “Backups”Scheduled and on-demand ClickHouse backups to a dedicated backup disk (the
BACKUP statement carries no storage credentials).
| Value | Default | Notes |
|---|---|---|
backup.enabled |
false |
|
backup.schedule |
0 2 * * * |
Daily at 02:00 UTC. |
backup.destination.type |
file |
Dedicated PVC. S3/GCS/Azure land later. |
backup.destination.persistence.size |
50Gi |
Use a different storageClass/AZ than the data volume. |
backup.prune.enabled |
false |
Sidecar prune; keeps a snapshot if EITHER keepLast or maxAgeDays still protects it. |
Lifecycle manager
Section titled “Lifecycle manager”Opt-in singleton that tracks a signed release channel and serves upgrade status to the portal’s Updates page.
| Value | Default | Notes |
|---|---|---|
lifecycleManager.enabled |
false |
|
lifecycleManager.channel |
stable |
stable (recommended) or latest. |
lifecycleManager.channelsRepo |
ghcr.io/ultrvlt/charts/ultrvlt-channels |
|
lifecycleManager.networkPolicy.enabled |
false |
Only the query API’s pods may connect to the lifecycle manager. Needs a cluster network that enforces NetworkPolicy. |
lifecycleManager.upgrades.enabled |
false |
Opt-in actuation — adds a verified “Deploy now” button via a privilege-separated Job. |
lifecycleManager.upgrades.schedule.enabled |
false |
Opt-in automatic weekly-window upgrades (requires upgrades.enabled). |