blob: d29fdaa0770a2c1a08a4de336092f5d6b2bd3227 [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//
62// .------------. .------------. .------------.
63// | dn: A.B | | dn: Z | | dn: A.B |
64// | time: 1 | | time: 2 | | time: 3 |
65// |------------| |------------| |------------|
66// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
67// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
68// |------------| |------------| n |------------|
69// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
70// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
71// '------------' | l '------------' | '------------'
72// ^ '----------------------' ^
73// | ^ |
74// | | |
75// ( head ) ( tails[Z] ) ( tail )
76// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020077type journal struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020078 // mu locks the rest of the structure. It must be taken during any operation on the
79 // journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020080 mu sync.RWMutex
81
Serge Bazanski216fe7b2021-05-21 18:36:16 +020082 // tail is the side of the global linked list that contains the newest log entry,
83 // ie. the one that has been pushed the most recently. It can be nil when no log
84 // entry has yet been pushed. The global linked list contains all log entries
85 // pushed to the journal.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020086 tail *entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020087 // head is the side of the global linked list that contains the oldest log entry.
88 // It can be nil when no log entry has yet been pushed.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020089 head *entry
90
Serge Bazanski216fe7b2021-05-21 18:36:16 +020091 // tails are the tail sides of a local linked list for a given DN, ie. the sides
92 // that contain the newest entry. They are nil if there are no log entries for that
93 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020094 tails map[DN]*entry
Serge Bazanski216fe7b2021-05-21 18:36:16 +020095 // heads are the head sides of a local linked list for a given DN, ie. the sides
96 // that contain the oldest entry. They are nil if there are no log entries for that
97 // DN.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020098 heads map[DN]*entry
99
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200100 // quota is a map from DN to quota structure, representing the quota policy of a
101 // particular DN-designated logger.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200102 quota map[DN]*quota
103
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200104 // subscribers are observer to logs. New log entries get emitted to channels
105 // present in the subscriber structure, after filtering them through subscriber-
106 // provided filters (eg. to limit events to subtrees that interest that particular
107 // subscriber).
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200108 subscribers []*subscriber
109}
110
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200111// newJournal creates a new empty journal. All journals are independent from
112// eachother, and as such, all LogTrees are also independent.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200113func newJournal() *journal {
114 return &journal{
115 tails: make(map[DN]*entry),
116 heads: make(map[DN]*entry),
117
118 quota: make(map[DN]*quota),
119 }
120}
121
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200122// filter is a predicate that returns true if a log subscriber or reader is
123// interested in a given log entry.
Serge Bazanskif68153c2020-10-26 13:54:37 +0100124type filter func(*entry) bool
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200125
Serge Bazanskif68153c2020-10-26 13:54:37 +0100126// filterAll returns a filter that accepts all log entries.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200127func filterAll() filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100128 return func(*entry) bool { return true }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200129}
130
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200131// filterExact returns a filter that accepts only log entries at a given exact
132// DN. This filter should not be used in conjunction with journal.scanEntries
133// - instead, journal.getEntries should be used, as it is much faster.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200134func filterExact(dn DN) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100135 return func(e *entry) bool {
136 return e.origin == dn
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200137 }
138}
139
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200140// filterSubtree returns a filter that accepts all log entries at a given DN and
141// sub-DNs. For example, filterSubtree at "foo.bar" would allow entries at
142// "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200143func filterSubtree(root DN) filter {
144 if root == "" {
145 return filterAll()
146 }
147
148 rootParts := strings.Split(string(root), ".")
Serge Bazanskif68153c2020-10-26 13:54:37 +0100149 return func(e *entry) bool {
150 parts := strings.Split(string(e.origin), ".")
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200151 if len(parts) < len(rootParts) {
152 return false
153 }
154
155 for i, p := range rootParts {
156 if parts[i] != p {
157 return false
158 }
159 }
160
161 return true
162 }
163}
164
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200165// filterSeverity returns a filter that accepts log entries at a given severity
166// level or above. See the Severity type for more information about severity
167// levels.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200168func filterSeverity(atLeast Severity) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100169 return func(e *entry) bool {
170 return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200171 }
172}
173
Serge Bazanskif68153c2020-10-26 13:54:37 +0100174func filterOnlyRaw(e *entry) bool {
175 return e.raw != nil
176}
177
178func filterOnlyLeveled(e *entry) bool {
179 return e.leveled != nil
180}
181
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200182// scanEntries does a linear scan through the global entry list and returns all
183// entries that match the given filters. If retrieving entries for an exact event,
184// getEntries should be used instead, as it will leverage DN-local linked lists to
185// retrieve them faster. journal.mu must be taken at R or RW level when calling
186// this function.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200187func (j *journal) scanEntries(filters ...filter) (res []*entry) {
188 cur := j.tail
189 for {
190 if cur == nil {
191 return
192 }
193
194 passed := true
195 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100196 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200197 passed = false
198 break
199 }
200 }
201 if passed {
202 res = append(res, cur)
203 }
204 cur = cur.nextGlobal
205 }
206}
207
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200208// getEntries returns all entries at a given DN. This is faster than a
209// scanEntries(filterExact), as it uses the special local linked list pointers to
210// traverse the journal. Additional filters can be passed to further limit the
211// entries returned, but a scan through this DN's local linked list will be
212// performed regardless. journal.mu must be taken at R or RW level when calling
213// this function.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200214func (j *journal) getEntries(exact DN, filters ...filter) (res []*entry) {
215 cur := j.tails[exact]
216 for {
217 if cur == nil {
218 return
219 }
220
221 passed := true
222 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100223 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200224 passed = false
225 break
226 }
227 }
228 if passed {
229 res = append(res, cur)
230 }
231 cur = cur.nextLocal
232 }
233
234}