blob: d29fdaa0770a2c1a08a4de336092f5d6b2bd3227 [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 (
"errors"
"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
var (
ErrInvalidDN = errors.New("invalid DN")
)
// 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, ErrInvalidDN
}
}
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 given log entry.
type filter func(*entry) bool
// filterAll returns a filter that accepts all log entries.
func filterAll() filter {
return func(*entry) 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(e *entry) bool {
return e.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(e *entry) bool {
parts := strings.Split(string(e.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(e *entry) bool {
return e.leveled != nil && e.leveled.severity.AtLeast(atLeast)
}
}
func filterOnlyRaw(e *entry) bool {
return e.raw != nil
}
func filterOnlyLeveled(e *entry) bool {
return e.leveled != nil
}
// 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) {
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) {
passed = false
break
}
}
if passed {
res = append(res, cur)
}
cur = cur.nextLocal
}
}