# Device info

What the agent learns about a MikroTik board without the RouterOS API (identity, ceilings, cadences) and how every sink receives it as facts, not samples.

Source: https://docs.jmrp.io/mikroscope/sinks/device-info/

At start the agent works out what it can about the board it runs on, with no RouterOS API
and no credential, and serves it on `/capabilities`. The collector hands that to every
sink as its own record: what it holds, when it is sent and where each sink puts it.

## Contents

- **Identity**: the device tree's model string (the board), the kernel, the core count,
  whether the container is privileged and whether it has cgroup2, the enabled sources, the
  port table's provenance, and a hash of the kernel, the core count and the enabled
  sources.
- **The device's own ceilings**, read once at agent start: each thermal zone's critical
  trip point and polling delay; each core's cpufreq range, frequency ladder, governor and
  cluster; the kernel's conntrack ceiling; the container's own `memory.max`.
- **Cadences**: the rate each level source is read and stored at, and the named reason it
  is not the sampler rate.

A ceiling the board does not publish is left out, never filled with a number from
somewhere else: every ceiling in the record is the device's own reading.

### Cadence reasons

The cadence reasons a source can report:

| `reason` | What it means |
| --- | --- |
| `rate` | read at the full sampler rate; nothing the device declares justifies less |
| `declared` | the device publishes its own refresh cadence, and reading faster returns the same value with new dither |
| `policy` | a setting says the value cannot move on its own: a `userspace` cpufreq governor |
| `budget` | a measured parse cost |
| `change` | read every tick, stored only when it moves |
| `override` | `FLOOR_HZ` is set, and every level source is on its one cadence |

Counters are never floored; only level sources carry a cadence. The floors themselves are
on [Source cadences](https://docs.jmrp.io/mikroscope/limits/source-floors/).

## When it is sent

When `forward` starts, whenever the agent's capability hash changes — an agent restarted
with another source set, for example — and otherwise every five minutes. The collector
checks the hash on the health read it makes every minute to re-measure the clock skew, so
a change reaches the sinks within about a minute. Both the direct and the relay transport
can fetch `/capabilities`; when the fetch fails the collector logs it, sends nothing, and
tries again at the next health read. Nothing sent is absence, not a board with no facts.

**Why it repeats.** These facts are rows with the collector's timestamp, so a store holds
them only at the instants they were sent, and a dashboard window that contains no
emission contains no facts at all: sent once at start, the four device panels would read
"No data" over every window after it. Five minutes puts the facts inside any window worth
reading them over and costs, per emission, one row for the identity, one per thermal zone,
one per core with cpufreq facts and one per level source, against the 864 000 sample rows
a day that `--rate 10` produces.

The repeat is marked as one, and the sinks split on it: the stores write it like any
other row, so every window holds the facts, and the streams meant for a reader — Loki, stdout,
a recording — skip it, because a log is for change. So Loki still carries exactly one
`device` line per set of facts.

The hash covers only the kernel string, the core count and the enabled source names; the
board, `privileged`, cgroup, the port table's provenance, every ceiling and every cadence
are left out of it. A restarted agent whose only change is a ceiling or a cadence — a new
`--memory-max`, or a `FLOOR_HZ` with the same source set — keeps its hash, so the change
is not what triggers the send; it reaches the sinks at the next five-minute repeat, and
the row that carries it is stamped then rather than when it happened. This is read from
the code (`capsHash` in [`internal/agent/source.go`](https://github.com/jmrplens/mikroscope/blob/main/internal/agent/source.go)) and not observed
([Tested on](https://docs.jmrp.io/mikroscope/about/status/#not-tested)).

These are facts, not samples. They have no clock of their own, so the sinks that
timestamp records stamp them with the collector's clock when they were sent; the file and
stdout `json` lines carry no timestamp, and Prometheus renders them at scrape. They are
never mixed into a sample row.

## Storage per sink

| Sink                            | Form                                                                                                                                               |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| InfluxDB, Telegraf, stdout `lp` | `mikroscope_device{board,kernel}`, `mikroscope_device_thermal{zone}`, `mikroscope_device_cpufreq{cpu}`, `mikroscope_device_cadence{source,reason}` |
| SQL (`--sql`, `--postgres`)     | the tables `mikroscope_device`, `mikroscope_device_thermal`, `mikroscope_device_cpufreq`, `mikroscope_device_cadence`                              |
| file, stdout `json`             | a `{"device":…}` line holding the capabilities as fetched                                                                                          |
| Elasticsearch                   | a document with `kind: device`                                                                                                                     |
| Graphite                        | the numeric facts under `device.*`; board, kernel and governor have no Graphite form                                                               |
| OTLP                            | gauges: `mikroscope.device.cores` with board, kernel and hash as attributes, the thermal and cpufreq ceilings, `mikroscope.device.source_cadence`  |
| Loki                            | one `source="device"`, `level="info"` line: board, kernel, cores, privileged, cgroup, sources, hash                                                |
| Prometheus                      | the device-info families, rendered by the collector's exposition                                                                                   |

### InfluxDB and SQL

| Measurement or table        | Identified by      | Holds                                                                                                                      |
| --------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------- |
| `mikroscope_device`         | `board`, `kernel`  | `cores`, `privileged`, `cgroup`, `sources` (comma-joined, sorted), `hash`, `conntrack_max`, `cgroup_mem_max`, `ports_from` |
| `mikroscope_device_thermal` | `zone`             | `critical_celsius`, `polling_ms`                                                                                           |
| `mikroscope_device_cpufreq` | `cpu`              | `cluster` (the lowest-numbered core that changes frequency with this one), `min_khz`, `max_khz`, `governor`, `steps`       |
| `mikroscope_device_cadence` | `source`, `reason` | `hz`                                                                                                                       |

In InfluxDB `board` and `kernel` are tags, `unknown` when empty; in SQL they are columns,
NULL when empty, and a ceiling that was not published is NULL. `steps` is the frequency
ladder as a space-joined list of kHz.

### Prometheus

| Family                                                                         | Carries                                        |
| ------------------------------------------------------------------------------ | ---------------------------------------------- |
| `mikroscope_device_info{board,kernel,cores,privileged,cgroup,ports_from,hash}` | always 1                                       |
| `mikroscope_thermal_critical_celsius{zone}`                                    | the zone's lowest critical trip                |
| `mikroscope_thermal_polling_seconds{zone}`                                     | the zone's polling delay                       |
| `mikroscope_cpu_frequency_limit_hertz{cpu,bound}`                              | the hardware clock range, `min` and `max`      |
| `mikroscope_cpu_frequency_step_hertz{cpu,step}`                                | every frequency the driver will use            |
| `mikroscope_cpu_frequency_governor_info{cpu,governor}`                         | always 1                                       |
| `mikroscope_cpu_frequency_cluster{cpu}`                                        | the cluster, named by its lowest-numbered core |
| `mikroscope_self_cgroup_memory_max_bytes`                                      | the container's own `memory.max`               |
| `mikroscope_source_cadence_hz{source,reason}`                                  | each level source's cadence                    |

The agent reads each core's cpufreq cluster from its `related_cpus`, per device. On a board
whose cores change frequency in pairs, for example clusters `{0,1}` and `{2,3}`, a per-core
frequency panel is really two series. The conntrack ceiling reaches Prometheus as
`mikroscope_slab_limit_objects` for the `nf_conntrack` cache rather than as a device-info
family.

## See also

- [HTTP endpoints](https://docs.jmrp.io/mikroscope/reference/http/): `/capabilities`, where this record
  comes from.
- [Source cadences](https://docs.jmrp.io/mikroscope/limits/source-floors/): the cadences and the reasons behind
  them.
- [InfluxDB 3](https://docs.jmrp.io/mikroscope/sinks/influxdb/): the other measurements the device records sit beside.
