From 33b33db927a3e67b19495c9c3d13879433f65fe1 Mon Sep 17 00:00:00 2001 From: Noteworthy Date: Thu, 27 Jun 2024 12:36:16 +1000 Subject: [PATCH] feat: add an option to provide a custom logger (#8) * feaT: add an option to provide a custom logger * update elfdump --- .github/workflows/ci.yaml | 2 +- README.md | 2 - cmd/elfdump.go | 2 +- file.go | 4 ++ log/README.md | 42 ++++++++++++ log/filter.go | 96 +++++++++++++++++++++++++++ log/filter_test.go | 133 ++++++++++++++++++++++++++++++++++++++ log/global.go | 122 ++++++++++++++++++++++++++++++++++ log/global_test.go | 85 ++++++++++++++++++++++++ log/helper.go | 130 +++++++++++++++++++++++++++++++++++++ log/helper_test.go | 79 ++++++++++++++++++++++ log/level.go | 56 ++++++++++++++++ log/level_test.go | 95 +++++++++++++++++++++++++++ log/log.go | 71 ++++++++++++++++++++ log/log_test.go | 25 +++++++ log/std.go | 47 ++++++++++++++ log/std_test.go | 17 +++++ log/value.go | 71 ++++++++++++++++++++ log/value_test.go | 30 +++++++++ parser.go | 51 +++++++++++++-- parser_test.go | 10 +-- 21 files changed, 1157 insertions(+), 13 deletions(-) create mode 100644 log/README.md create mode 100644 log/filter.go create mode 100644 log/filter_test.go create mode 100644 log/global.go create mode 100644 log/global_test.go create mode 100644 log/helper.go create mode 100644 log/helper_test.go create mode 100644 log/level.go create mode 100644 log/level_test.go create mode 100644 log/log.go create mode 100644 log/log_test.go create mode 100644 log/std.go create mode 100644 log/std_test.go create mode 100644 log/value.go create mode 100644 log/value_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0b81a4c..1f3d05a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -39,7 +39,7 @@ jobs: go vet . if: matrix.os == 'ubuntu-latest' && matrix.go-version == '1.21.x' - - name: Go vet + - name: Go Static Check run: | go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck . diff --git a/README.md b/README.md index dd55253..2580caf 100644 --- a/README.md +++ b/README.md @@ -54,5 +54,3 @@ func main() { - https://refspecs.linuxfoundation.org/elf/elf.pdf - https://github.com/freebsd/freebsd-src/blob/main/sys/sys/elf_common.h - - diff --git a/cmd/elfdump.go b/cmd/elfdump.go index a196db2..2c8ce7e 100644 --- a/cmd/elfdump.go +++ b/cmd/elfdump.go @@ -48,7 +48,7 @@ func dumpFileHeader(fileHdr elf.FileHeader) { } func parse(filename string, cfg config) { - p, err := elf.New(filename) + p, err := elf.New(filename, nil) defer p.CloseFile() if err != nil { panic(err) diff --git a/file.go b/file.go index 683dbf3..e46de43 100644 --- a/file.go +++ b/file.go @@ -3,6 +3,8 @@ package elf import ( "encoding/binary" "errors" + + "github.com/saferwall/elf/log" ) // FileIdent is a representation of the raw ident array (first 16 bytes of an ELF file) @@ -65,6 +67,8 @@ type File struct { ELFBin32 `json:",omitempty"` ELFBin64 `json:",omitempty"` ELFSymbols `json:",omitempty"` + opts *Options + logger *log.Helper } func NewBinaryFile() *File { diff --git a/log/README.md b/log/README.md new file mode 100644 index 0000000..3b357d2 --- /dev/null +++ b/log/README.md @@ -0,0 +1,42 @@ +# Logger + +This code was taken from the go microservice framework [kratos](https://github.com/go-kratos/kratos). + +## Usage + +### Structured logging + +```go +logger := log.NewStdLogger(os.Stdout) +// fields & valuer +logger = log.With(logger, + "service.name", "hellworld", + "service.version", "v1.0.0", + "ts", log.DefaultTimestamp, + "caller", log.DefaultCaller, +) +logger.Log(log.LevelInfo, "key", "value") + +// helper +helper := log.NewHelper(logger) +helper.Log(log.LevelInfo, "key", "value") +helper.Info("info message") +helper.Infof("info %s", "message") +helper.Infow("key", "value") + +// filter +log := log.NewHelper(log.NewFilter(logger, + log.FilterLevel(log.LevelInfo), + log.FilterKey("foo"), + log.FilterValue("bar"), + log.FilterFunc(customFilter), +)) +log.Debug("debug log") +log.Info("info log") +log.Warn("warn log") +log.Error("warn log") +``` + +## Third party log library + +If you need to implement a third party logging library like `zap`, have a look this [url](https://github.com/go-kratos/kratos/tree/main/contrib/log). \ No newline at end of file diff --git a/log/filter.go b/log/filter.go new file mode 100644 index 0000000..9ef87dd --- /dev/null +++ b/log/filter.go @@ -0,0 +1,96 @@ +package log + +// FilterOption is filter option. +type FilterOption func(*Filter) + +const fuzzyStr = "***" + +// FilterLevel with filter level. +func FilterLevel(level Level) FilterOption { + return func(opts *Filter) { + opts.level = level + } +} + +// FilterKey with filter key. +func FilterKey(key ...string) FilterOption { + return func(o *Filter) { + for _, v := range key { + o.key[v] = struct{}{} + } + } +} + +// FilterValue with filter value. +func FilterValue(value ...string) FilterOption { + return func(o *Filter) { + for _, v := range value { + o.value[v] = struct{}{} + } + } +} + +// FilterFunc with filter func. +func FilterFunc(f func(level Level, keyvals ...interface{}) bool) FilterOption { + return func(o *Filter) { + o.filter = f + } +} + +// Filter is a logger filter. +type Filter struct { + logger Logger + level Level + key map[interface{}]struct{} + value map[interface{}]struct{} + filter func(level Level, keyvals ...interface{}) bool +} + +// NewFilter new a logger filter. +func NewFilter(logger Logger, opts ...FilterOption) *Filter { + options := Filter{ + logger: logger, + key: make(map[interface{}]struct{}), + value: make(map[interface{}]struct{}), + } + for _, o := range opts { + o(&options) + } + return &options +} + +// Log Print log by level and keyvals. +func (f *Filter) Log(level Level, keyvals ...interface{}) error { + if level < f.level { + return nil + } + // fkv is used to provide a slice to contains both logger.prefix and keyvals for filter + var fkv []interface{} + if l, ok := f.logger.(*logger); ok { + if len(l.prefix) > 0 { + fkv = make([]interface{}, 0, len(l.prefix)+len(keyvals)) + fkv = append(fkv, l.prefix...) + fkv = append(fkv, keyvals...) + } + } else { + fkv = keyvals + } + if f.filter != nil && f.filter(level, fkv...) { + return nil + } + if len(f.key) > 0 || len(f.value) > 0 { + for i := 0; i < len(keyvals); i += 2 { + v := i + 1 + if v >= len(keyvals) { + continue + } + if _, ok := f.key[keyvals[i]]; ok { + keyvals[v] = fuzzyStr + } + if _, ok := f.value[keyvals[v]]; ok { + keyvals[v] = fuzzyStr + } + } + } + return f.logger.Log(level, keyvals...) +} diff --git a/log/filter_test.go b/log/filter_test.go new file mode 100644 index 0000000..d3c8d54 --- /dev/null +++ b/log/filter_test.go @@ -0,0 +1,133 @@ +package log + +import ( + "bytes" + "io" + "testing" +) + +func TestFilterAll(t *testing.T) { + logger := With(DefaultLogger, "ts", DefaultTimestamp, "caller", DefaultCaller) + log := NewHelper(NewFilter(logger, + FilterLevel(LevelDebug), + FilterKey("username"), + FilterValue("hello"), + FilterFunc(testFilterFunc), + )) + log.Log(LevelDebug, "msg", "test debug") + log.Info("hello") + log.Infow("password", "123456") + log.Infow("username", "kratos") + log.Warn("warn log") +} + +func TestFilterLevel(t *testing.T) { + logger := With(DefaultLogger, "ts", DefaultTimestamp, "caller", DefaultCaller) + log := NewHelper(NewFilter(NewFilter(logger, FilterLevel(LevelWarn)))) + log.Log(LevelDebug, "msg1", "te1st debug") + log.Debug("test debug") + log.Debugf("test %s", "debug") + log.Debugw("log", "test debug") + log.Warn("warn log") +} + +func TestFilterCaller(t *testing.T) { + logger := With(DefaultLogger, "ts", DefaultTimestamp, "caller", DefaultCaller) + log := NewFilter(logger) + _ = log.Log(LevelDebug, "msg1", "te1st debug") + logHelper := NewHelper(NewFilter(logger)) + logHelper.Log(LevelDebug, "msg1", "te1st debug") +} + +func TestFilterKey(t *testing.T) { + logger := With(DefaultLogger, "ts", DefaultTimestamp, "caller", DefaultCaller) + log := NewHelper(NewFilter(logger, FilterKey("password"))) + log.Debugw("password", "123456") +} + +func TestFilterValue(t *testing.T) { + logger := With(DefaultLogger, "ts", DefaultTimestamp, "caller", DefaultCaller) + log := NewHelper(NewFilter(logger, FilterValue("debug"))) + log.Debugf("test %s", "debug") +} + +func TestFilterFunc(t *testing.T) { + logger := With(DefaultLogger, "ts", DefaultTimestamp, "caller", DefaultCaller) + log := NewHelper(NewFilter(logger, FilterFunc(testFilterFunc))) + log.Debug("debug level") + log.Infow("password", "123456") +} + +func BenchmarkFilterKey(b *testing.B) { + log := NewHelper(NewFilter(NewStdLogger(io.Discard), FilterKey("password"))) + for i := 0; i < b.N; i++ { + log.Infow("password", "123456") + } +} + +func BenchmarkFilterValue(b *testing.B) { + log := NewHelper(NewFilter(NewStdLogger(io.Discard), FilterValue("password"))) + for i := 0; i < b.N; i++ { + log.Infow("password") + } +} + +func BenchmarkFilterFunc(b *testing.B) { + log := NewHelper(NewFilter(NewStdLogger(io.Discard), FilterFunc(testFilterFunc))) + for i := 0; i < b.N; i++ { + log.Info("password", "123456") + } +} + +func testFilterFunc(level Level, keyvals ...interface{}) bool { + if level == LevelWarn { + return true + } + for i := 0; i < len(keyvals); i++ { + if keyvals[i] == "password" { + keyvals[i+1] = fuzzyStr + } + } + return false +} + +func TestFilterFuncWitchLoggerPrefix(t *testing.T) { + buf := new(bytes.Buffer) + tests := []struct { + logger Logger + want string + }{ + { + logger: NewFilter(With(NewStdLogger(buf), "caller", "caller", "prefix", "whaterver"), FilterFunc(testFilterFuncWithLoggerPrefix)), + want: "", + }, + { + logger: NewFilter(With(NewStdLogger(buf), "caller", "caller"), FilterFunc(testFilterFuncWithLoggerPrefix)), + want: "INFO caller=caller msg=msg\n", + }, + } + + for _, tt := range tests { + err := tt.logger.Log(LevelInfo, "msg", "msg") + if err != nil { + t.Fatal("err should be nil") + } + got := buf.String() + if got != tt.want { + t.Fatalf("filter should catch prefix, want %s, got %s.", tt.want, got) + } + buf.Reset() + } +} + +func testFilterFuncWithLoggerPrefix(level Level, keyvals ...interface{}) bool { + if level == LevelWarn { + return true + } + for i := 0; i < len(keyvals); i += 2 { + if keyvals[i] == "prefix" { + return true + } + } + return false +} diff --git a/log/global.go b/log/global.go new file mode 100644 index 0000000..0e98bae --- /dev/null +++ b/log/global.go @@ -0,0 +1,122 @@ +package log + +import ( + "sync" +) + +// globalLogger is designed as a global logger in current process. +var global = &loggerAppliance{} + +// loggerAppliance is the proxy of `Logger` to +// make logger change will affect all sub-logger. +type loggerAppliance struct { + lock sync.Mutex + Logger + helper *Helper +} + +func init() { + global.SetLogger(DefaultLogger) +} + +func (a *loggerAppliance) SetLogger(in Logger) { + a.lock.Lock() + defer a.lock.Unlock() + a.Logger = in + a.helper = NewHelper(a.Logger) +} + +func (a *loggerAppliance) GetLogger() Logger { + return a.Logger +} + +// SetLogger should be called before any other log call. +// And it is NOT THREAD SAFE. +func SetLogger(logger Logger) { + global.SetLogger(logger) +} + +// GetLogger returns global logger appliance as logger in current process. +func GetLogger() Logger { + return global +} + +// Log Print log by level and keyvals. +func Log(level Level, keyvals ...interface{}) { + global.helper.Log(level, keyvals...) +} + +// Debug logs a message at debug level. +func Debug(a ...interface{}) { + global.helper.Debug(a...) +} + +// Debugf logs a message at debug level. +func Debugf(format string, a ...interface{}) { + global.helper.Debugf(format, a...) +} + +// Debugw logs a message at debug level. +func Debugw(keyvals ...interface{}) { + global.helper.Debugw(keyvals...) +} + +// Info logs a message at info level. +func Info(a ...interface{}) { + global.helper.Info(a...) +} + +// Infof logs a message at info level. +func Infof(format string, a ...interface{}) { + global.helper.Infof(format, a...) +} + +// Infow logs a message at info level. +func Infow(keyvals ...interface{}) { + global.helper.Infow(keyvals...) +} + +// Warn logs a message at warn level. +func Warn(a ...interface{}) { + global.helper.Warn(a...) +} + +// Warnf logs a message at warnf level. +func Warnf(format string, a ...interface{}) { + global.helper.Warnf(format, a...) +} + +// Warnw logs a message at warnf level. +func Warnw(keyvals ...interface{}) { + global.helper.Warnw(keyvals...) +} + +// Error logs a message at error level. +func Error(a ...interface{}) { + global.helper.Error(a...) +} + +// Errorf logs a message at error level. +func Errorf(format string, a ...interface{}) { + global.helper.Errorf(format, a...) +} + +// Errorw logs a message at error level. +func Errorw(keyvals ...interface{}) { + global.helper.Errorw(keyvals...) +} + +// Fatal logs a message at fatal level. +func Fatal(a ...interface{}) { + global.helper.Fatal(a...) +} + +// Fatalf logs a message at fatal level. +func Fatalf(format string, a ...interface{}) { + global.helper.Fatalf(format, a...) +} + +// Fatalw logs a message at fatal level. +func Fatalw(keyvals ...interface{}) { + global.helper.Fatalw(keyvals...) +} diff --git a/log/global_test.go b/log/global_test.go new file mode 100644 index 0000000..5d2f2c8 --- /dev/null +++ b/log/global_test.go @@ -0,0 +1,85 @@ +package log + +import ( + "bytes" + "fmt" + "os" + "strings" + "testing" +) + +func TestGlobalLog(t *testing.T) { + buffer := &bytes.Buffer{} + SetLogger(NewStdLogger(buffer)) + + testCases := []struct { + level Level + content []interface{} + }{ + { + LevelDebug, + []interface{}{"test debug"}, + }, + { + LevelInfo, + []interface{}{"test info"}, + }, + { + LevelInfo, + []interface{}{"test %s", "info"}, + }, + { + LevelWarn, + []interface{}{"test warn"}, + }, + { + LevelError, + []interface{}{"test error"}, + }, + { + LevelError, + []interface{}{"test %s", "error"}, + }, + } + + expected := []string{} + for _, tc := range testCases { + msg := fmt.Sprintf(tc.content[0].(string), tc.content[1:]...) + switch tc.level { + case LevelDebug: + Debugf(tc.content[0].(string), tc.content[1:]...) + expected = append(expected, fmt.Sprintf("%s msg=%s", "DEBUG", msg)) + case LevelInfo: + Infof(tc.content[0].(string), tc.content[1:]...) + expected = append(expected, fmt.Sprintf("%s msg=%s", "INFO", msg)) + case LevelWarn: + Warnf(tc.content[0].(string), tc.content[1:]...) + expected = append(expected, fmt.Sprintf("%s msg=%s", "WARN", msg)) + case LevelError: + Errorf(tc.content[0].(string), tc.content[1:]...) + expected = append(expected, fmt.Sprintf("%s msg=%s", "ERROR", msg)) + } + } + expected = append(expected, "") + + t.Logf("Content: %s", buffer.String()) + if buffer.String() != strings.Join(expected, "\n") { + t.Errorf("Expected: %s, got: %s", strings.Join(expected, "\n"), buffer.String()) + } +} + +func TestGlobalLogUpdate(t *testing.T) { + l := &loggerAppliance{} + l.SetLogger(NewStdLogger(os.Stdout)) + LOG := NewHelper(l) + LOG.Info("Log to stdout") + + buffer := &bytes.Buffer{} + l.SetLogger(NewStdLogger(buffer)) + LOG.Info("Log to buffer") + + expected := "INFO msg=Log to buffer\n" + if buffer.String() != expected { + t.Errorf("Expected: %s, got: %s", expected, buffer.String()) + } +} diff --git a/log/helper.go b/log/helper.go new file mode 100644 index 0000000..b01c947 --- /dev/null +++ b/log/helper.go @@ -0,0 +1,130 @@ +package log + +import ( + "context" + "fmt" + "os" +) + +// DefaultMessageKey default message key. +var DefaultMessageKey = "msg" + +// Option is Helper option. +type Option func(*Helper) + +// Helper is a logger helper. +type Helper struct { + logger Logger + msgKey string +} + +// WithMessageKey with message key. +func WithMessageKey(k string) Option { + return func(opts *Helper) { + opts.msgKey = k + } +} + +// NewHelper new a logger helper. +func NewHelper(logger Logger, opts ...Option) *Helper { + options := &Helper{ + msgKey: DefaultMessageKey, // default message key + logger: logger, + } + for _, o := range opts { + o(options) + } + return options +} + +// WithContext returns a shallow copy of h with its context changed +// to ctx. The provided ctx must be non-nil. +func (h *Helper) WithContext(ctx context.Context) *Helper { + return &Helper{ + msgKey: h.msgKey, + logger: WithContext(ctx, h.logger), + } +} + +// Log Print log by level and keyvals. +func (h *Helper) Log(level Level, keyvals ...interface{}) { + _ = h.logger.Log(level, keyvals...) +} + +// Debug logs a message at debug level. +func (h *Helper) Debug(a ...interface{}) { + _ = h.logger.Log(LevelDebug, h.msgKey, fmt.Sprint(a...)) +} + +// Debugf logs a message at debug level. +func (h *Helper) Debugf(format string, a ...interface{}) { + _ = h.logger.Log(LevelDebug, h.msgKey, fmt.Sprintf(format, a...)) +} + +// Debugw logs a message at debug level. +func (h *Helper) Debugw(keyvals ...interface{}) { + _ = h.logger.Log(LevelDebug, keyvals...) +} + +// Info logs a message at info level. +func (h *Helper) Info(a ...interface{}) { + _ = h.logger.Log(LevelInfo, h.msgKey, fmt.Sprint(a...)) +} + +// Infof logs a message at info level. +func (h *Helper) Infof(format string, a ...interface{}) { + _ = h.logger.Log(LevelInfo, h.msgKey, fmt.Sprintf(format, a...)) +} + +// Infow logs a message at info level. +func (h *Helper) Infow(keyvals ...interface{}) { + _ = h.logger.Log(LevelInfo, keyvals...) +} + +// Warn logs a message at warn level. +func (h *Helper) Warn(a ...interface{}) { + _ = h.logger.Log(LevelWarn, h.msgKey, fmt.Sprint(a...)) +} + +// Warnf logs a message at warnf level. +func (h *Helper) Warnf(format string, a ...interface{}) { + _ = h.logger.Log(LevelWarn, h.msgKey, fmt.Sprintf(format, a...)) +} + +// Warnw logs a message at warnf level. +func (h *Helper) Warnw(keyvals ...interface{}) { + _ = h.logger.Log(LevelWarn, keyvals...) +} + +// Error logs a message at error level. +func (h *Helper) Error(a ...interface{}) { + _ = h.logger.Log(LevelError, h.msgKey, fmt.Sprint(a...)) +} + +// Errorf logs a message at error level. +func (h *Helper) Errorf(format string, a ...interface{}) { + _ = h.logger.Log(LevelError, h.msgKey, fmt.Sprintf(format, a...)) +} + +// Errorw logs a message at error level. +func (h *Helper) Errorw(keyvals ...interface{}) { + _ = h.logger.Log(LevelError, keyvals...) +} + +// Fatal logs a message at fatal level. +func (h *Helper) Fatal(a ...interface{}) { + _ = h.logger.Log(LevelFatal, h.msgKey, fmt.Sprint(a...)) + os.Exit(1) +} + +// Fatalf logs a message at fatal level. +func (h *Helper) Fatalf(format string, a ...interface{}) { + _ = h.logger.Log(LevelFatal, h.msgKey, fmt.Sprintf(format, a...)) + os.Exit(1) +} + +// Fatalw logs a message at fatal level. +func (h *Helper) Fatalw(keyvals ...interface{}) { + _ = h.logger.Log(LevelFatal, keyvals...) + os.Exit(1) +} diff --git a/log/helper_test.go b/log/helper_test.go new file mode 100644 index 0000000..dc888a3 --- /dev/null +++ b/log/helper_test.go @@ -0,0 +1,79 @@ +package log + +import ( + "context" + "io" + "os" + "testing" +) + +func TestHelper(t *testing.T) { + logger := With(DefaultLogger, "ts", DefaultTimestamp, "caller", DefaultCaller) + log := NewHelper(logger) + + log.Log(LevelDebug, "msg", "test debug") + log.Debug("test debug") + log.Debugf("test %s", "debug") + log.Debugw("log", "test debug") + + log.Warn("test warn") + log.Warnf("test %s", "warn") + log.Warnw("log", "test warn") +} + +func TestHelperWithMsgKey(t *testing.T) { + logger := With(DefaultLogger, "ts", DefaultTimestamp, "caller", DefaultCaller) + log := NewHelper(logger, WithMessageKey("message")) + log.Debugf("test %s", "debug") + log.Debugw("log", "test debug") +} + +func TestHelperLevel(t *testing.T) { + log := NewHelper(DefaultLogger) + log.Debug("test debug") + log.Info("test info") + log.Infof("test %s", "info") + log.Warn("test warn") + log.Error("test error") + log.Errorf("test %s", "error") + log.Errorw("log", "test error") +} + +func BenchmarkHelperPrint(b *testing.B) { + log := NewHelper(NewStdLogger(io.Discard)) + for i := 0; i < b.N; i++ { + log.Debug("test") + } +} + +func BenchmarkHelperPrintf(b *testing.B) { + log := NewHelper(NewStdLogger(io.Discard)) + for i := 0; i < b.N; i++ { + log.Debugf("%s", "test") + } +} + +func BenchmarkHelperPrintw(b *testing.B) { + log := NewHelper(NewStdLogger(io.Discard)) + for i := 0; i < b.N; i++ { + log.Debugw("key", "value") + } +} + +type traceKey struct{} + +func TestContext(t *testing.T) { + logger := With(NewStdLogger(os.Stdout), + "trace", Trace(), + ) + log := NewHelper(logger) + ctx := context.WithValue(context.Background(), traceKey{}, "2233") + log.WithContext(ctx).Info("got trace!") +} + +func Trace() Valuer { + return func(ctx context.Context) interface{} { + s := ctx.Value(traceKey{}).(string) + return s + } +} diff --git a/log/level.go b/log/level.go new file mode 100644 index 0000000..22f41c7 --- /dev/null +++ b/log/level.go @@ -0,0 +1,56 @@ +package log + +import "strings" + +// Level is a logger level. +type Level int8 + +// LevelKey is logger level key. +const LevelKey = "level" + +const ( + // LevelDebug is logger debug level. + LevelDebug Level = iota - 1 + // LevelInfo is logger info level. + LevelInfo + // LevelWarn is logger warn level. + LevelWarn + // LevelError is logger error level. + LevelError + // LevelFatal is logger fatal level + LevelFatal +) + +func (l Level) String() string { + switch l { + case LevelDebug: + return "DEBUG" + case LevelInfo: + return "INFO" + case LevelWarn: + return "WARN" + case LevelError: + return "ERROR" + case LevelFatal: + return "FATAL" + default: + return "" + } +} + +// ParseLevel parses a level string into a logger Level value. +func ParseLevel(s string) Level { + switch strings.ToUpper(s) { + case "DEBUG": + return LevelDebug + case "INFO": + return LevelInfo + case "WARN": + return LevelWarn + case "ERROR": + return LevelError + case "FATAL": + return LevelFatal + } + return LevelInfo +} diff --git a/log/level_test.go b/log/level_test.go new file mode 100644 index 0000000..d368829 --- /dev/null +++ b/log/level_test.go @@ -0,0 +1,95 @@ +package log + +import "testing" + +func TestLevel_String(t *testing.T) { + tests := []struct { + name string + l Level + want string + }{ + { + name: "DEBUG", + l: LevelDebug, + want: "DEBUG", + }, + { + name: "INFO", + l: LevelInfo, + want: "INFO", + }, + { + name: "WARN", + l: LevelWarn, + want: "WARN", + }, + { + name: "ERROR", + l: LevelError, + want: "ERROR", + }, + { + name: "FATAL", + l: LevelFatal, + want: "FATAL", + }, + { + name: "other", + l: 10, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.l.String(); got != tt.want { + t.Errorf("String() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestParseLevel(t *testing.T) { + tests := []struct { + name string + s string + want Level + }{ + { + name: "DEBUG", + want: LevelDebug, + s: "DEBUG", + }, + { + name: "INFO", + want: LevelInfo, + s: "INFO", + }, + { + name: "WARN", + want: LevelWarn, + s: "WARN", + }, + { + name: "ERROR", + want: LevelError, + s: "ERROR", + }, + { + name: "FATAL", + want: LevelFatal, + s: "FATAL", + }, + { + name: "other", + want: LevelInfo, + s: "other", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ParseLevel(tt.s); got != tt.want { + t.Errorf("ParseLevel() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..1b612f8 --- /dev/null +++ b/log/log.go @@ -0,0 +1,71 @@ +package log + +import ( + "context" + "log" +) + +// DefaultLogger is default logger. +var DefaultLogger = NewStdLogger(log.Writer()) + +// Logger is a logger interface. +type Logger interface { + Log(level Level, keyvals ...interface{}) error +} + +type logger struct { + logs []Logger + prefix []interface{} + hasValuer bool + ctx context.Context +} + +func (c *logger) Log(level Level, keyvals ...interface{}) error { + kvs := make([]interface{}, 0, len(c.prefix)+len(keyvals)) + kvs = append(kvs, c.prefix...) + if c.hasValuer { + bindValues(c.ctx, kvs) + } + kvs = append(kvs, keyvals...) + for _, l := range c.logs { + if err := l.Log(level, kvs...); err != nil { + return err + } + } + return nil +} + +// With with logger fields. +func With(l Logger, kv ...interface{}) Logger { + if c, ok := l.(*logger); ok { + kvs := make([]interface{}, 0, len(c.prefix)+len(kv)) + kvs = append(kvs, kv...) + kvs = append(kvs, c.prefix...) + return &logger{ + logs: c.logs, + prefix: kvs, + hasValuer: containsValuer(kvs), + ctx: c.ctx, + } + } + return &logger{logs: []Logger{l}, prefix: kv, hasValuer: containsValuer(kv)} +} + +// WithContext returns a shallow copy of l with its context changed +// to ctx. The provided ctx must be non-nil. +func WithContext(ctx context.Context, l Logger) Logger { + if c, ok := l.(*logger); ok { + return &logger{ + logs: c.logs, + prefix: c.prefix, + hasValuer: c.hasValuer, + ctx: ctx, + } + } + return &logger{logs: []Logger{l}, ctx: ctx} +} + +// MultiLogger wraps multi logger. +func MultiLogger(logs ...Logger) Logger { + return &logger{logs: logs} +} diff --git a/log/log_test.go b/log/log_test.go new file mode 100644 index 0000000..dea00f4 --- /dev/null +++ b/log/log_test.go @@ -0,0 +1,25 @@ +package log + +import ( + "context" + "os" + "testing" +) + +func TestInfo(t *testing.T) { + logger := DefaultLogger + logger = With(logger, "ts", DefaultTimestamp, "caller", DefaultCaller) + _ = logger.Log(LevelInfo, "key1", "value1") +} + +func TestWrapper(t *testing.T) { + out := NewStdLogger(os.Stdout) + err := NewStdLogger(os.Stderr) + + l := With(MultiLogger(out, err), "ts", DefaultTimestamp, "caller", DefaultCaller) + _ = l.Log(LevelInfo, "msg", "test") +} + +func TestWithContext(t *testing.T) { + WithContext(context.Background(), nil) +} diff --git a/log/std.go b/log/std.go new file mode 100644 index 0000000..8d79f5c --- /dev/null +++ b/log/std.go @@ -0,0 +1,47 @@ +package log + +import ( + "bytes" + "fmt" + "io" + "log" + "sync" +) + +var _ Logger = (*stdLogger)(nil) + +type stdLogger struct { + log *log.Logger + pool *sync.Pool +} + +// NewStdLogger new a logger with writer. +func NewStdLogger(w io.Writer) Logger { + return &stdLogger{ + log: log.New(w, "", 0), + pool: &sync.Pool{ + New: func() interface{} { + return new(bytes.Buffer) + }, + }, + } +} + +// Log print the kv pairs log. +func (l *stdLogger) Log(level Level, keyvals ...interface{}) error { + if len(keyvals) == 0 { + return nil + } + if (len(keyvals) & 1) == 1 { + keyvals = append(keyvals, "KEYVALS UNPAIRED") + } + buf := l.pool.Get().(*bytes.Buffer) + buf.WriteString(level.String()) + for i := 0; i < len(keyvals); i += 2 { + _, _ = fmt.Fprintf(buf, " %s=%v", keyvals[i], keyvals[i+1]) + } + _ = l.log.Output(4, buf.String()) //nolint:gomnd + buf.Reset() + l.pool.Put(buf) + return nil +} diff --git a/log/std_test.go b/log/std_test.go new file mode 100644 index 0000000..e9da773 --- /dev/null +++ b/log/std_test.go @@ -0,0 +1,17 @@ +package log + +import "testing" + +func TestStdLogger(t *testing.T) { + logger := DefaultLogger + logger = With(logger, "caller", DefaultCaller, "ts", DefaultTimestamp) + + _ = logger.Log(LevelInfo, "msg", "test debug") + _ = logger.Log(LevelInfo, "msg", "test info") + _ = logger.Log(LevelInfo, "msg", "test warn") + _ = logger.Log(LevelInfo, "msg", "test error") + _ = logger.Log(LevelDebug, "singular") + + logger2 := DefaultLogger + _ = logger2.Log(LevelDebug) +} diff --git a/log/value.go b/log/value.go new file mode 100644 index 0000000..86e9155 --- /dev/null +++ b/log/value.go @@ -0,0 +1,71 @@ +package log + +import ( + "context" + "runtime" + "strconv" + "strings" + "time" +) + +var ( + defaultDepth = 3 + // DefaultCaller is a Valuer that returns the file and line. + DefaultCaller = Caller(defaultDepth) + + // DefaultTimestamp is a Valuer that returns the current wallclock time. + DefaultTimestamp = Timestamp(time.RFC3339) +) + +// Valuer is returns a log value. +type Valuer func(ctx context.Context) interface{} + +// Value return the function value. +func Value(ctx context.Context, v interface{}) interface{} { + if v, ok := v.(Valuer); ok { + return v(ctx) + } + return v +} + +// Caller returns a Valuer that returns a pkg/file:line description of the caller. +func Caller(depth int) Valuer { + return func(context.Context) interface{} { + d := depth + _, file, line, _ := runtime.Caller(d) + if strings.LastIndex(file, "/log/filter.go") > 0 { + d++ + _, file, line, _ = runtime.Caller(d) + } + if strings.LastIndex(file, "/log/helper.go") > 0 { + d++ + _, file, line, _ = runtime.Caller(d) + } + idx := strings.LastIndexByte(file, '/') + return file[idx+1:] + ":" + strconv.Itoa(line) + } +} + +// Timestamp returns a timestamp Valuer with a custom time format. +func Timestamp(layout string) Valuer { + return func(context.Context) interface{} { + return time.Now().Format(layout) + } +} + +func bindValues(ctx context.Context, keyvals []interface{}) { + for i := 1; i < len(keyvals); i += 2 { + if v, ok := keyvals[i].(Valuer); ok { + keyvals[i] = v(ctx) + } + } +} + +func containsValuer(keyvals []interface{}) bool { + for i := 1; i < len(keyvals); i += 2 { + if _, ok := keyvals[i].(Valuer); ok { + return true + } + } + return false +} diff --git a/log/value_test.go b/log/value_test.go new file mode 100644 index 0000000..fd9167a --- /dev/null +++ b/log/value_test.go @@ -0,0 +1,30 @@ +package log + +import ( + "context" + "testing" +) + +func TestValue(t *testing.T) { + logger := DefaultLogger + logger = With(logger, "ts", DefaultTimestamp, "caller", DefaultCaller) + _ = logger.Log(LevelInfo, "msg", "helloworld") + + logger = DefaultLogger + logger = With(logger) + _ = logger.Log(LevelDebug, "msg", "helloworld") + + var v1 interface{} + got := Value(context.Background(), v1) + if got != v1 { + t.Errorf("Value() = %v, want %v", got, v1) + } + var v2 Valuer = func(ctx context.Context) interface{} { + return 3 + } + got = Value(context.Background(), v2) + res := got.(int) + if res != 3 { + t.Errorf("Value() = %v, want %v", res, 3) + } +} diff --git a/parser.go b/parser.go index 6672dc8..6c34281 100644 --- a/parser.go +++ b/parser.go @@ -6,10 +6,20 @@ import ( "errors" "fmt" "io" + "os" + + "github.com/saferwall/elf/log" "github.com/saferwall/binstream" ) +// Options that influence the PE parsing behaviour. +type Options struct { + + // A custom logger. + Logger log.Logger +} + // Parser implements a parsing engine for the ELF file format. type Parser struct { fs binstream.Stream @@ -17,27 +27,60 @@ type Parser struct { } // New creates a new instance of parser. -func New(filename string) (*Parser, error) { +func New(filename string, opts *Options) (*Parser, error) { fs, err := binstream.NewFileStream(filename) if err != nil { return nil, err } + file := File{} + if opts != nil { + file.opts = opts + } else { + file.opts = &Options{} + } + + var logger log.Logger + if file.opts.Logger == nil { + logger = log.NewStdLogger(os.Stdout) + file.logger = log.NewHelper(log.NewFilter(logger, + log.FilterLevel(log.LevelError))) + } else { + file.logger = log.NewHelper(file.opts.Logger) + } + p := &Parser{ fs: fs, - F: &File{}, + F: &file, } return p, nil } // NewBytes creates a new instance of parser from a byte slice representig the ELF binary. -func NewBytes(data []byte) (*Parser, error) { +func NewBytes(data []byte, opts *Options) (*Parser, error) { fs, err := binstream.NewByteStream(data) if err != nil { return nil, err } + + file := File{} + if opts != nil { + file.opts = opts + } else { + file.opts = &Options{} + } + + var logger log.Logger + if file.opts.Logger == nil { + logger = log.NewStdLogger(os.Stdout) + file.logger = log.NewHelper(log.NewFilter(logger, + log.FilterLevel(log.LevelError))) + } else { + file.logger = log.NewHelper(file.opts.Logger) + } + p := &Parser{ fs: fs, - F: &File{}, + F: &file, } return p, nil } diff --git a/parser_test.go b/parser_test.go index 6b8d178..6f75548 100644 --- a/parser_test.go +++ b/parser_test.go @@ -86,7 +86,7 @@ func TestParser(t *testing.T) { } for _, tt := range testCases { - p, err := New(tt.path) + p, err := New(tt.path, nil) if err != nil { t.Fatal("failed to create new parser with error :", err) } @@ -473,7 +473,7 @@ func TestParser(t *testing.T) { } for _, tt := range testCases { - p, err := New(tt.path) + p, err := New(tt.path, nil) if err != nil { t.Fatal("failed to create new parser with error :", err) } @@ -898,7 +898,7 @@ func TestParser(t *testing.T) { } for _, tt := range testCases { - p, err := New(tt.path) + p, err := New(tt.path, nil) if err != nil { t.Fatal("failed to create new parser with error :", err) } @@ -1086,7 +1086,7 @@ func TestParser(t *testing.T) { } for _, tt := range testCases { - p, err := New(tt.path) + p, err := New(tt.path, nil) if err != nil { t.Fatal("failed to create new parser with error :", err) } @@ -1514,7 +1514,7 @@ func TestParser(t *testing.T) { } for _, tt := range testCases { - p, err := New(tt.path) + p, err := New(tt.path, nil) if err != nil { t.Fatal("failed to create new parser with error :", err) }