blob: 968a5a9ff229d5768ece921cd762f7484b707c61 [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"
"source.monogon.dev/metropolis/pkg/logbuffer"
)
// LogTree is a tree-shaped logging system. For more information, see the package-
// level documentation.
type LogTree struct {
// journal is the tree's journal, storing all log data and managing subscribers.
journal *journal
// root is the root node of the actual tree of the log tree. The nodes contain per-
// DN configuration options, notably the current verbosity level of that DN.
root *node
}
func New() *LogTree {
lt := &LogTree{
journal: newJournal(),
}
lt.root = newNode(lt, "")
return lt
}
// node represents a given DN as a discrete 'logger'. It implements the
// LeveledLogger interface for log publishing, entries from which it passes over to
// the logtree's journal.
type node struct {
// dn is the DN which this node represents (or "" if this is the root node).
dn DN
// tree is the LogTree to which this node belongs.
tree *LogTree
// verbosity is the current verbosity level of this DN/node, affecting .V(n)
// LeveledLogger calls
verbosity VerbosityLevel
rawLineBuffer *logbuffer.LineBuffer
// mu guards children.
mu sync.Mutex
// children is a map of DN-part to a children node in the logtree. A DN-part is a
// string representing a part of the DN between the deliming dots, as returned by
// DN.Path.
children map[string]*node
}
// newNode returns a node at a given DN in the LogTree - but doesn't set up the
// LogTree to insert it accordingly.
func newNode(tree *LogTree, dn DN) *node {
n := &node{
dn: dn,
tree: tree,
children: make(map[string]*node),
}
// TODO(q3k): make this limit configurable. If this happens, or the default (1024) gets changes, max chunk size
// calculations when serving the logs (eg. in NodeDebugService) must reflect this.
n.rawLineBuffer = logbuffer.NewLineBuffer(1024, n.logRaw)
return n
}
// nodeByDN returns the LogTree node corresponding to a given DN. If either the
// node or some of its parents do not exist they will be created as needed.
func (l *LogTree) nodeByDN(dn DN) (*node, error) {
traversal, err := newTraversal(dn)
if err != nil {
return nil, fmt.Errorf("traversal failed: %w", err)
}
return traversal.execute(l.root), nil
}
// nodeTraversal represents a request to traverse the LogTree in search of a given
// node by DN.
type nodeTraversal struct {
// want is the DN of the node's that requested to be found.
want DN
// current is the path already taken to find the node, in the form of DN parts. It
// starts out as want.Parts() and progresses to become empty as the traversal
// continues.
current []string
// left is the path that's still needed to be taken in order to find the node, in
// the form of DN parts. It starts out empty and progresses to become wants.Parts()
// as the traversal continues.
left []string
}
// next adjusts the traversal's current/left slices to the next element of the
// traversal, returns the part that's now being looked for (or "" if the traveral
// is done) and the full DN of the element that's being looked for.
//
// For example, a traversal of foo.bar.baz will cause .next() to return the
// following on each invocation:
// - part: foo, full: foo
// - part: bar, full: foo.bar
// - part: baz, full: foo.bar.baz
// - part: "", full: foo.bar.baz
func (t *nodeTraversal) next() (part string, full DN) {
if len(t.left) == 0 {
return "", t.want
}
part = t.left[0]
t.current = append(t.current, part)
t.left = t.left[1:]
full = DN(strings.Join(t.current, "."))
return
}
// newTraversal returns a nodeTraversal fora a given wanted DN.
func newTraversal(dn DN) (*nodeTraversal, error) {
parts, err := dn.Path()
if err != nil {
return nil, err
}
return &nodeTraversal{
want: dn,
left: parts,
}, nil
}
// execute the traversal in order to find the node. This can only be called once
// per traversal. Nodes will be created within the tree until the target node is
// reached. Existing nodes will be reused. This is effectively an idempotent way of
// accessing a node in the tree based on a traversal.
func (t *nodeTraversal) execute(n *node) *node {
cur := n
for {
part, full := t.next()
if part == "" {
return cur
}
mu := &cur.mu
mu.Lock()
if _, ok := cur.children[part]; !ok {
cur.children[part] = newNode(n.tree, DN(full))
}
cur = cur.children[part]
mu.Unlock()
}
}