carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package apperr
2
3import (
4 "context"
5 "sort"
6 "time"
7
8 "github.com/carlosframework/platform/internal/applog"
9 "github.com/carlosframework/platform/internal/release"
10)
11
12// Prefix is the app's errors/ subtree (spec §3.2): a sibling family of
13// logs/ with the same key shape below it.
14// An invalid account or app is refused (applog.ErrBadKeySegment).
15func Prefix(acct, app string) (string, error) {
16 return applog.FamilyPrefix(applog.FamilyErrors, acct, app)
17}
18
19// BoxPrefix is one box's subtree within an app's errors.
20func BoxPrefix(acct, app, box string) (string, error) {
21 return applog.FamilyBoxPrefix(applog.FamilyErrors, acct, app, box)
22}
23
24// ChunkKey names one flushed errors chunk; see applog.ChunkKey.
25func ChunkKey(acct, app, box, hour string, seq uint64, size int) (string, error) {
26 return applog.FamilyChunkKey(applog.FamilyErrors, acct, app, box, hour, seq, size)
27}
28
29// HourOf is applog.HourOf, re-exported so shippers need one import.
30func HourOf(t time.Time) string { return applog.HourOf(t) }
31
32// EncodeChunk renders records as gzipped JSONL.
33func EncodeChunk(recs []Record) ([]byte, error) { return applog.EncodeJSONL(recs) }
34
35// DecodeChunk parses a chunk, skipping malformed lines.
36func DecodeChunk(b []byte) ([]Record, error) { return applog.DecodeJSONL[Record](b) }
37
38// Filter is the read-side match. A coalesced record belongs to a window
39// when [First, Last] overlaps [Since, Until] and is counted whole (spec
40// §3.2: a window edge is wrong by at most one flush interval).
41type Filter struct {
42 Since, Until time.Time
43 Host string
44 Source string
45 Release string
46 FP string
47}
48
49// Match reports whether r survives f.
50func (f Filter) Match(r Record) bool {
51 last, first := r.Last, r.First
52 if last.IsZero() {
53 last = r.TS
54 }
55 if first.IsZero() {
56 first = r.TS
57 }
58 if !f.Since.IsZero() && last.Before(f.Since) {
59 return false
60 }
61 if !f.Until.IsZero() && first.After(f.Until) {
62 return false
63 }
64 if f.Host != "" && r.Host != f.Host {
65 return false
66 }
67 if f.Source != "" && r.Source != f.Source {
68 return false
69 }
70 if f.Release != "" && r.Release != f.Release {
71 return false
72 }
73 if f.FP != "" && r.FP != f.FP {
74 return false
75 }
76 return true
77}
78
79// ReadWindow assembles one app's errors/ records across boxes: every hour
80// intersecting the window, filtered through f, merged by (Last, Box, Seq).
81// collectCap bounds memory; past it the oldest go and the bool says so.
82func ReadWindow(ctx context.Context, s release.Store, acct, app string, f Filter, collectCap int) ([]Record, bool, error) {
83 // A record's chunk sits under the hour of its FIRST occurrence (TS),
84 // but its Last can be much later — a group that started before the
85 // window opened and is still being added to at read time. ReadChunks
86 // prunes hour directories at listing time using the SAME since/until
87 // it is given, so listing with the exact f.Since would skip that
88 // chunk's hour entirely. Widen the LISTING window by one hour behind
89 // f.Since; f.Match (which tests [First, Last] against the true
90 // window) still does the exact filtering once the chunk is read.
91 listSince := f.Since
92 if !listSince.IsZero() {
93 listSince = listSince.Add(-time.Hour)
94 }
95 prefix, err := Prefix(acct, app)
96 if err != nil {
97 return nil, false, err
98 }
99 perBox, trimmed, err := applog.ReadChunks[Record](ctx, s, prefix, listSince, f.Until, f.Match, collectCap)
100 if err != nil {
101 return nil, false, err
102 }
103 merged := MergeRecords(perBox...)
104 if over := len(merged) - collectCap; over > 0 {
105 merged = merged[over:]
106 trimmed = true
107 }
108 return merged, trimmed, nil
109}
110
111// MergeRecords folds per-source slices into one timeline ordered by
112// (Last, Box, Seq), oldest first.
113func MergeRecords(slices ...[]Record) []Record {
114 var out []Record
115 for _, s := range slices {
116 out = append(out, s...)
117 }
118 sort.SliceStable(out, func(i, j int) bool {
119 a, b := out[i], out[j]
120 if !a.Last.Equal(b.Last) {
121 return a.Last.Before(b.Last)
122 }
123 if a.Box != b.Box {
124 return a.Box < b.Box
125 }
126 return a.Seq < b.Seq
127 })
128 return out
129}
130