|  | // 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() | 
|  | } | 
|  | } |