blob: c6cc503650764bb270b81a7fe04a3794670ff316 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +02002// SPDX-License-Identifier: Apache-2.0
Serge Bazanski5faa2fc2020-09-07 14:09:30 +02003
4package logtree
5
6import (
Serge Bazanskib0272182020-11-02 18:39:44 +01007 "errors"
Serge Bazanski8fab0142023-03-29 16:48:16 +02008 "sort"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +02009 "strings"
10 "sync"
Serge Bazanski3c5d0632024-09-12 10:49:12 +000011
12 "source.monogon.dev/go/logging"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020013)
14
Serge Bazanski216fe7b2021-05-21 18:36:16 +020015// DN is the Distinguished Name, a dot-delimited path used to address loggers
16// within a LogTree. For example, "foo.bar" designates the 'bar' logger node
17// under the 'foo' logger node under the root node of the logger. An empty
18// string is the root node of the tree.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020019type DN string
20
Serge Bazanskib0272182020-11-02 18:39:44 +010021var (
22 ErrInvalidDN = errors.New("invalid DN")
23)
24
Serge Bazanski216fe7b2021-05-21 18:36:16 +020025// Path return the parts of a DN, ie. all the elements of the dot-delimited DN
26// path. For the root node, an empty list will be returned. An error will be
27// returned if the DN is invalid (contains empty parts, eg. `foo..bar`, `.foo`
28// or `foo.`.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020029func (d DN) Path() ([]string, error) {
30 if d == "" {
31 return nil, nil
32 }
33 parts := strings.Split(string(d), ".")
34 for _, p := range parts {
35 if p == "" {
Serge Bazanskib0272182020-11-02 18:39:44 +010036 return nil, ErrInvalidDN
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020037 }
38 }
39 return parts, nil
40}
41
Serge Bazanski216fe7b2021-05-21 18:36:16 +020042// journal is the main log recording structure of logtree. It manages linked lists
43// containing the actual log entries, and implements scans across them. It does not
44// understand the hierarchical nature of logtree, and instead sees all entries as
45// part of a global linked list and a local linked list for a given DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020046//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020047// The global linked list is represented by the head/tail pointers in journal and
48// nextGlobal/prevGlobal pointers in entries. The local linked lists are
49// represented by heads[DN]/tails[DN] pointers in journal and nextLocal/prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020050// pointers in entries:
51//
Serge Bazanski8fab0142023-03-29 16:48:16 +020052// .------------. .------------. .------------.
53// | dn: A.B | | dn: Z | | dn: A.B |
54// | time: 1 | | time: 2 | | time: 3 |
55// |------------| |------------| |------------|
56// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
57// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
58// |------------| |------------| n |------------|
59// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
60// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
61// '------------' | l '------------' | '------------'
62// ^ '----------------------' ^
63// | ^ |
64// | | |
65// ( head ) ( tails[Z] ) ( tail )
66// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020067type journal struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020068 // mu locks the rest of the structure. It must be taken during any operation on the
69 // journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020070 mu sync.RWMutex
71
Serge Bazanski216fe7b2021-05-21 18:36:16 +020072 // tail is the side of the global linked list that contains the newest log entry,
73 // ie. the one that has been pushed the most recently. It can be nil when no log
74 // entry has yet been pushed. The global linked list contains all log entries
75 // pushed to the journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020076 tail *entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020077 // head is the side of the global linked list that contains the oldest log entry.
78 // It can be nil when no log entry has yet been pushed.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020079 head *entry
80
Serge Bazanski216fe7b2021-05-21 18:36:16 +020081 // tails are the tail sides of a local linked list for a given DN, ie. the sides
82 // that contain the newest entry. They are nil if there are no log entries for that
83 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020084 tails map[DN]*entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020085 // heads are the head sides of a local linked list for a given DN, ie. the sides
86 // that contain the oldest entry. They are nil if there are no log entries for that
87 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020088 heads map[DN]*entry
89
Serge Bazanski216fe7b2021-05-21 18:36:16 +020090 // quota is a map from DN to quota structure, representing the quota policy of a
91 // particular DN-designated logger.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020092 quota map[DN]*quota
93
Serge Bazanski216fe7b2021-05-21 18:36:16 +020094 // subscribers are observer to logs. New log entries get emitted to channels
95 // present in the subscriber structure, after filtering them through subscriber-
96 // provided filters (eg. to limit events to subtrees that interest that particular
97 // subscriber).
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020098 subscribers []*subscriber
99}
100
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200101// newJournal creates a new empty journal. All journals are independent from
102// eachother, and as such, all LogTrees are also independent.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200103func newJournal() *journal {
104 return &journal{
105 tails: make(map[DN]*entry),
106 heads: make(map[DN]*entry),
107
108 quota: make(map[DN]*quota),
109 }
110}
111
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200112// filter is a predicate that returns true if a log subscriber or reader is
113// interested in a given log entry.
Serge Bazanskif68153c2020-10-26 13:54:37 +0100114type filter func(*entry) bool
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200115
Serge Bazanskif68153c2020-10-26 13:54:37 +0100116// filterAll returns a filter that accepts all log entries.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200117func filterAll() filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100118 return func(*entry) bool { return true }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200119}
120
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200121// filterExact returns a filter that accepts only log entries at a given exact
122// DN. This filter should not be used in conjunction with journal.scanEntries
123// - instead, journal.getEntries should be used, as it is much faster.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200124func filterExact(dn DN) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100125 return func(e *entry) bool {
126 return e.origin == dn
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200127 }
128}
129
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200130// filterSubtree returns a filter that accepts all log entries at a given DN and
131// sub-DNs. For example, filterSubtree at "foo.bar" would allow entries at
132// "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200133func filterSubtree(root DN) filter {
134 if root == "" {
135 return filterAll()
136 }
137
138 rootParts := strings.Split(string(root), ".")
Serge Bazanskif68153c2020-10-26 13:54:37 +0100139 return func(e *entry) bool {
140 parts := strings.Split(string(e.origin), ".")
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200141 if len(parts) < len(rootParts) {
142 return false
143 }
144
145 for i, p := range rootParts {
146 if parts[i] != p {
147 return false
148 }
149 }
150
151 return true
152 }
153}
154
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200155// filterSeverity returns a filter that accepts log entries at a given severity
156// level or above. See the Severity type for more information about severity
157// levels.
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000158func filterSeverity(atLeast logging.Severity) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100159 return func(e *entry) bool {
160 return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200161 }
162}
163
Serge Bazanskif68153c2020-10-26 13:54:37 +0100164func filterOnlyRaw(e *entry) bool {
165 return e.raw != nil
166}
167
168func filterOnlyLeveled(e *entry) bool {
169 return e.leveled != nil
170}
171
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200172// scanEntries does a linear scan through the global entry list and returns all
173// entries that match the given filters. If retrieving entries for an exact event,
174// getEntries should be used instead, as it will leverage DN-local linked lists to
175// retrieve them faster. journal.mu must be taken at R or RW level when calling
176// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200177func (j *journal) scanEntries(count int, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200178 cur := j.tail
179 for {
180 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200181 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200182 }
183
184 passed := true
185 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100186 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200187 passed = false
188 break
189 }
190 }
191 if passed {
192 res = append(res, cur)
193 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200194 if count != BacklogAllAvailable && len(res) >= count {
195 break
196 }
197 cur = cur.prevGlobal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200198 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200199
200 // Reverse entries back into chronological order.
201 sort.SliceStable(res, func(i, j int) bool {
202 return i > j
203 })
204 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200205}
206
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200207// getEntries returns all entries at a given DN. This is faster than a
208// scanEntries(filterExact), as it uses the special local linked list pointers to
209// traverse the journal. Additional filters can be passed to further limit the
210// entries returned, but a scan through this DN's local linked list will be
211// performed regardless. journal.mu must be taken at R or RW level when calling
212// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200213func (j *journal) getEntries(count int, exact DN, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200214 cur := j.tails[exact]
215 for {
216 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200217 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200218 }
219
220 passed := true
221 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100222 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200223 passed = false
224 break
225 }
226 }
227 if passed {
228 res = append(res, cur)
229 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200230 if count != BacklogAllAvailable && len(res) >= count {
231 break
232 }
233 cur = cur.prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200234 }
235
Serge Bazanski8fab0142023-03-29 16:48:16 +0200236 // Reverse entries back into chronological order.
237 sort.SliceStable(res, func(i, j int) bool {
238 return i > j
239 })
240 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200241}
Serge Bazanski367ee272023-03-16 17:50:39 +0100242
243// Shorten returns a shortened version of this DN for constrained logging
244// environments like tty0 logging.
245//
246// If ShortenDictionary is given, it will be used to replace DN parts with
247// shorter equivalents. For example, with the dictionary:
248//
249// { "foobar": "foo", "manager": "mgr" }
250//
251// The DN some.foobar.logger will be turned into some.foo.logger before further
252// being processed by the shortening mechanism.
253//
254// The shortening rules applied are Metropolis-specific.
255func (d DN) Shorten(dict ShortenDictionary, maxLen int) string {
256 path, _ := d.Path()
257 // Apply DN part shortening rules.
258 if dict != nil {
259 for i, p := range path {
260 if sh, ok := dict[p]; ok {
261 path[i] = sh
262 }
263 }
264 }
265
266 // This generally shouldn't happen.
267 if len(path) == 0 {
268 return "?"
269 }
270
271 // Strip 'root.' prefix.
272 if len(path) > 1 && path[0] == "root" {
273 path = path[1:]
274 }
275
276 // Replace role.xxx.yyy.zzz with xxx.zzz - stripping everything between the role
277 // name and the last element of the path.
278 if path[0] == "role" && len(path) > 1 {
279 if len(path) == 2 {
280 path = path[1:]
281 } else {
282 path = []string{
283 path[1],
284 path[len(path)-1],
285 }
286 }
287 }
288
289 // Join back to be ' '-delimited, and ellipsize if too long.
290 s := strings.Join(path, " ")
291 if overflow := len(s) - maxLen; overflow > 0 {
292 s = "..." + s[overflow+3:]
293 }
294 return s
295}
296
297type ShortenDictionary map[string]string
298
299var MetropolisShortenDict = ShortenDictionary{
300 "controlplane": "cplane",
301 "map-cluster-membership": "map-membership",
302 "cluster-membership": "cluster",
303 "controller-manager": "controllers",
304 "networking": "net",
305 "network": "net",
306 "interfaces": "ifaces",
307 "kubernetes": "k8s",
308}