Merge pull request #2172 from rcourtman/maintainer/20260922T223953Z

Fix TrueNAS CORE CPU and memory telemetry over legacy REST
This commit is contained in:
pulse-triage[bot]
2026-09-22 23:18:05 +00:00
committed by GitHub
3 changed files with 103 additions and 12 deletions
@@ -117,16 +117,28 @@ FreeBSD `active` pages are not a total-used-memory alias. Without an explicit
used series, history derives usage from the known capacity and reported free
memory, subtracting separately reported ARC as the live projection does.
The legacy REST graph request also matches the request shape the native TrueNAS
13 GUI itself sends (#2077): a graph that is not scoped to a device omits the
`identifier` key instead of sending an explicit null. The reporter's CORE
13.0-U6.1 WebSocket capture shows `reporting.get_data` graphs as
`{"name":"cpu"}` (also memory, arcsize, load, swap, cputemp), with an
`identifier` present only for parameterized graphs such as
`{"name":"arcresult","identifier":"demand_data"}`. Sending `identifier: null`
is not the native contract and is a plausible whole-batch rejection that would
discard otherwise usable CPU and memory telemetry.
`TestRESTReportingGraphFailurePreservesSnapshotTelemetry`,
`TestRESTReportingGraphFailureBoundaries`,
`TestRESTReportingPartialAndMalformedResponses` and
`TestReportingActiveMemoryIsNotTotalUsed` in
`TestRESTReportingPartialAndMalformedResponses`,
`TestReportingActiveMemoryIsNotTotalUsed` and
`TestRESTReportingRequestMatchesNativeGraphShape` in
`internal/truenas/client_test.go` exercise request-validating synthetic REST
responses, repeated full snapshots, CPU/free/ARC projections, matching history,
partial data, bounded errors and cancellation. They do not establish the native
CORE request schema, row timestamps/units, appliance acceptance or the reporter's
exact failure. Native response evidence remains required before claiming that
this repair resolves the reported telemetry journey.
partial data, bounded errors, cancellation and the native identifier-less graph
request shape. They do not establish the native CORE response schema, row
timestamps/units, appliance acceptance or the reporter's exact failure. Native
response evidence remains required before claiming that this repair resolves the
reported telemetry journey.
**Availability backfill preserves concurrent discovery changes (7 September 2026)**
+22 -6
View File
@@ -348,15 +348,31 @@ func latestTimeSeriesValue(series []TimeSeriesPoint) (float64, bool) {
return latest.Value, true
}
// reportingGraph builds a reporting.get_data graph entry in the shape the
// TrueNAS 13 middleware itself accepts: a graph that is not scoped to a device
// omits the identifier key entirely rather than sending an explicit null. The
// native GUI requests CPU, memory and ARC as `{"name":"cpu"}` and only supplies
// an identifier for parameterized graphs such as `arcresult` (#2077).
func reportingGraph(name, identifier string) map[string]any {
graph := map[string]any{"name": name}
if trimmed := strings.TrimSpace(identifier); trimmed != "" {
graph["identifier"] = trimmed
}
return graph
}
// legacyRESTReportingGraphs is the reporting.get_data graph set used to
// reconstruct live telemetry and history over the legacy REST transport.
// reconstruct live telemetry and history over the legacy REST transport. The
// identifier is omitted for device-independent graphs so the request matches
// the native contract instead of sending a null identifier that the legacy
// REST schema can reject for the whole batch (#2077).
func legacyRESTReportingGraphs() []map[string]any {
return []map[string]any{
{"name": "cpu", "identifier": nil},
{"name": "memory", "identifier": nil},
{"name": "arcsize", "identifier": nil},
{"name": "interface", "identifier": nil},
{"name": "disk", "identifier": nil},
reportingGraph("cpu", ""),
reportingGraph("memory", ""),
reportingGraph("arcsize", ""),
reportingGraph("interface", ""),
reportingGraph("disk", ""),
}
}
+63
View File
@@ -2281,3 +2281,66 @@ func TestRESTReportingPartialAndMalformedResponses(t *testing.T) {
}
}
}
// nativeShapeReportingTransport models the request shape the TrueNAS 13
// middleware accepts: a graph that carries an explicit identifier key (even a
// null one) is rejected for the whole request, while a device-independent graph
// that omits the key succeeds. This is a schema-faithful synthetic transport,
// not an appliance capture; it encodes the native request shape the reporter
// supplied in #2077.
type nativeShapeReportingTransport struct {
routes alertArgsTransport
graphs []map[string]any
}
func (s *nativeShapeReportingTransport) RoundTrip(r *http.Request) (*http.Response, error) {
if r.URL.Path != "/api/v2.0/reporting/get_data" {
return s.routes.RoundTrip(r)
}
var request struct {
Graphs []map[string]any `json:"graphs"`
Query map[string]any `json:"query"`
}
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
return nil, err
}
s.graphs = append(s.graphs, request.Graphs...)
for _, graph := range request.Graphs {
if _, present := graph["identifier"]; present {
return (alertArgsTransport{r.URL.Path: apiResponse{
status: http.StatusUnprocessableEntity,
body: `{"error":"identifier: Not a string"}`,
}}).RoundTrip(r)
}
}
body := `[
{"name":"cpu","legend":["user","idle"],"data":[[1789000060,12,88]]},
{"name":"memory","legend":["used","free","total"],"data":[[1789000060,6000000000,2000000000,16000000000]]},
{"name":"arcsize","legend":["size"],"data":[[1789000060,8000000000]]}
]`
return (alertArgsTransport{r.URL.Path: apiResponse{status: http.StatusOK, body: body}}).RoundTrip(r)
}
func TestRESTReportingRequestMatchesNativeGraphShape(t *testing.T) {
routes := alertArgsTransport(defaultAPIResponses())
routes["/api/v2.0/system/info"] = apiResponse{body: `{"hostname":"core","version":"TrueNAS-13.0-U6.1","physmem":16000000000}`}
transport := &nativeShapeReportingTransport{routes: routes}
client := newLegacyRESTReportingClient(t, routes)
client.httpClient.Transport = transport
telemetry, err := client.GetSystemTelemetry(context.Background())
if err != nil {
t.Fatalf("GetSystemTelemetry() error = %v", err)
}
if telemetry.CPUPercent != 12 || telemetry.MemoryAvailableBytes != 2000000000 || telemetry.ARCSizeBytes != 8000000000 {
t.Fatalf("native-shape telemetry lost: %+v", telemetry)
}
if len(transport.graphs) == 0 {
t.Fatal("no reporting graphs requested")
}
for _, graph := range transport.graphs {
if _, present := graph["identifier"]; present {
t.Fatalf("legacy REST graph %v sends an identifier key the native client omits", graph)
}
}
}