Skip to content

Commit

Permalink
Merge 5ed45bc into 49e221a
Browse files Browse the repository at this point in the history
  • Loading branch information
edoger committed Apr 10, 2021
2 parents 49e221a + 5ed45bc commit 3b97e54
Show file tree
Hide file tree
Showing 3 changed files with 305 additions and 0 deletions.
24 changes: 24 additions & 0 deletions internal/fields.go
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, ", ")
}
165 changes: 165 additions & 0 deletions text_formatter.go
@@ -0,0 +1,165 @@
// 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"
"fmt"
"regexp"
"strconv"
"strings"

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

// This regular expression is used to analyze placeholders in text formatter format.
var formatRegexp = regexp.MustCompile(`{(name|time|level|message|caller|fields)(?:@?([^{}]*)?)?}`)

// NewTextFormatter creates and returns an instance of the log text formatter.
// The format parameter is used to control the format of the log, and it has many control parameters.
// For example:
// "[{name}][{time}][{level}] {caller} {message} {fields}"
// The supported placeholders are:
// {name} The name of the logger that recorded this log.
// {time} Record the time of this log.
// {level} The level of this log.
// {caller} The name and line number of the file where this log was generated. (If enabled)
// {message} The message of this log.
// {fields} The extended fields of this log. (if it exists)
// It is worth knowing:
// 1. For the {time} parameter, we can specify time format, like this: {time@2006-01-02 15:04:05}.
// 2. For the {level} parameter, we can specify level format, like this: {level@sc},
// {level@sc} or {level@cs} will call the Level.ShortCapitalString method.
// {level@s} will call the Level.ShortString method.
// {level@c} will call the Level.CapitalString method.
// For other will call the Level.String method.
// The quote parameter is used to escape invisible characters in the log.
func NewTextFormatter(format string, quote bool) (Formatter, error) {
sub := formatRegexp.FindAllStringSubmatch(format, -1)
if len(sub) == 0 {
return nil, fmt.Errorf("invalid text formatter format %q", format)
}
// If sub is not empty, then idx is definitely not empty.
idx := formatRegexp.FindAllStringIndex(format, -1)
f := &textFormatter{quote: quote}

var parts []string
var start int
for i, j := 0, len(sub); i < j; i++ {
key, args := sub[i][1], sub[i][2]
switch key {
case "name":
f.encoders = append(f.encoders, f.encodeName)
case "time":
f.encoders = append(f.encoders, f.encodeTime)
f.timeFormat = args
case "level":
f.encoders = append(f.encoders, f.encodeLevel)
f.levelFormat = args
case "message":
f.encoders = append(f.encoders, f.encodeMessage)
case "caller":
f.encoders = append(f.encoders, f.encodeCaller)
case "fields":
f.encoders = append(f.encoders, f.encodeFields)
}
parts = append(parts, format[start:idx[i][0]])
start = idx[i][1]
}

f.format = strings.Join(append(parts, format[start:]), "%s")
return f, nil
}

// MustNewTextFormatter is like NewTextFormatter, but triggers a panic when an error occurs.
func MustNewTextFormatter(format string, quote bool) Formatter {
f, err := NewTextFormatter(format, quote)
if err != nil {
panic(err)
}
return f
}

// The built-in text formatter.
type textFormatter struct {
format string
quote bool
encoders []func(Entity) string
timeFormat string
levelFormat string
}

// Format formats the given log entity into character data and writes it to the given buffer.
func (f *textFormatter) Format(e Entity, b *bytes.Buffer) (err error) {
args := make([]interface{}, len(f.encoders))
for i, j := 0, len(f.encoders); i < j; i++ {
args[i] = f.encoders[i](e)
}
if f.quote {
// The quoted[0] and quoted[len(s)-1] is '"', they need to be removed.
quoted := strconv.AppendQuote(nil, fmt.Sprintf(f.format, args...))
quoted[len(quoted)-1] = '\n'
_, err = b.Write(quoted[1:])
} else {
_, err = b.WriteString(fmt.Sprintf(f.format, args...) + "\n")
}
return
}

// Encode the name of the log.
func (f *textFormatter) encodeName(e Entity) string {
return e.Name()
}

// Encode the level of the log.
func (f *textFormatter) encodeLevel(e Entity) string {
if f.timeFormat != "" {
switch f.levelFormat {
case "sc", "cs":
return e.Level().ShortCapitalString()
case "s":
return e.Level().ShortString()
case "c":
return e.Level().CapitalString()
}
}
return e.Level().String()
}

// Encode the time of the log.
func (f *textFormatter) encodeTime(e Entity) string {
if f.timeFormat == "" {
return e.TimeString()
}
return e.Time().Format(f.timeFormat)
}

// Encode the caller of the log.
func (f *textFormatter) encodeCaller(e Entity) string {
return e.Caller()
}

// Encode the message of the log.
func (f *textFormatter) encodeMessage(e Entity) string {
return e.Message()
}

// Encode the fields of the log.
func (f *textFormatter) encodeFields(e Entity) string {
if fields := e.Fields(); len(fields) > 0 {
return internal.FormatFieldsToText(e.Fields())
}
return ""
}
116 changes: 116 additions & 0 deletions text_formatter_test.go
@@ -0,0 +1,116 @@
// 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 TestNewTextFormatter(t *testing.T) {
format := "[{name}][{time@2006-01-02 15:04:05}][{level@sc}] {caller} {message} {fields}"
if f, err := NewTextFormatter(format, true); err != nil {
t.Fatalf("NewTextFormatter(): error %s", err)
} else {
if f == nil {
t.Fatal("NewTextFormatter(): nil")
}
}

format = "hello"
if _, err := NewTextFormatter(format, true); err == nil {
t.Fatalf("NewTextFormatter(): no error with format: %q", format)
}

format = "hello {world}"
if _, err := NewTextFormatter(format, true); err == nil {
t.Fatalf("NewTextFormatter(): no error with format: %q", format)
}
}

func TestMustNewTextFormatter(t *testing.T) {
format := "[{name}][{time@2006-01-02 15:04:05}][{level@sc}] {caller} {message} {fields}"
if MustNewTextFormatter(format, true) == nil {
t.Fatal("MustNewTextFormatter(): nil")
}

format = "hello"
defer func() {
if recover() == nil {
t.Fatalf("MustNewTextFormatter(): no panic with format : %q", format)
}
}()

MustNewTextFormatter(format, true)
}

func TestTextFormatter_Format(t *testing.T) {
l := New("test")
l.SetFormatter(MustNewTextFormatter("{name} - {time@test} [{level@sc}] {caller} {message} {fields}", true))
buf := new(bytes.Buffer)
l.SetOutput(buf)

l.WithField("foo", 1).WithField("bar", []byte("bar")).Info("test\n test")

got := buf.String()
want := "test - test [INF] test\\n test bar=bar, foo=1\n"
if got != want {
t.Fatalf("TextFormatter.Format(): want %q, got %q", want, got)
}

buf.Reset()
l.SetFormatter(MustNewTextFormatter("{name} - {time@test} [{level@sc}] {caller} {message} {fields}", false))

l.WithField("foo", 1).WithField("bar", []byte("bar")).Info("test\n test")

got = buf.String()
want = "test - test [INF] test\n test bar=bar, foo=1\n"
if got != want {
t.Fatalf("TextFormatter.Format(): want %q, got %q", want, got)
}

buf.Reset()
l.SetFormatter(MustNewTextFormatter("{name} - {time@test} [{level@s}] {caller} {message} {fields}", true))

l.Info("test")

got = buf.String()
want = "test - test [inf] test \n"
if got != want {
t.Fatalf("TextFormatter.Format(): want %q, got %q", want, got)
}

buf.Reset()
l.SetFormatter(MustNewTextFormatter("{name} - {time@test} [{level@c}] {caller} {message} {fields}", true))

l.Info("test")

got = buf.String()
want = "test - test [INFO] test \n"
if got != want {
t.Fatalf("TextFormatter.Format(): want %q, got %q", want, got)
}

buf.Reset()
l.SetFormatter(MustNewTextFormatter("{name} - {time} [{level}] {caller} {message} {fields}", true))
l.SetDefaultTimeFormat("test")
l.Info("test")

got = buf.String()
want = "test - test [info] test \n"
if got != want {
t.Fatalf("TextFormatter.Format(): want %q, got %q", want, got)
}
}

0 comments on commit 3b97e54

Please sign in to comment.