blob: 412c042ad728344cd94f4d84c469a957f06d6e80 [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"
24)
25
Serge Bazanski216fe7b2021-05-21 18:36:16 +020026// DN is the Distinguished Name, a dot-delimited path used to address loggers
27// within a LogTree. For example, "foo.bar" designates the 'bar' logger node
28// under the 'foo' logger node under the root node of the logger. An empty
29// string is the root node of the tree.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020030type DN string
31
Serge Bazanskib0272182020-11-02 18:39:44 +010032var (
33 ErrInvalidDN = errors.New("invalid DN")
34)
35
Serge Bazanski216fe7b2021-05-21 18:36:16 +020036// Path return the parts of a DN, ie. all the elements of the dot-delimited DN
37// path. For the root node, an empty list will be returned. An error will be
38// returned if the DN is invalid (contains empty parts, eg. `foo..bar`, `.foo`
39// or `foo.`.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020040func (d DN) Path() ([]string, error) {
41 if d == "" {
42 return nil, nil
43 }
44 parts := strings.Split(string(d), ".")
45 for _, p := range parts {
46 if p == "" {
Serge Bazanskib0272182020-11-02 18:39:44 +010047 return nil, ErrInvalidDN
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020048 }
49 }
50 return parts, nil
51}
52
Serge Bazanski216fe7b2021-05-21 18:36:16 +020053// journal is the main log recording structure of logtree. It manages linked lists
54// containing the actual log entries, and implements scans across them. It does not
55// understand the hierarchical nature of logtree, and instead sees all entries as
56// part of a global linked list and a local linked list for a given DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020057//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020058// The global linked list is represented by the head/tail pointers in journal and
59// nextGlobal/prevGlobal pointers in entries. The local linked lists are
60// represented by heads[DN]/tails[DN] pointers in journal and nextLocal/prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020061// pointers in entries:
62//
Serge Bazanski8fab0142023-03-29 16:48:16 +020063// .------------. .------------. .------------.
64// | dn: A.B | | dn: Z | | dn: A.B |
65// | time: 1 | | time: 2 | | time: 3 |
66// |------------| |------------| |------------|
67// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
68// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
69// |------------| |------------| n |------------|
70// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
71// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
72// '------------' | l '------------' | '------------'
73// ^ '----------------------' ^
74// | ^ |
75// | | |
76// ( head ) ( tails[Z] ) ( tail )
77// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020078type journal struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020079 // mu locks the rest of the structure. It must be taken during any operation on the
80 // journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020081 mu sync.RWMutex
82
Serge Bazanski216fe7b2021-05-21 18:36:16 +020083 // tail is the side of the global linked list that contains the newest log entry,
84 // ie. the one that has been pushed the most recently. It can be nil when no log
85 // entry has yet been pushed. The global linked list contains all log entries
86 // pushed to the journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020087 tail *entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020088 // head is the side of the global linked list that contains the oldest log entry.
89 // It can be nil when no log entry has yet been pushed.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020090 head *entry
91
Serge Bazanski216fe7b2021-05-21 18:36:16 +020092 // tails are the tail sides of a local linked list for a given DN, ie. the sides
93 // that contain the newest entry. They are nil if there are no log entries for that
94 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020095 tails map[DN]*entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020096 // heads are the head sides of a local linked list for a given DN, ie. the sides
97 // that contain the oldest entry. They are nil if there are no log entries for that
98 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020099 heads map[DN]*entry
100
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200101 // quota is a map from DN to quota structure, representing the quota policy of a
102 // particular DN-designated logger.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200103 quota map[DN]*quota
104
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200105 // subscribers are observer to logs. New log entries get emitted to channels
106 // present in the subscriber structure, after filtering them through subscriber-
107 // provided filters (eg. to limit events to subtrees that interest that particular
108 // subscriber).
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200109 subscribers []*subscriber
110}
111
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200112// newJournal creates a new empty journal. All journals are independent from
113// eachother, and as such, all LogTrees are also independent.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200114func newJournal() *journal {
115 return &journal{
116 tails: make(map[DN]*entry),
117 heads: make(map[DN]*entry),
118
119 quota: make(map[DN]*quota),
120 }
121}
122
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200123// filter is a predicate that returns true if a log subscriber or reader is
124// interested in a given log entry.
Serge Bazanskif68153c2020-10-26 13:54:37 +0100125type filter func(*entry) bool
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200126
Serge Bazanskif68153c2020-10-26 13:54:37 +0100127// filterAll returns a filter that accepts all log entries.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200128func filterAll() filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100129 return func(*entry) bool { return true }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200130}
131
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200132// filterExact returns a filter that accepts only log entries at a given exact
133// DN. This filter should not be used in conjunction with journal.scanEntries
134// - instead, journal.getEntries should be used, as it is much faster.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200135func filterExact(dn DN) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100136 return func(e *entry) bool {
137 return e.origin == dn
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200138 }
139}
140
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200141// filterSubtree returns a filter that accepts all log entries at a given DN and
142// sub-DNs. For example, filterSubtree at "foo.bar" would allow entries at
143// "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200144func filterSubtree(root DN) filter {
145 if root == "" {
146 return filterAll()
147 }
148
149 rootParts := strings.Split(string(root), ".")
Serge Bazanskif68153c2020-10-26 13:54:37 +0100150 return func(e *entry) bool {
151 parts := strings.Split(string(e.origin), ".")
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200152 if len(parts) < len(rootParts) {
153 return false
154 }
155
156 for i, p := range rootParts {
157 if parts[i] != p {
158 return false
159 }
160 }
161
162 return true
163 }
164}
165
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200166// filterSeverity returns a filter that accepts log entries at a given severity
167// level or above. See the Severity type for more information about severity
168// levels.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200169func filterSeverity(atLeast Severity) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100170 return func(e *entry) bool {
171 return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200172 }
173}
174
Serge Bazanskif68153c2020-10-26 13:54:37 +0100175func filterOnlyRaw(e *entry) bool {
176 return e.raw != nil
177}
178
179func filterOnlyLeveled(e *entry) bool {
180 return e.leveled != nil
181}
182
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200183// scanEntries does a linear scan through the global entry list and returns all
184// entries that match the given filters. If retrieving entries for an exact event,
185// getEntries should be used instead, as it will leverage DN-local linked lists to
186// retrieve them faster. journal.mu must be taken at R or RW level when calling
187// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200188func (j *journal) scanEntries(count int, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200189 cur := j.tail
190 for {
191 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200192 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200193 }
194
195 passed := true
196 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100197 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200198 passed = false
199 break
200 }
201 }
202 if passed {
203 res = append(res, cur)
204 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200205 if count != BacklogAllAvailable && len(res) >= count {
206 break
207 }
208 cur = cur.prevGlobal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200209 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200210
211 // Reverse entries back into chronological order.
212 sort.SliceStable(res, func(i, j int) bool {
213 return i > j
214 })
215 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200216}
217
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200218// getEntries returns all entries at a given DN. This is faster than a
219// scanEntries(filterExact), as it uses the special local linked list pointers to
220// traverse the journal. Additional filters can be passed to further limit the
221// entries returned, but a scan through this DN's local linked list will be
222// performed regardless. journal.mu must be taken at R or RW level when calling
223// this function.
Serge Bazanski8fab0142023-03-29 16:48:16 +0200224func (j *journal) getEntries(count int, exact DN, filters ...filter) (res []*entry) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200225 cur := j.tails[exact]
226 for {
227 if cur == nil {
Serge Bazanski8fab0142023-03-29 16:48:16 +0200228 break
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200229 }
230
231 passed := true
232 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100233 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200234 passed = false
235 break
236 }
237 }
238 if passed {
239 res = append(res, cur)
240 }
Serge Bazanski8fab0142023-03-29 16:48:16 +0200241 if count != BacklogAllAvailable && len(res) >= count {
242 break
243 }
244 cur = cur.prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200245 }
246
Serge Bazanski8fab0142023-03-29 16:48:16 +0200247 // Reverse entries back into chronological order.
248 sort.SliceStable(res, func(i, j int) bool {
249 return i > j
250 })
251 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200252}
Serge Bazanski367ee272023-03-16 17:50:39 +0100253
254// Shorten returns a shortened version of this DN for constrained logging
255// environments like tty0 logging.
256//
257// If ShortenDictionary is given, it will be used to replace DN parts with
258// shorter equivalents. For example, with the dictionary:
259//
260// { "foobar": "foo", "manager": "mgr" }
261//
262// The DN some.foobar.logger will be turned into some.foo.logger before further
263// being processed by the shortening mechanism.
264//
265// The shortening rules applied are Metropolis-specific.
266func (d DN) Shorten(dict ShortenDictionary, maxLen int) string {
267 path, _ := d.Path()
268 // Apply DN part shortening rules.
269 if dict != nil {
270 for i, p := range path {
271 if sh, ok := dict[p]; ok {
272 path[i] = sh
273 }
274 }
275 }
276
277 // This generally shouldn't happen.
278 if len(path) == 0 {
279 return "?"
280 }
281
282 // Strip 'root.' prefix.
283 if len(path) > 1 && path[0] == "root" {
284 path = path[1:]
285 }
286
287 // Replace role.xxx.yyy.zzz with xxx.zzz - stripping everything between the role
288 // name and the last element of the path.
289 if path[0] == "role" && len(path) > 1 {
290 if len(path) == 2 {
291 path = path[1:]
292 } else {
293 path = []string{
294 path[1],
295 path[len(path)-1],
296 }
297 }
298 }
299
300 // Join back to be ' '-delimited, and ellipsize if too long.
301 s := strings.Join(path, " ")
302 if overflow := len(s) - maxLen; overflow > 0 {
303 s = "..." + s[overflow+3:]
304 }
305 return s
306}
307
308type ShortenDictionary map[string]string
309
310var MetropolisShortenDict = ShortenDictionary{
311 "controlplane": "cplane",
312 "map-cluster-membership": "map-membership",
313 "cluster-membership": "cluster",
314 "controller-manager": "controllers",
315 "networking": "net",
316 "network": "net",
317 "interfaces": "ifaces",
318 "kubernetes": "k8s",
319}