Telemetry and diagnostics
What the scripts report, when, from which machine, how failures are described, and how to switch reporting off.
When diagnostics are enabled, a script run reports to
https://telemetry.community-scripts.org/telemetry: that an install started,
that it is progressing, and how it ended. The data shows which scripts break,
on which OS and host version, and at which command. The aggregated result is
public at community-scripts.org/analytics.
Everything here is implemented in the engine's
api/ folder; the
file-by-file reference is in API reference.
Opting in and out
Reporting is controlled by one file with one line:
DIAGNOSTICS=yes| Platform and user | File |
|---|---|
| Proxmox VE | /usr/local/community-scripts/diagnostics |
| Incus as root | /usr/local/community-scripts/diagnostics |
| Incus as a normal user | ~/.config/community-scripts/diagnostics (see the Incus guide) |
The first run asks. If the file does not exist, diagnostics_check shows a
dialog titled DIAGNOSTICS before the install menu:
Send Diagnostics of LXC Installation?
(This only transmits data without user data, just RAM, CPU, LXC name, ...)Yes writes DIAGNOSTICS=yes, No writes DIAGNOSTICS=no. Later runs read the
file and do not ask again.
Changing it later. In the install menu choose Settings, then
Manage API-Diagnostic Setting. The dialog DIAGNOSTIC SETTINGS shows the
current value and offers the opposite. Or edit the file:
sed -i 's/^DIAGNOSTICS=.*/DIAGNOSTICS=no/' /usr/local/community-scripts/diagnosticsDeciding before the first run, so that no dialog appears:
mkdir -p /usr/local/community-scripts
echo "DIAGNOSTICS=no" > /usr/local/community-scripts/diagnosticsWith DIAGNOSTICS=no, or with the variable unset, every sender returns before
it builds a payload. Nothing is sent without curl either.
The host passes its setting into the container it builds. During the install
the container writes its own /usr/local/community-scripts/diagnostics with that
value, which is what add-on scripts run inside the container later read. VM
scripts and host tools read /usr/local/community-scripts/diagnostics
directly.
The comment block the dialog writes into the file lists nine fields. That list is older than the payload; the table below is what the code sends today.
What is sent
Every record from the host is one JSON document, built by _tm_payload in
api/telemetry.func.
All fields are present in every record; the error fields are empty unless the
run failed.
Identity and status
| Field | Content |
|---|---|
payload_version | Format version, currently 3 |
random_id | A random UUID from /proc/sys/kernel/random/uuid, generated for this run and not reused |
execution_id | Same as random_id on the first attempt. A retry from the recovery menu gets a new one |
type | lxc, vm, pve (host tool) or addon |
nsapp | Script name in lower case without spaces, for example debian |
status | installing, validation, configuring, success, failed, aborted or unknown |
method | How the settings were chosen: default, advanced, appdefaults, generated, mydefaults-global; VM scripts also send unattended |
repo_source | ProxmoxVE, DevScripts or external |
repo_slug | owner/repo the script was loaded from, for example community-scripts/ProxmoxVE |
app_version | Application version, when the script knows it |
Guest settings
| Field | Content |
|---|---|
ct_type | 1 unprivileged container, 0 privileged container, 2 VM. Tools and add-ons send 0 |
disk_size | Disk size in GB |
core_count | CPU cores |
ram_size | RAM in MiB |
os_type, os_version | Guest OS, from var_os and var_version. Add-ons report ID and VERSION_ID from the container's /etc/os-release |
gpu_passthrough | yes when GPU passthrough was requested for the guest, otherwise no |
Host facts
Collected once per run by telemetry_collect_sysinfo in
api/sysinfo.func.
| Field | Content | Read from |
|---|---|---|
platform | pve or incus | which of pveversion and incus exists |
pve_version | Proxmox VE version such as 9.0.3; on Incus incus-<version> | pveversion, incus version |
kernel_version | Kernel release | uname -r |
arch | Debian spelling: amd64 or arm64 (armhf and i386 are normalised the same way) | dpkg --print-architecture, else uname -m |
has_arm | true on arm64 | same |
cpu_vendor | intel, amd, arm or unknown | /proc/cpuinfo |
cpu_model | CPU model name, at most 64 characters | /proc/cpuinfo |
gpu_vendor | intel, amd, nvidia or unknown | first display controller in lspci |
gpu_model | GPU model name, at most 64 characters | lspci |
ram_speed | Memory speed, digits only; empty when dmidecode is missing or reports nothing | dmidecode -t memory |
Outcome
| Field | Content |
|---|---|
exit_code | Exit status, 0 to 255. A non-numeric status becomes 199 |
install_duration | Seconds since the first record of this run, capped at 86400 |
error | Empty unless the run failed or was aborted. See Failure records |
error_category | See Categories |
error_message, error_step, error_phase | See Failure records |
failed_command, failed_line, failed_function | See Failure records |
A complete installing record looks like this:
{
"payload_version": 3,
"random_id": "5f0c3a52-8a1e-4c1b-9a41-2f6f0b7d9e11",
"execution_id": "5f0c3a52-8a1e-4c1b-9a41-2f6f0b7d9e11",
"type": "lxc",
"nsapp": "debian",
"status": "installing",
"ct_type": 1,
"disk_size": 2,
"core_count": 1,
"ram_size": 512,
"os_type": "debian",
"os_version": "13",
"pve_version": "9.0.3",
"method": "default",
"exit_code": 0,
"error": "",
"error_category": "",
"error_message": "",
"error_step": "",
"error_phase": "",
"install_duration": 0,
"cpu_vendor": "intel",
"cpu_model": "Intel Core i5-8500T CPU @ 2.10GHz",
"gpu_vendor": "intel",
"gpu_model": "Intel Corporation CoffeeLake-S GT2 [UHD Graphics 630]",
"gpu_passthrough": "no",
"ram_speed": "2666",
"arch": "amd64",
"platform": "pve",
"has_arm": false,
"kernel_version": "6.14.8-2-pve",
"app_version": "",
"failed_command": "",
"failed_line": 0,
"failed_function": "",
"repo_source": "ProxmoxVE",
"repo_slug": "community-scripts/ProxmoxVE"
}The hardware values in the example are illustrative.
When it is sent, and from where
Container installs
| Step | Sent by | status | Where in the run |
|---|---|---|---|
| 1 | host | installing | After the checks, the menu and all dialogs, when container creation begins. Up to 3 attempts |
| 2 | host | validation | Directly after, before storage and template checks |
| 3 | host | configuring | Container created, before network and device setup |
| 4 | container | configuring | Progress pings during the install, see below |
| 5 | host | success, failed or aborted | Once, when the run ends |
On Incus the host additionally sends a progress record with status
installing right before the install script starts.
Nothing before step 1. A run that ends in the preflight checks, in the menu or in a dialog reports nothing at all, not even a failure. The terminal sender checks whether the install record was due and returns otherwise. A host that was not ready is not a failed install.
One terminal record per attempt. post_update_to_api sends once and then
ignores further calls. If a run exits with status 0 but never reported success
(you backed out of a late dialog), it is reported as aborted, not as
success.
Retries are separate executions. Choosing a retry in the recovery menu
calls telemetry_new_attempt: the random_id stays, the execution_id
changes, and the new attempt reports its own start and end.
Progress pings from inside the container
The container sends progress pings and nothing else. They are fired by
lxc/install.func
at the end of setting_up_container, update_os, motd_ssh and customize.
The server uses them to tell a running install from a stuck one.
A ping is much smaller than a host record. It contains exactly these fields:
{
"payload_version": 3,
"random_id": "5f0c3a52-8a1e-4c1b-9a41-2f6f0b7d9e11",
"execution_id": "5f0c3a52-8a1e-4c1b-9a41-2f6f0b7d9e11",
"type": "lxc",
"nsapp": "debian",
"status": "configuring",
"platform": "pve",
"repo_source": "ProxmoxVE",
"repo_slug": "community-scripts/ProxmoxVE"
}The container never sends a terminal status. When the install fails inside the container, the error handler writes two files there instead:
| File in the container | Content |
|---|---|
/root/.install-<session>.failed | The exit code |
/root/.install-<session>.log.errinfo | Exit code, line, command, function, step, message and the output of the failing command |
The host reads both after the install returns and sends the one failed
record. That keeps the report complete and prevents two records with different
verdicts for the same run.
Virtual machines
VM scripts call post_to_api_vm, which sets type to vm and ct_type to
2 and then behaves like a container install on the host side: one
installing record, one terminal record. There are no pings from inside a VM.
Host tools and add-ons
Tool and add-on scripts call init_tool_telemetry once, for example
init_tool_telemetry "add-tailscale-lxc" "addon" in
tools/addon/add-tailscale-lxc.sh. It installs an EXIT trap that sends a
single record when the script ends:
typeispvefor a host tool andaddonfor an add-on;nsappis the tool's name.- Every run gets its own fresh UUID.
- There is no
installingrecord. - A failure with one of the "requirement not met" exit codes sends nothing: 10, 103, 104, 106, 232, 233, 234, 236, 238 (wrong shell, not root, wrong architecture, wrong host, a prerequisite missing).
- Cancelling a
whiptaildialog (exit status 1 or 255) is reported asabortedwith the reason "Dialog cancelled, or no terminal to show it".
Most add-ons run inside the guest, so their record is sent from the container and uses the container's diagnostics file.
Updates
Running a script again inside a container to update the application sends nothing. An update never sends the install record, and without it the terminal sender stays silent.
Delivery
Records are sent with curl as an HTTP POST with
Content-Type: application/json and the user agent
community-scripts-core/3.
| Record | Timeout | Attempts |
|---|---|---|
installing | 5 s | 3 |
Progress (validation, configuring) | 5 s | 1 |
| Container ping | 5 s | 1 |
| Terminal status | 10 s | 2, then 2 more with the error cut to its first line |
| Tool or add-on | 5 s | 1 |
A failed delivery is ignored. The senders run with errexit and the caller's
ERR trap switched off, so an unreachable telemetry server can never turn
into an install failure.
Failure records
A failed record carries a description of the failure, assembled by
api/errorlog.func
from the error handler's capture.
| Field | Content | Limit |
|---|---|---|
error_step | Text of the step that was running (the last msg_info), or after <text> when the failure fell between two steps | 120 characters |
error_message | The first msg_error of that step; empty if none was printed | 300 characters |
error_phase | host or container: where the command failed | |
failed_command | The command that failed, on one line | 300 characters |
failed_line | Its line number in the script | |
failed_function | The function it ran in; empty at top level | |
error | The trace, described below | 60 lines, 10240 bytes |
The error field starts with a header line and, after a line containing
---, continues with output:
exit_code=100 | APT: Package manager error (broken packages / dependency problems) | step: Installing Dependencies | at line 23: apt install -y example-package
---
E: Unable to locate package example-packageThe header is always exit_code=<n> | <explanation> followed, when known, by
| step: <step>, | in <function> and | at line <n>: <command> (or
| command: <command> when the line is not known).
The output part is the output of the failing command when the handler captured
it, otherwise the log lines of the failing step (everything after the last
[INFO] or [OK] log entry). Before it is sent, _tm_clean_log and
_tm_focus:
- remove ANSI colour codes and carriage returns;
- replace the last three octets of every IPv4 address:
192.168.1.50becomes192.x.x.x; - drop lines that never explain a failure: package download and unpack
progress from
apt,apk,dnf,pip,npm,cargo,go,git,curlandwgetprogress, engine status lines and blank lines; - cut each line to 1000 characters, keep the last 60 lines, and of those the last 10240 bytes.
error_message gets the same IPv4 replacement.
An aborted record carries only the header (exit code, explanation, step) and
no command, line, function, message or output.
If the terminal record cannot be delivered, it is retried with the error
field reduced to the first 600 bytes of its header line.
Categories
error_category is computed by categorize_error in
api/exitcodes.func.
The first rule that applies wins:
1. An explicit override set by the engine (ERROR_CATEGORY_OVERRIDE).
Aborted runs get user_aborted this way.
2. Exit codes that mean the same whichever program returned them:
| Exit codes | Category |
|---|---|
| 113, 114, 122, 129, 130, 143, 253, 254 | user_aborted |
| 134, 137 | resource |
| 139, 141 | signal |
3. The program that failed, taken from the first word of the failing command. Several tools share exit codes, so the tool is a better guide than the number:
| Failing command starts with | Category |
|---|---|
systemctl | service |
pct, qm, pvesm, pvesh, pveam | proxmox |
4. The exit code:
| Exit codes | Category |
|---|---|
| 6, 7, 22, 35 | network |
| 28, 124, 211 | timeout |
| 214, 217, 219, 224 | storage |
| 100, 101, 102, 127, 160, 161, 162, 255 | dependency |
| 126, 152, 229 | permission |
| 10, 64, 128, 203 to 208 | config |
| 200, 209, 210, 212, 213, 215, 216, 218, 220 to 223, 225, 227, 228, 231 | proxmox |
| 150, 151, 153, 154 | service |
| 170 to 173, 180 to 183, 190 to 193 | database |
| 243, 245 to 249 | runtime |
| 1, 2 | shell |
| anything else | unknown |
The same exit codes 113, 114, 122, 129, 130, 143, 253 and 254 also turn a
failed status into aborted: they are a declined question or a signal, a
decision by the user and not a defect. What each code means is listed in
Exit codes.
What is never sent
The payload has no field for any of the following, and the engine does not read them for telemetry:
- the host name, the container's host name or its ID;
- IP addresses, MAC addresses, bridge, VLAN, gateway or DNS settings;
- passwords, SSH keys, API tokens;
- storage names, paths or pool sizes;
- user names or anything from your files.
No persistent identifier exists. random_id is generated for the run and
thrown away; two installs from the same host cannot be linked through it.
Two limits to be aware of:
- A failure trace is log output. It is filtered and capped as described above, and IPv4 addresses are masked, but a line an installer prints (a path, a domain name, an IPv6 address) is sent as printed. Successful runs send no log output at all.
- Sending a record is an HTTP request, so the server sees the address it comes from, like any web server does.
Inspecting and redirecting
TELEMETRY_URL overrides the endpoint, on the host and, because the host
passes it on, in the container:
TELEMETRY_URL=http://192.0.2.10:8080/telemetry mode=default \
bash -c "$(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/ct/debian.sh)"When the variable DEV_MODE is true, _tm_send prints every payload and the
HTTP status it got back to standard error, prefixed with [DEBUG] telemetry.
Related
- API reference: the
api/folder function by function - Telemetry service: the endpoints
- Exit codes
- Runtime and errors