Skip to content
Open
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
21 changes: 21 additions & 0 deletions .github/workflows/godoc.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Generate GoDoc

on: [pull_request]

jobs:
build:

runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v1
- name: Generate GoDoc
uses: ktr0731/godoc-action@v0.1.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: gh-pages
20 changes: 17 additions & 3 deletions log/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Fields struct{ m frozen.Map }
// From returns a copied logger from the context that you can use to access logger API.
func From(ctx context.Context) Logger {
f := getFields(ctx)
return f.getCopiedLogger().(internalLoggerOps).PutFields(f.resolveFields(ctx))
return f.configureLogger(ctx, f.getCopiedLogger().(fieldSetter))
}

// Suppress will ensure that suppressed keys are not logged.
Expand All @@ -25,6 +25,11 @@ func With(key string, val interface{}) Fields {
return Fields{}.With(key, val)
}

// WithConfigs adds extra configuration for the logger.
func WithConfigs(configs ...Config) Fields {
return Fields{}.WithConfigs(configs...)
}

// WithCtxRef creates a field with a key that refers to the provided context key,
// fields will use key as the fields property and take the value that corresponds
// to ctxKey.
Expand Down Expand Up @@ -83,6 +88,13 @@ func (f Fields) With(key string, val interface{}) Fields {
return f.with(key, val)
}

// WithConfigs adds extra configuration for the logger.
func (f Fields) WithConfigs(configs ...Config) Fields {
return f.Chain(Fields{
createConfigMap(configs...),
})
}

// WithCtxRef adds key and the context key to the fields.
func (f Fields) WithCtxRef(key string, ctxKey interface{}) Fields {
return f.with(key, ctxRef{ctxKey})
Expand All @@ -95,12 +107,14 @@ func (f Fields) WithFunc(key string, val func(context.Context) interface{}) Fiel

// WithLogger adds logger which will be used for the log operation.
func (f Fields) WithLogger(logger Logger) Fields {
return f.with(loggerKey{}, logger.(internalLoggerOps).Copy())
return f.with(loggerKey{}, logger.(copyable).Copy())
}

// String returns a string that represent the current fields
func (f Fields) String(ctx context.Context) string {
return f.resolveFields(ctx).String()
fields := &fieldsCollector{}
f.configureLogger(ctx, fields)
return fields.fields.String()
}

// MergedString returns a string that represents the current fields merged by fields in context
Expand Down
20 changes: 16 additions & 4 deletions log/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"testing"

"github.com/alecthomas/assert"
"github.com/anz-bank/pkg/log/testutil"
"github.com/arr-ai/frozen"
"github.com/stretchr/testify/mock"
)
Expand Down Expand Up @@ -40,6 +39,15 @@ func TestChain(t *testing.T) {
assert.True(t, expected.Equal(init.Chain(fields1, fields2, fields3).m))
}

func TestWithConfigsSameConfigType(t *testing.T) {
t.Parallel()

expectedConfig := frozen.Map{}.
With(standardFormat{}.TypeKey(), standardFormat{})
f := WithConfigs(NewJSONFormat(), NewStandardFormat())
assert.True(t, expectedConfig.Equal(f.m))
}

func TestFrom(t *testing.T) {
for _, c := range getUnresolvedFieldsCases() {
c := c
Expand All @@ -59,7 +67,7 @@ func TestFrom(t *testing.T) {
}

func TestOnto(t *testing.T) {
cases := testutil.GenerateMultipleFieldsCases()
cases := generateMultipleFieldsCases()
for _, c := range cases {
c := c
t.Run(c.Name, func(t *testing.T) {
Expand Down Expand Up @@ -159,6 +167,10 @@ func TestWithLogger(t *testing.T) {

func setLogMockAssertion(logger *mockLogger, fields frozen.Map) {
setMockCopyAssertion(logger)
setPutFieldsAssertion(logger, fields)
}

func setPutFieldsAssertion(logger *mockLogger, fields frozen.Map) {
logger.On(
"PutFields",
mock.MatchedBy(
Expand All @@ -177,11 +189,11 @@ func getLoggerFromContext(t *testing.T, ctx context.Context) *mockLogger {
return m.MustGet(loggerKey{}).(*mockLogger)
}

func setMockCopyAssertion(logger *mockLogger) {
func setMockCopyAssertion(logger *mockLogger) *mock.Call {
// set to return the same logger for testing purposes, in real case it will return
// a copied logger. Tests that use these usually are not checked for their return value
// as the return value is mocked
logger.On("Copy").Return(logger)
return logger.On("Copy").Return(logger)
}

func runFieldsMethod(t *testing.T, empty, nonEmpty func(*testing.T)) {
Expand Down
7 changes: 3 additions & 4 deletions log/benchmark/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"testing"

"github.com/anz-bank/pkg/log"
"github.com/anz-bank/pkg/log/loggers"
"github.com/sirupsen/logrus"
)

Expand All @@ -32,7 +31,7 @@ func BenchmarkLog1000Fields(b *testing.B) {
}

func BenchmarkWith(b *testing.B) {
logger := loggers.NewNullLogger()
logger := log.NewNullLogger()
ctx := log.With("key", "val").With("abc", 123).WithLogger(logger).Onto(context.Background())

for i := 0; i < b.N; i++ {
Expand All @@ -55,7 +54,7 @@ func BenchmarkLogrus(b *testing.B) {
}

func BenchmarkLog(b *testing.B) {
logger := loggers.NewNullLogger()
logger := log.NewNullLogger()
ctx := log.With("x-user-id", "12344").
With("x-trace-id", "acbdd").
WithLogger(logger).
Expand All @@ -72,6 +71,6 @@ func runBenchmark(b *testing.B, l int) {
for j := 0; j < l; j++ {
f = f.With(strconv.Itoa(j), j)
}
f.WithLogger(loggers.NewNullLogger()).From(context.Background()).Info("test")
f.WithLogger(log.NewNullLogger()).From(context.Background()).Info("test")
}
}
29 changes: 29 additions & 0 deletions log/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package log

type typeKey int

const Formatter typeKey = iota

type Config interface {
TypeKey() interface{}
Apply(logger Logger) error
}

type standardFormat struct{}
type jsonFormat struct{}

func NewStandardFormat() Config { return standardFormat{} }
func (standardFormat) TypeKey() interface{} { return Formatter }
func (sf standardFormat) Apply(logger Logger) error {
return applyFormatter(sf, logger)
}

func NewJSONFormat() Config { return jsonFormat{} }
func (jsonFormat) TypeKey() interface{} { return Formatter }
func (jf jsonFormat) Apply(logger Logger) error {
return applyFormatter(jf, logger)
}

func applyFormatter(formatter Config, logger Logger) error {
return logger.(formattable).SetFormatter(formatter)
}
36 changes: 36 additions & 0 deletions log/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,40 @@ func loggingDemo(ctx context.Context) {

```

### Logging in different formats

You can specify JSON output by adding `WithConfig(NewJSONFormat())`. It will log in the following format:
```js
{
"fields": {
"key1": "value1", // value can be any data types
"key2": "value2",
},
"level": "log level", // string, either INFO or DEBUG
"message": "log message", // string,
"timestamp": "log time", // timestamp in RFC3339Nano format
}
```

## Configuring logger

Logger can be configured using the `WithConfig` API and giving it the correct configuration struct.
```go
func configLogDemo(ctx context.Context) {
// Adding configuration can be done by adding the correct struct. Configurations are once again
// treated as fields, which means it will replace old configurations when a configuration
// of the same type is added. For example, if before you added StandardFormatter, calling WithConfig
// with JSONFormatter will replace StandardFormatter. Just like Fields, it will also be stored
// in the context.
ctx = log.WithConfigs(log.NewJSONFormat()).Onto(ctx)

// You can also have a log-specific configs by not saving it to the context.
log.WithConfigs(log.NewStandardFormat(), log.NewStandardFormat()).
WithLogger(log.NewStandardLogger()).
With("yeet", map[string]interface{}{"foo": "bar", "doesn't": "matter"}).
From(ctx).
Info("json formatted log")
}
```

Code snippets can be run in the [example file](examples/example.go)
24 changes: 20 additions & 4 deletions log/examples/example.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"fmt"

"github.com/anz-bank/pkg/log"
"github.com/anz-bank/pkg/log/loggers"
)

type contextKey struct{}
Expand All @@ -15,6 +14,7 @@ func main() {
ctx := context.Background()
fieldsDemo(ctx)
loggingDemo(ctx)
configLogDemo(ctx)
}

func fieldsDemo(ctx context.Context) {
Expand Down Expand Up @@ -90,7 +90,7 @@ func fieldsDemo(ctx context.Context) {
// that logger has been added. Logger is treated as another fields, so you can just chain it
// with the WithLogger API. Trying to generate a logger without setting up the logger will make the
// program panic. More about logging in another function.
logger1, logger2 := loggers.NewNullLogger(), loggers.NewStandardLogger()
logger1, logger2 := log.NewNullLogger(), log.NewStandardLogger()

// Creating a context that contains logger1.
ctx = log.WithLogger(logger1).Onto(ctx)
Expand All @@ -106,7 +106,7 @@ func fieldsDemo(ctx context.Context) {
func loggingDemo(ctx context.Context) {
// You can choose different loggers or implement your own.
// For this example, we are using the standard logger.
logger := loggers.NewStandardLogger()
logger := log.NewStandardLogger()

// Adding logger can be done through the WithLogger API. Do not forget to finalize it with
// the Onto API if you want the logger to be contained in the context.
Expand All @@ -118,7 +118,7 @@ func loggingDemo(ctx context.Context) {
// the format counterpart, Debugf and Infof.
// Logging will log in the following format:
// (time in RFC3339Nano Format) (Fields) (Level) (Message)
// Fields themselves are logged as a space separated list of key=value
// Fields themselves are logged as a space separated list of key=value.
log.From(ctx).Debug("This does not have any fields")

ctx = log.With("this", "one").With("have", "fields").Onto(ctx)
Expand All @@ -132,3 +132,19 @@ func loggingDemo(ctx context.Context) {
log.With("have", "additional fields").With("log-specific", "fields").From(ctx).Debug("log-specific fields")
log.From(ctx).Info("context fields are still untouched as long as the context is unchanged")
}

func configLogDemo(ctx context.Context) {
// Adding configuration can be done by adding the correct struct. Configurations are once again
// treated as fields, which means it will replace old configurations when a configuration
// of the same type is added. For example, if before you added StandardFormatter, calling WithConfig
// with JSONFormatter will replace StandardFormatter. Just like Fields, it will also be stored
// in the context.
ctx = log.WithConfigs(log.NewJSONFormat()).Onto(ctx)

// You can also have a log-specific configs by not saving it to the context.
log.WithConfigs(log.NewStandardFormat(), log.NewStandardFormat()).
WithLogger(log.NewStandardLogger()).
With("yeet", map[string]interface{}{"foo": "bar", "doesn't": "matter"}).
From(ctx).
Info("json formatted log")
}
14 changes: 11 additions & 3 deletions log/loggerInterface.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ type Logger interface {
Infof(format string, args ...interface{})
}

type internalLoggerOps interface {
// PutFields returns the Logger with the new fields added
PutFields(fields frozen.Map) Logger
type copyable interface {
// Copy returns a logger whose data is copied from the caller
Copy() Logger
}

type fieldSetter interface {
// PutFields returns the Logger with the new fields added
PutFields(fields frozen.Map) Logger
}

type formattable interface {
// SetFormatter sets the formatter for the logger
SetFormatter(formatter Config) error
}
Loading