blob: 78c55a194e5c31f0a75c62180f8252cb37321e33 [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
25// DN is the Distinguished Name, a dot-delimited path used to address loggers within a LogTree. For example, "foo.bar"
26// designates the 'bar' logger node under the 'foo' logger node under the root node of the logger. An empty string is
27// the root node of the tree.
28type DN string
29
Serge Bazanskib0272182020-11-02 18:39:44 +010030var (
31 ErrInvalidDN = errors.New("invalid DN")
32)
33
Serge Bazanski06d65bc2020-09-24 10:51:59 +020034// Path return the parts of a DN, ie. all the elements of the dot-delimited DN path. For the root node, an empty list
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020035// will be returned. An error will be returned if the DN is invalid (contains empty parts, eg. `foo..bar`, `.foo` or
36// `foo.`.
37func (d DN) Path() ([]string, error) {
38 if d == "" {
39 return nil, nil
40 }
41 parts := strings.Split(string(d), ".")
42 for _, p := range parts {
43 if p == "" {
Serge Bazanskib0272182020-11-02 18:39:44 +010044 return nil, ErrInvalidDN
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020045 }
46 }
47 return parts, nil
48}
49
50// journal is the main log recording structure of logtree. It manages linked lists containing the actual log entries,
51// and implements scans across them. It does not understand the hierarchical nature of logtree, and instead sees all
52// entries as part of a global linked list and a local linked list for a given DN.
53//
54// The global linked list is represented by the head/tail pointers in journal and nextGlobal/prevGlobal pointers in
55// entries. The local linked lists are represented by heads[DN]/tails[DN] pointers in journal and nextLocal/prevLocal
56// pointers in entries:
57//
58// .------------. .------------. .------------.
59// | dn: A.B | | dn: Z | | dn: A.B |
60// | time: 1 | | time: 2 | | time: 3 |
61// |------------| |------------| |------------|
62// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
63// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
64// |------------| |------------| n |------------|
65// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
66// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
67// '------------' | l '------------' | '------------'
68// ^ '----------------------' ^
69// | ^ |
70// | | |
71// ( head ) ( tails[Z] ) ( tail )
72// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
73//
74type journal struct {
75 // mu locks the rest of the structure. It must be taken during any operation on the journal.
76 mu sync.RWMutex
77
78 // tail is the side of the global linked list that contains the newest log entry, ie. the one that has been pushed
79 // the most recently. It can be nil when no log entry has yet been pushed. The global linked list contains all log
80 // entries pushed to the journal.
81 tail *entry
82 // head is the side of the global linked list that contains the oldest log entry. It can be nil when no log entry
83 // has yet been pushed.
84 head *entry
85
86 // tails are the tail sides of a local linked list for a given DN, ie. the sides that contain the newest entry. They
87 // are nil if there are no log entries for that DN.
88 tails map[DN]*entry
89 // heads are the head sides of a local linked list for a given DN, ie. the sides that contain the oldest entry. They
90 // are nil if there are no log entries for that DN.
91 heads map[DN]*entry
92
93 // quota is a map from DN to quota structure, representing the quota policy of a particular DN-designated logger.
94 quota map[DN]*quota
95
96 // subscribers are observer to logs. New log entries get emitted to channels present in the subscriber structure,
97 // after filtering them through subscriber-provided filters (eg. to limit events to subtrees that interest that
98 // particular subscriber).
99 subscribers []*subscriber
100}
101
102// newJournal creates a new empty journal. All journals are independent from eachother, and as such, all LogTrees are
103// also independent.
104func newJournal() *journal {
105 return &journal{
106 tails: make(map[DN]*entry),
107 heads: make(map[DN]*entry),
108
109 quota: make(map[DN]*quota),
110 }
111}
112
Serge Bazanskif68153c2020-10-26 13:54:37 +0100113// filter is a predicate that returns true if a log subscriber or reader is interested in a given log entry.
114type filter func(*entry) bool
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200115
Serge Bazanskif68153c2020-10-26 13:54:37 +0100116// filterAll returns a filter that accepts all log entries.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200117func filterAll() filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100118 return func(*entry) bool { return true }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200119}
120
121// filterExact returns a filter that accepts only log entries at a given exact DN. This filter should not be used in
122// conjunction with journal.scanEntries - instead, journal.getEntries should be used, as it is much faster.
123func filterExact(dn DN) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100124 return func(e *entry) bool {
125 return e.origin == dn
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200126 }
127}
128
129// filterSubtree returns a filter that accepts all log entries at a given DN and sub-DNs. For example, filterSubtree at
130// "foo.bar" would allow entries at "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
131func filterSubtree(root DN) filter {
132 if root == "" {
133 return filterAll()
134 }
135
136 rootParts := strings.Split(string(root), ".")
Serge Bazanskif68153c2020-10-26 13:54:37 +0100137 return func(e *entry) bool {
138 parts := strings.Split(string(e.origin), ".")
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200139 if len(parts) < len(rootParts) {
140 return false
141 }
142
143 for i, p := range rootParts {
144 if parts[i] != p {
145 return false
146 }
147 }
148
149 return true
150 }
151}
152
153// filterSeverity returns a filter that accepts log entries at a given severity level or above. See the Severity type
154// for more information about severity levels.
155func filterSeverity(atLeast Severity) filter {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100156 return func(e *entry) bool {
157 return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200158 }
159}
160
Serge Bazanskif68153c2020-10-26 13:54:37 +0100161func filterOnlyRaw(e *entry) bool {
162 return e.raw != nil
163}
164
165func filterOnlyLeveled(e *entry) bool {
166 return e.leveled != nil
167}
168
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200169// scanEntries does a linear scan through the global entry list and returns all entries that match the given filters. If
170// retrieving entries for an exact event, getEntries should be used instead, as it will leverage DN-local linked lists
171// to retrieve them faster.
172// journal.mu must be taken at R or RW level when calling this function.
173func (j *journal) scanEntries(filters ...filter) (res []*entry) {
174 cur := j.tail
175 for {
176 if cur == nil {
177 return
178 }
179
180 passed := true
181 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100182 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200183 passed = false
184 break
185 }
186 }
187 if passed {
188 res = append(res, cur)
189 }
190 cur = cur.nextGlobal
191 }
192}
193
Serge Bazanski06d65bc2020-09-24 10:51:59 +0200194// getEntries returns all entries at a given DN. This is faster than a scanEntries(filterExact), as it uses the special
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200195// local linked list pointers to traverse the journal. Additional filters can be passed to further limit the entries
196// returned, but a scan through this DN's local linked list will be performed regardless.
197// journal.mu must be taken at R or RW level when calling this function.
198func (j *journal) getEntries(exact DN, filters ...filter) (res []*entry) {
199 cur := j.tails[exact]
200 for {
201 if cur == nil {
202 return
203 }
204
205 passed := true
206 for _, filter := range filters {
Serge Bazanskif68153c2020-10-26 13:54:37 +0100207 if !filter(cur) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200208 passed = false
209 break
210 }
211 }
212 if passed {
213 res = append(res, cur)
214 }
215 cur = cur.nextLocal
216 }
217
218}