| // 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 | 
 | 	} | 
 |  | 
 | } | 
 |  | 
 | // Shorten returns a shortened version of this DN for constrained logging | 
 | // environments like tty0 logging. | 
 | // | 
 | // If ShortenDictionary is given, it will be used to replace DN parts with | 
 | // shorter equivalents. For example, with the dictionary: | 
 | // | 
 | // { "foobar": "foo", "manager": "mgr" } | 
 | // | 
 | // The DN some.foobar.logger will be turned into some.foo.logger before further | 
 | // being processed by the shortening mechanism. | 
 | // | 
 | // The shortening rules applied are Metropolis-specific. | 
 | func (d DN) Shorten(dict ShortenDictionary, maxLen int) string { | 
 | 	path, _ := d.Path() | 
 | 	// Apply DN part shortening rules. | 
 | 	if dict != nil { | 
 | 		for i, p := range path { | 
 | 			if sh, ok := dict[p]; ok { | 
 | 				path[i] = sh | 
 | 			} | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// This generally shouldn't happen. | 
 | 	if len(path) == 0 { | 
 | 		return "?" | 
 | 	} | 
 |  | 
 | 	// Strip 'root.' prefix. | 
 | 	if len(path) > 1 && path[0] == "root" { | 
 | 		path = path[1:] | 
 | 	} | 
 |  | 
 | 	// Replace role.xxx.yyy.zzz with xxx.zzz - stripping everything between the role | 
 | 	// name and the last element of the path. | 
 | 	if path[0] == "role" && len(path) > 1 { | 
 | 		if len(path) == 2 { | 
 | 			path = path[1:] | 
 | 		} else { | 
 | 			path = []string{ | 
 | 				path[1], | 
 | 				path[len(path)-1], | 
 | 			} | 
 | 		} | 
 | 	} | 
 |  | 
 | 	// Join back to be ' '-delimited, and ellipsize if too long. | 
 | 	s := strings.Join(path, " ") | 
 | 	if overflow := len(s) - maxLen; overflow > 0 { | 
 | 		s = "..." + s[overflow+3:] | 
 | 	} | 
 | 	return s | 
 | } | 
 |  | 
 | type ShortenDictionary map[string]string | 
 |  | 
 | var MetropolisShortenDict = ShortenDictionary{ | 
 | 	"controlplane":           "cplane", | 
 | 	"map-cluster-membership": "map-membership", | 
 | 	"cluster-membership":     "cluster", | 
 | 	"controller-manager":     "controllers", | 
 | 	"networking":             "net", | 
 | 	"network":                "net", | 
 | 	"interfaces":             "ifaces", | 
 | 	"kubernetes":             "k8s", | 
 | } |