mirror of
https://github.com/hanzoai/observability.git
synced 2026-10-30 20:22:53 +00:00
hanzoai/dashboards shipped as its own binary (msgType 206) serving five console
tRPC routers: dashboards, widgets, table batch-actions, view presets, monitors.
It is now this service's 200+ ordinal range and its dash_*.go files.
They were never two concerns. A dashboard is a saved QUERY over exactly the
traces, observations and scores this service owns, and both halves were blocked
on the SAME ClickHouse shim — kept apart, that shim had to be written twice
against two copies of one OLTP model, and chart/scoreHistogram could not reach
the scores they aggregate without a network hop. The duplication was already
visible in the capability bits: both services had independently minted a
PermAnalyticsRead. Merged, there is one.
Ordinals are the standalone binary's 0–29 offset by 200, not renumbered into the
gaps: the offset is mechanical and reversible, renumbering would silently
repoint a client built against the old ids.
Three conventions were doubled and are now single:
- TENANCY. authorize() answered both "may this caller invoke this method" and
"whose rows may it touch", returning a defaultOrg the handlers ignored while
each re-read projectId from its own params. Split: authorize() is the cap gate,
scope() is the ONE tenant resolver, and its project is passed to every handler.
A pipelined call now INHERITS its target's project instead of re-reading the
payload, so pipelining cannot be used to hop tenants. Server.defaultOrg was
dead once scope() existed and is deleted rather than left as unread state on a
security-relevant struct.
- NOT-IMPLEMENTED. Observability answered stubs 200 + Empty{Stubbed:true};
dashboards answered 501. Unified on 501 through one Server.stub helper. A
stubbed 200 carrying an empty result is indistinguishable at the call site from
a real query that matched nothing — the UI renders "no data" and the missing
backend never surfaces. The status is the one field a caller cannot skip
reading; a flag in the body is one it can, and eventually does.
- COLUMN NAMES. fDashDescription, fWidgetDescription and fDescription were three
constants for the column "description". One constant per column name now.
scope() reads the payload's field at text @0 without knowing its type, which is
silently wrong for any request struct that puts another text field first — and
wrong by reading ANOTHER TENANT'S rows, not by failing. That assumption is now
pinned by TestEveryRequestStructCarriesProjectIdAtZero over the schema itself
(43 declarations, 9 result structs exempted). Verified it fails when the
invariant is broken, not just that it passes.
Verified: 22 tests green (14 obs + 8 dashboards, bodies ported byte-identical so
they diff against the originals) plus the invariant; go vet clean; make zap-gen
byte-identical on regen; CGO_ENABLED=0 build ok.
go.sum carried the same moved-tag corruption fixed in hanzoai/dashboards
(age/pq/zap hashes restored to the checksum DB; threshold v1.9.4→v1.9.5 and
orm v0.6.14→v0.6.16, both unbuildable from a clean cache).
Co-authored-by: Hanzo Dev <[email protected]>
85 lines
2.9 KiB
Go
85 lines
2.9 KiB
Go
package server
|
|
|
|
import (
|
|
"math"
|
|
|
|
"github.com/hanzoai/base/core"
|
|
"github.com/hanzoai/dbx"
|
|
zaplib "github.com/luxfi/zap"
|
|
)
|
|
|
|
// dash_helpers.go — the query + response helpers the dashboards surface uses.
|
|
//
|
|
// Two scoped lookups exist in this package and they are NOT duplicates:
|
|
//
|
|
// - findByExt (collections.go) addresses an obs_* row by its EXTERNAL id — the
|
|
// caller-facing trace/observation/score id, stored in its own extId column
|
|
// because ingestion assigns it upstream.
|
|
// - findScoped (here) addresses a dashboards-surface row by its BASE record id,
|
|
// which is the caller-facing id for these models: nothing upstream assigns
|
|
// them, so the store's own id is the identity.
|
|
//
|
|
// Both scope by projectId. That is the invariant; which id column identifies the
|
|
// row is a property of where the id comes from.
|
|
|
|
// ok answers a call with a successful body.
|
|
func (s *Server) ok(req Call, body []byte) (*zaplib.Message, error) {
|
|
return buildResponse(StatusOK, req.PromiseID, body)
|
|
}
|
|
|
|
// fail answers a call with a status and a JSON error body.
|
|
func (s *Server) fail(req Call, status uint32, msg string) (*zaplib.Message, error) {
|
|
return buildResponse(status, req.PromiseID, errorBody(msg))
|
|
}
|
|
|
|
// findScoped reads one dashboards-surface row by Base id within a project. A row
|
|
// in another project is not found — never read.
|
|
func (s *Server) findScoped(collection, project, id string) (*core.Record, error) {
|
|
return s.app.FindFirstRecordByFilter(collection,
|
|
"id = {:id} && projectId = {:p}", dbx.Params{"id": id, "p": project})
|
|
}
|
|
|
|
// listScoped returns one page of a project's rows plus the unpaginated total,
|
|
// which the list envelopes carry so the UI can render pagination.
|
|
func (s *Server) listScoped(collection, project, sort string, page, limit int) ([]*core.Record, int, error) {
|
|
all, err := s.app.FindRecordsByFilter(collection,
|
|
"projectId = {:p}", "", 0, 0, dbx.Params{"p": project})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
total := len(all)
|
|
rows, err := s.app.FindRecordsByFilter(collection,
|
|
"projectId = {:p}", sort, limit, offsetOf(page, limit), dbx.Params{"p": project})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return rows, total, nil
|
|
}
|
|
|
|
func offsetOf(page, limit int) int {
|
|
if page <= 1 || limit <= 0 {
|
|
return 0
|
|
}
|
|
return (page - 1) * limit
|
|
}
|
|
|
|
// sortExpr maps the tRPC orderBy object onto Base's sort syntax, defaulting to
|
|
// newest-first when the caller names no column.
|
|
func sortExpr(column, order string) string {
|
|
if column == "" {
|
|
return "-created"
|
|
}
|
|
if order == "ASC" {
|
|
return "+" + column
|
|
}
|
|
return "-" + column
|
|
}
|
|
|
|
// nullThreshold is the wire sentinel for a nullable f64 that is null. The source
|
|
// column is z.number().nullable(); the wire carries f64, so NaN marks "absent"
|
|
// — the one float that is never a legitimate threshold.
|
|
func nullThreshold() float64 { return math.NaN() }
|
|
|
|
// isNull reports whether a float carries the null sentinel.
|
|
func isNull(f float64) bool { return math.IsNaN(f) }
|