Observability Backends
Ship host and container metrics from your VM deployment spaces to your own VictoriaMetrics, Grafana Mimir or Prometheus, and container logs to your own Grafana Loki, VictoriaLogs or Datadog, and read both back in ZopDay.
An observability backend is a metrics store you run — VictoriaMetrics, Grafana Mimir or Prometheus — connected to ZopDay (the ZopDev deploy app) as an org-level integration. Attach it to a VM deployment space and that space’s VM ships host and container metrics to it every 30 seconds. The service Observability tab then reads those numbers back from your store, so the tab answers in well under a second and shows usage history instead of a single point-in-time reading.
A logs backend works the same way for container logs: a Grafana Loki, VictoriaLogs or Datadog store you run, attached to a VM space as its Logs backend. The VM ships every service container’s output to it, and the service log viewer reads it live (refreshed every 10 seconds) with time-range, search and level filters. A space can have a metrics backend, a logs backend, or both. See Logs backends.
Without a backend attached, or while the backend is unreachable, the Observability tab works exactly as before: it reads the VM directly over the cloud provider’s exec channel and shows the Checked on the VM pill.
Add a backend
Settings → Integrations → Observability → Add backend. Integrations are shared by every project in the organisation, so any VM space can attach the same backend.
| Field | What to enter |
|---|---|
| Name | A name your team recognises, e.g. prod-victoria. |
| Provider | VictoriaMetrics, Grafana Mimir or Prometheus. |
| Write URL | Where the VM pushes metrics (Prometheus remote write). https://<host>/api/v1/write for VictoriaMetrics and Prometheus, https://<host>/api/v1/push for Mimir. https only. |
| Read URL | The query_range endpoint ZopDay queries for the tab. https://<host>/api/v1/query_range, or https://<host>/prometheus/api/v1/query_range for Mimir. Must be reachable from ZopDay. https only. |
| Tenant | Optional. Sent as X-Scope-OrgID on every write and read. Required for Mimir. |
| Authentication | Username (basic auth), Bearer token, or a Custom header name and value. |
The paths are the stores’ own. See the Mimir HTTP API reference for /api/v1/push and /prometheus/api/v1/query_range, the Prometheus HTTP API for /api/v1/write (needs --web.enable-remote-write-receiver) and /api/v1/query_range, and the VictoriaMetrics URL examples for both.
Click Test connection before saving. The probe writes one sample to the write URL and then reads it back from the read URL:
- The write must pass; Save stays disabled until it does.
- A read that answers correctly but does not yet show the sample is reported as a note, not a failure — most stores hide the newest 30 seconds or so.
- A read that is rejected (wrong path, wrong credential) is reported as a warning so you can fix it before attaching.
Editing a saved backend re-runs the same probe, so a rotated credential is verified before it replaces the stored one.
/integrations/test Run the write/read connection probe against a backend without saving it. Same body as POST /integrations with category=observability.
curl -X POST https://api.zop.dev/integrations/test \ -H "Authorization: Bearer <token>" \ -H "Content-Type: application/json" \ -d '{ "name": "prod-victoria", "provider": "victoriametrics", "category": "observability", "config": "{\"signal\":\"metrics\",\"writeUrl\":\"https://metrics.example.com/api/v1/write\",\"readUrl\":\"https://metrics.example.com/api/v1/query_range\"}", "credential": "{\"authType\":\"basic\",\"username\":\"vmadmin\",\"password\":\"…\"}" }'{ "data": { "writeOk": true, "readOk": true, "readNote": "read endpoint verified; the probe sample was not visible yet (ingestion delay)" }}POST /integrations, PUT /integrations/{id} and DELETE /integrations/{id} work as for every other integration. For a metrics backend config carries signal: "metrics", writeUrl, readUrl and optional tenant; credential carries authType (basic, bearer or header) with username/password, token, or headerName/headerValue. The credential is never returned by any read.
Logs backends
Settings → Integrations → Observability → Add backend, choose Logs, then the provider. config.signal is logs. Every URL is https only and must resolve to a public address, as for metrics.
| Provider | What you enter | Authentication | What the credential must be allowed to do |
|---|---|---|---|
Grafana Loki (loki) | Write URL — Loki’s OTLP base, https://<host>/otlp (the agent appends /v1/logs). Read URL — https://<host>/loki/api/v1/query_range. Tenant — optional, sent as X-Scope-OrgID; letters, digits, _ . : - only. | Username (basic), Bearer token or Custom header | Push to and query that tenant. On Grafana Cloud leave the tenant empty and use your user ID and a token with logs write and read access as the username and password. |
VictoriaLogs (victorialogs) | Write URL — https://<host>/insert/opentelemetry/v1/logs. Read URL — https://<host>/select/logsql/query. Account ID — optional, sent as AccountID; letters, digits, _ . : - only. | Username (basic), Bearer token or Custom header | Whatever your auth proxy requires on both paths. |
Datadog (datadog) | Site — the domain in your Datadog address bar: datadoghq.com, us3.datadoghq.com, us5.datadoghq.com, datadoghq.eu, ap1.datadoghq.com, ap2.datadoghq.com or ddog-gov.com. No URLs. | API key and Application key, both required | The API key sends logs to log intake. The Application key reads them back for the log viewer through the Logs Search API, so it needs the logs_read_data permission (a scoped key must include that scope). |
The connection test writes one log line, zopday probe <id>, and reads it back. The line carries service.name and vm_uid zopday-probe, so it never mixes with a VM’s container logs. As for metrics, the write must pass, and a read that answers but does not show the line yet is only a note.
{ "signal": "logs", "writeUrl": "https://logs.example.com/otlp", "readUrl": "https://logs.example.com/loki/api/v1/query_range", "tenant": "acme" }{ "signal": "logs", "options": { "site": "datadoghq.eu" } }credential.authType is basic, bearer or header as for metrics, or apikey with apiKey and appKey for Datadog.
Attach it to a VM space
Attaching is what installs the agent. Two entry points, same result:
- Infrastructure → your VM space → Settings → Metrics backend or Logs backend. Pick the backend and Save; one Save covers both. The confirmation says exactly what will happen on the VM.
- Infrastructure → your VM space → Components → Observability agent → Install. Pick the backend in the dialog and Install.
Either way ZopDay records the attach on the space and runs the observability-agent component on the VM. The Components card shows the install in progress and then Installed · enabled; the first samples and log lines reach your store within about a minute.
- Switch to another backend by picking it and saving: the agent is re-applied with the new settings and metrics resume within a minute.
- Detach by choosing No backend: the agent stops shipping that signal, and is removed from the VM once no metrics or logs backend is attached and Service Alerts are off. The tab goes back to reading the VM directly.
- A backend that any space still uses cannot be deleted; the Disconnect dialog names the spaces to detach first.
What runs on the VM
The agent is a single pinned OpenTelemetry Collector container (otel/opentelemetry-collector-contrib), capped at 0.25 CPU and 512 MiB. Measured footprint is 120 to 200 MiB for a VM with 20 to 30 live containers. It reads the Docker socket for per-container stats and the host for CPU, memory, disk, filesystem, load and network, every 30 seconds, and pushes them with Prometheus remote write. Its configuration and a root-only environment file with the credential are written to /etc/zop-observability/ on the VM. Docker must already be present, which is true for any VM space that has deployed a service.
With a logs backend attached, the same agent also tails the Docker log of every running zopday-* service container and sends each line to your store, tagged with container.name and service.name (the container name), service.instance.id and vm_uid (the VM id), vm_uid_lc (the VM id in lowercase, which Datadog reads by), org_id and space_id. It starts from the moment of attaching (older lines are not replayed) and remembers its position; re-applying the agent stops it gracefully, so lines neither go missing nor repeat. Detaching the logs backend clears that position, so attaching again later starts from that moment rather than replaying the gap. A space with only a logs backend runs the agent without collecting any metrics.
/spaces/{spaceID} Attach, switch or detach: body { config: { observability: { metrics: '<integrationID>' | null, logs: '<integrationID>' | null } } }. Each id must be an observability integration of this org for that signal. Follow with the component install below; the UI does both on Save.
/components Apply the agent: body { spaceId, targetType: 'vm', component: 'observability-agent' }. Installs or re-applies the collector for the space's current attach, or removes it when nothing is attached. Returns an actionId to poll.
What the Observability tab shows
Open a VM service → Observability. With a backend attached and the agent shipping, the header shows a Metrics backend pill and a green Live · Ns ago pill that says how old the newest sample is.
- Status strip — status, replicas ready and restarts. A container that exited cleanly (exit code 0 or 143, not out-of-memory) rolls up to Scaled down; a crash stays Degraded; a VM that cannot be reached stays Unreachable.
- Usage — CPU, Memory, Network and Block I/O over 15m / 1h / 6h / 24h. The CPU chart draws the container’s
--cpuscap and the Memory chart its memory limit as dashed ceilings. Gaps in collection are drawn as gaps, not bridged. A scaled-down service shows dashes and the time of its last sample. - Replicas — one card per container: uptime, restarts, PIDs and exit code, plus CPU, memory, network and block I/O when there are no charts.
If the agent stops reporting for more than 90 seconds — the VM rebooted, the store is down, the credential was rotated without updating the integration — the tab falls back to reading the VM directly and the pill switches to Checked on the VM. It returns to the backend on its own within a minute of samples resuming.
/resources/{resourceID}/metrics/live Current state of the VM's containers. scope=vm; optional container=<name> narrows to one service. source is 'backend' or 'exec'.
/resources/{resourceID}/metrics/range Usage history for one container from the attached backend only. scope=vm, container=<name>, range=15m|1h|6h|24h. About 120 points per series; counters are returned as bytes per second. 404 when the space has no backend.
Viewing logs
Open a VM service’s logs with View logs. There is one live view; where its lines come from depends on the space:
- Logs backend attached — lines are read from the backend and refreshed every 10 seconds. Pick Last 15 minutes / 1 hour / 6 hours / 24 hours, search case-insensitively, filter by level, and use Show earlier logs to page further back. On a pool with several VMs, each VM is read and the lines are merged by time. This still works when the VM itself cannot be reached.
- No logs backend — lines are read straight from the VM, as before.
- A shared space you do not own — your containers’ lines are read from the space owner’s logs backend, the same way; you only ever see your own services. If your container has no saved line in the last 24 hours (for example, the pool’s agent has not been re-applied yet), the viewer reads its logs straight from the VM instead, and stays there until you reload the page.
/resources/{resourceID}/logs/range One page of a container's log lines from the attached logs backend only. scope=vm, container=<name> (one of your own services), range=15m|1h|6h|24h (default 1h), limit (default 200, at most 1000), optional q (case-insensitive text, up to 200 characters), cursor (the previous page's nextCursor). Returns { items: [{ timestamp, container, line }], hasMore, nextCursor, provider }, newest first. 404 when the space has no logs backend, the container is not yours, or — on a shared space you do not own, with no q — your container has no line in the last 24 hours (read the VM's live logs); 502 when the store is unreachable or rejects the stored credential.
Metrics and labels
Everything the agent ships is standard OpenTelemetry hostmetrics and docker_stats data, so you can build your own Grafana dashboards on it. The names below are what lands in a Prometheus-compatible store after the exporter’s unit suffixes.
Every series carries these labels
| Label | Value |
|---|---|
org_id | Your ZopDay organisation id |
space_id | The deployment space id |
vm_uid | The cloud provider’s VM id, e.g. projects/<project>/zones/<zone>/instances/<name> on GCP |
cloud | aws, gcp or azure |
Container series also carry container_name (zopday-<service>-<id8>), service_id (the ZopDay service id) and container_runtime.
| Metric | Type | Extra labels | Used on the tab for |
|---|---|---|---|
container_cpu_utilization_ratio | gauge, fraction of one core | CPU chart and card | |
container_cpu_limit | gauge, cores | CPU chart ceiling | |
container_memory_usage_total_bytes | gauge | Memory chart and card | |
container_memory_usage_limit_bytes | gauge | Memory chart ceiling | |
container_network_io_usage_rx_bytes_total / _tx_bytes_total | counter | interface | Network chart, rate per interface then summed |
container_blockio_io_service_bytes_recursive_total | counter | operation (read/write), device_major, device_minor | Block I/O chart |
container_pids_count | gauge | PIDs on the replica card | |
container_restarts_total | counter | Restarts | |
container_uptime_seconds | gauge | Uptime |
Host series
| Metric | Type | Extra labels |
|---|---|---|
system_cpu_time_seconds_total | counter | cpu, state |
system_cpu_load_average_1m / _5m / _15m | gauge | |
system_memory_usage_bytes | gauge | state (used, free, cached, buffered, …) |
system_network_io_bytes_total and related | counter | device, direction |
system_disk_io_bytes_total and related | counter | device, direction |
system_filesystem_usage_bytes | gauge | device, mountpoint, mode, state |
system_memory_usage_bytes is also the liveness signal: the tab treats the agent as up while a host sample is newer than 90 seconds.
Application metrics per service
The agent can also collect a service’s own metrics. Set configs.metrics on the deployment and redeploy:
{ "metrics": { "mode": "scrape", "port": 8000, "path": "/metrics" } }scrape— the collector discovers the container and scrapeshttp://<container>:<port><path>every 30 seconds. Every scraped series carries the sameorg_id,space_id,vm_uid,container_nameandservice_idlabels, plus anupseries that reads 1 while the endpoint answers and 0 when it does not.otlp— the container is given the collector’s OTLP endpoint on the Docker bridge (http://172.17.0.1:4318, gRPC on4317); push from an OpenTelemetry SDK.off, or nometricskey — nothing is collected.modeis required whenever the key is present.
Application metrics are shipped as-is, with no name filter; the per-service switch is the gate.
From the MCP server
test_integration and create_integration cover metrics backends only; connect a logs backend in the app or with the routes above, then attach and read it with the logs tools below.
The metrics and logs flows are available to an agent through the ZopNight MCP server. These tools cover them, in the order you would use them:
| Tool | What it does |
|---|---|
test_integration | Runs the write/read probe on a backend without saving it. provider is victoriametrics, mimir or prometheus; integration_config and credential are the same objects as below. Returns writeOk, readOk and the per-direction messages. |
create_integration | Connects the backend: category: "observability", integration_config: { signal: "metrics", writeUrl, readUrl, tenant? }, credential: { authType: "basic" | "bearer" | "header", … }. The connect itself repeats the write probe and refuses a backend whose write fails. update_integration rotates the credential or changes the URLs; delete_integration disconnects it (refused while a space still uses it). |
set_infrastructure_metrics_backend | Attaches the integration to a VM pool (infrastructure_id from list_infrastructure), switches it, or detaches with detach: true. This records the attach only. |
set_infrastructure_logs_backend | The same for the logs signal: attaches a connected loki, victorialogs or datadog integration (or detaches with detach: true) and leaves the metrics backend as it is. This records the attach only. |
install_infrastructure_component | With component: "observability-agent", installs, re-configures or removes the collector on the pool’s VMs according to the current attach. Poll list_infrastructure_jobs until the job settles. |
Reading back takes three tools:
get_live_metricswithscope: "vm"(and optionallycontainer) returns the live container rows the Observability tab’s strip shows.get_vm_usage_historyreturns one container’s CPU, memory, network and block I/O series over15m,1h,6hor24hfrom the backend.get_vm_logs_historyreturns one container’s saved log lines from the logs backend over the same windows, newest first:limitlines per page (default 200, at most 1000),searchfor lines containing a text (case-insensitive, at most 200 characters), andcursorset to the previous page’snextCursorfor older lines whilehasMoreis true.
All three address the VM by resource_id. Start from get_service_infrastructure: it names the pool a VM service runs on, its target, its infrastructureOrgId, and the pool’s machines under vms. Which id to pass depends on who owns the pool:
- Your own pool. Pass the VM’s id from the ZopDay inventory, the value
list_resourcesreports asidfor the machine of that name. - A shared pool (
target: "zopcloud"). Pass the machine’sinstanceIdfromvms, the provider’s id such assep-vm-0. A machine listed with anamebut noinstanceIdhas no key that fits the route and cannot be read this way yet. The VM belongs to the platform org, so also passinfrastructureOrgIdasresource_org_idon every read while keepingorg_idas your own org. Without it the reads see no containers.
Container names follow zopday-<service>-<first 8 characters of the service id>. A 404 from get_vm_usage_history or get_vm_logs_history means one of three things, in the order to check them: the VM id is wrong or not visible to your org, the container is not one of your org’s deployments on that VM, or the pool has no metrics (or logs) backend attached yet. For get_vm_logs_history on a shared pool you do not own, a fourth: with no search, your container has no saved line in the last 24 hours — read its live logs. On a shared pool, pass the pool owner’s org as resource_org_id; your containers’ saved history is read from the owner’s logs backend.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| Test connection: write fails with 401 | Wrong credential or auth type for the store | Check the username/password or token; Mimir also needs the tenant. |
| Test connection: “resolves to a private address” | The store is only reachable inside your network | Expose it on a public https endpoint (a tunnel or ingress) that ZopDay and the VM can reach. |
| Tab shows Checked on the VM although a backend is attached | Agent not shipping: VM rebooted, store down, or credential changed | Check the Components card for the agent’s state; re-save the backend to re-apply; confirm the store accepts writes. The tab recovers on its own within a minute of samples resuming. |
| Tab is slow and says Collecting metrics… | The read fell back to the VM’s exec channel | Same as above; the exec read takes 20 to 60 seconds. |
| Charts have a gap | The agent or the store was down for that period, or the service was scaled to zero | Expected. A scaled-down service shows the time of its last sample. |
| Delete backend refused with 409 | A space still uses it | Detach in that space’s Settings first; the message names the spaces. |
| Logs: “rejected the stored credentials” | The key was rotated or lost its read permission (Datadog: logs_read_data) | Update the integration’s credential; it is re-tested before it is saved. |