Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recursive json serialization for structured attributes (log.Any) #50

Merged
merged 6 commits into from
Nov 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions http_logging_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ func TestLogRequest(t *testing.T) {
w.Flush()
actual := b.String()
//nolint: lll
expected := `{"level":"info","msg":"test1","method":"","url":"<nil>","proto":"","remote_addr":"","host":"foo-host:123","header.x-forwarded-proto":"","header.x-forwarded-for":"","user-agent":"","tls":true,"tls.peer_cn":"x\nyz","header.foo":"bar1,bar2"}
{"level":"info","msg":"test2","method":"","url":"<nil>","proto":"","remote_addr":"","host":"foo-host:123","header.x-forwarded-proto":"","header.x-forwarded-for":"","user-agent":"","extra1":"v1","extra2":"v2"}
expected := `{"level":"info","msg":"test1","method":"","url":null,"proto":"","remote_addr":"","host":"foo-host:123","header.x-forwarded-proto":"","header.x-forwarded-for":"","user-agent":"","tls":true,"tls.peer_cn":"x\nyz","header.foo":"bar1,bar2"}
{"level":"info","msg":"test2","method":"","url":null,"proto":"","remote_addr":"","host":"foo-host:123","header.x-forwarded-proto":"","header.x-forwarded-for":"","user-agent":"","extra1":"v1","extra2":"v2"}
`
if actual != expected {
t.Errorf("unexpected:\n%s\nvs:\n%s\n", actual, expected)
Expand Down
54 changes: 16 additions & 38 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@ package log // import "fortio.org/log"

import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"math"
"os"
"runtime"
"sort"
"strconv"
"strings"
"sync"
Expand Down Expand Up @@ -589,53 +589,31 @@ type ValueType[T ValueTypes] struct {
Val T
}

func arrayToString(s []interface{}) string {
var buf strings.Builder
buf.WriteString("[")
for i, e := range s {
if i != 0 {
buf.WriteString(",")
}
vv := ValueType[interface{}]{Val: e}
buf.WriteString(vv.String())
}
buf.WriteString("]")
return buf.String()
}

func mapToString(s map[string]interface{}) string {
var buf strings.Builder
buf.WriteString("{")
keys := make([]string, 0, len(s))
for k := range s {
keys = append(keys, k)
func toJSON(v any) string {
bytes, err := json.Marshal(v)
if err != nil {
return strconv.Quote(fmt.Sprintf("ERR marshaling %v: %v", v, err))
}
sort.Strings(keys)
for i, k := range keys {
if i != 0 {
buf.WriteString(",")
}
buf.WriteString(fmt.Sprintf("%q", k))
buf.WriteString(":")
vv := ValueType[interface{}]{Val: s[k]}
buf.WriteString(vv.String())
str := string(bytes)
// This is kinda hacky way to handle both structured and custom serialization errors, and
// struct with no public fields for which we need to call Error() to get a useful string.
if e, isError := v.(error); isError && str == "{}" {
return fmt.Sprintf("%q", e.Error())
}
buf.WriteString("}")
return buf.String()
return str
}

func (v ValueType[T]) String() string {
// if the type is numeric, use Sprint(v.val) otherwise use Sprintf("%q", v.Val) to quote it.
switch s := any(v.Val).(type) {
case bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64,
float32, float64:
return fmt.Sprint(v.Val)
case []interface{}:
return arrayToString(s)
case map[string]interface{}:
return mapToString(s)
return fmt.Sprint(s)
case string:
return fmt.Sprintf("%q", s)
/* It's all handled by json fallback now even though slightly more expensive at runtime, it's a lot simpler */
default:
return fmt.Sprintf("%q", fmt.Sprint(v.Val))
return toJSON(v.Val) // was fmt.Sprintf("%q", fmt.Sprint(v.Val))
}
}

Expand Down
46 changes: 46 additions & 0 deletions logger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,52 @@ func TestNoLevel(t *testing.T) {
}
}

type customError struct {
Msg string
Code int
}

func (e customError) Error() string {
return fmt.Sprintf("custom error %s (code %d)", e.Msg, e.Code)
}

func TestSerializationOfError(t *testing.T) {
var err error
kv := Any("err", err)
kvStr := kv.StringValue()
expected := `null`
if kvStr != expected {
t.Errorf("unexpected:\n%s\nvs:\n%s\n", kvStr, expected)
}
err = fmt.Errorf("test error")
Errf("Error on purpose: %v", err)
S(Error, "Error on purpose", Any("err", err))
kv = Any("err", err)
kvStr = kv.StringValue()
expected = `"test error"`
if kvStr != expected {
t.Errorf("unexpected:\n%s\nvs:\n%s\n", kvStr, expected)
}
err = customError{Msg: "custom error", Code: 42}
kv = Any("err", err)
kvStr = kv.StringValue()
expected = `{"Msg":"custom error","Code":42}`
if kvStr != expected {
t.Errorf("unexpected:\n%s\nvs:\n%s\n", kvStr, expected)
}
}

func TestToJSON_MarshalError(t *testing.T) {
badValue := make(chan int)

expected := fmt.Sprintf("\"ERR marshaling %v: %v\"", badValue, "json: unsupported type: chan int")
actual := toJSON(badValue)

if actual != expected {
t.Errorf("Expected %q, got %q", expected, actual)
}
}

// io.Discard but specially known to by logger optimizations for instance.
type discard struct{}

Expand Down