blob: 5df6e1a8d3a22ce0c8538f64a3bfb2fa789f2dea [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 Bazanski5faa2fc2020-09-07 14:09:30 +020021 "strings"
22 "sync"
23)
24
Serge Bazanski216fe7b2021-05-21 18:36:16 +020025// DN is the Distinguished Name, a dot-delimited path used to address loggers
26// within a LogTree. For example, "foo.bar" designates the 'bar' logger node
27// under the 'foo' logger node under the root node of the logger. An empty
28// string is the root node of the tree.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020029type DN string
30
Serge Bazanskib0272182020-11-02 18:39:44 +010031var (
32 ErrInvalidDN = errors.New("invalid DN")
33)
34
Serge Bazanski216fe7b2021-05-21 18:36:16 +020035// Path return the parts of a DN, ie. all the elements of the dot-delimited DN
36// path. For the root node, an empty list will be returned. An error will be
37// returned if the DN is invalid (contains empty parts, eg. `foo..bar`, `.foo`
38// or `foo.`.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020039func (d DN) Path() ([]string, error) {
40 if d == "" {
41 return nil, nil
42 }
43 parts := strings.Split(string(d), ".")
44 for _, p := range parts {
45 if p == "" {
Serge Bazanskib0272182020-11-02 18:39:44 +010046 return nil, ErrInvalidDN
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020047 }
48 }
49 return parts, nil
50}
51
Serge Bazanski216fe7b2021-05-21 18:36:16 +020052// journal is the main log recording structure of logtree. It manages linked lists
53// containing the actual log entries, and implements scans across them. It does not
54// understand the hierarchical nature of logtree, and instead sees all entries as
55// part of a global linked list and a local linked list for a given DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020056//
Serge Bazanski216fe7b2021-05-21 18:36:16 +020057// The global linked list is represented by the head/tail pointers in journal and
58// nextGlobal/prevGlobal pointers in entries. The local linked lists are
59// represented by heads[DN]/tails[DN] pointers in journal and nextLocal/prevLocal
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020060// pointers in entries:
61//
Serge Bazanski367ee272023-03-16 17:50:39 +010062// .------------. .------------. .------------.
63// | dn: A.B | | dn: Z | | dn: A.B |
64// | time: 1 | | time: 2 | | time: 3 |
65// |------------| |------------| |------------|
66// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
67//
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020068// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
Serge Bazanski367ee272023-03-16 17:50:39 +010069//
70// |------------| |------------| n |------------|
71// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
72//
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020073// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
Serge Bazanski367ee272023-03-16 17:50:39 +010074//
75// '------------' | l '------------' | '------------'
76// ^ '----------------------' ^
77// | ^ |
78// | | |
79// ( head ) ( tails[Z] ) ( tail )
80// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020081type journal struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020082 // mu locks the rest of the structure. It must be taken during any operation on the
83 // journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020084 mu sync.RWMutex
85
Serge Bazanski216fe7b2021-05-21 18:36:16 +020086 // tail is the side of the global linked list that contains the newest log entry,
87 // ie. the one that has been pushed the most recently. It can be nil when no log
88 // entry has yet been pushed. The global linked list contains all log entries
89 // pushed to the journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020090 tail *entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020091 // head is the side of the global linked list that contains the oldest log entry.
92 // It can be nil when no log entry has yet been pushed.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020093 head *entry
94
Serge Bazanski216fe7b2021-05-21 18:36:16 +020095 // tails are the tail sides of a local linked list for a given DN, ie. the sides
96 // that contain the newest entry. They are nil if there are no log entries for that
97 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020098 tails map[DN]*entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020099 // heads are the head sides of a local linked list for a given DN, ie. the sides
100 // that contain the oldest entry. They are nil if there are no log entries for that
101 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200102 heads map[DN]*entry
103
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200104 // quota is a map from DN to quota structure, representing the quota policy of a
105 // particular DN-designated logger.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200106 quota map[DN]*quota
107
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200108 // subscribers are observer to logs. New log entries get emitted to channels
109 // present in the subscriber structure, after filtering them through subscriber-
110 // provided filters (eg. to limit events to subtrees that interest that particular
111 // subscriber).
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200112 subscribers []*subscriber
113}
114
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200115// newJournal creates a new empty journal. All journals are independent from
116// eachother, and as such, all LogTrees are also independent.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200117func newJournal() *journal {
118 return &journal{
119 tails: make(map[DN]*entry),
120 heads: make(map[DN]*entry),
121
122 quota: make(map[DN]*quota),
123 }
124}
125
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200126// filter is a predicate that returns true if a log subscriber or reader is
127// interested in a given log entry.
Serge Bazanskif68153c2020-10-26 13:54:37 +0100128type filter func(*entry) bool
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200129
Serge Bazanskif68153c2020-10-26 13:54:37 +0100130// filterAll returns a filter that accepts all log entries.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200131func filterAll() filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100132 return func(*entry) bool { return true }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200133}
134
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200135// filterExact returns a filter that accepts only log entries at a given exact
136// DN. This filter should not be used in conjunction with journal.scanEntries
137// - instead, journal.getEntries should be used, as it is much faster.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200138func filterExact(dn DN) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100139 return func(e *entry) bool {
140 return e.origin == dn
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200141 }
142}
143
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200144// filterSubtree returns a filter that accepts all log entries at a given DN and
145// sub-DNs. For example, filterSubtree at "foo.bar" would allow entries at
146// "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200147func filterSubtree(root DN) filter {
148 if root == "" {
149 return filterAll()
150 }
151
152 rootParts := strings.Split(string(root), ".")
Serge Bazanskif68153c2020-10-26 13:54:37 +0100153 return func(e *entry) bool {
154 parts := strings.Split(string(e.origin), ".")
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200155 if len(parts) < len(rootParts) {
156 return false
157 }
158
159 for i, p := range rootParts {
160 if parts[i] != p {
161 return false
162 }
163 }
164
165 return true
166 }
167}
168
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200169// filterSeverity returns a filter that accepts log entries at a given severity
170// level or above. See the Severity type for more information about severity
171// levels.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200172func filterSeverity(atLeast Severity) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100173 return func(e *entry) bool {
174 return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200175 }
176}
177
Serge Bazanskif68153c2020-10-26 13:54:37 +0100178func filterOnlyRaw(e *entry) bool {
179 return e.raw != nil
180}
181
182func filterOnlyLeveled(e *entry) bool {
183 return e.leveled != nil
184}
185
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200186// scanEntries does a linear scan through the global entry list and returns all
187// entries that match the given filters. If retrieving entries for an exact event,
188// getEntries should be used instead, as it will leverage DN-local linked lists to
189// retrieve them faster. journal.mu must be taken at R or RW level when calling
190// this function.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200191func (j *journal) scanEntries(filters ...filter) (res []*entry) {
192 cur := j.tail
193 for {
194 if cur == nil {
195 return
196 }
197
198 passed := true
199 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100200 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200201 passed = false
202 break
203 }
204 }
205 if passed {
206 res = append(res, cur)
207 }
208 cur = cur.nextGlobal
209 }
210}
211
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200212// getEntries returns all entries at a given DN. This is faster than a
213// scanEntries(filterExact), as it uses the special local linked list pointers to
214// traverse the journal. Additional filters can be passed to further limit the
215// entries returned, but a scan through this DN's local linked list will be
216// performed regardless. journal.mu must be taken at R or RW level when calling
217// this function.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200218func (j *journal) getEntries(exact DN, filters ...filter) (res []*entry) {
219 cur := j.tails[exact]
220 for {
221 if cur == nil {
222 return
223 }
224
225 passed := true
226 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100227 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200228 passed = false
229 break
230 }
231 }
232 if passed {
233 res = append(res, cur)
234 }
235 cur = cur.nextLocal
236 }
237
238}
Serge Bazanski367ee272023-03-16 17:50:39 +0100239
240// Shorten returns a shortened version of this DN for constrained logging
241// environments like tty0 logging.
242//
243// If ShortenDictionary is given, it will be used to replace DN parts with
244// shorter equivalents. For example, with the dictionary:
245//
246// { "foobar": "foo", "manager": "mgr" }
247//
248// The DN some.foobar.logger will be turned into some.foo.logger before further
249// being processed by the shortening mechanism.
250//
251// The shortening rules applied are Metropolis-specific.
252func (d DN) Shorten(dict ShortenDictionary, maxLen int) string {
253 path, _ := d.Path()
254 // Apply DN part shortening rules.
255 if dict != nil {
256 for i, p := range path {
257 if sh, ok := dict[p]; ok {
258 path[i] = sh
259 }
260 }
261 }
262
263 // This generally shouldn't happen.
264 if len(path) == 0 {
265 return "?"
266 }
267
268 // Strip 'root.' prefix.
269 if len(path) > 1 && path[0] == "root" {
270 path = path[1:]
271 }
272
273 // Replace role.xxx.yyy.zzz with xxx.zzz - stripping everything between the role
274 // name and the last element of the path.
275 if path[0] == "role" && len(path) > 1 {
276 if len(path) == 2 {
277 path = path[1:]
278 } else {
279 path = []string{
280 path[1],
281 path[len(path)-1],
282 }
283 }
284 }
285
286 // Join back to be ' '-delimited, and ellipsize if too long.
287 s := strings.Join(path, " ")
288 if overflow := len(s) - maxLen; overflow > 0 {
289 s = "..." + s[overflow+3:]
290 }
291 return s
292}
293
294type ShortenDictionary map[string]string
295
296var MetropolisShortenDict = ShortenDictionary{
297 "controlplane": "cplane",
298 "map-cluster-membership": "map-membership",
299 "cluster-membership": "cluster",
300 "controller-manager": "controllers",
301 "networking": "net",
302 "network": "net",
303 "interfaces": "ifaces",
304 "kubernetes": "k8s",
305}