blob: 833081ff13a842d6abf6b66a92284c9ba50cbd37 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +01002// SPDX-License-Identifier: Apache-2.0
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +01003
4package logtree
5
6import (
7 "fmt"
8 "strings"
9
Serge Bazanski367ee272023-03-16 17:50:39 +010010 "github.com/mitchellh/go-wordwrap"
11
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020012 "source.monogon.dev/osbase/logbuffer"
13 lpb "source.monogon.dev/osbase/logtree/proto"
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +010014)
15
Serge Bazanski216fe7b2021-05-21 18:36:16 +020016// LogEntry contains a log entry, combining both leveled and raw logging into a
17// single stream of events. A LogEntry will contain exactly one of either
18// LeveledPayload or RawPayload.
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +010019type LogEntry struct {
20 // If non-nil, this is a leveled logging entry.
21 Leveled *LeveledPayload
22 // If non-nil, this is a raw logging entry line.
23 Raw *logbuffer.Line
24 // DN from which this entry was logged.
25 DN DN
26}
27
Serge Bazanski216fe7b2021-05-21 18:36:16 +020028// String returns a canonical representation of this payload as a single string
29// prefixed with metadata. If the entry is a leveled log entry that originally was
30// logged with newlines this representation will also contain newlines, with each
31// original message part prefixed by the metadata. For an alternative call that
32// will instead return a canonical prefix and a list of lines in the message, see
33// Strings().
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +010034func (l *LogEntry) String() string {
35 if l.Leveled != nil {
36 prefix, messages := l.Leveled.Strings()
37 res := make([]string, len(messages))
38 for i, m := range messages {
39 res[i] = fmt.Sprintf("%-32s %s%s", l.DN, prefix, m)
40 }
41 return strings.Join(res, "\n")
42 }
43 if l.Raw != nil {
44 return fmt.Sprintf("%-32s R %s", l.DN, l.Raw)
45 }
46 return "INVALID"
47}
48
Serge Bazanski367ee272023-03-16 17:50:39 +010049// ConciseString returns a concise representation of this log entry for
50// constrained environments, like TTY consoles.
51//
52// The output format is as follows:
53//
54// shortened dn I Hello there
55// some component W Something went wrong
56// shortened dn I Goodbye there
57// external stuff R I am en external process using raw logging.
58//
59// The above output is the result of calling ConciseString on three different
60// LogEntries.
61//
62// If maxWidth is greater than zero, word wrapping will be applied. For example,
63// with maxWidth set to 40:
64//
65// shortened I Hello there
66// some component W Something went wrong and here are the very long details that
67// | describe this particular issue: according to all known laws of
68// | aviation, there is no way a bee should be able to fly.
69// shortened dn I Goodbye there
70// external stuff R I am en external process using raw logging.
71//
72// The above output is also the result of calling ConciseString on three
73// different LogEntries.
74//
75// Multi-line log entries will emit 'continuation' lines (with '|') in the same
76// way as word wrapping does. That means that even with word wrapping disabled,
77// the result of this function might be multiline.
78//
79// The width of the first column (the 'shortened DN' column) is automatically
80// selected based on maxWidth. If maxWidth is less than 60, the column will be
81// omitted. For example, with maxWidth set to 20:
82//
83// I Hello there
84// W Something went wrong and here are the very long details that
85// | describe this particular issue: according to all known laws of
86// | aviation, there is no way a bee should be able to fly.
87// I Goodbye there
88// R I am en external process using raw logging.
89//
90// The given `dict` implements simple replacement rules for shortening the DN
91// parts of a log entry's DN. Some rules are hardcoded for Metropolis' DN tree.
92// If no extra shortening rules should be applied, dict can be set to ni// The
93// given `dict` implements simple replacement rules for shortening the DN parts
94// of a log entry's DN. Some rules are hardcoded for Metropolis' DN tree. If no
95// extra shortening rules should be applied, dict can be set to nil.
96func (l *LogEntry) ConciseString(dict ShortenDictionary, maxWidth int) string {
97 // Decide on a dnWidth.
98 dnWidth := 0
99 switch {
100 case maxWidth >= 80:
101 dnWidth = 20
102 case maxWidth >= 60:
103 dnWidth = 16
104 case maxWidth <= 0:
105 // No word wrapping.
106 dnWidth = 20
107 }
108
109 // Compute shortened DN, if needed.
110 sh := ""
111 if dnWidth > 0 {
112 sh = l.DN.Shorten(dict, dnWidth)
113 sh = fmt.Sprintf("%*s ", dnWidth, sh)
114 }
115
116 // Prefix of the first line emitted.
117 var prefix string
118 switch {
119 case l.Leveled != nil:
120 prefix = sh + string(l.Leveled.Severity()) + " "
121 case l.Raw != nil:
122 prefix = sh + "R "
123 }
124 // Prefix of rest of lines emitted.
125 continuationPrefix := strings.Repeat(" ", len(sh)) + "| "
126
127 // Collect lines based on the type of LogEntry.
128 var lines []string
129 collect := func(message string) {
130 if maxWidth > 0 {
131 message = wordwrap.WrapString(message, uint(maxWidth-len(prefix)))
132 }
133 for _, m2 := range strings.Split(message, "\n") {
134 if len(m2) == 0 {
135 continue
136 }
137 if len(lines) == 0 {
138 lines = append(lines, prefix+m2)
139 } else {
140 lines = append(lines, continuationPrefix+m2)
141 }
142 }
143 }
144 switch {
145 case l.Leveled != nil:
146 _, messages := l.Leveled.Strings()
147 for _, m := range messages {
148 collect(m)
149 }
150 case l.Raw != nil:
151 collect(l.Raw.String())
152 default:
153 return ""
154 }
155
156 return strings.Join(lines, "\n")
157}
158
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200159// Strings returns the canonical representation of this payload split into a
160// prefix and all lines that were contained in the original message. This is
161// meant to be displayed to the user by showing the prefix before each line,
162// concatenated together - possibly in a table form with the prefixes all
163// unified with a rowspan- like mechanism.
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100164//
165// For example, this function can return:
166// prefix = "root.foo.bar I1102 17:20:06.921395 0 foo.go:42] "
167// lines = []string{"current tags:", " - one", " - two"}
168//
169// With this data, the result should be presented to users this way in text form:
170// root.foo.bar I1102 17:20:06.921395 foo.go:42] current tags:
171// root.foo.bar I1102 17:20:06.921395 foo.go:42] - one
172// root.foo.bar I1102 17:20:06.921395 foo.go:42] - two
173//
174// Or, in a table layout:
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200175// .----------------------------------------------------------------------.
176// | root.foo.bar I1102 17:20:06.921395 foo.go:42] : current tags: |
177// | :------------------|
178// | : - one |
179// | :------------------|
180// | : - two |
181// '----------------------------------------------------------------------'
182
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100183func (l *LogEntry) Strings() (prefix string, lines []string) {
184 if l.Leveled != nil {
185 prefix, messages := l.Leveled.Strings()
186 prefix = fmt.Sprintf("%-32s %s", l.DN, prefix)
187 return prefix, messages
188 }
189 if l.Raw != nil {
190 return fmt.Sprintf("%-32s R ", l.DN), []string{l.Raw.Data}
191 }
192 return "INVALID ", []string{"INVALID"}
193}
194
Tim Windelschmidt51daf252024-04-18 23:18:43 +0200195// Proto converts this LogEntry to proto. Returned value may be nil if given
196// LogEntry is invalid, eg. contains neither a Raw nor Leveled entry.
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200197func (l *LogEntry) Proto() *lpb.LogEntry {
198 p := &lpb.LogEntry{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100199 Dn: string(l.DN),
200 }
201 switch {
202 case l.Leveled != nil:
203 leveled := l.Leveled
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200204 p.Kind = &lpb.LogEntry_Leveled_{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100205 Leveled: leveled.Proto(),
206 }
207 case l.Raw != nil:
208 raw := l.Raw
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200209 p.Kind = &lpb.LogEntry_Raw_{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100210 Raw: raw.ProtoLog(),
211 }
212 default:
213 return nil
214 }
215 return p
216}
217
Tim Windelschmidt51daf252024-04-18 23:18:43 +0200218// LogEntryFromProto parses a proto LogEntry back into internal structure.
219// This can be used in log proto API consumers to easily print received log
220// entries.
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200221func LogEntryFromProto(l *lpb.LogEntry) (*LogEntry, error) {
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100222 dn := DN(l.Dn)
223 if _, err := dn.Path(); err != nil {
224 return nil, fmt.Errorf("could not convert DN: %w", err)
225 }
226 res := &LogEntry{
227 DN: dn,
228 }
229 switch inner := l.Kind.(type) {
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200230 case *lpb.LogEntry_Leveled_:
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100231 leveled, err := LeveledPayloadFromProto(inner.Leveled)
232 if err != nil {
233 return nil, fmt.Errorf("could not convert leveled entry: %w", err)
234 }
235 res.Leveled = leveled
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200236 case *lpb.LogEntry_Raw_:
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100237 line, err := logbuffer.LineFromLogProto(inner.Raw)
238 if err != nil {
239 return nil, fmt.Errorf("could not convert raw entry: %w", err)
240 }
241 res.Raw = line
242 default:
243 return nil, fmt.Errorf("proto has neither Leveled nor Raw set")
244 }
245 return res, nil
246}