blob: c0852aef46479435378723933943c4f24c6a454b [file] [log] [blame] [edit]
// Copyright 2020 The Monogon Project Authors.
//
// SPDX-License-Identifier: Apache-2.0
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package logtree
import (
"fmt"
"strconv"
"strings"
"time"
tpb "google.golang.org/protobuf/types/known/timestamppb"
cpb "source.monogon.dev/metropolis/proto/common"
)
// LeveledPayload is a log entry for leveled logs (as per leveled.go). It contains
// the input to these calls (severity and message split into newline-delimited
// messages) and additional metadata that would be usually seen in a text
// representation of a leveled log entry.
type LeveledPayload struct {
// messages is the list of messages contained in this payload. This list is built
// from splitting up the given message from the user by newline.
messages []string
// timestamp is the time at which this message was emitted.
timestamp time.Time
// severity is the leveled Severity at which this message was emitted.
severity Severity
// file is the filename of the caller that emitted this message.
file string
// line is the line number within the file of the caller that emitted this message.
line int
}
// String returns a canonical representation of this payload as a single string
// prefixed with metadata. If the original message was logged with newlines, this
// representation will also contain newlines, with each original message part
// prefixed by the metadata. For an alternative call that will instead return a
// canonical prefix and a list of lines in the message, see Strings().
func (p *LeveledPayload) String() string {
prefix, lines := p.Strings()
res := make([]string, len(p.messages))
for i, line := range lines {
res[i] = fmt.Sprintf("%s%s", prefix, line)
}
return strings.Join(res, "\n")
}
// Strings returns the canonical representation of this payload split into a
// prefix and all lines that were contained in the original message. This is
// meant to be displayed to the user by showing the prefix before each line,
// concatenated together - possibly in a table form with the prefixes all
// unified with a rowspan- like mechanism.
//
// For example, this function can return:
//
// prefix = "I1102 17:20:06.921395 foo.go:42] "
// lines = []string{"current tags:", " - one", " - two"}
//
// With this data, the result should be presented to users this way in text form:
// I1102 17:20:06.921395 foo.go:42] current tags:
// I1102 17:20:06.921395 foo.go:42] - one
// I1102 17:20:06.921395 foo.go:42] - two
//
// Or, in a table layout:
// .-----------------------------------------------------------.
// | I1102 17:20:06.921395 0 foo.go:42] : current tags: |
// | :------------------|
// | : - one |
// | :------------------|
// | : - two |
// '-----------------------------------------------------------'
func (p *LeveledPayload) Strings() (prefix string, lines []string) {
_, month, day := p.timestamp.Date()
hour, minute, second := p.timestamp.Clock()
nsec := p.timestamp.Nanosecond() / 1000
// Same format as in glog, but without treadid.
// Lmmdd hh:mm:ss.uuuuuu file:line]
// TODO(q3k): rewrite this to printf-less code.
prefix = fmt.Sprintf("%s%02d%02d %02d:%02d:%02d.%06d %s:%d] ", p.severity, month, day, hour, minute, second, nsec, p.file, p.line)
lines = p.messages
return
}
// Message returns the inner message lines of this entry, ie. what was passed to
// the actual logging method, but split by newlines.
func (p *LeveledPayload) Messages() []string { return p.messages }
func (p *LeveledPayload) MessagesJoined() string { return strings.Join(p.messages, "\n") }
// Timestamp returns the time at which this entry was logged.
func (p *LeveledPayload) Timestamp() time.Time { return p.timestamp }
// Location returns a string in the form of file_name:line_number that shows the
// origin of the log entry in the program source.
func (p *LeveledPayload) Location() string { return fmt.Sprintf("%s:%d", p.file, p.line) }
// Severity returns the Severity with which this entry was logged.
func (p *LeveledPayload) Severity() Severity { return p.severity }
// Proto converts a LeveledPayload to protobuf format.
func (p *LeveledPayload) Proto() *cpb.LogEntry_Leveled {
return &cpb.LogEntry_Leveled{
Lines: p.Messages(),
Timestamp: tpb.New(p.Timestamp()),
Severity: p.Severity().ToProto(),
Location: p.Location(),
}
}
// LeveledPayloadFromProto parses a protobuf message into the internal format.
func LeveledPayloadFromProto(p *cpb.LogEntry_Leveled) (*LeveledPayload, error) {
severity, err := SeverityFromProto(p.Severity)
if err != nil {
return nil, fmt.Errorf("could not convert severity: %w", err)
}
parts := strings.Split(p.Location, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("invalid location, must be two :-delimited parts, is %d parts", len(parts))
}
file := parts[0]
line, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid location line number: %w", err)
}
return &LeveledPayload{
messages: p.Lines,
timestamp: p.Timestamp.AsTime(),
severity: severity,
file: file,
line: line,
}, nil
}
// ExternalLeveledPayload is a LeveledPayload received from an external source,
// eg. from parsing the logging output of third-party programs. It can be
// converted into a LeveledPayload and inserted into a leveled logger, but will
// be sanitized before that, ensuring that potentially buggy
// emitters/converters do not end up polluting the leveled logger data.
//
// This type should be used only when inserting data from external systems, not
// by code that just wishes to log things. In the future, data inserted this
// way might be explicitly marked as tainted so operators can understand that
// parts of this data might not give the same guarantees as the log entries
// emitted by the native LeveledLogger API.
type ExternalLeveledPayload struct {
// Log line. If any newlines are found, they will split the message into
// multiple messages within LeveledPayload. Empty messages are accepted
// verbatim.
Message string
// Timestamp when this payload was emitted according to its source. If not
// given, will default to the time of conversion to LeveledPayload.
Timestamp time.Time
// Log severity. If invalid or unset will default to INFO.
Severity Severity
// File name of originating code. Defaults to "unknown" if not set.
File string
// Line in File. Zero indicates the line is not known.
Line int
}
// sanitize the given ExternalLeveledPayload by creating a corresponding
// LeveledPayload. The original object is unaltered.
func (e *ExternalLeveledPayload) sanitize() *LeveledPayload {
l := &LeveledPayload{
messages: strings.Split(e.Message, "\n"),
timestamp: e.Timestamp,
severity: e.Severity,
file: e.File,
line: e.Line,
}
if l.timestamp.IsZero() {
l.timestamp = time.Now()
}
if !l.severity.Valid() {
l.severity = INFO
}
if l.file == "" {
l.file = "unknown"
}
if l.line < 0 {
l.line = 0
}
return l
}