mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-24 04:33:17 +00:00
Merge pull request #2172 from rcourtman/maintainer/20260922T223953Z
Fix TrueNAS CORE CPU and memory telemetry over legacy REST
This commit is contained in:
@@ -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)**
|
||||
|
||||
@@ -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", ""),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user