package log
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"reflect"
"time"
"github.com/sirupsen/logrus"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
)
// TimeFormat is [time.RFC3339Nano] with nanoseconds padded using
// zeros to ensure the formatted time is always the same number of
// characters.
// Based on RFC3339NanoFixed from github.com/containerd/log
const TimeFormat = "2006-01-02T15:04:05.000000000Z07:00"
func FormatTime(t time.Time) string {
return t.Format(TimeFormat)
}
// DurationFormat formats a [time.Duration] log entry.
//
// A nil value signals an error with the formatting.
type DurationFormat func(time.Duration) interface{}
func DurationFormatString(d time.Duration) interface{} { return d.String() }
func DurationFormatSeconds(d time.Duration) interface{} { return d.Seconds() }
func DurationFormatMilliseconds(d time.Duration) interface{} { return d.Milliseconds() }
// FormatIO formats net.Conn and other types that have an `Addr()` or `Name()`.
//
// See FormatEnabled for more information.
func FormatIO(ctx context.Context, v interface{}) string {
m := make(map[string]string)
m["type"] = reflect.TypeOf(v).String()
switch t := v.(type) {
case net.Conn:
m["localAddress"] = formatAddr(t.LocalAddr())
m["remoteAddress"] = formatAddr(t.RemoteAddr())
case interface{ Addr() net.Addr }:
m["address"] = formatAddr(t.Addr())
default:
return Format(ctx, t)
}
return Format(ctx, m)
}
func formatAddr(a net.Addr) string {
return a.Network() + "://" + a.String()
}
// Format formats an object into a JSON string, without any indendtation or
// HTML escapes.
// Context is used to output a log waring if the conversion fails.
//
// This is intended primarily for `trace.StringAttribute()`
func Format(ctx context.Context, v interface{}) string {
b, err := encode(v)
if err != nil {
// logging errors aren't really warning worthy, and can potentially spam a lot of logs out
G(ctx).WithFields(logrus.Fields{
logrus.ErrorKey: err,
"type": fmt.Sprintf("%T", v),
}).Debug("could not format value")
return ""
}
return string(b)
}
func encode(v interface{}) (_ []byte, err error) {
if m, ok := v.(proto.Message); ok {
// use canonical JSON encoding for protobufs (instead of [encoding/json])
// https://protobuf.dev/programming-guides/proto3/#json
var b []byte
b, err = protojson.MarshalOptions{
AllowPartial: true,
// protobuf defaults to camel case for JSON encoding; use proto field name instead (snake case)
UseProtoNames: true,
}.Marshal(m)
if err == nil {
// the protojson marshaller tries to unmarshal anypb.Any fields, which can
// fail for types encoded with "github.com/containerd/typeurl/v2"
// we can try creating a dedicated protoregistry.MessageTypeResolver that uses typeurl, but, its
// more robust to fall back on json marshalling for errors in general
return b, nil
}
}
buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetEscapeHTML(false)
enc.SetIndent("", "")
if jErr := enc.Encode(v); jErr != nil {
if err != nil {
return nil, fmt.Errorf("protojson encoding: %w; json encoding: %w", err, jErr)
}
return nil, fmt.Errorf("json encoding: %w", jErr)
}
// encoder.Encode appends a newline to the end
return bytes.TrimSpace(buf.Bytes()), nil
}