feat: sum aggregate rows across backends on event-counts and /reports
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful

- group by the row's non-aggregate fields and add the numeric columns; X-Records on a summed endpoint is the merged row count
This commit is contained in:
2026-09-05 11:46:20 +10:00
parent 4bb44fb331
commit b1ecbf31ac
5 changed files with 776 additions and 12 deletions
+41 -6
View File
@@ -16,11 +16,13 @@ import (
)
const (
factsPath = "/pdb/query/v4/facts"
nodesPath = "/pdb/query/v4/nodes"
reportsPath = "/pdb/query/v4/reports"
eventsPath = "/pdb/query/v4/events"
queryV4 = "/pdb/query/v4/"
factsPath = "/pdb/query/v4/facts"
nodesPath = "/pdb/query/v4/nodes"
reportsPath = "/pdb/query/v4/reports"
eventsPath = "/pdb/query/v4/events"
eventCountsPath = "/pdb/query/v4/event-counts"
aggregateEventCountsPath = "/pdb/query/v4/aggregate-event-counts"
queryV4 = "/pdb/query/v4/"
// PuppetDB only sends this when the request carries include_total=true.
recordsHeader = "X-Records"
@@ -70,9 +72,11 @@ func (s *Server) handleQuery(w http.ResponseWriter, r *http.Request) {
case factsPath:
s.serveMerged(w, r, factsPath, s.mergeFactsResponse)
case reportsPath:
s.serveUnion(w, r, reportsPath, reportKey)
s.serveReports(w, r)
case eventsPath:
s.serveUnion(w, r, eventsPath, rawKey)
case eventCountsPath, aggregateEventCountsPath:
s.serveSummed(w, r, r.URL.Path, inferredColumns)
default:
if isReportSubResource(r.URL.Path) {
s.serveFirstHolder(w, r)
@@ -131,6 +135,37 @@ func (s *Server) serveUnion(w http.ResponseWriter, r *http.Request, path string,
writeJSON(w, page.apply(merged))
}
// An `extract` query with a `function` column returns synthetic aggregate rows that carry no identity, so they are summed rather than unioned.
func (s *Server) serveReports(w http.ResponseWriter, r *http.Request) {
if spec := parseAggregate(r.URL.Query().Get("query")); spec != nil {
s.serveSummed(w, r, reportsPath, spec.columns)
return
}
s.serveUnion(w, r, reportsPath, reportKey)
}
// Merged rows are fewer than the backends' combined records, so include_total reports the merged count rather than a sum of X-Records.
func (s *Server) serveSummed(w http.ResponseWriter, r *http.Request, path string, columns func(map[string]json.RawMessage) ([]string, []string)) {
in := r.URL.Query()
page, err := parsePaging(in)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
alive, ok := s.aliveResults(w, r, path, page.upstreamParams(in))
if !ok {
return
}
merged := sumRows(s.byPrecedence(alive), columns)
sortRecords(merged, page.order)
if page.wantTotal {
w.Header().Set(recordsHeader, strconv.Itoa(len(merged)))
}
writeJSON(w, page.apply(merged))
}
// A backend without the report answers 404, indistinguishable from a failure, so every backend is consulted before serving empty.
func (s *Server) serveFirstHolder(w http.ResponseWriter, r *http.Request) {
results := s.fanOut(r.Context(), r.URL.Path, r.URL.Query())