Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lx9FNLG2CqeBbVGGd9bFsP
100 lines
3.5 KiB
Go
100 lines
3.5 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"github.com/danielgtaylor/huma/v2"
|
|
|
|
"github.com/v10networks/nmon/internal/p95"
|
|
)
|
|
|
|
// P95Response is a monthly 95th percentile in bits per second. It is never a bare number: source says
|
|
// whether it was materialised or computed now, and coverage how much of the month had data.
|
|
type P95Response struct {
|
|
Scope string `json:"scope" enum:"iface,group"`
|
|
Month string `json:"month" doc:"YYYY-MM in the site timezone."`
|
|
InBPS float64 `json:"in_bps"`
|
|
OutBPS float64 `json:"out_bps"`
|
|
Coverage float64 `json:"coverage" doc:"Share of the month's 5-minute windows that had samples, 0 to 1."`
|
|
Partial bool `json:"partial" doc:"Coverage is below 90%."`
|
|
Source string `json:"source" enum:"materialised,live"`
|
|
MembersHash string `json:"members_digest,omitempty" doc:"Groups: names the membership the figure was computed from."`
|
|
Flags []GroupFlag `json:"flags,omitempty"`
|
|
}
|
|
|
|
func (s *server) registerP95(api huma.API) {
|
|
huma.Register(api, huma.Operation{
|
|
OperationID: "get-p95", Method: http.MethodGet, Path: "/api/v1/p95", Tags: []string{"graph"},
|
|
Summary: "Monthly 95th percentile of an interface or a group",
|
|
Description: "Nearest rank over the 5-minute average rates of one calendar month in the site's timezone, per direction. " +
|
|
"A finished month is answered from the materialised record when there is one; otherwise it is computed now. " +
|
|
"Give exactly one of iface and group. An interface with unreliable counters, or a month without samples, is a 422.",
|
|
Errors: []int{http.StatusNotFound, http.StatusUnprocessableEntity, http.StatusServiceUnavailable},
|
|
}, s.getP95)
|
|
}
|
|
|
|
type p95In struct {
|
|
Iface string `query:"iface" doc:"Interface ULID."`
|
|
Group string `query:"group" doc:"Group id."`
|
|
Month string `query:"month" required:"true" pattern:"^\\d{4}-(0[1-9]|1[0-2])$" doc:"YYYY-MM."`
|
|
}
|
|
|
|
func (s *server) getP95(ctx context.Context, in *p95In) (*struct{ Body P95Response }, error) {
|
|
if (in.Iface == "") == (in.Group == "") {
|
|
return nil, huma.Error422UnprocessableEntity("give exactly one of iface and group")
|
|
}
|
|
release, err := s.admit(ctx, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer release()
|
|
qctx, cancel := context.WithTimeout(ctx, QueryTimeout)
|
|
defer cancel()
|
|
if in.Iface != "" {
|
|
return s.ifaceP95(qctx, in)
|
|
}
|
|
return s.groupP95(qctx, in)
|
|
}
|
|
|
|
func (s *server) ifaceP95(ctx context.Context, in *p95In) (*struct{ Body P95Response }, error) {
|
|
if s.P95 == nil {
|
|
return nil, errNotConfigured("p95")
|
|
}
|
|
r, err := s.P95.P95(ctx, in.Iface, in.Month)
|
|
if err != nil {
|
|
return nil, s.p95Err(ctx, err)
|
|
}
|
|
return &struct{ Body P95Response }{p95Response("iface", r)}, nil
|
|
}
|
|
|
|
func (s *server) groupP95(ctx context.Context, in *p95In) (*struct{ Body P95Response }, error) {
|
|
if s.Groups == nil {
|
|
return nil, errNotConfigured("group traffic")
|
|
}
|
|
id, err := idParam(in.Group)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := s.Store.GetGroup(ctx, id); err != nil {
|
|
return nil, s.storeErr(err)
|
|
}
|
|
r, err := s.Groups.GroupP95(ctx, id, in.Month)
|
|
if err != nil {
|
|
return nil, s.p95Err(ctx, err)
|
|
}
|
|
out := p95Response("group", r.Result)
|
|
out.Flags = []GroupFlag{}
|
|
for _, f := range r.Flags {
|
|
out.Flags = append(out.Flags, GroupFlag{Kind: f.Kind, A: f.A, B: f.B})
|
|
}
|
|
return &struct{ Body P95Response }{out}, nil
|
|
}
|
|
|
|
func p95Response(scope string, r p95.Result) P95Response {
|
|
return P95Response{
|
|
Scope: scope, Month: r.Month, InBPS: r.InBps, OutBPS: r.OutBps, Coverage: r.Coverage, Partial: r.Partial(),
|
|
Source: string(r.Source), MembersHash: r.MembersHash,
|
|
}
|
|
}
|