blob: d0e866353677ccd42bcdfb0b47670ec68b72e1c1 [file] [log] [blame]
Serge Bazanski5faa2fc2020-09-07 14:09:30 +02001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package logtree
18
19import (
Serge Bazanskib0272182020-11-02 18:39:44 +010020 "errors"
Serge Bazanski8fab0142023-03-29 16:48:16 +020021 "sort"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020022 "strings"
23 "sync"
Serge Bazanski3c5d0632024-09-12 10:49:12 +000024
25 "source.monogon.dev/go/logging"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020026)
27
Serge Bazanski216fe7b2021-05-21 18:36:16 +020028// DN is the Distinguished Name, a dot-delimited path used to address loggers
29// within a LogTree. For example, "foo.bar" designates the 'bar' logger node
30// under the 'foo' logger node under the root node of the logger. An empty
31// string is the root node of the tree.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020032type DN string
33
Serge Bazanskib0272182020-11-02 18:39:44 +010034var (
35 ErrInvalidDN = errors.New("invalid DN")
36)
37
Serge Bazanski216fe7b2021-05-21 18:36:16 +020038// Path return the parts of a DN, ie. all the elements of the dot-delimited DN
39// path. For the root node, an empty list will be returned. An error will be
40// returned if the DN is invalid (contains empty parts, eg. `foo..bar`, `.foo`
41// or `foo.`.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020042func (d DN) Path() ([]string, error) {
43 if d == "" {
44 return nil, nil
45 }
46 parts := strings.Split(string(d), ".")
47 for _, p := range parts {
48 if p == "" {
Serge Bazanskib0272182020-11-02 18:39:44 +010049 return nil, ErrInvalidDN
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020050 }
51 }
52 return parts, nil
53}
54
Serge Bazanski216fe7b2021-05-21 18:36:16 +020055// journal is the main log recording structure of logtree. It manages linked lists
56// containing the actual log entries, and implements scans across them. It does not
57// understand the hierarchical nature of logtree, and instead sees all entries as
58// part of a global linked list and a local linked list for a given DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020059//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020060// The global linked list is represented by the head/tail pointers in journal and
61// nextGlobal/prevGlobal pointers in entries. The local linked lists are
62// represented by heads[DN]/tails[DN] pointers in journal and nextLocal/prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020063// pointers in entries:
64//
Serge Bazanski8fab0142023-03-29 16:48:16 +020065// .------------. .------------. .------------.
66// | dn: A.B | | dn: Z | | dn: A.B |
67// | time: 1 | | time: 2 | | time: 3 |
68// |------------| |------------| |------------|
69// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
70// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
71// |------------| |------------| n |------------|
72// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
73// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
74// '------------' | l '------------' | '------------'
75// ^ '----------------------' ^
76// | ^ |
77// | | |
78// ( head ) ( tails[Z] ) ( tail )
79// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020080type journal struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020081 // mu locks the rest of the structure. It must be taken during any operation on the
82 // journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020083 mu sync.RWMutex
84
Serge Bazanski216fe7b2021-05-21 18:36:16 +020085 // tail is the side of the global linked list that contains the newest log entry,
86 // ie. the one that has been pushed the most recently. It can be nil when no log
87 // entry has yet been pushed. The global linked list contains all log entries
88 // pushed to the journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020089 tail *entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020090 // head is the side of the global linked list that contains the oldest log entry.
91 // It can be nil when no log entry has yet been pushed.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020092 head *entry
93
Serge Bazanski216fe7b2021-05-21 18:36:16 +020094 // tails are the tail sides of a local linked list for a given DN, ie. the sides
95 // that contain the newest entry. They are nil if there are no log entries for that
96 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020097 tails map[DN]*entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020098 // heads are the head sides of a local linked list for a given DN, ie. the sides
99 // that contain the oldest entry. They are nil if there are no log entries for that
100 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200101 heads map[DN]*entry
102
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200103 // quota is a map from DN to quota structure, representing the quota policy of a
104 // particular DN-designated logger.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200105 quota map[DN]*quota
106
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200107 // subscribers are observer to logs. New log entries get emitted to channels
108 // present in the subscriber structure, after filtering them through subscriber-
109 // provided filters (eg. to limit events to subtrees that interest that particular
110 // subscriber).
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200111 subscribers []*subscriber
112}
113
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200114// newJournal creates a new empty journal. All journals are independent from
115// eachother, and as such, all LogTrees are also independent.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200116func newJournal() *journal {
117 return &journal{
118 tails: make(map[DN]*entry),
119 heads: make(map[DN]*entry),
120
121 quota: make(map[DN]*quota),
122 }
123}
124
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200125// filter is a predicate that returns true if a log subscriber or reader is
126// interested in a given log entry.
Serge Bazanskif68153c2020-10-26 13:54:37 +0100127type filter func(*entry) bool
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200128
Serge Bazanskif68153c2020-10-26 13:54:37 +0100129// filterAll returns a filter that accepts all log entries.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200130func filterAll() filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100131 return func(*entry) bool { return true }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200132}
133
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200134// filterExact returns a filter that accepts only log entries at a given exact
135// DN. This filter should not be used in conjunction with journal.scanEntries
136// - instead, journal.getEntries should be used, as it is much faster.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200137func filterExact(dn DN) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100138 return func(e *entry) bool {
139 return e.origin == dn
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200140 }
141}
142
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200143// filterSubtree returns a filter that accepts all log entries at a given DN and
144// sub-DNs. For example, filterSubtree at "foo.bar" would allow entries at
145// "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200146func filterSubtree(root DN) filter {
147 if root == "" {
148 return filterAll()
149 }
150
151 rootParts := strings.Split(string(root), ".")
Serge Bazanskif68153c2020-10-26 13:54:37 +0100152 return func(e *entry) bool {
153 parts := strings.Split(string(e.origin), ".")
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200154 if len(parts) < len(rootParts) {
155 return false
156 }
157
158 for i, p := range rootParts {
159 if parts[i] != p {
160 return false
161 }
162 }
163
164 return true
165 }
166}
167
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200168// filterSeverity returns a filter that accepts log entries at a given severity
169// level or above. See the Severity type for more information about severity
170// levels.
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000171func filterSeverity(atLeast logging.Severity) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100172 return func(e *entry) bool {
173 return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200174 }
175}
176
Serge Bazanskif68153c2020-10-26 13:54:37 +0100177func filterOnlyRaw(e *entry) bool {
178 return e.raw != nil
179}
180
181func filterOnlyLeveled(e *entry) bool {
182 return e.leveled != nil
183}
184
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200185// scanEntries does a linear scan through the global entry list and returns all
186// entries that match the given filters. If retrieving entries for an exact event,
187// getEntries should be used instead, as it will leverage DN-local linked lists to
188// retrieve them faster. journal.mu must be taken at R or RW level when calling
189// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200190func (j *journal) scanEntries(count int, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200191 cur := j.tail
192 for {
193 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200194 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200195 }
196
197 passed := true
198 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100199 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200200 passed = false
201 break
202 }
203 }
204 if passed {
205 res = append(res, cur)
206 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200207 if count != BacklogAllAvailable && len(res) >= count {
208 break
209 }
210 cur = cur.prevGlobal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200211 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200212
213 // Reverse entries back into chronological order.
214 sort.SliceStable(res, func(i, j int) bool {
215 return i > j
216 })
217 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200218}
219
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200220// getEntries returns all entries at a given DN. This is faster than a
221// scanEntries(filterExact), as it uses the special local linked list pointers to
222// traverse the journal. Additional filters can be passed to further limit the
223// entries returned, but a scan through this DN's local linked list will be
224// performed regardless. journal.mu must be taken at R or RW level when calling
225// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200226func (j *journal) getEntries(count int, exact DN, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200227 cur := j.tails[exact]
228 for {
229 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200230 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200231 }
232
233 passed := true
234 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100235 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200236 passed = false
237 break
238 }
239 }
240 if passed {
241 res = append(res, cur)
242 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200243 if count != BacklogAllAvailable && len(res) >= count {
244 break
245 }
246 cur = cur.prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200247 }
248
Serge Bazanski8fab0142023-03-29 16:48:16 +0200249 // Reverse entries back into chronological order.
250 sort.SliceStable(res, func(i, j int) bool {
251 return i > j
252 })
253 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200254}
Serge Bazanski367ee272023-03-16 17:50:39 +0100255
256// Shorten returns a shortened version of this DN for constrained logging
257// environments like tty0 logging.
258//
259// If ShortenDictionary is given, it will be used to replace DN parts with
260// shorter equivalents. For example, with the dictionary:
261//
262// { "foobar": "foo", "manager": "mgr" }
263//
264// The DN some.foobar.logger will be turned into some.foo.logger before further
265// being processed by the shortening mechanism.
266//
267// The shortening rules applied are Metropolis-specific.
268func (d DN) Shorten(dict ShortenDictionary, maxLen int) string {
269 path, _ := d.Path()
270 // Apply DN part shortening rules.
271 if dict != nil {
272 for i, p := range path {
273 if sh, ok := dict[p]; ok {
274 path[i] = sh
275 }
276 }
277 }
278
279 // This generally shouldn't happen.
280 if len(path) == 0 {
281 return "?"
282 }
283
284 // Strip 'root.' prefix.
285 if len(path) > 1 && path[0] == "root" {
286 path = path[1:]
287 }
288
289 // Replace role.xxx.yyy.zzz with xxx.zzz - stripping everything between the role
290 // name and the last element of the path.
291 if path[0] == "role" && len(path) > 1 {
292 if len(path) == 2 {
293 path = path[1:]
294 } else {
295 path = []string{
296 path[1],
297 path[len(path)-1],
298 }
299 }
300 }
301
302 // Join back to be ' '-delimited, and ellipsize if too long.
303 s := strings.Join(path, " ")
304 if overflow := len(s) - maxLen; overflow > 0 {
305 s = "..." + s[overflow+3:]
306 }
307 return s
308}
309
310type ShortenDictionary map[string]string
311
312var MetropolisShortenDict = ShortenDictionary{
313 "controlplane": "cplane",
314 "map-cluster-membership": "map-membership",
315 "cluster-membership": "cluster",
316 "controller-manager": "controllers",
317 "networking": "net",
318 "network": "net",
319 "interfaces": "ifaces",
320 "kubernetes": "k8s",
321}