blob: a434d08ecd6687cb15a9fb12d2d996cc49d8bf4f [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
Tim Windelschmidt01491a72025-07-23 21:49:11 +020026 // Position of this entry in the global journal. This is only available
27 // locally and is not set if the entry was obtained via protobuf.
28 Position int
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +010029}
30
Serge Bazanski216fe7b2021-05-21 18:36:16 +020031// String returns a canonical representation of this payload as a single string
32// prefixed with metadata. If the entry is a leveled log entry that originally was
33// logged with newlines this representation will also contain newlines, with each
34// original message part prefixed by the metadata. For an alternative call that
35// will instead return a canonical prefix and a list of lines in the message, see
36// Strings().
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +010037func (l *LogEntry) String() string {
38 if l.Leveled != nil {
39 prefix, messages := l.Leveled.Strings()
40 res := make([]string, len(messages))
41 for i, m := range messages {
42 res[i] = fmt.Sprintf("%-32s %s%s", l.DN, prefix, m)
43 }
44 return strings.Join(res, "\n")
45 }
46 if l.Raw != nil {
47 return fmt.Sprintf("%-32s R %s", l.DN, l.Raw)
48 }
49 return "INVALID"
50}
51
Serge Bazanski367ee272023-03-16 17:50:39 +010052// ConciseString returns a concise representation of this log entry for
53// constrained environments, like TTY consoles.
54//
55// The output format is as follows:
56//
57// shortened dn I Hello there
58// some component W Something went wrong
59// shortened dn I Goodbye there
60// external stuff R I am en external process using raw logging.
61//
62// The above output is the result of calling ConciseString on three different
63// LogEntries.
64//
65// If maxWidth is greater than zero, word wrapping will be applied. For example,
66// with maxWidth set to 40:
67//
68// shortened I Hello there
69// some component W Something went wrong and here are the very long details that
70// | describe this particular issue: according to all known laws of
71// | aviation, there is no way a bee should be able to fly.
72// shortened dn I Goodbye there
73// external stuff R I am en external process using raw logging.
74//
75// The above output is also the result of calling ConciseString on three
76// different LogEntries.
77//
78// Multi-line log entries will emit 'continuation' lines (with '|') in the same
79// way as word wrapping does. That means that even with word wrapping disabled,
80// the result of this function might be multiline.
81//
82// The width of the first column (the 'shortened DN' column) is automatically
83// selected based on maxWidth. If maxWidth is less than 60, the column will be
84// omitted. For example, with maxWidth set to 20:
85//
86// I Hello there
87// W Something went wrong and here are the very long details that
88// | describe this particular issue: according to all known laws of
89// | aviation, there is no way a bee should be able to fly.
90// I Goodbye there
91// R I am en external process using raw logging.
92//
93// The given `dict` implements simple replacement rules for shortening the DN
94// parts of a log entry's DN. Some rules are hardcoded for Metropolis' DN tree.
95// If no extra shortening rules should be applied, dict can be set to ni// The
96// given `dict` implements simple replacement rules for shortening the DN parts
97// of a log entry's DN. Some rules are hardcoded for Metropolis' DN tree. If no
98// extra shortening rules should be applied, dict can be set to nil.
99func (l *LogEntry) ConciseString(dict ShortenDictionary, maxWidth int) string {
100 // Decide on a dnWidth.
101 dnWidth := 0
102 switch {
103 case maxWidth >= 80:
104 dnWidth = 20
105 case maxWidth >= 60:
106 dnWidth = 16
107 case maxWidth <= 0:
108 // No word wrapping.
109 dnWidth = 20
110 }
111
112 // Compute shortened DN, if needed.
113 sh := ""
114 if dnWidth > 0 {
115 sh = l.DN.Shorten(dict, dnWidth)
116 sh = fmt.Sprintf("%*s ", dnWidth, sh)
117 }
118
119 // Prefix of the first line emitted.
120 var prefix string
121 switch {
122 case l.Leveled != nil:
123 prefix = sh + string(l.Leveled.Severity()) + " "
124 case l.Raw != nil:
125 prefix = sh + "R "
126 }
127 // Prefix of rest of lines emitted.
128 continuationPrefix := strings.Repeat(" ", len(sh)) + "| "
129
130 // Collect lines based on the type of LogEntry.
131 var lines []string
132 collect := func(message string) {
133 if maxWidth > 0 {
134 message = wordwrap.WrapString(message, uint(maxWidth-len(prefix)))
135 }
136 for _, m2 := range strings.Split(message, "\n") {
137 if len(m2) == 0 {
138 continue
139 }
140 if len(lines) == 0 {
141 lines = append(lines, prefix+m2)
142 } else {
143 lines = append(lines, continuationPrefix+m2)
144 }
145 }
146 }
147 switch {
148 case l.Leveled != nil:
149 _, messages := l.Leveled.Strings()
150 for _, m := range messages {
151 collect(m)
152 }
153 case l.Raw != nil:
154 collect(l.Raw.String())
155 default:
156 return ""
157 }
158
159 return strings.Join(lines, "\n")
160}
161
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200162// Strings returns the canonical representation of this payload split into a
163// prefix and all lines that were contained in the original message. This is
164// meant to be displayed to the user by showing the prefix before each line,
165// concatenated together - possibly in a table form with the prefixes all
166// unified with a rowspan- like mechanism.
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100167//
168// For example, this function can return:
169// prefix = "root.foo.bar I1102 17:20:06.921395 0 foo.go:42] "
170// lines = []string{"current tags:", " - one", " - two"}
171//
172// With this data, the result should be presented to users this way in text form:
173// root.foo.bar I1102 17:20:06.921395 foo.go:42] current tags:
174// root.foo.bar I1102 17:20:06.921395 foo.go:42] - one
175// root.foo.bar I1102 17:20:06.921395 foo.go:42] - two
176//
177// Or, in a table layout:
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200178// .----------------------------------------------------------------------.
179// | root.foo.bar I1102 17:20:06.921395 foo.go:42] : current tags: |
180// | :------------------|
181// | : - one |
182// | :------------------|
183// | : - two |
184// '----------------------------------------------------------------------'
185
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100186func (l *LogEntry) Strings() (prefix string, lines []string) {
187 if l.Leveled != nil {
188 prefix, messages := l.Leveled.Strings()
189 prefix = fmt.Sprintf("%-32s %s", l.DN, prefix)
190 return prefix, messages
191 }
192 if l.Raw != nil {
193 return fmt.Sprintf("%-32s R ", l.DN), []string{l.Raw.Data}
194 }
195 return "INVALID ", []string{"INVALID"}
196}
197
Tim Windelschmidt51daf252024-04-18 23:18:43 +0200198// Proto converts this LogEntry to proto. Returned value may be nil if given
199// LogEntry is invalid, eg. contains neither a Raw nor Leveled entry.
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200200func (l *LogEntry) Proto() *lpb.LogEntry {
201 p := &lpb.LogEntry{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100202 Dn: string(l.DN),
203 }
204 switch {
205 case l.Leveled != nil:
206 leveled := l.Leveled
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200207 p.Kind = &lpb.LogEntry_Leveled_{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100208 Leveled: leveled.Proto(),
209 }
210 case l.Raw != nil:
211 raw := l.Raw
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200212 p.Kind = &lpb.LogEntry_Raw_{
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100213 Raw: raw.ProtoLog(),
214 }
215 default:
216 return nil
217 }
218 return p
219}
220
Tim Windelschmidt51daf252024-04-18 23:18:43 +0200221// LogEntryFromProto parses a proto LogEntry back into internal structure.
222// This can be used in log proto API consumers to easily print received log
223// entries.
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200224func LogEntryFromProto(l *lpb.LogEntry) (*LogEntry, error) {
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100225 dn := DN(l.Dn)
226 if _, err := dn.Path(); err != nil {
227 return nil, fmt.Errorf("could not convert DN: %w", err)
228 }
229 res := &LogEntry{
230 DN: dn,
231 }
232 switch inner := l.Kind.(type) {
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200233 case *lpb.LogEntry_Leveled_:
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100234 leveled, err := LeveledPayloadFromProto(inner.Leveled)
235 if err != nil {
236 return nil, fmt.Errorf("could not convert leveled entry: %w", err)
237 }
238 res.Leveled = leveled
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200239 case *lpb.LogEntry_Raw_:
Serge Bazanskiedf5c4f2020-11-25 13:45:31 +0100240 line, err := logbuffer.LineFromLogProto(inner.Raw)
241 if err != nil {
242 return nil, fmt.Errorf("could not convert raw entry: %w", err)
243 }
244 res.Raw = line
245 default:
246 return nil, fmt.Errorf("proto has neither Leveled nor Raw set")
247 }
248 return res, nil
249}