blob: bffe1b50bdf3050e0f4d213426867c768c24d1f7 [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 (
20 "fmt"
21 "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 Bazanski06d65bc2020-09-24 10:51:59 +020030// 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 +020031// will be returned. An error will be returned if the DN is invalid (contains empty parts, eg. `foo..bar`, `.foo` or
32// `foo.`.
33func (d DN) Path() ([]string, error) {
34 if d == "" {
35 return nil, nil
36 }
37 parts := strings.Split(string(d), ".")
38 for _, p := range parts {
39 if p == "" {
40 return nil, fmt.Errorf("invalid DN")
41 }
42 }
43 return parts, nil
44}
45
46// journal is the main log recording structure of logtree. It manages linked lists containing the actual log entries,
47// and implements scans across them. It does not understand the hierarchical nature of logtree, and instead sees all
48// entries as part of a global linked list and a local linked list for a given DN.
49//
50// The global linked list is represented by the head/tail pointers in journal and nextGlobal/prevGlobal pointers in
51// entries. The local linked lists are represented by heads[DN]/tails[DN] pointers in journal and nextLocal/prevLocal
52// pointers in entries:
53//
54// .------------. .------------. .------------.
55// | dn: A.B | | dn: Z | | dn: A.B |
56// | time: 1 | | time: 2 | | time: 3 |
57// |------------| |------------| |------------|
58// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
59// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
60// |------------| |------------| n |------------|
61// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
62// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
63// '------------' | l '------------' | '------------'
64// ^ '----------------------' ^
65// | ^ |
66// | | |
67// ( head ) ( tails[Z] ) ( tail )
68// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
69//
70type journal struct {
71 // mu locks the rest of the structure. It must be taken during any operation on the journal.
72 mu sync.RWMutex
73
74 // tail is the side of the global linked list that contains the newest log entry, ie. the one that has been pushed
75 // the most recently. It can be nil when no log entry has yet been pushed. The global linked list contains all log
76 // entries pushed to the journal.
77 tail *entry
78 // head is the side of the global linked list that contains the oldest log entry. It can be nil when no log entry
79 // has yet been pushed.
80 head *entry
81
82 // tails are the tail sides of a local linked list for a given DN, ie. the sides that contain the newest entry. They
83 // are nil if there are no log entries for that DN.
84 tails map[DN]*entry
85 // heads are the head sides of a local linked list for a given DN, ie. the sides that contain the oldest entry. They
86 // are nil if there are no log entries for that DN.
87 heads map[DN]*entry
88
89 // quota is a map from DN to quota structure, representing the quota policy of a particular DN-designated logger.
90 quota map[DN]*quota
91
92 // subscribers are observer to logs. New log entries get emitted to channels present in the subscriber structure,
93 // after filtering them through subscriber-provided filters (eg. to limit events to subtrees that interest that
94 // particular subscriber).
95 subscribers []*subscriber
96}
97
98// newJournal creates a new empty journal. All journals are independent from eachother, and as such, all LogTrees are
99// also independent.
100func newJournal() *journal {
101 return &journal{
102 tails: make(map[DN]*entry),
103 heads: make(map[DN]*entry),
104
105 quota: make(map[DN]*quota),
106 }
107}
108
109// filter is a predicate that returns true if a log subscriber or reader is interested in a log entry at a given
110// severity and logged to a given DN.
111type filter func(origin DN, severity Severity) bool
112
113// filterALl returns a filter that accepts all log entries.
114func filterAll() filter {
115 return func(origin DN, _ Severity) bool { return true }
116}
117
118// filterExact returns a filter that accepts only log entries at a given exact DN. This filter should not be used in
119// conjunction with journal.scanEntries - instead, journal.getEntries should be used, as it is much faster.
120func filterExact(dn DN) filter {
121 return func(origin DN, _ Severity) bool {
122 return origin == dn
123 }
124}
125
126// filterSubtree returns a filter that accepts all log entries at a given DN and sub-DNs. For example, filterSubtree at
127// "foo.bar" would allow entries at "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
128func filterSubtree(root DN) filter {
129 if root == "" {
130 return filterAll()
131 }
132
133 rootParts := strings.Split(string(root), ".")
134 return func(origin DN, _ Severity) bool {
135 parts := strings.Split(string(origin), ".")
136 if len(parts) < len(rootParts) {
137 return false
138 }
139
140 for i, p := range rootParts {
141 if parts[i] != p {
142 return false
143 }
144 }
145
146 return true
147 }
148}
149
150// filterSeverity returns a filter that accepts log entries at a given severity level or above. See the Severity type
151// for more information about severity levels.
152func filterSeverity(atLeast Severity) filter {
153 return func(origin DN, s Severity) bool {
154 return s.AtLeast(atLeast)
155 }
156}
157
158// scanEntries does a linear scan through the global entry list and returns all entries that match the given filters. If
159// retrieving entries for an exact event, getEntries should be used instead, as it will leverage DN-local linked lists
160// to retrieve them faster.
161// journal.mu must be taken at R or RW level when calling this function.
162func (j *journal) scanEntries(filters ...filter) (res []*entry) {
163 cur := j.tail
164 for {
165 if cur == nil {
166 return
167 }
168
169 passed := true
170 for _, filter := range filters {
171 if !filter(cur.origin, cur.payload.severity) {
172 passed = false
173 break
174 }
175 }
176 if passed {
177 res = append(res, cur)
178 }
179 cur = cur.nextGlobal
180 }
181}
182
Serge Bazanski06d65bc2020-09-24 10:51:59 +0200183// 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 +0200184// local linked list pointers to traverse the journal. Additional filters can be passed to further limit the entries
185// returned, but a scan through this DN's local linked list will be performed regardless.
186// journal.mu must be taken at R or RW level when calling this function.
187func (j *journal) getEntries(exact DN, filters ...filter) (res []*entry) {
188 cur := j.tails[exact]
189 for {
190 if cur == nil {
191 return
192 }
193
194 passed := true
195 for _, filter := range filters {
196 if !filter(cur.origin, cur.payload.severity) {
197 passed = false
198 break
199 }
200 }
201 if passed {
202 res = append(res, cur)
203 }
204 cur = cur.nextLocal
205 }
206
207}