carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package metrics
2
3import (
4 "context"
5 "encoding/json"
6 "errors"
7 "os"
8 "time"
9
10 "github.com/carlosframework/platform/internal/release"
11)
12
13// Agg is a min/avg/max/count summary of one metric over some window. N
14// counts only the samples that actually carried the metric (nil readings —
15// a parser that couldn't read a value — are excluded, never treated as
16// zero).
17type Agg struct {
18 Min float64 `json:"min"`
19 Avg float64 `json:"avg"`
20 Max float64 `json:"max"`
21 N int `json:"n"`
22}
23
24// HourAgg is one hour's rollup: whole-host metrics plus per-unit metrics.
25// A metric that no sample in the hour carried is simply absent from the
26// map — never a zero-valued Agg standing in for "no data".
27type HourAgg struct {
28 Host map[string]Agg `json:"host"` // "cpu","mem","disk","load1"
29 Units map[string]map[string]Agg `json:"units"` // unit -> "cpu","mem_bytes" -> Agg
30}
31
32// DayRollup is a box's rolled-up day, keyed by UTC hour ("00".."23") —
33// the object stored under metrics/boxes/<box>/days/<date>.json.
34type DayRollup struct {
35 V int `json:"v"` // 1
36 Box string `json:"box"`
37 Date string `json:"date"` // UTC 2006-01-02
38 Hours map[string]HourAgg `json:"hours"`
39}
40
41// aggOf reduces vals to an Agg, reporting ok=false for an empty slice — the
42// "this metric was never carried" case, which callers turn into "key
43// absent from the map" rather than a zero-valued Agg.
44func aggOf(vals []float64) (Agg, bool) {
45 if len(vals) == 0 {
46 return Agg{}, false
47 }
48 min, max, sum := vals[0], vals[0], 0.0
49 for _, v := range vals {
50 if v < min {
51 min = v
52 }
53 if v > max {
54 max = v
55 }
56 sum += v
57 }
58 return Agg{Min: min, Avg: sum / float64(len(vals)), Max: max, N: len(vals)}, true
59}
60
61// RollupHour aggregates a slice of raw samples (typically an hour's worth)
62// into one HourAgg. Nil metric readings are excluded from N and, when a
63// metric is nil in every sample, its key is absent from the returned map
64// entirely (both at the host level and per-unit). Host metrics are
65// "cpu","mem","disk","load1"; unit metrics are "cpu" (from
66// UnitSample.CPUFrac) and "mem_bytes" (from MemBytes, cast to float64).
67func RollupHour(samples []Sample) HourAgg {
68 var hostCPU, hostMem, hostDisk, hostLoad1 []float64
69 for _, s := range samples {
70 if v := s.Host.CPUFrac; v != nil {
71 hostCPU = append(hostCPU, *v)
72 }
73 if v := s.Host.MemFrac; v != nil {
74 hostMem = append(hostMem, *v)
75 }
76 if v := s.Host.DiskFrac; v != nil {
77 hostDisk = append(hostDisk, *v)
78 }
79 if v := s.Host.Load1; v != nil {
80 hostLoad1 = append(hostLoad1, *v)
81 }
82 }
83
84 host := map[string]Agg{}
85 for key, vals := range map[string][]float64{
86 "cpu": hostCPU, "mem": hostMem, "disk": hostDisk, "load1": hostLoad1,
87 } {
88 if agg, ok := aggOf(vals); ok {
89 host[key] = agg
90 }
91 }
92
93 unitCPU := map[string][]float64{}
94 unitMem := map[string][]float64{}
95 unitNames := map[string]bool{}
96 for _, s := range samples {
97 for name, u := range s.Units {
98 unitNames[name] = true
99 if u.CPUFrac != nil {
100 unitCPU[name] = append(unitCPU[name], *u.CPUFrac)
101 }
102 if u.MemBytes != nil {
103 unitMem[name] = append(unitMem[name], float64(*u.MemBytes))
104 }
105 }
106 }
107
108 units := map[string]map[string]Agg{}
109 for name := range unitNames {
110 inner := map[string]Agg{}
111 if agg, ok := aggOf(unitCPU[name]); ok {
112 inner["cpu"] = agg
113 }
114 if agg, ok := aggOf(unitMem[name]); ok {
115 inner["mem_bytes"] = agg
116 }
117 if len(inner) > 0 {
118 units[name] = inner
119 }
120 }
121
122 return HourAgg{Host: host, Units: units}
123}
124
125// UpsertRollup reads box's day object for hour's UTC date (a missing
126// object is not an error — it starts a fresh DayRollup), sets the entry
127// for hour's UTC hour bucket ("00".."23") to agg, and writes the day back
128// with the same read-etag, Put, retry-once-on-ErrCASConflict pattern
129// WriteRaw uses: a second conflict is not retried again and is returned to
130// the caller.
131func UpsertRollup(ctx context.Context, s release.Store, box string, hour time.Time, agg HourAgg) error {
132 date := hour.UTC().Format("2006-01-02")
133 hourKey := hour.UTC().Format("15")
134 key := DayKey(box, date)
135
136 day, etag, err := currentDayRollup(ctx, s, key, box, date)
137 if err != nil {
138 return err
139 }
140 day.Hours[hourKey] = agg
141 body, err := json.MarshalIndent(day, "", " ")
142 if err != nil {
143 return err
144 }
145 if _, err := s.Put(ctx, key, body, etag); err != nil {
146 if !errors.Is(err, release.ErrCASConflict) {
147 return err
148 }
149 day, etag, err = currentDayRollup(ctx, s, key, box, date)
150 if err != nil {
151 return err
152 }
153 day.Hours[hourKey] = agg
154 body, err = json.MarshalIndent(day, "", " ")
155 if err != nil {
156 return err
157 }
158 if _, err := s.Put(ctx, key, body, etag); err != nil {
159 return err
160 }
161 }
162 return nil
163}
164
165// currentDayRollup reads key's current DayRollup and etag for a
166// replace-only Put, folding "doesn't exist yet" into a fresh DayRollup at
167// ifMatch "" (create-only) — the same absent-means-empty convention
168// currentRawEtag uses for raw chunks.
169func currentDayRollup(ctx context.Context, s release.Store, key, box, date string) (DayRollup, string, error) {
170 body, etag, err := s.Get(ctx, key)
171 if err != nil {
172 if errors.Is(err, os.ErrNotExist) {
173 return DayRollup{V: 1, Box: box, Date: date, Hours: map[string]HourAgg{}}, "", nil
174 }
175 return DayRollup{}, "", err
176 }
177 var day DayRollup
178 if err := json.Unmarshal(body, &day); err != nil {
179 return DayRollup{}, "", err
180 }
181 if day.Hours == nil {
182 day.Hours = map[string]HourAgg{}
183 }
184 return day, etag, nil
185}
186