blob: bffe1b50bdf3050e0f4d213426867c768c24d1f7 [file] [log] [blame]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package logtree
import (
"fmt"
"strings"
"sync"
)
// DN is the Distinguished Name, a dot-delimited path used to address loggers within a LogTree. For example, "foo.bar"
// designates the 'bar' logger node under the 'foo' logger node under the root node of the logger. An empty string is
// the root node of the tree.
type DN string
// Path return the parts of a DN, ie. all the elements of the dot-delimited DN path. For the root node, an empty list
// will be returned. An error will be returned if the DN is invalid (contains empty parts, eg. `foo..bar`, `.foo` or
// `foo.`.
func (d DN) Path() ([]string, error) {
if d == "" {
return nil, nil
}
parts := strings.Split(string(d), ".")
for _, p := range parts {
if p == "" {
return nil, fmt.Errorf("invalid DN")
}
}
return parts, nil
}
// journal is the main log recording structure of logtree. It manages linked lists containing the actual log entries,
// and implements scans across them. It does not understand the hierarchical nature of logtree, and instead sees all
// entries as part of a global linked list and a local linked list for a given DN.
//
// The global linked list is represented by the head/tail pointers in journal and nextGlobal/prevGlobal pointers in
// entries. The local linked lists are represented by heads[DN]/tails[DN] pointers in journal and nextLocal/prevLocal
// pointers in entries:
//
// .------------. .------------. .------------.
// | dn: A.B | | dn: Z | | dn: A.B |
// | time: 1 | | time: 2 | | time: 3 |
// |------------| |------------| |------------|
// | nextGlobal :------->| nextGlobal :------->| nextGlobal :--> nil
// nil <-: prevGlobal |<-------: prevGlobal |<-------| prevGlobal |
// |------------| |------------| n |------------|
// | nextLocal :---. n | nextLocal :->i .-->| nextLocal :--> nil
// nil <-: prevLocal |<--: i<-: prevLocal | l :---| prevLocal |
// '------------' | l '------------' | '------------'
// ^ '----------------------' ^
// | ^ |
// | | |
// ( head ) ( tails[Z] ) ( tail )
// ( heads[A.B] ) ( heads[Z] ) ( tails[A.B] )
//
type journal struct {
// mu locks the rest of the structure. It must be taken during any operation on the journal.
mu sync.RWMutex
// tail is the side of the global linked list that contains the newest log entry, ie. the one that has been pushed
// the most recently. It can be nil when no log entry has yet been pushed. The global linked list contains all log
// entries pushed to the journal.
tail *entry
// head is the side of the global linked list that contains the oldest log entry. It can be nil when no log entry
// has yet been pushed.
head *entry
// tails are the tail sides of a local linked list for a given DN, ie. the sides that contain the newest entry. They
// are nil if there are no log entries for that DN.
tails map[DN]*entry
// heads are the head sides of a local linked list for a given DN, ie. the sides that contain the oldest entry. They
// are nil if there are no log entries for that DN.
heads map[DN]*entry
// quota is a map from DN to quota structure, representing the quota policy of a particular DN-designated logger.
quota map[DN]*quota
// subscribers are observer to logs. New log entries get emitted to channels present in the subscriber structure,
// after filtering them through subscriber-provided filters (eg. to limit events to subtrees that interest that
// particular subscriber).
subscribers []*subscriber
}
// newJournal creates a new empty journal. All journals are independent from eachother, and as such, all LogTrees are
// also independent.
func newJournal() *journal {
return &journal{
tails: make(map[DN]*entry),
heads: make(map[DN]*entry),
quota: make(map[DN]*quota),
}
}
// filter is a predicate that returns true if a log subscriber or reader is interested in a log entry at a given
// severity and logged to a given DN.
type filter func(origin DN, severity Severity) bool
// filterALl returns a filter that accepts all log entries.
func filterAll() filter {
return func(origin DN, _ Severity) bool { return true }
}
// filterExact returns a filter that accepts only log entries at a given exact DN. This filter should not be used in
// conjunction with journal.scanEntries - instead, journal.getEntries should be used, as it is much faster.
func filterExact(dn DN) filter {
return func(origin DN, _ Severity) bool {
return origin == dn
}
}
// filterSubtree returns a filter that accepts all log entries at a given DN and sub-DNs. For example, filterSubtree at
// "foo.bar" would allow entries at "foo.bar", "foo.bar.baz", but not "foo" or "foo.barr".
func filterSubtree(root DN) filter {
if root == "" {
return filterAll()
}
rootParts := strings.Split(string(root), ".")
return func(origin DN, _ Severity) bool {
parts := strings.Split(string(origin), ".")
if len(parts) < len(rootParts) {
return false
}
for i, p := range rootParts {
if parts[i] != p {
return false
}
}
return true
}
}
// filterSeverity returns a filter that accepts log entries at a given severity level or above. See the Severity type
// for more information about severity levels.
func filterSeverity(atLeast Severity) filter {
return func(origin DN, s Severity) bool {
return s.AtLeast(atLeast)
}
}
// scanEntries does a linear scan through the global entry list and returns all entries that match the given filters. If
// retrieving entries for an exact event, getEntries should be used instead, as it will leverage DN-local linked lists
// to retrieve them faster.
// journal.mu must be taken at R or RW level when calling this function.
func (j *journal) scanEntries(filters ...filter) (res []*entry) {
cur := j.tail
for {
if cur == nil {
return
}
passed := true
for _, filter := range filters {
if !filter(cur.origin, cur.payload.severity) {
passed = false
break
}
}
if passed {
res = append(res, cur)
}
cur = cur.nextGlobal
}
}
// getEntries returns all entries at a given DN. This is faster than a scanEntries(filterExact), as it uses the special
// local linked list pointers to traverse the journal. Additional filters can be passed to further limit the entries
// returned, but a scan through this DN's local linked list will be performed regardless.
// journal.mu must be taken at R or RW level when calling this function.
func (j *journal) getEntries(exact DN, filters ...filter) (res []*entry) {
cur := j.tails[exact]
for {
if cur == nil {
return
}
passed := true
for _, filter := range filters {
if !filter(cur.origin, cur.payload.severity) {
passed = false
break
}
}
if passed {
res = append(res, cur)
}
cur = cur.nextLocal
}
}