| 1 | // Package apperr is the pure half of the Errors tab (spec |
| 2 | // 2026-09-07-errors-and-analytics-design.md §3.1–3.2): the wire record, |
| 3 | // sanitisation, fingerprinting, the box-side coalescing sink, the chunk |
| 4 | // read and the grouping fold. No I/O and no clock of its own — the same |
| 5 | // split internal/applog keeps: internal/host ships, internal/edge and the |
| 6 | // activator produce, internal/console reads. |
| 7 | package apperr |
| 8 | |
| 9 | import ( |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/applog" |
| 13 | ) |
| 14 | |
| 15 | // Sources an error can come from (spec §3.1). log is never stored in the |
| 16 | // errors/ family — it is derived on read from logs/ (§3.2). |
| 17 | const ( |
| 18 | SourceLog = "log" |
| 19 | SourceEdge = "edge" |
| 20 | SourceServer = "server" |
| 21 | SourceBrowser = "browser" |
| 22 | ) |
| 23 | |
| 24 | // Kinds the platform itself produces. A server or browser caller sets its |
| 25 | // own kind (an exception class, typically). |
| 26 | const ( |
| 27 | KindHTTP = "http" // an edge-observed 5xx |
| 28 | KindLog = "log" // a log line at or above the configured level |
| 29 | ) |
| 30 | |
| 31 | // FPVersion stamps the fingerprint algorithm so it can change without |
| 32 | // merging old groups into new ones. |
| 33 | const FPVersion = 1 |
| 34 | |
| 35 | // Record is one coalesced error on the wire — one JSONL line inside an |
| 36 | // errors/ chunk. Additive shape (AGENTS.md rule): every field but TS, |
| 37 | // Source, Name and FP is omitempty, unknown keys are ignored on read. |
| 38 | // Count, First and Last are the coalescing state: one record stands for |
| 39 | // Count occurrences of FP on Host under Release between First and Last; |
| 40 | // the other fields are the first occurrence's sample. |
| 41 | type Record struct { |
| 42 | TS time.Time `json:"ts"` |
| 43 | Box string `json:"box,omitempty"` |
| 44 | Host string `json:"host,omitempty"` |
| 45 | Source string `json:"source"` |
| 46 | Kind string `json:"kind,omitempty"` |
| 47 | Name string `json:"name"` |
| 48 | Level string `json:"level,omitempty"` // log source: warn | error |
| 49 | Path string `json:"path,omitempty"` |
| 50 | Status int `json:"status,omitempty"` |
| 51 | Cause string `json:"cause,omitempty"` // Availability: why the edge could not get an answer (availability.go) |
| 52 | Stream string `json:"stream,omitempty"` // log source only: the applog stream, derived on read and never stored |
| 53 | Release string `json:"release,omitempty"` |
| 54 | Stack string `json:"stack,omitempty"` |
| 55 | Fields map[string]string `json:"fields,omitempty"` |
| 56 | FP string `json:"fp"` |
| 57 | FPV int `json:"fpv,omitempty"` |
| 58 | Count int `json:"count,omitempty"` |
| 59 | First time.Time `json:"first,omitempty"` |
| 60 | Last time.Time `json:"last,omitempty"` |
| 61 | Seq uint64 `json:"seq,omitempty"` |
| 62 | } |
| 63 | |
| 64 | // MaxStack bounds a stored stack (spec §3.1). |
| 65 | const MaxStack = 8 << 10 |
| 66 | |
| 67 | // FromLog derives the log-source record for one applog line (spec §3.2: |
| 68 | // read-derived, never stored twice). Fingerprinted on the normalised |
| 69 | // message alone — a log line has no frame. |
| 70 | func FromLog(r applog.Record) Record { |
| 71 | name := Sanitize(r.Msg) |
| 72 | return Record{ |
| 73 | TS: r.TS, Box: r.Box, Host: r.Host, |
| 74 | Source: SourceLog, Kind: KindLog, Name: name, Level: r.Level, Stream: r.Stream, |
| 75 | Fields: r.Fields, |
| 76 | FP: Fingerprint(SourceLog, KindLog, name, ""), FPV: FPVersion, |
| 77 | Count: 1, First: r.TS, Last: r.TS, Seq: r.Seq, |
| 78 | } |
| 79 | } |
| 80 | |