blob: 72cc409bfa654e72ff1a06db74566624201388f2 [file] [log] [blame]
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +01001// Copyright 2020 The Monogon Project Authors.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package logtree
18
19import (
20 "fmt"
21 "strings"
22
Serge Bazanski367ee272023-03-16 17:50:39 +010023 "github.com/mitchellh/go-wordwrap"
24
Serge Bazanski31370b02021-01-07 16:31:14 +010025 "source.monogon.dev/metropolis/pkg/logbuffer"
Serge Bazanskida114862023-03-29 17:46:42 +020026 cpb "source.monogon.dev/metropolis/proto/common"
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +010027)
28
Serge Bazanski216fe7b2021-05-21 18:36:16 +020029// LogEntry contains a log entry, combining both leveled and raw logging into a
30// single stream of events. A LogEntry will contain exactly one of either
31// LeveledPayload or RawPayload.
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +010032type LogEntry struct {
33 // If non-nil, this is a leveled logging entry.
34 Leveled *LeveledPayload
35 // If non-nil, this is a raw logging entry line.
36 Raw *logbuffer.Line
37 // DN from which this entry was logged.
38 DN DN
39}
40
Serge Bazanski216fe7b2021-05-21 18:36:16 +020041// String returns a canonical representation of this payload as a single string
42// prefixed with metadata. If the entry is a leveled log entry that originally was
43// logged with newlines this representation will also contain newlines, with each
44// original message part prefixed by the metadata. For an alternative call that
45// will instead return a canonical prefix and a list of lines in the message, see
46// Strings().
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +010047func (l *LogEntry) String() string {
48 if l.Leveled != nil {
49 prefix, messages := l.Leveled.Strings()
50 res := make([]string, len(messages))
51 for i, m := range messages {
52 res[i] = fmt.Sprintf("%-32s %s%s", l.DN, prefix, m)
53 }
54 return strings.Join(res, "\n")
55 }
56 if l.Raw != nil {
57 return fmt.Sprintf("%-32s R %s", l.DN, l.Raw)
58 }
59 return "INVALID"
60}
61
Serge Bazanski367ee272023-03-16 17:50:39 +010062// ConciseString returns a concise representation of this log entry for
63// constrained environments, like TTY consoles.
64//
65// The output format is as follows:
66//
67// shortened dn I Hello there
68// some component W Something went wrong
69// shortened dn I Goodbye there
70// external stuff R I am en external process using raw logging.
71//
72// The above output is the result of calling ConciseString on three different
73// LogEntries.
74//
75// If maxWidth is greater than zero, word wrapping will be applied. For example,
76// with maxWidth set to 40:
77//
78// shortened I Hello there
79// some component W Something went wrong and here are the very long details that
80// | describe this particular issue: according to all known laws of
81// | aviation, there is no way a bee should be able to fly.
82// shortened dn I Goodbye there
83// external stuff R I am en external process using raw logging.
84//
85// The above output is also the result of calling ConciseString on three
86// different LogEntries.
87//
88// Multi-line log entries will emit 'continuation' lines (with '|') in the same
89// way as word wrapping does. That means that even with word wrapping disabled,
90// the result of this function might be multiline.
91//
92// The width of the first column (the 'shortened DN' column) is automatically
93// selected based on maxWidth. If maxWidth is less than 60, the column will be
94// omitted. For example, with maxWidth set to 20:
95//
96// I Hello there
97// W Something went wrong and here are the very long details that
98// | describe this particular issue: according to all known laws of
99// | aviation, there is no way a bee should be able to fly.
100// I Goodbye there
101// R I am en external process using raw logging.
102//
103// The given `dict` implements simple replacement rules for shortening the DN
104// parts of a log entry's DN. Some rules are hardcoded for Metropolis' DN tree.
105// If no extra shortening rules should be applied, dict can be set to ni// The
106// given `dict` implements simple replacement rules for shortening the DN parts
107// of a log entry's DN. Some rules are hardcoded for Metropolis' DN tree. If no
108// extra shortening rules should be applied, dict can be set to nil.
109func (l *LogEntry) ConciseString(dict ShortenDictionary, maxWidth int) string {
110 // Decide on a dnWidth.
111 dnWidth := 0
112 switch {
113 case maxWidth >= 80:
114 dnWidth = 20
115 case maxWidth >= 60:
116 dnWidth = 16
117 case maxWidth <= 0:
118 // No word wrapping.
119 dnWidth = 20
120 }
121
122 // Compute shortened DN, if needed.
123 sh := ""
124 if dnWidth > 0 {
125 sh = l.DN.Shorten(dict, dnWidth)
126 sh = fmt.Sprintf("%*s ", dnWidth, sh)
127 }
128
129 // Prefix of the first line emitted.
130 var prefix string
131 switch {
132 case l.Leveled != nil:
133 prefix = sh + string(l.Leveled.Severity()) + " "
134 case l.Raw != nil:
135 prefix = sh + "R "
136 }
137 // Prefix of rest of lines emitted.
138 continuationPrefix := strings.Repeat(" ", len(sh)) + "| "
139
140 // Collect lines based on the type of LogEntry.
141 var lines []string
142 collect := func(message string) {
143 if maxWidth > 0 {
144 message = wordwrap.WrapString(message, uint(maxWidth-len(prefix)))
145 }
146 for _, m2 := range strings.Split(message, "\n") {
147 if len(m2) == 0 {
148 continue
149 }
150 if len(lines) == 0 {
151 lines = append(lines, prefix+m2)
152 } else {
153 lines = append(lines, continuationPrefix+m2)
154 }
155 }
156 }
157 switch {
158 case l.Leveled != nil:
159 _, messages := l.Leveled.Strings()
160 for _, m := range messages {
161 collect(m)
162 }
163 case l.Raw != nil:
164 collect(l.Raw.String())
165 default:
166 return ""
167 }
168
169 return strings.Join(lines, "\n")
170}
171
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200172// Strings returns the canonical representation of this payload split into a
173// prefix and all lines that were contained in the original message. This is
174// meant to be displayed to the user by showing the prefix before each line,
175// concatenated together - possibly in a table form with the prefixes all
176// unified with a rowspan- like mechanism.
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100177//
178// For example, this function can return:
179// prefix = "root.foo.bar I1102 17:20:06.921395 0 foo.go:42] "
180// lines = []string{"current tags:", " - one", " - two"}
181//
182// With this data, the result should be presented to users this way in text form:
183// root.foo.bar I1102 17:20:06.921395 foo.go:42] current tags:
184// root.foo.bar I1102 17:20:06.921395 foo.go:42] - one
185// root.foo.bar I1102 17:20:06.921395 foo.go:42] - two
186//
187// Or, in a table layout:
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200188// .----------------------------------------------------------------------.
189// | root.foo.bar I1102 17:20:06.921395 foo.go:42] : current tags: |
190// | :------------------|
191// | : - one |
192// | :------------------|
193// | : - two |
194// '----------------------------------------------------------------------'
195
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100196func (l *LogEntry) Strings() (prefix string, lines []string) {
197 if l.Leveled != nil {
198 prefix, messages := l.Leveled.Strings()
199 prefix = fmt.Sprintf("%-32s %s", l.DN, prefix)
200 return prefix, messages
201 }
202 if l.Raw != nil {
203 return fmt.Sprintf("%-32s R ", l.DN), []string{l.Raw.Data}
204 }
205 return "INVALID ", []string{"INVALID"}
206}
207
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200208// Convert this LogEntry to proto. Returned value may be nil if given LogEntry is
209// invalid, eg. contains neither a Raw nor Leveled entry.
Serge Bazanskida114862023-03-29 17:46:42 +0200210func (l *LogEntry) Proto() *cpb.LogEntry {
211 p := &cpb.LogEntry{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100212 Dn: string(l.DN),
213 }
214 switch {
215 case l.Leveled != nil:
216 leveled := l.Leveled
Serge Bazanskida114862023-03-29 17:46:42 +0200217 p.Kind = &cpb.LogEntry_Leveled_{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100218 Leveled: leveled.Proto(),
219 }
220 case l.Raw != nil:
221 raw := l.Raw
Serge Bazanskida114862023-03-29 17:46:42 +0200222 p.Kind = &cpb.LogEntry_Raw_{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100223 Raw: raw.ProtoLog(),
224 }
225 default:
226 return nil
227 }
228 return p
229}
230
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200231// Parse a proto LogEntry back into internal structure. This can be used in log
232// proto API consumers to easily print received log entries.
Serge Bazanskida114862023-03-29 17:46:42 +0200233func LogEntryFromProto(l *cpb.LogEntry) (*LogEntry, error) {
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100234 dn := DN(l.Dn)
235 if _, err := dn.Path(); err != nil {
236 return nil, fmt.Errorf("could not convert DN: %w", err)
237 }
238 res := &LogEntry{
239 DN: dn,
240 }
241 switch inner := l.Kind.(type) {
Serge Bazanskida114862023-03-29 17:46:42 +0200242 case *cpb.LogEntry_Leveled_:
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100243 leveled, err := LeveledPayloadFromProto(inner.Leveled)
244 if err != nil {
245 return nil, fmt.Errorf("could not convert leveled entry: %w", err)
246 }
247 res.Leveled = leveled
Serge Bazanskida114862023-03-29 17:46:42 +0200248 case *cpb.LogEntry_Raw_:
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100249 line, err := logbuffer.LineFromLogProto(inner.Raw)
250 if err != nil {
251 return nil, fmt.Errorf("could not convert raw entry: %w", err)
252 }
253 res.Raw = line
254 default:
255 return nil, fmt.Errorf("proto has neither Leveled nor Raw set")
256 }
257 return res, nil
258}