blob: 524c81b78950356f13721111a15f75fc44d1689f [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
Serge Bazanski5faa2fc2020-09-07 14:09:30 +02002// SPDX-License-Identifier: Apache-2.0
Serge Bazanski5faa2fc2020-09-07 14:09:30 +02003
4package logtree
5
6import (
7 "fmt"
Serge Bazanskib0272182020-11-02 18:39:44 +01008 "strconv"
9 "strings"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020010 "time"
Serge Bazanskib0272182020-11-02 18:39:44 +010011
Mateusz Zalegacf92f402022-07-08 15:08:48 +020012 tpb "google.golang.org/protobuf/types/known/timestamppb"
13
Serge Bazanski3c5d0632024-09-12 10:49:12 +000014 "source.monogon.dev/go/logging"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020015 lpb "source.monogon.dev/osbase/logtree/proto"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020016)
17
Serge Bazanski216fe7b2021-05-21 18:36:16 +020018// LeveledPayload is a log entry for leveled logs (as per leveled.go). It contains
19// the input to these calls (severity and message split into newline-delimited
20// messages) and additional metadata that would be usually seen in a text
Serge Bazanski12971d62020-11-17 12:12:58 +010021// representation of a leveled log entry.
Serge Bazanski1bfa0c22020-10-14 16:45:07 +020022type LeveledPayload struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020023 // messages is the list of messages contained in this payload. This list is built
24 // from splitting up the given message from the user by newline.
Serge Bazanski12971d62020-11-17 12:12:58 +010025 messages []string
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020026 // timestamp is the time at which this message was emitted.
27 timestamp time.Time
28 // severity is the leveled Severity at which this message was emitted.
Serge Bazanski3c5d0632024-09-12 10:49:12 +000029 severity logging.Severity
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020030 // file is the filename of the caller that emitted this message.
31 file string
32 // line is the line number within the file of the caller that emitted this message.
33 line int
34}
35
Serge Bazanski216fe7b2021-05-21 18:36:16 +020036// String returns a canonical representation of this payload as a single string
37// prefixed with metadata. If the original message was logged with newlines, this
38// representation will also contain newlines, with each original message part
39// prefixed by the metadata. For an alternative call that will instead return a
40// canonical prefix and a list of lines in the message, see Strings().
Serge Bazanski1bfa0c22020-10-14 16:45:07 +020041func (p *LeveledPayload) String() string {
Serge Bazanski12971d62020-11-17 12:12:58 +010042 prefix, lines := p.Strings()
43 res := make([]string, len(p.messages))
44 for i, line := range lines {
45 res[i] = fmt.Sprintf("%s%s", prefix, line)
46 }
47 return strings.Join(res, "\n")
48}
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020049
Serge Bazanski216fe7b2021-05-21 18:36:16 +020050// Strings returns the canonical representation of this payload split into a
51// prefix and all lines that were contained in the original message. This is
52// meant to be displayed to the user by showing the prefix before each line,
53// concatenated together - possibly in a table form with the prefixes all
54// unified with a rowspan- like mechanism.
Serge Bazanski12971d62020-11-17 12:12:58 +010055//
56// For example, this function can return:
Serge Bazanskida114862023-03-29 17:46:42 +020057//
58// prefix = "I1102 17:20:06.921395 foo.go:42] "
59// lines = []string{"current tags:", " - one", " - two"}
Serge Bazanski12971d62020-11-17 12:12:58 +010060//
61// With this data, the result should be presented to users this way in text form:
62// I1102 17:20:06.921395 foo.go:42] current tags:
63// I1102 17:20:06.921395 foo.go:42] - one
64// I1102 17:20:06.921395 foo.go:42] - two
65//
66// Or, in a table layout:
67// .-----------------------------------------------------------.
68// | I1102 17:20:06.921395 0 foo.go:42] : current tags: |
69// | :------------------|
70// | : - one |
71// | :------------------|
72// | : - two |
73// '-----------------------------------------------------------'
Serge Bazanski12971d62020-11-17 12:12:58 +010074func (p *LeveledPayload) Strings() (prefix string, lines []string) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020075 _, month, day := p.timestamp.Date()
76 hour, minute, second := p.timestamp.Clock()
77 nsec := p.timestamp.Nanosecond() / 1000
78
Serge Bazanski12971d62020-11-17 12:12:58 +010079 // Same format as in glog, but without treadid.
80 // Lmmdd hh:mm:ss.uuuuuu file:line]
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020081 // TODO(q3k): rewrite this to printf-less code.
Serge Bazanski12971d62020-11-17 12:12:58 +010082 prefix = fmt.Sprintf("%s%02d%02d %02d:%02d:%02d.%06d %s:%d] ", p.severity, month, day, hour, minute, second, nsec, p.file, p.line)
83
84 lines = p.messages
85 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020086}
87
Tim Windelschmidt51daf252024-04-18 23:18:43 +020088// Messages returns the inner message lines of this entry, ie. what was passed
89// to the actual logging method, but split by newlines.
Serge Bazanski12971d62020-11-17 12:12:58 +010090func (p *LeveledPayload) Messages() []string { return p.messages }
91
92func (p *LeveledPayload) MessagesJoined() string { return strings.Join(p.messages, "\n") }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020093
94// Timestamp returns the time at which this entry was logged.
Serge Bazanski1bfa0c22020-10-14 16:45:07 +020095func (p *LeveledPayload) Timestamp() time.Time { return p.timestamp }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020096
Serge Bazanski216fe7b2021-05-21 18:36:16 +020097// Location returns a string in the form of file_name:line_number that shows the
98// origin of the log entry in the program source.
Serge Bazanski1bfa0c22020-10-14 16:45:07 +020099func (p *LeveledPayload) Location() string { return fmt.Sprintf("%s:%d", p.file, p.line) }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200100
101// Severity returns the Severity with which this entry was logged.
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000102func (p *LeveledPayload) Severity() logging.Severity { return p.severity }
Serge Bazanskib0272182020-11-02 18:39:44 +0100103
104// Proto converts a LeveledPayload to protobuf format.
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200105func (p *LeveledPayload) Proto() *lpb.LogEntry_Leveled {
106 return &lpb.LogEntry_Leveled{
Serge Bazanski12971d62020-11-17 12:12:58 +0100107 Lines: p.Messages(),
Mateusz Zalegacf92f402022-07-08 15:08:48 +0200108 Timestamp: tpb.New(p.Timestamp()),
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000109 Severity: SeverityToProto(p.Severity()),
Serge Bazanskib0272182020-11-02 18:39:44 +0100110 Location: p.Location(),
111 }
112}
113
114// LeveledPayloadFromProto parses a protobuf message into the internal format.
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200115func LeveledPayloadFromProto(p *lpb.LogEntry_Leveled) (*LeveledPayload, error) {
Serge Bazanskib0272182020-11-02 18:39:44 +0100116 severity, err := SeverityFromProto(p.Severity)
117 if err != nil {
118 return nil, fmt.Errorf("could not convert severity: %w", err)
119 }
120 parts := strings.Split(p.Location, ":")
121 if len(parts) != 2 {
122 return nil, fmt.Errorf("invalid location, must be two :-delimited parts, is %d parts", len(parts))
123 }
124 file := parts[0]
125 line, err := strconv.Atoi(parts[1])
126 if err != nil {
127 return nil, fmt.Errorf("invalid location line number: %w", err)
128 }
129 return &LeveledPayload{
Serge Bazanski12971d62020-11-17 12:12:58 +0100130 messages: p.Lines,
Mateusz Zalegacf92f402022-07-08 15:08:48 +0200131 timestamp: p.Timestamp.AsTime(),
Serge Bazanskib0272182020-11-02 18:39:44 +0100132 severity: severity,
133 file: file,
134 line: line,
135 }, nil
136}
Serge Bazanski020b7c52021-07-07 14:22:28 +0200137
138// ExternalLeveledPayload is a LeveledPayload received from an external source,
139// eg. from parsing the logging output of third-party programs. It can be
140// converted into a LeveledPayload and inserted into a leveled logger, but will
141// be sanitized before that, ensuring that potentially buggy
142// emitters/converters do not end up polluting the leveled logger data.
143//
144// This type should be used only when inserting data from external systems, not
145// by code that just wishes to log things. In the future, data inserted this
146// way might be explicitly marked as tainted so operators can understand that
147// parts of this data might not give the same guarantees as the log entries
148// emitted by the native LeveledLogger API.
149type ExternalLeveledPayload struct {
150 // Log line. If any newlines are found, they will split the message into
151 // multiple messages within LeveledPayload. Empty messages are accepted
152 // verbatim.
153 Message string
154 // Timestamp when this payload was emitted according to its source. If not
155 // given, will default to the time of conversion to LeveledPayload.
156 Timestamp time.Time
157 // Log severity. If invalid or unset will default to INFO.
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000158 Severity logging.Severity
Serge Bazanski020b7c52021-07-07 14:22:28 +0200159 // File name of originating code. Defaults to "unknown" if not set.
160 File string
161 // Line in File. Zero indicates the line is not known.
162 Line int
163}
164
165// sanitize the given ExternalLeveledPayload by creating a corresponding
166// LeveledPayload. The original object is unaltered.
167func (e *ExternalLeveledPayload) sanitize() *LeveledPayload {
168 l := &LeveledPayload{
169 messages: strings.Split(e.Message, "\n"),
170 timestamp: e.Timestamp,
171 severity: e.Severity,
172 file: e.File,
173 line: e.Line,
174 }
175 if l.timestamp.IsZero() {
176 l.timestamp = time.Now()
177 }
178 if !l.severity.Valid() {
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000179 l.severity = logging.INFO
Serge Bazanski020b7c52021-07-07 14:22:28 +0200180 }
181 if l.file == "" {
182 l.file = "unknown"
183 }
184 if l.line < 0 {
185 l.line = 0
186 }
187 return l
188}