Skip to content
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
13 changes: 10 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ witchcraft-go-logging
[![](https://godoc.org/github.com/palantir/witchcraft-go-logging?status.svg)](http://godoc.org/github.com/palantir/witchcraft-go-logging)

`witchcraft-go-logging` is a Go implementation of the Witchcraft logging specification. It provides an API that can be
used for logging and some default implementations of the logging API using different existing popular Go logging
libraries. `witchcraft-go-logging` includes implementations that use [zap](https://github.com/uber-go/zap),
[zerolog](https://github.com/rs/zerolog) and [glog](https://github.com/golang/glog).
used for logging along with several implementations and adapters.

**Implementations** wrap existing Go logging libraries in order to implement the wlog interface. We currently provide
- [zap](https://github.com/uber-go/zap) via [wlog-zap](wlog-zap)
- [zerolog](https://github.com/rs/zerolog) via [wlog-zerolog](wlog-zerolog)
- [glog](https://github.com/golang/glog) via [wlog-glog](wlog-glog)
- [wlog-tmpl](wlog-tmpl) for rendering structured logging using human-friendly templates.

**Adapters** wrap the witchcraft-go-logging logger implementations (svc1log, ev2log, req2log, etc) to allow interoperability with other Go logging interfaces. We currently provide
- [svc1zap](adapters/svc1zap) wraps a svc1log.Logger to provide a [zap](https://github.com/uber-go/zap) Logger.

Architecture
------------
Expand Down
147 changes: 147 additions & 0 deletions adapters/svc1zap/svc1zap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) 2022 Palantir Technologies. All rights reserved.
//
// 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 svc1zap

import (
"strings"

"github.com/palantir/witchcraft-go-logging/internal/gopath"
"github.com/palantir/witchcraft-go-logging/wlog"
"github.com/palantir/witchcraft-go-logging/wlog/svclog/svc1log"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

type svc1zapCore struct {
log svc1log.Logger
originFromCallLine bool
newParamFunc func(key string, value interface{}) svc1log.Param
}

// New returns a zap logger that delegates to the provided svc1log logger.
// The enabled/disabled log level configuration on the returned zap logger is ignored in favor of the svc1log configuration.
func New(logger svc1log.Logger, opts ...Option) *zap.Logger {
core := NewCore(logger, opts...)
z := zap.New(core)
if core.(*svc1zapCore).originFromCallLine {
z.WithOptions(zap.AddCaller())
}
return z
}

func NewCore(logger svc1log.Logger, opts ...Option) zapcore.Core {
core := &svc1zapCore{log: logger}
for _, opt := range opts {
opt(core)
}
return core
}

type Option func(*svc1zapCore)

// WithOriginFromZapCaller enables zap.AddCaller() and uses the caller file and line to construct the origin value.
// Similar to svc1log.OriginFromCallLine().
func WithOriginFromZapCaller() Option {
return func(core *svc1zapCore) { core.originFromCallLine = true }
}

// WithNewParamFunc provides a function for constructing svc1log.Param values from zap fields.
// Use this option to control parameter safety. By default, all fields are converted to unsafe params.
// If newParam returns nil, the field is skipped.
func WithNewParamFunc(newParam func(key string, value interface{}) svc1log.Param) Option {
return func(core *svc1zapCore) { core.newParamFunc = newParam }
}

func (c svc1zapCore) Enabled(level zapcore.Level) bool {
if checker, ok := c.log.(wlog.LevelChecker); ok {
switch level {
case zapcore.DebugLevel:
return checker.Enabled(wlog.DebugLevel)
case zapcore.InfoLevel:
return checker.Enabled(wlog.InfoLevel)
case zapcore.WarnLevel:
return checker.Enabled(wlog.WarnLevel)
default:
return checker.Enabled(wlog.ErrorLevel)
}
}
return true
}

func (c svc1zapCore) With(fields []zapcore.Field) zapcore.Core {
return svc1zapCore{log: svc1log.WithParams(c.log, c.fieldsToWlogParams(fields)...)}
}

func (c svc1zapCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
return ce.AddCore(entry, c)
}

func (c svc1zapCore) Write(entry zapcore.Entry, fields []zapcore.Field) error {
message := formatMessage(entry)
params := c.fieldsToWlogParams(fields)
if c.originFromCallLine && entry.Caller.Defined {
params = append(params, svc1log.Origin(gopath.TrimPrefix(entry.Caller.FullPath())))
}
switch entry.Level {
case zapcore.DebugLevel:
c.log.Debug(message, params...)
case zapcore.InfoLevel:
c.log.Info(message, params...)
case zapcore.WarnLevel:
c.log.Warn(message, params...)
default:
c.log.Error(message, params...)
}
return nil
}

func (c svc1zapCore) Sync() error { return nil }

func (c svc1zapCore) fieldsToWlogParams(fields []zapcore.Field) []svc1log.Param {
var params []svc1log.Param
for key, value := range fieldsToMap(fields) {
if c.newParamFunc != nil {
if p := c.newParamFunc(key, value); p != nil {
params = append(params, p)
}
} else {
params = append(params, svc1log.UnsafeParam(key, value))
}
}
return params
}

func formatMessage(entry zapcore.Entry) string {
if entry.LoggerName == "" {
return entry.Message
}
sb := strings.Builder{}
sb.Grow(len(entry.LoggerName) + 2 + len(entry.Message))
sb.WriteString(entry.LoggerName)
sb.WriteString(": ")
sb.WriteString(entry.Message)
return sb.String()
}

func fieldsToMap(fields []zapcore.Field) map[string]interface{} {
params := zapcore.NewMapObjectEncoder()
for _, field := range fields {
if field.Key == "token" {
continue // who logs a token...?
}
field.AddTo(params)
}
return params.Fields
}
116 changes: 116 additions & 0 deletions adapters/svc1zap/svc1zap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// Copyright (c) 2022 Palantir Technologies. All rights reserved.
//
// 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 svc1zap

import (
"bytes"
"encoding/json"
"strings"
"testing"

"github.com/palantir/pkg/objmatcher"
"github.com/palantir/witchcraft-go-logging/wlog"
"github.com/palantir/witchcraft-go-logging/wlog/svclog/svc1log"
"github.com/stretchr/testify/assert"
"go.uber.org/zap"

// Use zap as logger implementation
_ "github.com/palantir/witchcraft-go-logging/wlog-zap"
)

func TestSvc1ZapWrapper(t *testing.T) {

prefixParamFunc := func(key string, value interface{}) svc1log.Param {
if strings.HasPrefix(key, "safe") {
return svc1log.SafeParam(key, value)
}
if !strings.HasPrefix(key, "forbidden") {
return svc1log.UnsafeParam(key, value)
}
return nil
}

t.Run("defaults to all unsafe params", func(t *testing.T) {
buf := new(bytes.Buffer)
logger := svc1log.New(buf, wlog.DebugLevel)
logr1 := New(logger)
logr1.Info("logr 1", zap.String("safeString", "string"), zap.String("forbiddenToken", "token"), zap.Int("unsafeInt", 42))
assertLogLine(t, buf.Bytes(), objmatcher.MapMatcher{
"level": objmatcher.NewEqualsMatcher("INFO"),
"time": objmatcher.NewRegExpMatcher(".+"),
"message": objmatcher.NewEqualsMatcher("logr 1"),
"type": objmatcher.NewEqualsMatcher(svc1log.TypeValue),
"unsafeParams": objmatcher.MapMatcher{
"forbiddenToken": objmatcher.NewEqualsMatcher("token"),
"safeString": objmatcher.NewEqualsMatcher("string"),
"unsafeInt": objmatcher.NewEqualsMatcher(float64(42)),
},
})
})

t.Run("caller origin and custom params", func(t *testing.T) {
buf := new(bytes.Buffer)
logger := svc1log.New(buf, wlog.DebugLevel)
logr2 := New(logger, WithOriginFromZapCaller(), WithNewParamFunc(prefixParamFunc)).WithOptions(zap.AddCaller())
logr2.Info("logr 2", zap.String("safeString", "string"), zap.String("forbiddenToken", "token"), zap.Int("unsafeInt", 42))
assertLogLine(t, buf.Bytes(), objmatcher.MapMatcher{
"level": objmatcher.NewEqualsMatcher("INFO"),
"time": objmatcher.NewRegExpMatcher(".+"),
"message": objmatcher.NewEqualsMatcher("logr 2"),
"type": objmatcher.NewEqualsMatcher(svc1log.TypeValue),
"origin": objmatcher.NewRegExpMatcher("^github.com/palantir/witchcraft-go-logging/adapters/svc1zap/svc1zap_test.go:\\d+"),
"params": objmatcher.MapMatcher{
"safeString": objmatcher.NewEqualsMatcher("string"),
},
"unsafeParams": objmatcher.MapMatcher{
"unsafeInt": objmatcher.NewEqualsMatcher(float64(42)),
},
})
})

t.Run("logger with attached params", func(t *testing.T) {
buf := new(bytes.Buffer)
logger := svc1log.New(buf, wlog.DebugLevel)
logr3 := New(logger).Named("logr3").With(zap.String("name", "logr3"))
logr3.Error("logr 3", zap.String("safeString", "string"), zap.String("forbiddenToken", "token"), zap.Int("unsafeInt", 42))
assertLogLine(t, buf.Bytes(), objmatcher.MapMatcher{
"level": objmatcher.NewEqualsMatcher("ERROR"),
"time": objmatcher.NewRegExpMatcher(".+"),
"message": objmatcher.NewEqualsMatcher("logr3: logr 3"),
"type": objmatcher.NewEqualsMatcher(svc1log.TypeValue),
"unsafeParams": objmatcher.MapMatcher{
"forbiddenToken": objmatcher.NewEqualsMatcher("token"),
"name": objmatcher.NewEqualsMatcher("logr3"),
"safeString": objmatcher.NewEqualsMatcher("string"),
"unsafeInt": objmatcher.NewEqualsMatcher(float64(42)),
},
})
})

t.Run("logger with disabled level", func(t *testing.T) {
buf := new(bytes.Buffer)
logger := svc1log.New(buf, wlog.InfoLevel)
logr4 := New(logger)
logr4.Debug("logr 4")
assert.Empty(t, buf.String())
})
}

func assertLogLine(t *testing.T, logLine []byte, matcher objmatcher.MapMatcher) {
logEntry := map[string]interface{}{}
err := json.Unmarshal(logLine, &logEntry)
assert.NoError(t, err)
assert.NoError(t, matcher.Matches(logEntry))
}
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-198.v2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type: feature
feature:
description: Add `adapters/svc1zap` for code that accepts `*zap.Logger`
links:
- https://github.com/palantir/witchcraft-go-logging/pull/198