# CubeObservability CubeSandbox sandbox observability deployment and operations. English | [简体中文](../zh/guide/observability.md) Observability is opt-in for one-click and Kubernetes installs. Enable it in the one-click environment file or in Helm values; there are no extra installer flags. Collected by default: - Metrics: the `cubesandbox_*` resource metrics from Cubelet at `127.0.0.1:9998/v1/metrics/resource`. - Logs: sandbox `stdout` and `stderr` at `/data/cubelet/log//{stdout,stderr}`. Helm can additionally collect CRI logs for an allowlist of CubeSandbox containers under `/var/log/pods`. This optional Collector DaemonSet is disabled by default and is available only through the Helm chart, not the one-click/systemd installer. Prometheus, Grafana, and one log backend (Loki by default, or Elasticsearch) ship with both installers. Both can also export to external backends. Ownership: - Grafana image and dashboards: `CubeObservability/`; - one-click CVM/systemd: `deploy/one-click/observability/`; - Helm Collector and backends: `deploy/kubernetes/chart/`. ## 1. Capabilities, versions, and preflight ### 1.1 Cubelet capability baseline This solution uses the following data from the current Cubelet: - Sandbox CPU and memory metrics from `/v1/metrics/resource`. Each per-sandbox series carries `sandbox_id`; `guest_workload` series also carry `container_id`. - The node-level `cubesandbox_node_sandbox_count` and `cubesandbox_node_sandbox_available_count` gauges. Both return `0` when no sandbox is active. - Sandbox `stdout` and `stderr` logs from `/data/cubelet/log//{stdout,stderr}`. For the complete metric names, types, units, and labels, see the [CubeSandbox resource metrics guide on GitHub](https://github.com/tencentcloud/CubeSandbox/blob/master/docs/guide/resource-metrics.md). The `host_sandbox` series carry `sandbox_id`; `guest_workload` series carry both `sandbox_id` and `container_id`. The [Cubelet Prometheus source](https://github.com/tencentcloud/CubeSandbox/blob/master/Cubelet/plugins/cube/internals/resourcemetrics/prometheus.go) is the implementation reference. Node-count gauges have no dynamic labels and do not contain a list of Sandbox IDs. #### Preflight Run these checks after Cubelet is installed. For a new one-click environment, complete the first-node steps in [2.1](#21-one-click) first: ```bash # Cubelet version and service /usr/local/services/cubetoolbox/Cubelet/bin/cubecli version sudo systemctl is-active cube-sandbox-cubelet.service # Sandbox resource endpoint and per-sandbox ID label curl -fsS http://127.0.0.1:9998/v1/metrics/resource | \ grep -E '^cubesandbox_(host_sandbox|guest_workload|node_sandbox_)' curl -fsS http://127.0.0.1:9998/v1/metrics/resource | \ grep 'sandbox_id=' # Standard sandbox stdout/stderr log path while a sandbox is active SANDBOX_ID=019abc-example ls -l "/data/cubelet/log/${SANDBOX_ID}"/{stdout,stderr} ``` The `sandbox_id` check can be empty when no sandbox is active; both `cubesandbox_node_sandbox_*` gauges should still return `0`. A disabled plugin or pending first sample can also produce an empty result, so interpret it together with the plugin configuration and service state. ### 1.2 Installer versions and external compatibility baselines Loki and Elasticsearch are mutually exclusive sandbox log backends. Loki is installed by default; Elasticsearch is installed only when explicitly selected with `CUBE_LOGS_BACKEND=elasticsearch` (or `observability.logs.backend=elasticsearch` for Helm). Only one log backend is active in a deployment, and logs are not duplicated. | Component | Installer-pinned and validated version | External/self-managed baseline | Default port | Role and compatibility notes | | --- | --- | --- | --- | --- | | OTel Collector Contrib | `0.162.0` | Prefer `0.162.x` | `127.0.0.1:13133` (health) | Per-node collector. The generated YAML uses the OTLP/HTTP and Elasticsearch exporter configuration from this release. Arbitrary later releases are not implicitly supported; rerun `otelcol validate` after any upgrade. | | Prometheus | one-click `3.13.4`; Helm `v3.2.1` | Prometheus `3.x` with OTLP and Remote Write receivers | `9092` | Sandbox metrics backend. `--web.enable-otlp-receiver`, `--web.enable-remote-write-receiver`, and the documented `otlp` configuration are required. | | Loki (default log backend; choose one) | `3.7.9` | Loki `3.x` with native OTLP and structured metadata enabled | `3100` | Default sandbox logs backend. The Collector writes to `/otlp`; Grafana queries it with LogQL. | | Elasticsearch (optional log backend; choose one) | `8.17.3` | Elasticsearch `8.x` | `9200` | Replaces Loki when explicitly selected; uses raw mapping and the Bulk API. Validate other major releases separately. | | Grafana OSS | `10.4.18` | `>= 10.4` with dashboard schema `39` support | `3001` | Metrics and logs UI. Validate the selected log data source, three metrics dashboards, and logs dashboard before upgrading. | Pinned versions keep bundled installs reproducible. An external baseline is not a guarantee for every future release. Upstream configuration references: [Prometheus receiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/receiver/prometheusreceiver), [filelog receiver](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/pkg/stanza/operator/input/file), [Elasticsearch exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/elasticsearchexporter), [Prometheus OTLP ingestion](https://prometheus.io/docs/guides/opentelemetry/), and [Loki OTLP ingestion](https://grafana.com/docs/loki/latest/send-data/otel/). The bundled CVM/systemd installers require 64-bit Linux, systemd, `curl`, `tar`, and `python3`; Python is used to extract and validate the Loki release archive safely. Kubernetes and Helm do not require these host binaries. ### 1.3 Loki or Elasticsearch - **Loki (default):** lower resource overhead, suited to quick deployment and primarily label-based log queries. - **Elasticsearch (optional):** suited to teams with an existing ES operations model or a need for full-text search. - Choose one. In production, prefer an existing team-managed or hosted log platform. Select the backend in `.env` for a new installation: ```dotenv CUBE_LOGS_BACKEND=loki # Or: # CUBE_LOGS_BACKEND=elasticsearch ``` Learn more in the [Loki documentation](https://grafana.com/docs/loki/latest/), [Loki OTLP ingestion guide](https://grafana.com/docs/loki/latest/send-data/otel/), [Elasticsearch logs documentation](https://www.elastic.co/docs/solutions/observability/logs), and [Elasticsearch Query DSL](https://www.elastic.co/docs/explore-analyze/query-filter/languages/querydsl). ## 2. Enable with a standard deployment | Deployment | Entrypoint | Enable | | --- | --- | --- | | first one-click node (all-in-one by default) | `deploy/one-click/install.sh` | Set `ONE_CLICK_ENABLE_OBSERVABILITY=1`, then run the normal installer | | one-click compute node | `deploy/one-click/install-compute.sh` | Set `ONE_CLICK_ENABLE_OBSERVABILITY=1` and the first-node address; only the node Collector is installed | | existing one-click deployment | original `install.sh` / `install-compute.sh` | Update the original `.env`, then run with `--mode=upgrade --yes`; see [2.1 C](#c-enable-observability-in-an-existing-one-click-deployment) | | Kubernetes Helm | `deploy/kubernetes/chart` | Pass `--set observability.enabled=true` | ### 2.1 one-click one-click keeps using its existing `.env` and installer entrypoints, and observability remains disabled by default. The first node is all-in-one by default. Additional compute nodes send metrics and logs to that first node; there is no separate monitoring control plane. #### A. Install the first all-in-one node 1. Enter `deploy/one-click` in a source checkout, or the one-click root of an extracted release. Copy the template only for a fresh installation: ```bash # Source checkout: cd deploy/one-click # Extracted release: # cd /path/to/cube-sandbox-one-click [[ -f .env ]] || cp env.example .env chmod 600 .env ``` 2. Choose a private IP reachable from later compute nodes, such as `10.0.0.10`, and set: ```bash # .env CUBE_SANDBOX_NODE_IP=10.0.0.10 ONE_CLICK_ENABLE_OBSERVABILITY=1 CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod CUBE_LOGS_BACKEND=loki CUBE_GRAFANA_ADMIN_PASSWORD='' ``` The installer rejects an unset or shorter Grafana password. Set `CUBE_LOGS_BACKEND=elasticsearch` to use Elasticsearch. The installer can detect `CUBE_SANDBOX_NODE_IP` when it is omitted, but confirm the resulting address after installation. 3. Run the normal installer: ```bash sudo ./install.sh ``` 4. Record the final first-node address. Additional compute nodes must use this reachable private IP: ```bash grep '^CUBE_SANDBOX_NODE_IP=' \ /usr/local/services/cubetoolbox/.one-click.env ``` 5. Allow only the required network paths: - compute nodes to the first node: TCP `9092`, plus `3100` for Loki or `9200` for Elasticsearch; - administrators to the first node: Grafana `3001`; - never expose `9092/3100/9200` directly to the public Internet. 6. Verify the first node: ```bash sudo /usr/local/services/cubetoolbox/observability/install.sh status sudo systemctl --no-pager --full status cube-sandbox-otelcol.service curl -fsS http://127.0.0.1:9092/-/ready curl -fsS http://127.0.0.1:3100/ready # Elasticsearch users replace the previous line with: # curl -fsS http://127.0.0.1:9200/ ``` Open Grafana at `http://10.0.0.10:3001` and check the **CubeSandbox sandbox resources** and **CubeSandbox sandbox logs** dashboards. Per-sandbox series and logs can be empty when no sandbox is active. #### B. Install additional compute nodes 1. Prepare the same one-click release and an `.env` on each compute node: ```bash [[ -f .env ]] || cp env.example .env chmod 600 .env ``` 2. Set the first-node address and keep the cluster ID and log backend identical to the first node: ```bash # .env ONE_CLICK_CONTROL_PLANE_IP=10.0.0.10 ONE_CLICK_ENABLE_OBSERVABILITY=1 CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod CUBE_LOGS_BACKEND=loki ``` `ONE_CLICK_CONTROL_PLANE_IP` is the first CubeSandbox node's private IP, not a newly created monitoring node. `install-compute.sh` exports `ONE_CLICK_DEPLOY_ROLE=compute` automatically, so do not repeat it in `.env`. With no explicit external endpoints, the installer derives the Prometheus and selected log-backend addresses from this IP. 3. Install and inspect the local Collector: ```bash sudo ./install-compute.sh sudo systemctl --no-pager --full status cube-sandbox-otelcol.service sudo journalctl -u cube-sandbox-otelcol.service -n 100 --no-pager ``` 4. Create a test sandbox, then query by `node_id` and `sandbox_id` in Grafana on the first node. If no data arrives, first check local `127.0.0.1:9998`, the log path, and connectivity to the required first-node ports. #### C. Enable observability in an existing one-click deployment Use a one-click release that contains the observability deployment files and keep maintaining the `.env` in the original installation directory. Do not overwrite it with `env.example`, and do not edit the generated `.one-click.env` directly. 1. Add or update these values in `.env` on the first node: ```dotenv ONE_CLICK_ENABLE_OBSERVABILITY=1 CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod CUBE_LOGS_BACKEND=loki CUBE_GRAFANA_ADMIN_PASSWORD='' ``` 2. Run the original one-click entrypoint in upgrade mode: ```bash sudo ./install.sh --mode=upgrade --yes ``` 3. After verifying the first node, add or update these values in `.env` on every compute node: ```dotenv ONE_CLICK_CONTROL_PLANE_IP=10.0.0.10 ONE_CLICK_ENABLE_OBSERVABILITY=1 CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod CUBE_LOGS_BACKEND=loki ``` ```bash sudo ./install-compute.sh --mode=upgrade --yes ``` Replace `10.0.0.10` with the first node's reachable private IP, and use the same cluster ID and log backend on every node. These commands reconcile the one-click stack and observability components from the original `.env`; back up the configuration through the normal operations process before running them. On upgrade, a wholesale copy of `env.example` with its default `ONE_CLICK_ENABLE_OBSERVABILITY=0` does not disable an already enabled stack. To explicitly disable it, run `sudo env ONE_CLICK_ENABLE_OBSERVABILITY=0 ./install.sh --mode=upgrade --yes` (or use `./install-compute.sh` on a compute node). #### D. Online installation Online installation follows the same environment-variable forwarding style as bundled MinIO: ```bash # First all-in-one node. Bundled Grafana requires an admin password of at least # 16 characters; store the generated value in your password manager. GRAFANA_ADMIN_PASSWORD="$(openssl rand -hex 16)" echo "Grafana admin password: ${GRAFANA_ADMIN_PASSWORD}" curl -fsSL https://raw.githubusercontent.com/tencentcloud/CubeSandbox/master/deploy/one-click/online-install.sh \ | sudo env ONE_CLICK_ENABLE_OBSERVABILITY=1 \ CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod \ CUBE_LOGS_BACKEND=loki \ CUBE_GRAFANA_ADMIN_PASSWORD="${GRAFANA_ADMIN_PASSWORD}" \ bash # Compute node curl -fsSL https://raw.githubusercontent.com/tencentcloud/CubeSandbox/master/deploy/one-click/online-install.sh \ | sudo env ONE_CLICK_DEPLOY_ROLE=compute \ ONE_CLICK_CONTROL_PLANE_IP=10.0.0.10 \ ONE_CLICK_ENABLE_OBSERVABILITY=1 \ CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod \ CUBE_LOGS_BACKEND=loki \ bash ``` `online-install.sh` downloads the release and invokes the same `install.sh`; it is not a separate deployment topology. For authentication tokens and other secrets, prefer downloading the release and storing values in a mode-`0600` `.env` rather than leaving secrets in shell history. #### E. External backends ```dotenv ONE_CLICK_OBSERVABILITY_ROLE=auto CUBE_LOGS_BACKEND=loki CUBE_METRICS_OTLP_HTTP_ENDPOINT=http://10.0.0.10:9092/api/v1/otlp CUBE_LOGS_OTLP_HTTP_ENDPOINT=http://10.0.0.10:3100/otlp ``` With explicit external endpoints, `auto` installs only the node Collector, not local backends. See [4.1](#41-core-environment-variables-and-export-authentication) for external-backend authentication. ### 2.2 Kubernetes Helm Deploy the in-cluster backend and the compute-node Collector: ```bash helm upgrade --install cube ./deploy/kubernetes/chart \ -n cube-system --create-namespace \ -f /path/to/runtime-values.yaml \ --set observability.enabled=true \ --set observability.clusterId=k8s-prod \ --set observability.logs.backend=loki ``` The chart generates a release-scoped Grafana admin Secret with a random password and reuses it on upgrades. Read it from the cluster instead of passing credentials on the command line: ```bash kubectl get secret -n cube-system \ -l app.kubernetes.io/component=grafana-admin \ -o jsonpath='{.items[0].data.admin-password}' | base64 --decode; echo ``` Set `observability.backend.grafana.existingSecret` to a Secret with `admin-user` and `admin-password` to manage credentials outside Helm. An inline `adminPassword` must be at least 16 characters. To also collect component container logs, add `--set observability.logs.components.enabled=true`, or set it in `runtime-values.yaml`: ```yaml observability: logs: components: enabled: true ``` This runs a second Collector that tails only the allowlisted containers (`observability.logs.components.containers`, default: master, API, Ops, TemplateCenter, Cubelet, proxy, lifecycle manager) in the release namespace, from a read-only `/var/log/pods` mount, on nodes matching `observability.logs.components.nodeSelector`, affinity, and tolerations. Collection starts at the file end, so existing history is not replayed. Query it in Grafana with `service_name="cubesandbox-cube-master"`, or use the **Component logs** row of the logs dashboard. Check component logs for secrets before enabling, and add redaction when needed. Collector, Prometheus, log backend, and Grafana Pods carry readiness/liveness probes, so Kubernetes gates rollout and restarts unhealthy monitoring Pods. This is not alerting; no Alertmanager rules are installed. Deploy only the Collector and send data to existing backends: ```bash helm upgrade --install cube ./deploy/kubernetes/chart \ -n cube-system --create-namespace \ -f /path/to/runtime-values.yaml \ --set observability.enabled=true \ --set observability.backend.enabled=false \ --set observability.clusterId=k8s-prod \ --set observability.metrics.otlpEndpoint="http://10.0.0.10:9092/api/v1/otlp" \ --set observability.logs.backend=loki \ --set observability.logs.otlpEndpoint="http://10.0.0.10:3100/otlp" ``` Use the same `runtime-values.yaml` as a normal chart installation; it must contain non-default database/Redis credentials and placement settings. Observability values are additions to that existing install command. For Elasticsearch, set `observability.logs.backend=elasticsearch` and, for an external backend, `observability.logs.esEndpoint`. Do not set both log endpoints. For authenticated external backends, create a Secret in the release namespace with your existing secret-management workflow and reference it from `runtime-values.yaml`. Store the complete `Authorization` header value (for example `Bearer ...` or Elasticsearch `ApiKey ...`) and do not pass credentials through `--set`: ```yaml # Example only. Create these Secrets through the cluster's secret-management workflow. apiVersion: v1 kind: Secret metadata: name: cube-observability-auth namespace: cube-system type: Opaque stringData: metrics-authorization: "Bearer " metrics-tenant-id: "cube-prod" logs-authorization: "Bearer " logs-tenant-id: "cube-prod" --- apiVersion: v1 kind: Secret metadata: name: cube-grafana-admin namespace: cube-system type: Opaque stringData: admin-user: "admin" admin-password: "" ``` For an Elasticsearch API key, set `logs-authorization` to `ApiKey `. Remove a tenant key when the backend does not require that header. Reference those Secret keys from `runtime-values.yaml`: ```yaml observability: metrics: auth: existingSecret: cube-observability-auth authorizationKey: metrics-authorization tenantIdKey: metrics-tenant-id # optional; empty disables X-Scope-OrgID logs: auth: existingSecret: cube-observability-auth authorizationKey: logs-authorization tenantIdKey: logs-tenant-id # set "" when the backend needs no tenant header backend: grafana: existingSecret: cube-grafana-admin adminUserKey: admin-user adminPasswordKey: admin-password ``` HTTPS endpoints use the container system CA. The chart does not yet mount a custom CA Secret; extend the CA mount for a private-CA deployment instead of disabling certificate verification. The Helm Collector always uses the host network and scrapes the same-node Cubelet at `127.0.0.1:9998`. The built-in topology therefore requires the default `cubeNode.hostNetwork=true`. The chart does not currently discover a reliable per-node metrics target when Cubelet is moved to the Pod network. Common values: | Value | Default | Meaning | | --- | --- | --- | | `observability.enabled` | `false` | Observability master switch | | `observability.clusterId` | `cubesandbox` | Cluster label for metrics and logs | | `observability.otelCollector.scrapeInterval` | `15s` | Cubelet scrape interval; it must not be shorter than the fixed `10s` scrape timeout | | `observability.otelCollector.cubeletMetricsTarget` | `127.0.0.1:9998` | Node-local Cubelet endpoint; the default requires `cubeNode.hostNetwork=true` | | `observability.otelCollector.stateHostPath` | `/var/lib/cube-sandbox/otelcol-k8s` | Per-node persistent filelog offsets, queues, and WAL | | `observability.backend.enabled` | `true` | Deploy Prometheus, the selected log backend, and Grafana with the Collector | | `observability.metrics.otlpEndpoint` | `""` | External Prometheus OTLP/HTTP base endpoint | | `observability.metrics.remoteWriteUrl` | `""` | External Prometheus Remote Write URL; alternative to OTLP | | `observability.metrics.auth.existingSecret` | `""` | Secret containing metrics Authorization / tenant headers | | `observability.logs.enabled` | `true` | Collect sandbox `stdout`/`stderr` logs | | `observability.logs.components.enabled` | `false` | Collect allowlisted component CRI logs from `/var/log/pods` with a second DaemonSet | | `observability.logs.components.hostPath` | `/var/log/pods` | Absolute, non-root host directory holding the pod log tree | | `observability.logs.components.containers` | Cube component list | Container-name allowlist; only these directories are tailed | | `observability.logs.components.nodeSelector` / `affinity` / `tolerations` | Linux nodes; control/compute labels and taints | Placement of the component-log Collector | | `observability.logs.backend` | `loki` | Log backend: `loki` or `elasticsearch` | | `observability.logs.otlpEndpoint` | `""` | External OTLP/HTTP base endpoint for `backend=loki` | | `observability.logs.esEndpoint` | `""` | External Elasticsearch endpoint for `backend=elasticsearch` | | `observability.logs.auth.existingSecret` | `""` | Secret containing logs Authorization / tenant headers | | `observability.backend..persistence.enabled` | `true` | Use a PVC for built-in Prometheus, log backend, and Grafana | | `observability.backend.grafana.adminPassword` | `""` | Empty generates a random password in a release-scoped Secret; inline values need 16+ characters | | `observability.backend.grafana.existingSecret` | `""` | Grafana admin Secret (`admin-user` / `admin-password`); preferred in production | ## 3. Standalone installation (optional) The observability components can also be installed without a one-click or Helm release. The standalone installer manages `cube-sandbox-otelcol`, Prometheus, Loki/Elasticsearch, and Grafana, and does not touch CubeMaster, Cubelet, or other services. Run the [preflight](#preflight) first, then pick a backend node reachable from the compute nodes. ### 3.1 Single node or first backend node If the backend node also runs sandboxes, install both the backend and its local Collector: ```bash sudo env \ CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod \ CUBE_LOGS_BACKEND=loki \ CUBE_GRAFANA_ADMIN_PASSWORD='REPLACE_WITH_STRONG_PASSWORD' \ ./deploy/one-click/observability/install.sh all ``` If that node hosts only the backend and runs no sandboxes, omit the local Collector: ```bash sudo env \ CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod \ CUBE_LOGS_BACKEND=loki \ CUBE_GRAFANA_ADMIN_PASSWORD='REPLACE_WITH_STRONG_PASSWORD' \ ./deploy/one-click/observability/install.sh backend ``` Replace the Grafana password before running either command. To use Elasticsearch, set `CUBE_LOGS_BACKEND=elasticsearch`. Restrict the backend ports as described in [2.1 A](#a-install-the-first-all-in-one-node). ### 3.2 Other CVM or bare-metal compute nodes Install the Collector on each compute node, replacing `10.0.0.10` with the backend node's private IP: ```bash sudo env \ CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod \ CUBE_METRICS_OTLP_HTTP_ENDPOINT=http://10.0.0.10:9092/api/v1/otlp \ CUBE_LOGS_OTLP_HTTP_ENDPOINT=http://10.0.0.10:3100/otlp \ ./deploy/one-click/observability/install.sh cvm ``` For Elasticsearch, remove `CUBE_LOGS_OTLP_HTTP_ENDPOINT` and use: ```bash sudo env \ CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod \ CUBE_LOGS_BACKEND=elasticsearch \ CUBE_METRICS_OTLP_HTTP_ENDPOINT=http://10.0.0.10:9092/api/v1/otlp \ CUBE_LOGS_ES_ENDPOINT=http://10.0.0.10:9200 \ ./deploy/one-click/observability/install.sh cvm ``` Add the variables from [4.1](#41-core-environment-variables-and-export-authentication) when the external backend requires a token, tenant header, ES API key, or Basic Auth. ### 3.3 Verification and uninstall ```bash # Full systemd all-in-one installation sudo ./deploy/one-click/observability/install.sh status # Collector-only systemd node sudo systemctl --no-pager --full status cube-sandbox-otelcol.service # Remove independently installed systemd components; data directories are retained. sudo ./deploy/one-click/observability/install.sh uninstall all ``` For a Kubernetes release, check the Collector DaemonSets instead (names are prefixed with the release fullname, `cube` below): ```bash kubectl -n cube-system rollout status daemonset/cube-node-otelcol # Only with observability.logs.components.enabled=true kubectl -n cube-system rollout status daemonset/cube-component-otelcol ``` ## 4. OTel Collector configuration Each node runs `otelcol-contrib 0.162.0` and generates up to two independent pipelines according to the configured destinations: | Pipeline | Receiver | Input | Exporter | | --- | --- | --- | --- | | `metrics` | `prometheus` | Cubelet `/v1/metrics/resource` | `otlp_http/metrics` or `prometheus_remote_write` | | `logs/sandbox` | `filelog/sandbox` | `/data/cubelet/log/*/{stdout,stderr}` | `elasticsearch/logs` or `otlp_http/logs` | These names come from the systemd generator. Helm uses the equivalent `prometheus/cubelet_resource_metrics`, `filelog/sandbox_stdio`, and `logs` names, plus one `logs/component_` pipeline per allowlisted container in the separate component-log Collector. Implementation details are documented in the [OTel design](../dev/sandbox-otel-recommendation.md). ### 4.1 Core environment variables and export authentication | Variable | Default | Example | Meaning | | --- | --- | --- | --- | | `CUBE_OBSERVABILITY_CLUSTER_ID` | `cubesandbox` | `cube-prod` | Cluster label written to metrics and logs | | `CUBE_LOGS_BACKEND` | `loki` | `elasticsearch` | Log-backend selector for one-click/systemd | | `CUBE_SANDBOX_NODE_IP` | auto-detected | `10.0.0.20` | systemd only: node label; set it when auto-detection is unsuitable | | `CUBE_METRICS_OTLP_HTTP_ENDPOINT` | empty | `http://10.0.0.10:9092/api/v1/otlp` | OTLP/HTTP base endpoint; omit `/v1/metrics` | | `CUBE_METRICS_REMOTE_WRITE_URL` | empty | `http://10.0.0.10:9092/api/v1/write` | Prometheus Remote Write URL; alternative to metrics OTLP | | `CUBE_METRICS_AUTHORIZATION` / `CUBE_METRICS_REMOTE_WRITE_TOKEN` | empty | `Bearer ...` | Metrics authentication for one-click/systemd; choose a complete header or a bare token | | `CUBE_METRICS_TENANT_ID` | empty | `cube-prod` | Metrics `X-Scope-OrgID` | | `CUBE_LOGS_ES_ENDPOINT` | empty | `http://10.0.0.10:9200` | Elasticsearch endpoint | | `CUBE_LOGS_ES_INDEX` | `cubesandbox-logs` | `cube-prod-logs` | Elasticsearch index | | `CUBE_LOGS_ES_USER` / `CUBE_LOGS_ES_PASSWORD` | empty | `cube` / `...` | one-click/systemd: ES Basic Auth; both values are required | | `CUBE_LOGS_ES_API_KEY` | empty | `...` | one-click/systemd: ES API key; alternative to Basic Auth | | `CUBE_LOGS_ES_TLS_INSECURE_SKIP_VERIFY` | `false` | `true` | Skip ES TLS verification for temporary diagnostics only | | `CUBE_LOGS_OTLP_HTTP_ENDPOINT` | empty | `http://10.0.0.10:3100/otlp` | OTLP/HTTP logs base endpoint; alternative to Elasticsearch | | `CUBE_LOGS_AUTHORIZATION` / `CUBE_LOGS_TOKEN` | empty | `Bearer ...` | Log authentication for one-click/systemd; choose a complete header or a bare token | | `CUBE_LOGS_TENANT_ID` | empty | `cube-prod` | Logs `X-Scope-OrgID` | | `CUBE_LOGS_SANDBOX_ID` | `*` | `019abc-example` | Optional sandbox ID filter for the systemd collector | | `CUBE_LOGS_FILE_START_AT` | `beginning` | `end` | systemd only: initial file position, either `beginning` or `end` | The bundled Grafana provisioning reuses these metrics/log credentials for its datasources. Running `grafana/install.sh dashboards` creates or updates datasources by UID. Helm uses the Kubernetes Node name as the node label and does not read these systemd-only variables. Its fixed `start_at: beginning` applies only when no saved offset exists. Collector state is persisted per node at `/var/lib/cube-sandbox/otelcol-k8s`, so Pod replacement on the same node resumes from `file_storage` instead of rereading existing files. One-click users can use the `Observability` section already present in `deploy/one-click/env.example`. After copying it to `.env` as shown in [2.1](#21-one-click), uncomment optional settings only when needed: ```dotenv ONE_CLICK_ENABLE_OBSERVABILITY=1 CUBE_OBSERVABILITY_CLUSTER_ID=cube-prod CUBE_LOGS_BACKEND=loki # The standard all-in-one/compute topology derives these automatically. # Set them only for a self-managed or external backend. CUBE_METRICS_OTLP_HTTP_ENDPOINT=http://10.0.0.10:9092/api/v1/otlp CUBE_LOGS_OTLP_HTTP_ENDPOINT=http://10.0.0.10:3100/otlp ``` Configure at least one metrics or logs destination. Do not configure both metrics destinations or both logs destinations at the same time. The standard one-click topology fills in its bundled backend endpoints automatically. For Elasticsearch, use `CUBE_LOGS_BACKEND=elasticsearch`, `CUBE_LOGS_ES_ENDPOINT=http://10.0.0.10:9200`, and optionally `CUBE_LOGS_ES_INDEX`. #### Authentication in every topology The supported one-click and Kubernetes workflows can export to authenticated **external** metrics and log backends. A minimal Bearer example is: ```dotenv CUBE_METRICS_OTLP_HTTP_ENDPOINT=https://prometheus.example.com/api/v1/otlp CUBE_METRICS_AUTHORIZATION='Bearer ' CUBE_LOGS_OTLP_HTTP_ENDPOINT=https://loki.example.com/otlp CUBE_LOGS_AUTHORIZATION='Bearer ' ``` - the one-click/systemd installer reads these variables from the node's `.env` file; - Helm references a Secret through `observability.metrics.auth.existingSecret` and `observability.logs.auth.existingSecret`; see [2.2](#22-kubernetes-helm); - Elasticsearch uses `CUBE_LOGS_ES_API_KEY`, or the paired `CUBE_LOGS_ES_USER` and `CUBE_LOGS_ES_PASSWORD`. An `*_AUTHORIZATION` value is the complete single-line header. An `*_TOKEN` value is a bare token that the installer converts to a Bearer header; do not set both forms. These settings configure Collector/Grafana client headers. They do not enable server-side authentication on bundled Prometheus, Loki, or Elasticsearch. Put an authentication gateway or reverse proxy in front of a bundled backend, or use a managed backend. Official references: [OTLP HTTP exporter](https://github.com/open-telemetry/opentelemetry-collector/tree/main/exporter/otlphttpexporter), [Prometheus Remote Write exporter](https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter/prometheusremotewriteexporter), [Loki authentication](https://grafana.com/docs/loki/latest/operations/authentication/), [Elasticsearch API keys](https://www.elastic.co/docs/deploy-manage/api-keys), and [Kubernetes Secrets](https://kubernetes.io/docs/concepts/configuration/secret/). Never commit real credentials. Set `.env` to mode `0600`. HTTPS uses the system CA by default; install or mount a private CA instead of disabling verification. ### 4.2 Config locations and rendering `render` and installation use the same generator. Values already configured in the environment are written into the generated YAML, so the YAML does not need a second manual edit. The one-click wrapper loads `.env` automatically; for a standalone run, export the variables first or pass them with `env`. | Topology | Installed configuration | Render with configured values | | --- | --- | --- | | systemd | `/etc/cube-sandbox/otelcol/otelcol.yaml` | `./deploy/one-click/observability/install.sh render cvm` | | Helm | ConfigMaps `-node-otelcol-config` and `-component-otelcol-config` | Use the `helm template` command below | Helm uses `observability.*` values from `values.yaml` or `--set`; it does not read `CUBE_*` environment variables automatically. Even a render-only Helm command must satisfy the chart-wide credential validation with non-default placeholders: ```bash helm template cube ./deploy/kubernetes/chart \ --set observability.enabled=true \ --set-string mysql.password=render-only \ --set-string mysql.rootPassword=render-only \ --set-string redis.password=render-only \ -s templates/observability.yaml ``` Use `render-only` only for local rendering. Supply real credentials through secret values for an actual installation. ### 4.3 Generated configuration example The excerpt below assumes the example values from 4.1 and node address `10.0.0.20`, sending metrics to Prometheus and logs to Loki. It omits some exporter discover/queue/retry and `service.telemetry` fields for readability; use `render` for the complete output. Comments explain environment-variable mappings and are not guaranteed to appear in the generated file. This is generated output, not a second configuration source. `render` and `install` substitute the configured environment values automatically. Rerun the installation after changing those values. ```yaml receivers: # Scrape sandbox resource metrics from the local Cubelet only. prometheus: trim_metric_suffixes: false config: scrape_configs: - job_name: cubesandbox-resource scrape_interval: 5s scrape_timeout: 3s metrics_path: /v1/metrics/resource static_configs: - targets: ["127.0.0.1:9998"] # CUBE_LOGS_SANDBOX_ID replaces "*"; the default collects every sandbox. filelog/sandbox: include: - /data/cubelet/log/*/stdout - /data/cubelet/log/*/stderr start_at: beginning # CUBE_LOGS_FILE_START_AT storage: file_storage include_file_path: true include_file_name: false max_log_size: 1MiB operators: - type: regex_parser parse_from: attributes["log.file.path"] regex: '^/data/cubelet/log/(?P[^/]+)/(?Pstdout|stderr)$' - type: copy from: attributes["log.file.path"] to: attributes.log_file_path - type: copy from: attributes.sandbox_id to: attributes.cubesandbox_sandbox_id - type: move from: attributes.sandbox_id to: resource["cubesandbox.sandbox.id"] - type: copy from: attributes.stream to: attributes.log_iostream - type: move from: attributes.stream to: attributes["log.iostream"] processors: memory_limiter: check_interval: 1s limit_mib: 256 # Values come from CUBE_OBSERVABILITY_CLUSTER_ID and # CUBE_SANDBOX_NODE_IP (auto-detected when unset). resource/metrics: attributes: - {key: cluster, value: "cube-prod", action: upsert} - {key: node, value: "10.0.0.20", action: upsert} - {key: cubesandbox.cluster.id, value: "cube-prod", action: upsert} - {key: host.name, value: "10.0.0.20", action: upsert} resource/logs_common: attributes: - {key: host.name, value: "10.0.0.20", action: upsert} - {key: cubesandbox.cluster.id, value: "cube-prod", action: upsert} attributes/logs_common: actions: - {key: host_name, value: "10.0.0.20", action: upsert} - {key: cubesandbox_cluster_id, value: "cube-prod", action: upsert} - {key: cubesandbox_sandbox_id, value: "none", action: insert} - {key: log_iostream, value: "none", action: insert} resource/sandbox: attributes: - {key: service.name, value: "cubesandbox-sandbox", action: upsert} - {key: cubesandbox.log.source, value: "sandbox-init", action: upsert} attributes/sandbox: actions: - {key: service_name, value: "cubesandbox-sandbox", action: upsert} - {key: cubesandbox_log_source, value: "sandbox-init", action: upsert} batch: timeout: 2s send_batch_size: 1024 exporters: otlp_http/metrics: # CUBE_METRICS_OTLP_HTTP_ENDPOINT; the Collector appends /v1/metrics. endpoint: "http://10.0.0.10:9092/api/v1/otlp" compression: gzip sending_queue: enabled: true storage: file_storage retry_on_failure: enabled: true max_elapsed_time: 0 otlp_http/logs: # CUBE_LOGS_OTLP_HTTP_ENDPOINT; the Collector appends /v1/logs. endpoint: "http://10.0.0.10:3100/otlp" compression: gzip sending_queue: enabled: true storage: file_storage retry_on_failure: enabled: true max_elapsed_time: 0 extensions: health_check: endpoint: 127.0.0.1:13133 # Persist file offsets and the OTLP sending queue across process restarts. file_storage: directory: /var/lib/cube-sandbox/otelcol/storage create_directory: true service: extensions: [health_check, file_storage] pipelines: # Metrics and logs are independent pipelines. metrics: receivers: [prometheus] processors: [memory_limiter, resource/metrics, batch] exporters: [otlp_http/metrics] logs/sandbox: receivers: [filelog/sandbox] processors: [memory_limiter, resource/logs_common, resource/sandbox, attributes/logs_common, attributes/sandbox, batch] exporters: [otlp_http/logs] ``` With Elasticsearch selected, the log exporter becomes `elasticsearch/logs`; its endpoint and index come from `CUBE_LOGS_ES_ENDPOINT` and `CUBE_LOGS_ES_INDEX`. The receiver, field extraction, and pipeline stay the same. ### 4.4 Supported rules and recommended settings #### Metric and log collection rules | Type | Supported rule | Configuration | | --- | --- | --- | | Sandbox metrics | Scrape only the local Cubelet `/v1/metrics/resource`; includes sandbox CPU, memory, and node sandbox counts | Cluster/node labels and either an OTLP or Remote Write destination; Helm also exposes `observability.otelCollector.scrapeInterval` | | Sandbox logs | Read `/data/cubelet/log//{stdout,stderr}` line by line and extract the sandbox ID and stream from the path | Send to Loki over OTLP by default, or to Elasticsearch; systemd also exposes `CUBE_LOGS_SANDBOX_ID` and `CUBE_LOGS_FILE_START_AT` | | Component logs (Helm opt-in) | Read CRI/containerd logs only for `observability.logs.components.containers` from `/var/log/pods`; start at end and use one pipeline per component | Separate read-only component-log Collector DaemonSet on matching control/compute nodes; one-click does not support this option | | State and retry | Use `file_storage` for file offsets and OTLP queues, a WAL for Remote Write, and an in-memory queue with at most five retries for Elasticsearch | File state lives on the host for systemd and at `/var/lib/cube-sandbox/otelcol-k8s` per Kubernetes node; component logs use a separate `-components` host path | For the systemd collector, each original log line is kept in `Body`, with a maximum log-record size of `1MiB`. Both backends retain these semantic fields (Loki normalizes `.` in OTel attribute names to `_`): - `service_name=cubesandbox-sandbox` - `cubesandbox_log_source=sandbox-init` - `cubesandbox_cluster_id` - `host_name` - `cubesandbox_sandbox_id` - `log_iostream=stdout|stderr` - `log_file_path` - the log body and timestamp Component CRI records use `service_name=cubesandbox-`, `cubesandbox_log_source=component`, and `cubesandbox_component_name=`; they do not contain the sandbox-only ID and stream fields above. Loki indexes only the low-cardinality `service_name`, `cubesandbox_cluster_id`, and `host_name` labels. `cubesandbox_sandbox_id`, `log_iostream`, and the remaining fields stay as structured metadata. This avoids creating one high-cardinality stream per sandbox. Common LogQL filters: ```logql # All sandbox stderr {service_name="cubesandbox-sandbox"} | log_iostream="stderr" # Error-like messages from one sandbox {service_name="cubesandbox-sandbox"} | cubesandbox_sandbox_id="" |~ "(?i)error|fatal|panic" ``` Elasticsearch documents additionally contain `Body` and `@timestamp`. Common Lucene filters: ```text # All sandbox stderr service_name:"cubesandbox-sandbox" AND log_iostream:"stderr" # Error-like messages from sandbox stdout/stderr cubesandbox_log_source:"sandbox-init" AND Body:(ERROR OR FATAL OR panic) # One sandbox cubesandbox_sandbox_id:"" ``` #### Recommended Collector settings | Setting | Built-in value | Recommendation | | --- | --- | --- | | Scrape interval / timeout | systemd: `5s / 3s`; Helm: `15s / 10s` | Keep script defaults; Helm intervals must not be shorter than the fixed `10s` timeout, and larger deployments should keep the default `15s` | | `CUBE_LOGS_FILE_START_AT` (systemd only) | `beginning` | Keep `beginning` to read the current files in full; use `end` to collect only records written after the Collector starts | | `CUBE_LOGS_SANDBOX_ID` (systemd only) | `*` | Keep `*` normally; specify one ID only for focused troubleshooting or temporary volume reduction | | Memory limiter | Fixed at `256MiB`; Kubernetes memory request/limit `128Mi / 512Mi` | Start with the defaults; raising only the Pod limit does not change the limiter, which currently has no installer setting | | Batch | `1024` records; timeout `2s` for scripts and `5s` for Helm | Keep the defaults for normal low-latency delivery | | OTLP queue | `1000`, disk-backed with `file_storage`, retry without an elapsed-time limit | Ensure the host state directory has enough capacity | | Elasticsearch queue | In-memory `1000`, `10` consumers, at most `5` retries | Sustained backend outages can eventually drop logs; monitor Collector logs and backend availability | | Remote Write | WAL-backed | Ensure the WAL directory has enough capacity and monitor remote-write failures | | Prometheus retention | one-click: `15d / 40GB`; Helm: `15d` | For one-click use `CUBE_PROMETHEUS_RETENTION_TIME` / `CUBE_PROMETHEUS_RETENTION_SIZE`; for Helm use `observability.backend.prometheus.retention`, which currently has no size value | | Loki retention | one-click and Helm: `15d` | Use `CUBE_LOKI_RETENTION_PERIOD` for one-click or `observability.backend.loki.retention` for Helm | | Elasticsearch heap | one-click and Helm: `1g` | Tune `CUBE_ES_HEAP_SIZE` or `observability.backend.elasticsearch.javaOpts`, normally keeping heap below 50% of available memory | Except for settings with an environment variable or Helm value listed above, scrape timeout, memory limiter, batch, and queue values are installer-owned defaults and should not be maintained by editing generated files. Helm's bundled Prometheus, selected log backend, and Grafana use PVCs by default. Configure each component's `persistence.storageClassName`, `existingClaim`, or size under `observability.backend`; set `persistence.enabled=false` only for disposable validation. Use an external managed backend when high availability is required. Bundled Loki defaults to `15d`; configure Elasticsearch ILM separately when Elasticsearch is selected. #### Suggested monitoring expressions The installer provisions dashboards but does not install Alertmanager, Prometheus alert rules, Grafana alerts, or Elasticsearch ILM. Suggested PromQL starting points are shown below. Use `for: 5m` for the CPU, throttling, memory ratio, and unavailable-sample conditions. Fire the memory-failure counter immediately or use only a short delay. ```promql # CPU usage above 80% of a finite sandbox CPU limit 100 * rate(cubesandbox_host_sandbox_cpu_usage_seconds_total[5m]) / clamp_min(cubesandbox_host_sandbox_cpu_limit_cores, 0.001) > 80 # CPU throttled periods above 20% 100 * rate(cubesandbox_host_sandbox_cpu_throttled_periods_total[5m]) / clamp_min(rate(cubesandbox_host_sandbox_cpu_periods_total[5m]), 0.001) > 20 # Memory usage above 85% of a finite sandbox memory limit 100 * cubesandbox_host_sandbox_memory_current_bytes / clamp_min(cubesandbox_host_sandbox_memory_limit_bytes, 1) > 85 # Any memory-limit failure during the last five minutes increase(cubesandbox_host_sandbox_memory_failures_total[5m]) > 0 # A sandbox exists but has no available resource sample cubesandbox_node_sandbox_count - cubesandbox_node_sandbox_available_count > 0 ``` CPU and memory ratios only apply when a finite resource-limit series exists. ### 4.5 Extending collection to component logs Helm already collects allowlisted component container logs (see [2.2](#22-kubernetes-helm)); `deploy/kubernetes/chart/templates/observability.yaml` is the reference implementation. The one-click/systemd installer collects sandbox logs only. Never edit the installed `/etc/cube-sandbox/otelcol/otelcol.yaml` or the ConfigMap: installation regenerates them. Change the generator instead: - systemd: `write_otel_config()` in `deploy/one-click/observability/otelcol/install.sh`; - Helm: the Collector ConfigMap and volumes in `deploy/kubernetes/chart/templates/observability.yaml`, behind a `values.yaml` switch that stays off by default. Use one receiver and pipeline per component (`filelog/component_cubelet`, `logs/component_cubelet`) so unrelated formats never share a parser, and set both the OTel resource attribute and the flat Elasticsearch field: - `service.name` / `service_name`: stable low-cardinality name, e.g. `cubesandbox-cubelet`; - `cubesandbox.log.source` / `cubesandbox_log_source`: `component`; - `cubesandbox.component.name` / `cubesandbox_component_name`: e.g. `cubelet`; - keep the existing `cubesandbox.cluster.id`, `host.name`, `log.file.path`, and timestamp fields. Constraints for any extension: - read files with `start_at: end` and a dedicated parser for JSON or multiline formats; a parser failure must keep the original `Body` and must not block other pipelines; - index only stable labels; keep PID, request ID, and sandbox ID as structured metadata; - mount only the required host directories, read-only; never the host root; - exclude credentials, tokens, and headers, adding a `redaction` or `transform` processor when needed; - verify `file_storage` offsets across log rotation, restarts, and backend outages; - update the generator, both documentation languages, the dashboards, and the Collector `validate` tests, and state which topologies stay unsupported. ## 5. Backends and dashboards The bundled backend is Prometheus, Grafana, and one log backend. Grafana provisions three separate metrics dashboards plus one logs dashboard: - `cubesandbox-cluster.json`: sandbox resource totals for one cluster. - `cubesandbox-node.json`: sandbox resource totals for one node in that cluster. - `cubesandbox-sandbox.json`: one sandbox's Host Sandbox and Guest Workload metrics, with an optional guest-container filter. - `cubesandbox-logs.json` (Loki, default) or `cubesandbox-logs-elasticsearch.json` (Elasticsearch), both under the UID `cubesandbox-logs`: stdout/stderr volume, log query, and a collapsed **Component logs** row (Helm opt-in). Metrics selectors cascade from `cluster` to `node`, `sandbox_id`, and `container_id`. All 24 currently exported metric families are covered; aggregate CPU rates are cores, and memory limits are sums of exported finite limits, not physical host capacity. Host and Guest measurements must not be added together. Missing data is not zero, and guest rates omit windows with observed epoch changes. The Loki sandbox filter remains a regex text input for historical IDs; Elasticsearch selectors cascade from cluster to node to sandbox. See [1.2](#12-installer-versions-and-external-compatibility-baselines) for backend versions, default ports, and external-component baselines. After a one-click/systemd installation, run `sudo ./deploy/one-click/observability/install.sh status` to check the Collector and backends. Check the default Loki with `curl -fsS http://127.0.0.1:3100/ready`; for Elasticsearch, use `curl -fsS 'http://127.0.0.1:9200/cubesandbox-logs*/_count'`. A sandbox removal deletes its local log directory, so the Collector must continuously tail and export logs while the sandbox exists.