Skip to content

Commit

Permalink
Add text and json formatter. (#5)
Browse files Browse the repository at this point in the history
* Add text formatter.

* Add json formatter.

* Add default text formatter.

* Update CI config.
  • Loading branch information
edoger committed Apr 10, 2021
1 parent 49e221a commit 7d34071
Show file tree
Hide file tree
Showing 10 changed files with 558 additions and 29 deletions.
2 changes: 1 addition & 1 deletion .appveyor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ install:
- go version

test_script:
- go test -v -coverprofile=coverage.out -covermode=count ./...
- go test -v -coverprofile=coverage.out -covermode=atomic ./...

on_success:
- go get -v -u github.com/mattn/goveralls
Expand Down
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ go:
- 1.16.x

script:
- go test -v -coverprofile=coverage.out -covermode=count ./...
- go test -v -coverprofile=coverage.out -covermode=atomic ./...

before_script:
- go env
Expand Down
24 changes: 24 additions & 0 deletions internal/fields.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@

package internal

import (
"fmt"
"sort"
"strings"
)

// Fields type defines the dynamic field collection of the log.
// After Fields are created, their stored keys will not change.
type Fields map[string]interface{}
Expand Down Expand Up @@ -58,3 +64,21 @@ func StandardiseFieldsForJSONEncoder(src map[string]interface{}) map[string]inte
}
return dst
}

// FormatFieldsToText standardizes the given log fields.
func FormatFieldsToText(src map[string]interface{}) string {
texts := make([]string, 0, len(src))
for k, v := range src {
switch o := v.(type) {
case []byte:
texts = append(texts, k+"="+string(o))
default:
texts = append(texts, k+"="+fmt.Sprint(v))
}
}
// Ensure that the order of log extension fields is consistent.
if len(texts) > 1 {
sort.Strings(texts)
}
return strings.Join(texts, ", ")
}
105 changes: 105 additions & 0 deletions json_formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2021 The ZKits Project Authors.
//
// 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 logger

import (
"bytes"
"encoding/json"
"fmt"

"github.com/edoger/zkits-logger/internal"
)

// The default json formatter.
var defaultJSONFormatter = MustNewJSONFormatter(nil, false)

// DefaultJSONFormatter returns the default json formatter.
func DefaultJSONFormatter() Formatter {
return defaultJSONFormatter
}

// NewJSONFormatter creates and returns an instance of the log json formatter.
// The keys parameter is used to modify the default json field name.
// If the full parameter is true, it will always ensure that all fields exist in
// the top-level json object.
func NewJSONFormatter(keys map[string]string, full bool) (Formatter, error) {
m := map[string]string{
"name": "name", "time": "time", "level": "level", "message": "message",
"fields": "fields", "caller": "caller",
}
if len(keys) > 0 {
for key, value := range keys {
if m[key] == "" {
return nil, fmt.Errorf("invalid json formatter key %q", key)
}
// We ignore the case where all fields are mapped as empty, which is more practical.
if value != "" {
m[key] = value
}
}
}
f := &jsonFormatter{
name: m["name"], time: m["time"], level: m["level"], message: m["message"],
fields: m["fields"], caller: m["caller"],
full: full,
}
return f, nil
}

// MustNewJSONFormatter is like NewJSONFormatter, but triggers a panic when an error occurs.
func MustNewJSONFormatter(keys map[string]string, full bool) Formatter {
f, err := NewJSONFormatter(keys, full)
if err != nil {
panic(err)
}
return f
}

// The built-in json formatter.
type jsonFormatter struct {
name string
time string
level string
message string
fields string
caller string
full bool
}

// Format formats the given log entity into character data and writes it to the given buffer.
func (f *jsonFormatter) Format(e Entity, b *bytes.Buffer) error {
kv := map[string]interface{}{
f.name: e.Name(),
f.time: e.TimeString(),
f.level: e.Level().String(),
f.message: e.Message(),
}
if fields := e.Fields(); len(fields) > 0 {
kv[f.fields] = internal.StandardiseFieldsForJSONEncoder(fields)
} else {
if f.full {
kv[f.fields] = struct{}{}
}
}
if caller := e.Caller(); caller != "" {
kv[f.caller] = caller
} else {
if f.full {
kv[f.caller] = ""
}
}
// The json.Encoder.Encode method automatically adds line breaks.
return json.NewEncoder(b).Encode(kv)
}
71 changes: 71 additions & 0 deletions json_formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// Copyright 2021 The ZKits Project Authors.
//
// 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 logger

import (
"bytes"
"testing"
)

func TestDefaultJSONFormatter(t *testing.T) {
if DefaultJSONFormatter() == nil {
t.Fatal("DefaultJSONFormatter(): nil")
}
}

func TestNewJSONFormatter(t *testing.T) {
f, err := NewJSONFormatter(map[string]string{"message": "msg"}, true)
if err != nil {
t.Fatalf("NewJSONFormatter(): error %s", err)
}
if f == nil {
t.Fatal("NewJSONFormatter(): nil")
}

f, err = NewJSONFormatter(map[string]string{"hello": "hello"}, true)
if err == nil {
t.Fatal("NewJSONFormatter(): no error")
}
}

func TestMustNewJSONFormatter(t *testing.T) {
if MustNewJSONFormatter(map[string]string{"message": "msg"}, true) == nil {
t.Fatal("MustNewJSONFormatter(): nil")
}

defer func() {
if recover() == nil {
t.Fatal("MustNewJSONFormatter(): no panic")
}
}()

MustNewJSONFormatter(map[string]string{"hello": "hello"}, true)
}

func TestJSONFormatter_Format(t *testing.T) {
l := New("test")
l.SetFormatter(MustNewJSONFormatter(map[string]string{"message": "msg"}, true))
buf := new(bytes.Buffer)
l.SetOutput(buf)
l.SetDefaultTimeFormat("test")

l.Info("test")

got := buf.String()
want := `{"caller":"","fields":{},"level":"info","msg":"test","name":"test","time":"test"}` + "\n"
if got != want {
t.Fatalf("JSONFormatter.Format(): want %q, got %q", want, got)
}
}
27 changes: 3 additions & 24 deletions log.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package logger
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"os"
Expand Down Expand Up @@ -132,7 +131,7 @@ type Log interface {
// Warningln uses the given parameters to record a WarnLevel log.
Warningln(...interface{})

// Warningln uses the given parameters to record a WarnLevel log.
// Warningf uses the given parameters to record a WarnLevel log.
Warningf(string, ...interface{})

// Error uses the given parameters to record a ErrorLevel log.
Expand Down Expand Up @@ -199,6 +198,7 @@ func newCore(name string) *core {
return &core{
name: name,
level: TraceLevel,
formatter: DefaultJSONFormatter(),
writer: os.Stdout,
levelWriter: make(map[Level]io.Writer),
pool: sync.Pool{New: func() interface{} { return new(logEntity) }},
Expand Down Expand Up @@ -301,7 +301,7 @@ func (o *log) record(level Level, message string) {
entity := o.core.getEntity(o, level, message, o.getCaller(level))
defer o.core.putEntity(entity)

if err := o.format(entity); err != nil {
if err := o.core.formatter.Format(entity, &entity.buffer); err != nil {
// When the format log fails, we terminate the logging and report the error.
internal.EchoError("(%s) Failed to format log: %s", o.core.name, err)
} else {
Expand All @@ -325,27 +325,6 @@ func (o *log) record(level Level, message string) {
}
}

// Format the current log.
func (o *log) format(entity *logEntity) error {
if formatter := o.core.formatter; formatter != nil {
return formatter.Format(entity, &entity.buffer)
}

kv := map[string]interface{}{
"name": entity.name,
"time": entity.TimeString(),
"level": entity.level.String(),
"message": entity.message,
}
if len(o.fields) > 0 {
kv["fields"] = internal.StandardiseFieldsForJSONEncoder(o.fields)
}
if entity.caller != "" {
kv["caller"] = entity.caller
}
return json.NewEncoder(&entity.buffer).Encode(kv)
}

// Write the current log.
func (o *log) write(entity *logEntity) (err error) {
var w io.Writer
Expand Down
10 changes: 7 additions & 3 deletions logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ type Logger interface {
// EnableCaller enables caller reporting on all levels of logs.
EnableCaller(...int) Logger

// EnableCaller enables caller reporting on logs of a given level.
// EnableLevelCaller enables caller reporting on logs of a given level.
EnableLevelCaller(Level, ...int) Logger

// AddHook adds the given log hook to the current logger.
Expand Down Expand Up @@ -170,7 +170,11 @@ func (o *logger) SetPanicFunc(f func(string)) Logger {
// SetFormatter sets the log formatter for the current logger.
// If the given log formatter is nil, we will record the log in JSON format.
func (o *logger) SetFormatter(formatter Formatter) Logger {
o.core.formatter = formatter
if formatter == nil {
o.core.formatter = DefaultJSONFormatter()
} else {
o.core.formatter = formatter
}
return o
}

Expand All @@ -195,7 +199,7 @@ func (o *logger) EnableCaller(skip ...int) Logger {
return o
}

// EnableCaller enables caller reporting on logs of a given level.
// EnableLevelCaller enables caller reporting on logs of a given level.
func (o *logger) EnableLevelCaller(level Level, skip ...int) Logger {
var n int
if len(skip) > 0 && skip[0] > 0 {
Expand Down
Loading

0 comments on commit 7d34071

Please sign in to comment.