| // 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" | 
 |  | 
 | 	apb "source.monogon.dev/metropolis/proto/api" | 
 | ) | 
 |  | 
 | // 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() *apb.LogEntry_Leveled { | 
 | 	return &apb.LogEntry_Leveled{ | 
 | 		Lines:     p.Messages(), | 
 | 		Timestamp: p.Timestamp().UnixNano(), | 
 | 		Severity:  p.Severity().ToProto(), | 
 | 		Location:  p.Location(), | 
 | 	} | 
 | } | 
 |  | 
 | // LeveledPayloadFromProto parses a protobuf message into the internal format. | 
 | func LeveledPayloadFromProto(p *apb.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: time.Unix(0, p.Timestamp), | 
 | 		severity:  severity, | 
 | 		file:      file, | 
 | 		line:      line, | 
 | 	}, nil | 
 | } |