blob: 55977bf3d12b2856074286a546b39011a891eb4d [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 Bazanski5faa2fc2020-09-07 14:09:30 +02008 "strings"
9 "sync"
Serge Bazanski3c5d0632024-09-12 10:49:12 +000010
11 "source.monogon.dev/go/logging"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020012)
13
Serge Bazanski216fe7b2021-05-21 18:36:16 +020014// DN is the Distinguished Name, a dot-delimited path used to address loggers
15// within a LogTree. For example, "foo.bar" designates the 'bar' logger node
16// under the 'foo' logger node under the root node of the logger. An empty
17// string is the root node of the tree.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020018type DN string
19
Serge Bazanskib0272182020-11-02 18:39:44 +010020var (
21 ErrInvalidDN = errors.New("invalid DN")
22)
23
Serge Bazanski216fe7b2021-05-21 18:36:16 +020024// Path return the parts of a DN, ie. all the elements of the dot-delimited DN
25// path. For the root node, an empty list will be returned. An error will be
26// returned if the DN is invalid (contains empty parts, eg. `foo..bar`, `.foo`
27// or `foo.`.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020028func (d DN) Path() ([]string, error) {
29 if d == "" {
30 return nil, nil
31 }
32 parts := strings.Split(string(d), ".")
33 for _, p := range parts {
34 if p == "" {
Serge Bazanskib0272182020-11-02 18:39:44 +010035 return nil, ErrInvalidDN
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020036 }
37 }
38 return parts, nil
39}
40
Serge Bazanski216fe7b2021-05-21 18:36:16 +020041// journal is the main log recording structure of logtree. It manages linked lists
42// containing the actual log entries, and implements scans across them. It does not
43// understand the hierarchical nature of logtree, and instead sees all entries as
44// part of a global linked list and a local linked list for a given DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020045//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020046// The global linked list is represented by the head/tail pointers in journal and
47// nextGlobal/prevGlobal pointers in entries. The local linked lists are
48// represented by heads[DN]/tails[DN] pointers in journal and nextLocal/prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020049// pointers in entries:
50//
Serge Bazanski8fab0142023-03-29 16:48:16 +020051// .------------. .------------. .------------.
52// | dn: A.B | | dn: Z | | dn: A.B |
53// | time: 1 | | time: 2 | | time: 3 |
54// |------------| |------------| |------------|
55// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
56// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
57// |------------| |------------| n |------------|
58// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
59// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
60// '------------' | l '------------' | '------------'
61// ^ '----------------------' ^
62// | ^ |
63// | | |
64// ( head ) ( tails[Z] ) ( tail )
65// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020066type journal struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020067 // mu locks the rest of the structure. It must be taken during any operation on the
68 // journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020069 mu sync.RWMutex
70
Serge Bazanski216fe7b2021-05-21 18:36:16 +020071 // tail is the side of the global linked list that contains the newest log entry,
72 // ie. the one that has been pushed the most recently. It can be nil when no log
73 // entry has yet been pushed. The global linked list contains all log entries
74 // pushed to the journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020075 tail *entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020076 // head is the side of the global linked list that contains the oldest log entry.
77 // It can be nil when no log entry has yet been pushed.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020078 head *entry
79
Serge Bazanski216fe7b2021-05-21 18:36:16 +020080 // tails are the tail sides of a local linked list for a given DN, ie. the sides
81 // that contain the newest entry. They are nil if there are no log entries for that
82 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020083 tails map[DN]*entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020084 // heads are the head sides of a local linked list for a given DN, ie. the sides
85 // that contain the oldest entry. They are nil if there are no log entries for that
86 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020087 heads map[DN]*entry
88
Serge Bazanski216fe7b2021-05-21 18:36:16 +020089 // quota is a map from DN to quota structure, representing the quota policy of a
90 // particular DN-designated logger.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020091 quota map[DN]*quota
92
Serge Bazanski216fe7b2021-05-21 18:36:16 +020093 // subscribers are observer to logs. New log entries get emitted to channels
94 // present in the subscriber structure, after filtering them through subscriber-
95 // provided filters (eg. to limit events to subtrees that interest that particular
96 // subscriber).
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020097 subscribers []*subscriber
98}
99
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200100// newJournal creates a new empty journal. All journals are independent from
101// eachother, and as such, all LogTrees are also independent.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200102func newJournal() *journal {
103 return &journal{
104 tails: make(map[DN]*entry),
105 heads: make(map[DN]*entry),
106
107 quota: make(map[DN]*quota),
108 }
109}
110
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200111// filter is a predicate that returns true if a log subscriber or reader is
112// interested in a given log entry.
Serge Bazanskif68153c2020-10-26 13:54:37 +0100113type filter func(*entry) bool
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200114
Serge Bazanskif68153c2020-10-26 13:54:37 +0100115// filterAll returns a filter that accepts all log entries.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200116func filterAll() filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100117 return func(*entry) bool { return true }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200118}
119
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200120// filterExact returns a filter that accepts only log entries at a given exact
121// DN. This filter should not be used in conjunction with journal.scanEntries
122// - instead, journal.getEntries should be used, as it is much faster.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200123func filterExact(dn DN) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100124 return func(e *entry) bool {
125 return e.origin == dn
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200126 }
127}
128
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200129// filterSubtree returns a filter that accepts all log entries at a given DN and
130// sub-DNs. For example, filterSubtree at "foo.bar" would allow entries at
131// "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200132func filterSubtree(root DN) filter {
133 if root == "" {
134 return filterAll()
135 }
136
137 rootParts := strings.Split(string(root), ".")
Serge Bazanskif68153c2020-10-26 13:54:37 +0100138 return func(e *entry) bool {
139 parts := strings.Split(string(e.origin), ".")
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200140 if len(parts) < len(rootParts) {
141 return false
142 }
143
144 for i, p := range rootParts {
145 if parts[i] != p {
146 return false
147 }
148 }
149
150 return true
151 }
152}
153
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200154// filterSeverity returns a filter that accepts log entries at a given severity
155// level or above. See the Severity type for more information about severity
156// levels.
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000157func filterSeverity(atLeast logging.Severity) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100158 return func(e *entry) bool {
159 return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200160 }
161}
162
Serge Bazanskif68153c2020-10-26 13:54:37 +0100163func filterOnlyRaw(e *entry) bool {
164 return e.raw != nil
165}
166
167func filterOnlyLeveled(e *entry) bool {
168 return e.leveled != nil
169}
170
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200171// scanEntries does a linear scan through the global entry list and returns all
172// entries that match the given filters. If retrieving entries for an exact event,
173// getEntries should be used instead, as it will leverage DN-local linked lists to
174// retrieve them faster. journal.mu must be taken at R or RW level when calling
175// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200176func (j *journal) scanEntries(count int, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200177 cur := j.tail
178 for {
179 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200180 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200181 }
182
183 passed := true
184 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100185 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200186 passed = false
187 break
188 }
189 }
190 if passed {
191 res = append(res, cur)
192 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200193 if count != BacklogAllAvailable && len(res) >= count {
194 break
195 }
196 cur = cur.prevGlobal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200197 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200198
Serge Bazanski8fab0142023-03-29 16:48:16 +0200199 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200200}
201
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200202// getEntries returns all entries at a given DN. This is faster than a
203// scanEntries(filterExact), as it uses the special local linked list pointers to
204// traverse the journal. Additional filters can be passed to further limit the
205// entries returned, but a scan through this DN's local linked list will be
206// performed regardless. journal.mu must be taken at R or RW level when calling
207// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200208func (j *journal) getEntries(count int, exact DN, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200209 cur := j.tails[exact]
210 for {
211 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200212 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200213 }
214
215 passed := true
216 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100217 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200218 passed = false
219 break
220 }
221 }
222 if passed {
223 res = append(res, cur)
224 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200225 if count != BacklogAllAvailable && len(res) >= count {
226 break
227 }
228 cur = cur.prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200229 }
230
Serge Bazanski8fab0142023-03-29 16:48:16 +0200231 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200232}
Serge Bazanski367ee272023-03-16 17:50:39 +0100233
234// Shorten returns a shortened version of this DN for constrained logging
235// environments like tty0 logging.
236//
237// If ShortenDictionary is given, it will be used to replace DN parts with
238// shorter equivalents. For example, with the dictionary:
239//
240// { "foobar": "foo", "manager": "mgr" }
241//
242// The DN some.foobar.logger will be turned into some.foo.logger before further
243// being processed by the shortening mechanism.
244//
245// The shortening rules applied are Metropolis-specific.
246func (d DN) Shorten(dict ShortenDictionary, maxLen int) string {
247 path, _ := d.Path()
248 // Apply DN part shortening rules.
249 if dict != nil {
250 for i, p := range path {
251 if sh, ok := dict[p]; ok {
252 path[i] = sh
253 }
254 }
255 }
256
257 // This generally shouldn't happen.
258 if len(path) == 0 {
259 return "?"
260 }
261
262 // Strip 'root.' prefix.
263 if len(path) > 1 && path[0] == "root" {
264 path = path[1:]
265 }
266
267 // Replace role.xxx.yyy.zzz with xxx.zzz - stripping everything between the role
268 // name and the last element of the path.
269 if path[0] == "role" && len(path) > 1 {
270 if len(path) == 2 {
271 path = path[1:]
272 } else {
273 path = []string{
274 path[1],
275 path[len(path)-1],
276 }
277 }
278 }
279
280 // Join back to be ' '-delimited, and ellipsize if too long.
281 s := strings.Join(path, " ")
282 if overflow := len(s) - maxLen; overflow > 0 {
283 s = "..." + s[overflow+3:]
284 }
285 return s
286}
287
288type ShortenDictionary map[string]string
289
290var MetropolisShortenDict = ShortenDictionary{
291 "controlplane": "cplane",
292 "map-cluster-membership": "map-membership",
293 "cluster-membership": "cluster",
294 "controller-manager": "controllers",
295 "networking": "net",
296 "network": "net",
297 "interfaces": "ifaces",
298 "kubernetes": "k8s",
299}