blob: 8697b21e085467179b66dbf3c288dee502130001 [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"
Tim Windelschmidt01491a72025-07-23 21:49:11 +02008 "slices"
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
Tim Windelschmidt01491a72025-07-23 21:49:11 +020099
100 // seq is a counter tracking the total amount of log entries appended since
101 // creation.
102 seq uint64
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200103}
104
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200105// newJournal creates a new empty journal. All journals are independent from
106// eachother, and as such, all LogTrees are also independent.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200107func newJournal() *journal {
108 return &journal{
109 tails: make(map[DN]*entry),
110 heads: make(map[DN]*entry),
111
112 quota: make(map[DN]*quota),
113 }
114}
115
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200116// filter is a predicate that returns true if a log subscriber or reader is
117// interested in a given log entry.
Serge Bazanskif68153c2020-10-26 13:54:37 +0100118type filter func(*entry) bool
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200119
Serge Bazanskif68153c2020-10-26 13:54:37 +0100120// filterAll returns a filter that accepts all log entries.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200121func filterAll() filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100122 return func(*entry) bool { return true }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200123}
124
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200125// filterExact returns a filter that accepts only log entries at a given exact
126// DN. This filter should not be used in conjunction with journal.scanEntries
127// - instead, journal.getEntries should be used, as it is much faster.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200128func filterExact(dn DN) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100129 return func(e *entry) bool {
130 return e.origin == dn
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200131 }
132}
133
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200134// filterSubtree returns a filter that accepts all log entries at a given DN and
135// sub-DNs. For example, filterSubtree at "foo.bar" would allow entries at
136// "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200137func filterSubtree(root DN) filter {
138 if root == "" {
139 return filterAll()
140 }
141
142 rootParts := strings.Split(string(root), ".")
Serge Bazanskif68153c2020-10-26 13:54:37 +0100143 return func(e *entry) bool {
144 parts := strings.Split(string(e.origin), ".")
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200145 if len(parts) < len(rootParts) {
146 return false
147 }
148
149 for i, p := range rootParts {
150 if parts[i] != p {
151 return false
152 }
153 }
154
155 return true
156 }
157}
158
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200159// filterSeverity returns a filter that accepts log entries at a given severity
160// level or above. See the Severity type for more information about severity
161// levels.
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000162func filterSeverity(atLeast logging.Severity) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100163 return func(e *entry) bool {
164 return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200165 }
166}
167
Serge Bazanskif68153c2020-10-26 13:54:37 +0100168func filterOnlyRaw(e *entry) bool {
169 return e.raw != nil
170}
171
172func filterOnlyLeveled(e *entry) bool {
173 return e.leveled != nil
174}
175
Tim Windelschmidt01491a72025-07-23 21:49:11 +0200176func filterStartPosition(count int, pos int, direction ReadDirection) filter {
177 return func(e *entry) bool {
178 switch direction {
179 case ReadDirectionAfter:
180 return e.seqGlobal >= uint64(pos) && (count == BacklogAllAvailable || e.seqGlobal < uint64(pos+count))
181 case ReadDirectionBefore:
182 return e.seqGlobal < uint64(pos) && (count == BacklogAllAvailable || e.seqGlobal >= uint64(max(0, pos-count)))
183 default:
184 panic("unreachable")
185 }
186 }
187}
188
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200189// scanEntries does a linear scan through the global entry list and returns all
190// entries that match the given filters. If retrieving entries for an exact event,
191// getEntries should be used instead, as it will leverage DN-local linked lists to
192// retrieve them faster. journal.mu must be taken at R or RW level when calling
193// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200194func (j *journal) scanEntries(count int, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200195 cur := j.tail
196 for {
197 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200198 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200199 }
200
201 passed := true
202 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100203 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200204 passed = false
205 break
206 }
207 }
208 if passed {
209 res = append(res, cur)
210 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200211 if count != BacklogAllAvailable && len(res) >= count {
212 break
213 }
214 cur = cur.prevGlobal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200215 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200216
Tim Windelschmidt1354c782025-07-23 19:01:08 +0000217 // Reverse entries back into chronological order.
Tim Windelschmidt01491a72025-07-23 21:49:11 +0200218 slices.Reverse(res)
219
Serge Bazanski8fab0142023-03-29 16:48:16 +0200220 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200221}
222
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200223// getEntries returns all entries at a given DN. This is faster than a
224// scanEntries(filterExact), as it uses the special local linked list pointers to
225// traverse the journal. Additional filters can be passed to further limit the
226// entries returned, but a scan through this DN's local linked list will be
227// performed regardless. journal.mu must be taken at R or RW level when calling
228// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200229func (j *journal) getEntries(count int, exact DN, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200230 cur := j.tails[exact]
231 for {
232 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200233 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200234 }
235
236 passed := true
237 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100238 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200239 passed = false
240 break
241 }
242 }
243 if passed {
244 res = append(res, cur)
245 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200246 if count != BacklogAllAvailable && len(res) >= count {
247 break
248 }
249 cur = cur.prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200250 }
251
Tim Windelschmidt1354c782025-07-23 19:01:08 +0000252 // Reverse entries back into chronological order.
Tim Windelschmidt01491a72025-07-23 21:49:11 +0200253 slices.Reverse(res)
254
Serge Bazanski8fab0142023-03-29 16:48:16 +0200255 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200256}
Serge Bazanski367ee272023-03-16 17:50:39 +0100257
258// Shorten returns a shortened version of this DN for constrained logging
259// environments like tty0 logging.
260//
261// If ShortenDictionary is given, it will be used to replace DN parts with
262// shorter equivalents. For example, with the dictionary:
263//
264// { "foobar": "foo", "manager": "mgr" }
265//
266// The DN some.foobar.logger will be turned into some.foo.logger before further
267// being processed by the shortening mechanism.
268//
269// The shortening rules applied are Metropolis-specific.
270func (d DN) Shorten(dict ShortenDictionary, maxLen int) string {
271 path, _ := d.Path()
272 // Apply DN part shortening rules.
273 if dict != nil {
274 for i, p := range path {
275 if sh, ok := dict[p]; ok {
276 path[i] = sh
277 }
278 }
279 }
280
281 // This generally shouldn't happen.
282 if len(path) == 0 {
283 return "?"
284 }
285
286 // Strip 'root.' prefix.
287 if len(path) > 1 && path[0] == "root" {
288 path = path[1:]
289 }
290
291 // Replace role.xxx.yyy.zzz with xxx.zzz - stripping everything between the role
292 // name and the last element of the path.
293 if path[0] == "role" && len(path) > 1 {
294 if len(path) == 2 {
295 path = path[1:]
296 } else {
297 path = []string{
298 path[1],
299 path[len(path)-1],
300 }
301 }
302 }
303
304 // Join back to be ' '-delimited, and ellipsize if too long.
305 s := strings.Join(path, " ")
306 if overflow := len(s) - maxLen; overflow > 0 {
307 s = "..." + s[overflow+3:]
308 }
309 return s
310}
311
312type ShortenDictionary map[string]string
313
314var MetropolisShortenDict = ShortenDictionary{
315 "controlplane": "cplane",
316 "map-cluster-membership": "map-membership",
317 "cluster-membership": "cluster",
318 "controller-manager": "controllers",
319 "networking": "net",
320 "network": "net",
321 "interfaces": "ifaces",
322 "kubernetes": "k8s",
323}