blob: b03884860192c624d5302e7eeceec81b634cc72f [file] [log] [blame]
Serge Bazanski5faa2fc2020-09-07 14:09:30 +02001// 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"
Serge Bazanskib0272182020-11-02 18:39:44 +010021 "strconv"
22 "strings"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020023 "time"
Serge Bazanskib0272182020-11-02 18:39:44 +010024
Mateusz Zalegacf92f402022-07-08 15:08:48 +020025 tpb "google.golang.org/protobuf/types/known/timestamppb"
26
Serge Bazanski3c5d0632024-09-12 10:49:12 +000027 "source.monogon.dev/go/logging"
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020028 lpb "source.monogon.dev/osbase/logtree/proto"
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020029)
30
Serge Bazanski216fe7b2021-05-21 18:36:16 +020031// LeveledPayload is a log entry for leveled logs (as per leveled.go). It contains
32// the input to these calls (severity and message split into newline-delimited
33// messages) and additional metadata that would be usually seen in a text
Serge Bazanski12971d62020-11-17 12:12:58 +010034// representation of a leveled log entry.
Serge Bazanski1bfa0c22020-10-14 16:45:07 +020035type LeveledPayload struct {
Serge Bazanski216fe7b2021-05-21 18:36:16 +020036 // messages is the list of messages contained in this payload. This list is built
37 // from splitting up the given message from the user by newline.
Serge Bazanski12971d62020-11-17 12:12:58 +010038 messages []string
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020039 // timestamp is the time at which this message was emitted.
40 timestamp time.Time
41 // severity is the leveled Severity at which this message was emitted.
Serge Bazanski3c5d0632024-09-12 10:49:12 +000042 severity logging.Severity
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020043 // file is the filename of the caller that emitted this message.
44 file string
45 // line is the line number within the file of the caller that emitted this message.
46 line int
47}
48
Serge Bazanski216fe7b2021-05-21 18:36:16 +020049// String returns a canonical representation of this payload as a single string
50// prefixed with metadata. If the original message was logged with newlines, this
51// representation will also contain newlines, with each original message part
52// prefixed by the metadata. For an alternative call that will instead return a
53// canonical prefix and a list of lines in the message, see Strings().
Serge Bazanski1bfa0c22020-10-14 16:45:07 +020054func (p *LeveledPayload) String() string {
Serge Bazanski12971d62020-11-17 12:12:58 +010055 prefix, lines := p.Strings()
56 res := make([]string, len(p.messages))
57 for i, line := range lines {
58 res[i] = fmt.Sprintf("%s%s", prefix, line)
59 }
60 return strings.Join(res, "\n")
61}
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020062
Serge Bazanski216fe7b2021-05-21 18:36:16 +020063// Strings returns the canonical representation of this payload split into a
64// prefix and all lines that were contained in the original message. This is
65// meant to be displayed to the user by showing the prefix before each line,
66// concatenated together - possibly in a table form with the prefixes all
67// unified with a rowspan- like mechanism.
Serge Bazanski12971d62020-11-17 12:12:58 +010068//
69// For example, this function can return:
Serge Bazanskida114862023-03-29 17:46:42 +020070//
71// prefix = "I1102 17:20:06.921395 foo.go:42] "
72// lines = []string{"current tags:", " - one", " - two"}
Serge Bazanski12971d62020-11-17 12:12:58 +010073//
74// With this data, the result should be presented to users this way in text form:
75// I1102 17:20:06.921395 foo.go:42] current tags:
76// I1102 17:20:06.921395 foo.go:42] - one
77// I1102 17:20:06.921395 foo.go:42] - two
78//
79// Or, in a table layout:
80// .-----------------------------------------------------------.
81// | I1102 17:20:06.921395 0 foo.go:42] : current tags: |
82// | :------------------|
83// | : - one |
84// | :------------------|
85// | : - two |
86// '-----------------------------------------------------------'
Serge Bazanski12971d62020-11-17 12:12:58 +010087func (p *LeveledPayload) Strings() (prefix string, lines []string) {
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020088 _, month, day := p.timestamp.Date()
89 hour, minute, second := p.timestamp.Clock()
90 nsec := p.timestamp.Nanosecond() / 1000
91
Serge Bazanski12971d62020-11-17 12:12:58 +010092 // Same format as in glog, but without treadid.
93 // Lmmdd hh:mm:ss.uuuuuu file:line]
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020094 // TODO(q3k): rewrite this to printf-less code.
Serge Bazanski12971d62020-11-17 12:12:58 +010095 prefix = fmt.Sprintf("%s%02d%02d %02d:%02d:%02d.%06d %s:%d] ", p.severity, month, day, hour, minute, second, nsec, p.file, p.line)
96
97 lines = p.messages
98 return
Serge Bazanski5faa2fc2020-09-07 14:09:30 +020099}
100
Tim Windelschmidt51daf252024-04-18 23:18:43 +0200101// Messages returns the inner message lines of this entry, ie. what was passed
102// to the actual logging method, but split by newlines.
Serge Bazanski12971d62020-11-17 12:12:58 +0100103func (p *LeveledPayload) Messages() []string { return p.messages }
104
105func (p *LeveledPayload) MessagesJoined() string { return strings.Join(p.messages, "\n") }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200106
107// Timestamp returns the time at which this entry was logged.
Serge Bazanski1bfa0c22020-10-14 16:45:07 +0200108func (p *LeveledPayload) Timestamp() time.Time { return p.timestamp }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200109
Serge Bazanski216fe7b2021-05-21 18:36:16 +0200110// Location returns a string in the form of file_name:line_number that shows the
111// origin of the log entry in the program source.
Serge Bazanski1bfa0c22020-10-14 16:45:07 +0200112func (p *LeveledPayload) Location() string { return fmt.Sprintf("%s:%d", p.file, p.line) }
Serge Bazanski5faa2fc2020-09-07 14:09:30 +0200113
114// Severity returns the Severity with which this entry was logged.
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000115func (p *LeveledPayload) Severity() logging.Severity { return p.severity }
Serge Bazanskib0272182020-11-02 18:39:44 +0100116
117// Proto converts a LeveledPayload to protobuf format.
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200118func (p *LeveledPayload) Proto() *lpb.LogEntry_Leveled {
119 return &lpb.LogEntry_Leveled{
Serge Bazanski12971d62020-11-17 12:12:58 +0100120 Lines: p.Messages(),
Mateusz Zalegacf92f402022-07-08 15:08:48 +0200121 Timestamp: tpb.New(p.Timestamp()),
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000122 Severity: SeverityToProto(p.Severity()),
Serge Bazanskib0272182020-11-02 18:39:44 +0100123 Location: p.Location(),
124 }
125}
126
127// LeveledPayloadFromProto parses a protobuf message into the internal format.
Tim Windelschmidt8814f522024-05-08 00:41:13 +0200128func LeveledPayloadFromProto(p *lpb.LogEntry_Leveled) (*LeveledPayload, error) {
Serge Bazanskib0272182020-11-02 18:39:44 +0100129 severity, err := SeverityFromProto(p.Severity)
130 if err != nil {
131 return nil, fmt.Errorf("could not convert severity: %w", err)
132 }
133 parts := strings.Split(p.Location, ":")
134 if len(parts) != 2 {
135 return nil, fmt.Errorf("invalid location, must be two :-delimited parts, is %d parts", len(parts))
136 }
137 file := parts[0]
138 line, err := strconv.Atoi(parts[1])
139 if err != nil {
140 return nil, fmt.Errorf("invalid location line number: %w", err)
141 }
142 return &LeveledPayload{
Serge Bazanski12971d62020-11-17 12:12:58 +0100143 messages: p.Lines,
Mateusz Zalegacf92f402022-07-08 15:08:48 +0200144 timestamp: p.Timestamp.AsTime(),
Serge Bazanskib0272182020-11-02 18:39:44 +0100145 severity: severity,
146 file: file,
147 line: line,
148 }, nil
149}
Serge Bazanski020b7c52021-07-07 14:22:28 +0200150
151// ExternalLeveledPayload is a LeveledPayload received from an external source,
152// eg. from parsing the logging output of third-party programs. It can be
153// converted into a LeveledPayload and inserted into a leveled logger, but will
154// be sanitized before that, ensuring that potentially buggy
155// emitters/converters do not end up polluting the leveled logger data.
156//
157// This type should be used only when inserting data from external systems, not
158// by code that just wishes to log things. In the future, data inserted this
159// way might be explicitly marked as tainted so operators can understand that
160// parts of this data might not give the same guarantees as the log entries
161// emitted by the native LeveledLogger API.
162type ExternalLeveledPayload struct {
163 // Log line. If any newlines are found, they will split the message into
164 // multiple messages within LeveledPayload. Empty messages are accepted
165 // verbatim.
166 Message string
167 // Timestamp when this payload was emitted according to its source. If not
168 // given, will default to the time of conversion to LeveledPayload.
169 Timestamp time.Time
170 // Log severity. If invalid or unset will default to INFO.
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000171 Severity logging.Severity
Serge Bazanski020b7c52021-07-07 14:22:28 +0200172 // File name of originating code. Defaults to "unknown" if not set.
173 File string
174 // Line in File. Zero indicates the line is not known.
175 Line int
176}
177
178// sanitize the given ExternalLeveledPayload by creating a corresponding
179// LeveledPayload. The original object is unaltered.
180func (e *ExternalLeveledPayload) sanitize() *LeveledPayload {
181 l := &LeveledPayload{
182 messages: strings.Split(e.Message, "\n"),
183 timestamp: e.Timestamp,
184 severity: e.Severity,
185 file: e.File,
186 line: e.Line,
187 }
188 if l.timestamp.IsZero() {
189 l.timestamp = time.Now()
190 }
191 if !l.severity.Valid() {
Serge Bazanski3c5d0632024-09-12 10:49:12 +0000192 l.severity = logging.INFO
Serge Bazanski020b7c52021-07-07 14:22:28 +0200193 }
194 if l.file == "" {
195 l.file = "unknown"
196 }
197 if l.line < 0 {
198 l.line = 0
199 }
200 return l
201}