diff --git a/cmd/http.go b/cmd/http.go index 199f990..2592148 100644 --- a/cmd/http.go +++ b/cmd/http.go @@ -3,18 +3,20 @@ package cmd import ( "encoding/json" "fmt" - "log/slog" "net/http" "os" + "time" - sloghttp "github.com/samber/slog-http" + "github.com/google/uuid" "github.com/spf13/cobra" + "github.com/truvami/decoder/internal/logger" "github.com/truvami/decoder/pkg/decoder" "github.com/truvami/decoder/pkg/decoder/nomadxl/v1" "github.com/truvami/decoder/pkg/decoder/nomadxs/v1" "github.com/truvami/decoder/pkg/decoder/tagsl/v1" "github.com/truvami/decoder/pkg/decoder/tagxl/v1" "github.com/truvami/decoder/pkg/loracloud" + "go.uber.org/zap" ) var host string @@ -34,7 +36,7 @@ var httpCmd = &cobra.Command{ Short: "Start the HTTP server for the decoder.", Run: func(cmd *cobra.Command, args []string) { if len(accessToken) == 0 { - slog.Warn("no access token provided for loracloud API") + logger.Logger.Warn("no access token provided for loracloud API") } router := http.NewServeMux() @@ -74,21 +76,20 @@ var httpCmd = &cobra.Command{ } // middleware - handler := sloghttp.Recovery(router) - handler = sloghttp.New(slog.Default())(handler) + handler := loggingMiddleware(logger.Logger, router) - slog.Info("starting HTTP server", slog.String("host", host), slog.Uint64("port", uint64(port))) + logger.Logger.Info("starting HTTP server", zap.String("host", host), zap.Uint64("port", uint64(port))) err := http.ListenAndServe(fmt.Sprintf("%v:%v", host, port), handler) if err != nil { - slog.Error("error while starting HTTP server", slog.Any("error", err)) + logger.Logger.Error("error while starting HTTP server", zap.Error(err)) os.Exit(1) } }, } func addDecoder(router *http.ServeMux, path string, decoder decoder.Decoder) { - slog.Debug("adding decoder", slog.String("path", path)) + logger.Logger.Debug("adding decoder", zap.String("path", path)) router.HandleFunc("POST /"+path, getHandler(decoder)) } @@ -103,46 +104,46 @@ func getHandler(decoder decoder.Decoder) func(http.ResponseWriter, *http.Request // decode the request var req request - slog.Debug("decoding request") + logger.Logger.Debug("decoding request") err := json.NewDecoder(r.Body).Decode(&req) if err != nil { - slog.Error("error while decoding request", slog.Any("error", err)) + logger.Logger.Error("error while decoding request", zap.Error(err)) setHeaders(w, http.StatusBadRequest) _, err = w.Write([]byte(err.Error())) if err != nil { - slog.Error("error while sending response", slog.Any("error", err)) + logger.Logger.Error("error while sending response", zap.Error(err)) } return } // decode the payload - slog.Debug("decoding payload") + logger.Logger.Debug("decoding payload") data, metadata, err := decoder.Decode(req.Payload, req.Port, req.DevEUI) if err != nil { - slog.Error("error while decoding payload", slog.Any("error", err)) + logger.Logger.Error("error while decoding payload", zap.Error(err)) setHeaders(w, http.StatusBadRequest) _, err = w.Write([]byte(err.Error())) if err != nil { - slog.Error("error while sending response", slog.Any("error", err)) + logger.Logger.Error("error while sending response", zap.Error(err)) } return } // data to json - slog.Debug("encoding response") + logger.Logger.Debug("encoding response") data, err = json.Marshal(map[string]interface{}{ "data": data, "metadata": metadata, }) if err != nil { - slog.Error("error while encoding response", slog.Any("error", err)) + logger.Logger.Error("error while encoding response", zap.Error(err)) setHeaders(w, http.StatusInternalServerError) _, err = w.Write([]byte(err.Error())) if err != nil { - slog.Error("error while sending response", slog.Any("error", err)) + logger.Logger.Error("error while sending response", zap.Error(err)) } return } @@ -151,14 +152,19 @@ func getHandler(decoder decoder.Decoder) func(http.ResponseWriter, *http.Request setHeaders(w, http.StatusOK) _, err = w.Write(data.([]byte)) if err != nil { - slog.Error("error while sending response", slog.Any("error", err)) + logger.Logger.Error("error while sending response", zap.Error(err)) return } - slog.Debug("response sent", slog.Any("response", string(data.([]byte)))) + logger.Logger.Debug("response sent", zap.Any("response", string(data.([]byte)))) } } +type responseWriter struct { + http.ResponseWriter + statusCode int +} + func setHeaders(w http.ResponseWriter, status int) { w.Header().Set("Content-Type", "application/json") w.Header().Set("Access-Control-Allow-Origin", "*") @@ -170,6 +176,34 @@ func healthHandler(w http.ResponseWriter, r *http.Request) { setHeaders(w, http.StatusOK) _, err := w.Write([]byte("OK")) if err != nil { - slog.Error("error while sending response", slog.Any("error", err)) + logger.Logger.Error("error while sending response", zap.Error(err)) } } + +func loggingMiddleware(logger *zap.Logger, next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // generate a unique request ID + requestID := uuid.New().String() + w.Header().Set("X-Request-ID", requestID) + + // start timer + start := time.Now() + + // use a ResponseWriter wrapper to capture the status code + rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK} + + // process the request + next.ServeHTTP(rw, r) + + // log the details + logger.Info("HTTP request", + zap.String("requestId", requestID), + zap.String("method", r.Method), + zap.String("url", r.URL.String()), + zap.Int("status", rw.statusCode), + zap.String("remoteAddress", r.RemoteAddr), + zap.String("userAgent", r.UserAgent()), + zap.Duration("latency", time.Since(start)), + ) + }) +} diff --git a/cmd/http_test.go b/cmd/http_test.go index 81c40a8..cb60d0e 100644 --- a/cmd/http_test.go +++ b/cmd/http_test.go @@ -10,10 +10,14 @@ import ( "testing" "time" + "github.com/truvami/decoder/internal/logger" "github.com/truvami/decoder/pkg/decoder/tagsl/v1" ) func TestAddDecoder(t *testing.T) { + logger.NewLogger() + defer logger.Sync() + router := http.NewServeMux() path := "test/path" decoder := tagsl.NewTagSLv1Decoder() diff --git a/cmd/nomadxl.go b/cmd/nomadxl.go index c618c28..eba6750 100644 --- a/cmd/nomadxl.go +++ b/cmd/nomadxl.go @@ -1,11 +1,12 @@ package cmd import ( - "log/slog" "strconv" "github.com/spf13/cobra" + "github.com/truvami/decoder/internal/logger" "github.com/truvami/decoder/pkg/decoder/nomadxl/v1" + "go.uber.org/zap" ) func init() { @@ -17,21 +18,21 @@ var nomadxlCmd = &cobra.Command{ Short: "decode nomad XL payloads", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { - slog.Debug("initializing nomadxs decoder") + logger.Logger.Debug("initializing nomadxs decoder") d := nomadxl.NewNomadXLv1Decoder( nomadxl.WithAutoPadding(AutoPadding), ) port, err := strconv.Atoi(args[0]) if err != nil { - slog.Error("error while parsing port", slog.Any("error", err), slog.String("port", args[0])) + logger.Logger.Error("error while parsing port", zap.Error(err), zap.String("port", args[0])) return } - slog.Debug("port parsed successfully", slog.Int("port", port)) + logger.Logger.Debug("port parsed successfully", zap.Int("port", port)) data, metadata, err := d.Decode(args[1], int16(port), "") if err != nil { - slog.Error("error while decoding data", slog.Any("error", err)) + logger.Logger.Error("error while decoding data", zap.Error(err)) return } diff --git a/cmd/nomadxs.go b/cmd/nomadxs.go index 4f69365..0fc777e 100644 --- a/cmd/nomadxs.go +++ b/cmd/nomadxs.go @@ -1,11 +1,12 @@ package cmd import ( - "log/slog" "strconv" "github.com/spf13/cobra" + "github.com/truvami/decoder/internal/logger" "github.com/truvami/decoder/pkg/decoder/nomadxs/v1" + "go.uber.org/zap" ) func init() { @@ -17,21 +18,21 @@ var nomadxsCmd = &cobra.Command{ Short: "decode nomad XS payloads", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { - slog.Debug("initializing nomadxs decoder") + logger.Logger.Debug("initializing nomadxs decoder") d := nomadxs.NewNomadXSv1Decoder( nomadxs.WithAutoPadding(AutoPadding), ) port, err := strconv.Atoi(args[0]) if err != nil { - slog.Error("error while parsing port", slog.Any("error", err), slog.String("port", args[0])) + logger.Logger.Error("error while parsing port", zap.Error(err), zap.String("port", args[0])) return } - slog.Debug("port parsed successfully", slog.Int("port", port)) + logger.Logger.Debug("port parsed successfully", zap.Int("port", port)) data, metadata, err := d.Decode(args[1], int16(port), "") if err != nil { - slog.Error("error while decoding data", slog.Any("error", err)) + logger.Logger.Error("error while decoding data", zap.Error(err)) return } diff --git a/cmd/root.go b/cmd/root.go index 3997f6c..b876fc5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,51 +1,47 @@ package cmd import ( - "log/slog" + "encoding/json" + "fmt" "os" "strings" "github.com/spf13/cobra" "github.com/spf13/viper" - "github.com/truvami/decoder/pkg/logger" + "github.com/truvami/decoder/internal/logger" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" ) var banner = []string{ - " _ _ ", + "\033[32m _ _ ", " | |_ _ __ _ ___ ____ _ _ __ ___ (_)", " | __| '__| | | \\ \\ / / _` | '_ ` _ \\| |", " | |_| | | |_| |\\ V / (_| | | | | | | |", - " \\__|_| \\__,_| \\_/ \\__,_|_| |_| |_|_|", + " \\__|_| \\__,_| \\_/ \\__,_|_| |_| |_|_|\033[0m", } var Debug bool -var Verbose bool var Json bool var AutoPadding bool func init() { - rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "Display more verbose output in console output. (default: false)") - err := viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose")) + rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "Display debugging output in the console. (default: \033[31mfalse\033[0m)") + err := viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) if err != nil { - slog.Error("error while binding verbose flag", slog.Any("error", err)) + logger.Logger.Error("error while binding debug flag", zap.Error(err)) } - rootCmd.PersistentFlags().BoolVarP(&Debug, "debug", "d", false, "Display debugging output in the console. (default: false)") - err = viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")) - if err != nil { - slog.Error("error while binding debug flag", slog.Any("error", err)) - } - - rootCmd.PersistentFlags().BoolVarP(&Json, "json", "j", false, "Output the result in JSON format. (default: false)") + rootCmd.PersistentFlags().BoolVarP(&Json, "json", "j", false, "Output the result in JSON format. (default: \033[31mfalse\033[0m)") err = viper.BindPFlag("json", rootCmd.PersistentFlags().Lookup("json")) if err != nil { - slog.Error("error while binding json flag", slog.Any("error", err)) + logger.Logger.Error("error while binding json flag", zap.Error(err)) } - rootCmd.PersistentFlags().BoolVarP(&AutoPadding, "auto-padding", "", false, "Enable automatic padding of payload. (default: false)\nWarning: this may lead to corrupted data.") + rootCmd.PersistentFlags().BoolVarP(&AutoPadding, "auto-padding", "", false, "Enable automatic padding of payload. (default: \033[31mfalse\033[0m)\n\033[33mWarning:\033[0m this may lead to corrupted data.") err = viper.BindPFlag("auto-padding", rootCmd.PersistentFlags().Lookup("auto-padding")) if err != nil { - slog.Error("error while binding auto-padding flag", slog.Any("error", err)) + logger.Logger.Error("error while binding auto-padding flag", zap.Error(err)) } } @@ -55,35 +51,64 @@ var rootCmd = &cobra.Command{ Long: strings.Join(banner, "\n") + ` A CLI tool to help decode @truvami payloads.`, -} - -func Execute() { - cobra.OnInitialize(func() { - opts := slog.HandlerOptions{ - Level: slog.LevelInfo, - } + PersistentPreRun: func(cmd *cobra.Command, args []string) { + options := []logger.Option{} if Debug { - opts.Level = slog.LevelDebug - opts.AddSource = true + options = append(options, logger.WithDebug()) } - var handler slog.Handler if Json { - handler = slog.NewJSONHandler(os.Stdout, &opts) - } else { - handler = logger.NewHandler(&opts) + // create a custom encoder + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "", // disable stack traces + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + } + + options = append(options, logger.WithEncoder(zapcore.NewJSONEncoder(encoderConfig))) } - slog.SetDefault(slog.New(handler)) - }) + logger.NewLogger(options...) + defer logger.Sync() + }, +} +func Execute() { if err := rootCmd.Execute(); err != nil { - slog.Error("error while executing command", slog.Any("error", err)) + logger.Logger.Error("error while executing command", zap.Error(err)) os.Exit(1) } } func printJSON(data interface{}, metadata interface{}) { - slog.Info("successfully decoded payload", slog.Any("data", data), slog.Any("metadata", metadata)) + if Json { + logger.Logger.Info("successfully decoded payload", zap.Any("data", data), zap.Any("metadata", metadata)) + return + } + + logger.Logger.Info("successfully decoded payload") + + // print data and metadata beautifully and formatted + marshaled, err := json.MarshalIndent(map[string]interface{}{ + "data": data, + "metadata": metadata, + }, "", " ") + + // handle marshaling error + if err != nil { + logger.Logger.Fatal("marshaling error", zap.Error(err)) + } + + // print the marshaled data + fmt.Println() + fmt.Println(string(marshaled)) + fmt.Println() } diff --git a/cmd/tagsl.go b/cmd/tagsl.go index f54dfd7..a9f2f4d 100644 --- a/cmd/tagsl.go +++ b/cmd/tagsl.go @@ -1,11 +1,12 @@ package cmd import ( - "log/slog" "strconv" "github.com/spf13/cobra" + "github.com/truvami/decoder/internal/logger" "github.com/truvami/decoder/pkg/decoder/tagsl/v1" + "go.uber.org/zap" ) func init() { @@ -17,21 +18,21 @@ var tagslCmd = &cobra.Command{ Short: "decode tag S / L payloads", Args: cobra.ExactArgs(2), Run: func(cmd *cobra.Command, args []string) { - slog.Debug("initializing tagsl decoder") + logger.Logger.Debug("initializing tagsl decoder") d := tagsl.NewTagSLv1Decoder( tagsl.WithAutoPadding(AutoPadding), ) port, err := strconv.Atoi(args[0]) if err != nil { - slog.Error("error while parsing port", slog.Any("error", err), slog.String("port", args[0])) + logger.Logger.Error("error while parsing port", zap.Error(err), zap.String("port", args[0])) return } - slog.Debug("port parsed successfully", slog.Int("port", port)) + logger.Logger.Debug("port parsed successfully", zap.Int("port", port)) data, metadata, err := d.Decode(args[1], int16(port), "") if err != nil { - slog.Error("error while decoding data", slog.Any("error", err)) + logger.Logger.Error("error while decoding data", zap.Error(err)) return } diff --git a/cmd/tagxl.go b/cmd/tagxl.go index c1b58a3..ea672d4 100644 --- a/cmd/tagxl.go +++ b/cmd/tagxl.go @@ -1,13 +1,14 @@ package cmd import ( - "log/slog" "strconv" "github.com/spf13/cobra" "github.com/spf13/viper" + "github.com/truvami/decoder/internal/logger" "github.com/truvami/decoder/pkg/decoder/tagxl/v1" "github.com/truvami/decoder/pkg/loracloud" + "go.uber.org/zap" ) var accessToken string @@ -24,11 +25,11 @@ var tagxlCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { err := viper.BindPFlag("token", rootCmd.Flags().Lookup("token")) if err != nil { - slog.Error("error while binding token flag", slog.Any("error", err)) + logger.Logger.Error("error while binding token flag", zap.Error(err)) return } - slog.Debug("initializing tagxl decoder") + logger.Logger.Debug("initializing tagxl decoder") d := tagxl.NewTagXLv1Decoder( loracloud.NewLoracloudMiddleware(accessToken), tagxl.WithAutoPadding(AutoPadding), @@ -36,14 +37,14 @@ var tagxlCmd = &cobra.Command{ port, err := strconv.Atoi(args[0]) if err != nil { - slog.Error("error while parsing port", slog.Any("error", err), slog.String("port", args[0])) + logger.Logger.Error("error while parsing port", zap.Error(err), zap.String("port", args[0])) return } - slog.Debug("port parsed successfully", slog.Int("port", port)) + logger.Logger.Debug("port parsed successfully", zap.Int("port", port)) data, metadata, err := d.Decode(args[1], int16(port), args[2]) if err != nil { - slog.Error("error while decoding data", slog.Any("error", err)) + logger.Logger.Error("error while decoding data", zap.Error(err)) return } diff --git a/demo.gif b/demo.gif index 8ba20c0..f87dae0 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/demo.tape b/demo.tape index 7ac6ee7..9f952b2 100644 --- a/demo.tape +++ b/demo.tape @@ -66,6 +66,10 @@ Set FontSize 12 Set Width 800 Set Height 600 +Type "decoder help" Sleep 500ms Enter + +Sleep 5s + Type "decoder tagsl 1 8002cdcd1300744f5e166018040b14341a" Sleep 500ms Enter Sleep 5s @@ -74,6 +78,6 @@ Type "decoder tagsl 4 0000003c0000012c000151800078012c05dc02020100010200005460 - Sleep 5s -Type "docker run ghcr.io/truvami/decoder:alpine-arm64 decoder tagsl 1 8002cdcd1300744f5e166018040b14341a --json | jq" Sleep 500ms Enter - -Sleep 5s +# Type "docker run ghcr.io/truvami/decoder:alpine-arm64 decoder tagsl 1 8002cdcd1300744f5e166018040b14341a --json | jq" Sleep 500ms Enter +# +# Sleep 5s diff --git a/go.mod b/go.mod index 227c6d0..bff5093 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,14 @@ module github.com/truvami/decoder go 1.22.0 require ( - github.com/samber/slog-http v1.4.2 + github.com/google/uuid v1.6.0 github.com/spf13/cobra v1.8.1 + go.uber.org/zap v1.27.0 ) require ( github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -20,10 +21,7 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect - go.uber.org/atomic v1.9.0 // indirect - go.uber.org/multierr v1.9.0 // indirect + go.uber.org/multierr v1.11.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect diff --git a/go.sum b/go.sum index ffdd6af..1ee15f8 100644 --- a/go.sum +++ b/go.sum @@ -35,8 +35,6 @@ github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6ke github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/samber/slog-http v1.4.2 h1:tOOhwE/rFpDzaSxdzttMFFaMDUM+ah7h2zppZ4UCNC0= -github.com/samber/slog-http v1.4.2/go.mod h1:n6h4x2ZBeTgLqMKf95EuNlU6mcJF1b/RVLxo1od5+V0= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= @@ -53,7 +51,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= @@ -61,14 +58,12 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= -go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= -go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..bdf8e7d --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,75 @@ +package logger + +import ( + "os" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var Logger *zap.Logger + +type Option func(*loggerConfig) + +type loggerConfig struct { + Level zapcore.Level + Encoder zapcore.Encoder +} + +func NewLogger(options ...Option) { + // create a custom encoder + encoderConfig := zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + NameKey: "logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "", // disable stack traces + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalColorLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + } + + // default options + config := &loggerConfig{ + Level: zapcore.InfoLevel, + Encoder: zapcore.NewConsoleEncoder(encoderConfig), + } + + for _, opt := range options { + opt(config) + } + + core := zapcore.NewCore( + config.Encoder, + zapcore.AddSync(os.Stdout), + config.Level, + ) + + Logger = zap.New(core) +} + +func WithDebug() Option { + return func(c *loggerConfig) { + c.Level = zapcore.DebugLevel + } +} + +func WithInfo() Option { + return func(c *loggerConfig) { + c.Level = zapcore.InfoLevel + } +} + +func WithEncoder(encoder zapcore.Encoder) Option { + return func(c *loggerConfig) { + c.Encoder = encoder + } +} + +func Sync() { + if Logger != nil { + _ = Logger.Sync() + } +} diff --git a/internal/logger/logger_test.go b/internal/logger/logger_test.go new file mode 100644 index 0000000..2a41d7b --- /dev/null +++ b/internal/logger/logger_test.go @@ -0,0 +1,107 @@ +package logger + +import ( + "bytes" + "testing" + + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +func TestNewLoggerWithDefaults(t *testing.T) { + // Redirect output to a buffer for testing + buffer := &bytes.Buffer{} + NewLogger(WithEncoder(zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + TimeKey: "", + LevelKey: "level", + MessageKey: "msg", + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + }))) + + Logger = zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + TimeKey: "", + LevelKey: "level", + MessageKey: "msg", + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + }), + zapcore.AddSync(buffer), + zapcore.InfoLevel, + )) + + Logger.Info("Test log message") + if !bytes.Contains(buffer.Bytes(), []byte("INFO")) { + t.Errorf("expected INFO level in log, got %s", buffer.String()) + } + if !bytes.Contains(buffer.Bytes(), []byte("Test log message")) { + t.Errorf("expected 'Test log message' in log, got %s", buffer.String()) + } +} + +func TestNewLoggerWithDebug(t *testing.T) { + // Redirect output to a buffer for testing + buffer := &bytes.Buffer{} + + NewLogger(WithDebug()) + + Logger = zap.New(zapcore.NewCore( + zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + TimeKey: "", + LevelKey: "level", + MessageKey: "msg", + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + }), + zapcore.AddSync(buffer), + zapcore.DebugLevel, + )) + + Logger.Debug("Debug log message") + if !bytes.Contains(buffer.Bytes(), []byte("DEBUG")) { + t.Errorf("expected DEBUG level in log, got %s", buffer.String()) + } + if !bytes.Contains(buffer.Bytes(), []byte("Debug log message")) { + t.Errorf("expected 'Debug log message' in log, got %s", buffer.String()) + } +} + +func TestWithEncoderOption(t *testing.T) { + buffer := &bytes.Buffer{} + + customEncoder := zapcore.NewJSONEncoder(zapcore.EncoderConfig{ + TimeKey: "time", + LevelKey: "level", + MessageKey: "msg", + EncodeLevel: zapcore.CapitalLevelEncoder, + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.StringDurationEncoder, + }) + + NewLogger(WithEncoder(customEncoder)) + + Logger = zap.New(zapcore.NewCore( + customEncoder, + zapcore.AddSync(buffer), + zapcore.InfoLevel, + )) + + Logger.Info("JSON log message") + if !bytes.Contains(buffer.Bytes(), []byte("level")) || !bytes.Contains(buffer.Bytes(), []byte("msg")) { + t.Errorf("expected JSON format in log, got %s", buffer.String()) + } +} + +func TestSyncWithoutLogger(t *testing.T) { + defer func() { + if r := recover(); r != nil { + t.Errorf("unexpected panic during Sync: %v", r) + } + }() + + Sync() // Should not panic +} diff --git a/main.go b/main.go index fec92be..9373566 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main -import "github.com/truvami/decoder/cmd" +import ( + "github.com/truvami/decoder/cmd" +) func main() { cmd.Execute() diff --git a/main_test.go b/main_test.go index ef64976..daab697 100644 --- a/main_test.go +++ b/main_test.go @@ -1,7 +1,13 @@ package main -import "testing" +import ( + "testing" + + "github.com/truvami/decoder/internal/logger" +) func TestMain(t *testing.T) { + logger.NewLogger() + defer logger.Sync() main() } diff --git a/pkg/decoder/helpers/helpers.go b/pkg/decoder/helpers/helpers.go index f7506b0..d34fdde 100644 --- a/pkg/decoder/helpers/helpers.go +++ b/pkg/decoder/helpers/helpers.go @@ -3,7 +3,7 @@ package helpers import ( h "encoding/hex" "fmt" - "log/slog" + "reflect" "strings" "time" @@ -169,7 +169,6 @@ func HexNullPad(payload *string, config *decoder.PayloadConfig) string { if providedBits < requiredBits { var paddingBits = (requiredBits - providedBits) / 4 - slog.Debug("padding payload with bits\n", slog.Int("bits", paddingBits)) *payload = strings.Repeat("0", paddingBits) + *payload } return *payload diff --git a/pkg/logger/logger.go b/pkg/logger/logger.go deleted file mode 100644 index 3f96a7e..0000000 --- a/pkg/logger/logger.go +++ /dev/null @@ -1,241 +0,0 @@ -package logger - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "log/slog" - "os" - "strconv" - "strings" - "sync" -) - -const ( - timeFormat = "[15:04:05.000]" - - reset = "\033[0m" - - black = 30 - red = 31 - green = 32 - yellow = 33 - blue = 34 - magenta = 35 - cyan = 36 - lightGray = 37 - darkGray = 90 - lightRed = 91 - lightGreen = 92 - lightYellow = 93 - lightBlue = 94 - lightMagenta = 95 - lightCyan = 96 - white = 97 -) - -func colorizer(colorCode int, v string) string { - return fmt.Sprintf("\033[%sm%s%s", strconv.Itoa(colorCode), v, reset) -} - -type Handler struct { - h slog.Handler - r func([]string, slog.Attr) slog.Attr - b *bytes.Buffer - m *sync.Mutex - writer io.Writer - colorize bool - outputEmptyAttrs bool -} - -func (h *Handler) Enabled(ctx context.Context, level slog.Level) bool { - return h.h.Enabled(ctx, level) -} - -func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { - return &Handler{h: h.h.WithAttrs(attrs), b: h.b, r: h.r, m: h.m, writer: h.writer, colorize: h.colorize} -} - -func (h *Handler) WithGroup(name string) slog.Handler { - return &Handler{h: h.h.WithGroup(name), b: h.b, r: h.r, m: h.m, writer: h.writer, colorize: h.colorize} -} - -func (h *Handler) computeAttrs( - ctx context.Context, - r slog.Record, -) (map[string]any, error) { - h.m.Lock() - defer func() { - h.b.Reset() - h.m.Unlock() - }() - if err := h.h.Handle(ctx, r); err != nil { - return nil, fmt.Errorf("error when calling inner handler's Handle: %w", err) - } - - var attrs map[string]any - err := json.Unmarshal(h.b.Bytes(), &attrs) - if err != nil { - return nil, fmt.Errorf("error when unmarshaling inner handler's Handle result: %w", err) - } - return attrs, nil -} - -func (h *Handler) Handle(ctx context.Context, r slog.Record) error { - colorize := func(code int, value string) string { - return value - } - if h.colorize { - colorize = colorizer - } - - var level string - levelAttr := slog.Attr{ - Key: slog.LevelKey, - Value: slog.AnyValue(r.Level), - } - if h.r != nil { - levelAttr = h.r([]string{}, levelAttr) - } - - if !levelAttr.Equal(slog.Attr{}) { - level = levelAttr.Value.String() + ":" - - if r.Level <= slog.LevelDebug { - level = colorize(lightGray, level) - } else if r.Level <= slog.LevelInfo { - level = colorize(cyan, level) - } else if r.Level < slog.LevelWarn { - level = colorize(lightBlue, level) - } else if r.Level < slog.LevelError { - level = colorize(lightYellow, level) - } - } - - var timestamp string - timeAttr := slog.Attr{ - Key: slog.TimeKey, - Value: slog.StringValue(r.Time.Format(timeFormat)), - } - if h.r != nil { - timeAttr = h.r([]string{}, timeAttr) - } - if !timeAttr.Equal(slog.Attr{}) { - timestamp = colorize(lightGray, timeAttr.Value.String()) - } - - var msg string - msgAttr := slog.Attr{ - Key: slog.MessageKey, - Value: slog.StringValue(r.Message), - } - if h.r != nil { - msgAttr = h.r([]string{}, msgAttr) - } - if !msgAttr.Equal(slog.Attr{}) { - msg = colorize(white, msgAttr.Value.String()) - } - - attrs, err := h.computeAttrs(ctx, r) - if err != nil { - return err - } - - var attrsAsBytes []byte - if h.outputEmptyAttrs || len(attrs) > 0 { - attrsAsBytes, err = json.MarshalIndent(attrs, "", " ") - if err != nil { - return fmt.Errorf("error when marshaling attrs: %w", err) - } - } - - out := strings.Builder{} - if len(timestamp) > 0 { - out.WriteString(timestamp) - out.WriteString(" ") - } - if len(level) > 0 { - out.WriteString(level) - out.WriteString(" ") - } - if len(msg) > 0 { - out.WriteString(msg) - out.WriteString(" ") - } - if len(attrsAsBytes) > 0 { - out.WriteString(colorize(darkGray, string(attrsAsBytes))) - } - - _, err = io.WriteString(h.writer, out.String()+"\n") - if err != nil { - return err - } - - return nil -} - -func suppressDefaults( - next func([]string, slog.Attr) slog.Attr, -) func([]string, slog.Attr) slog.Attr { - return func(groups []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey || - a.Key == slog.LevelKey || - a.Key == slog.MessageKey { - return slog.Attr{} - } - if next == nil { - return a - } - return next(groups, a) - } -} - -func New(handlerOptions *slog.HandlerOptions, options ...Option) *Handler { - if handlerOptions == nil { - handlerOptions = &slog.HandlerOptions{} - } - - buf := &bytes.Buffer{} - handler := &Handler{ - b: buf, - h: slog.NewJSONHandler(buf, &slog.HandlerOptions{ - Level: handlerOptions.Level, - AddSource: handlerOptions.AddSource, - ReplaceAttr: suppressDefaults(handlerOptions.ReplaceAttr), - }), - r: handlerOptions.ReplaceAttr, - m: &sync.Mutex{}, - } - - for _, opt := range options { - opt(handler) - } - - return handler -} - -func NewHandler(opts *slog.HandlerOptions) *Handler { - return New(opts, WithDestinationWriter(os.Stdout), WithColor(), WithOutputEmptyAttrs()) -} - -type Option func(h *Handler) - -func WithDestinationWriter(writer io.Writer) Option { - return func(h *Handler) { - h.writer = writer - } -} - -func WithColor() Option { - return func(h *Handler) { - h.colorize = true - } -} - -func WithOutputEmptyAttrs() Option { - return func(h *Handler) { - h.outputEmptyAttrs = true - } -} diff --git a/pkg/logger/logger_test.go b/pkg/logger/logger_test.go deleted file mode 100644 index 35d3227..0000000 --- a/pkg/logger/logger_test.go +++ /dev/null @@ -1,102 +0,0 @@ -package logger - -import ( - "log/slog" - "os" - "regexp" - "strings" - "testing" -) - -type captureStream struct { - lines [][]byte -} - -func (cs *captureStream) Write(bytes []byte) (int, error) { - cs.lines = append(cs.lines, bytes) - return len(bytes), nil -} - -func TestWritesToProvidedStream(t *testing.T) { - cs := &captureStream{} - handler := New(nil, WithDestinationWriter(cs), WithOutputEmptyAttrs()) - logger := slog.New(handler) - - logger.Info("testing logger") - if len(cs.lines) != 1 { - t.Errorf("expected 1 lines logged, got: %d", len(cs.lines)) - } - - lineMatcher := regexp.MustCompile(`\[\d{2}:\d{2}:\d{2}\.\d{3}\] INFO: testing logger {}`) - line := string(cs.lines[0]) - if lineMatcher.MatchString(line) == false { - t.Errorf("expected `testing logger` but found `%s`", line) - } - if !strings.HasSuffix(line, "\n") { - t.Errorf("expected line to be terminated with `\\n` but found `%s`", line[len(line)-1:]) - } -} - -func TestSkipEmptyAttributes(t *testing.T) { - cs := &captureStream{} - handler := New(nil, WithDestinationWriter(cs)) - logger := slog.New(handler) - - logger.Info("testing logger") - if len(cs.lines) != 1 { - t.Errorf("expected 1 lines logged, got: %d", len(cs.lines)) - } - - lineMatcher := regexp.MustCompile(`\[\d{2}:\d{2}:\d{2}\.\d{3}\] INFO: testing logger`) - line := string(cs.lines[0]) - if lineMatcher.MatchString(line) == false { - t.Errorf("expected `testing logger` but found `%s`", line) - } - if !strings.HasSuffix(line, "\n") { - t.Errorf("expected line to be terminated with `\\n` but found `%s`", line[len(line)-1:]) - } -} - -func TestColorizer(t *testing.T) { - // Test case 1: color code 31 (red) - expected1 := "\033[31mHello\033[0m" - result1 := colorizer(31, "Hello") - if result1 != expected1 { - t.Errorf("Expected: %s, but got: %s", expected1, result1) - } - - // Test case 2: color code 92 (light green) - expected2 := "\033[92mWorld\033[0m" - result2 := colorizer(92, "World") - if result2 != expected2 { - t.Errorf("Expected: %s, but got: %s", expected2, result2) - } - - // Test case 3: color code 97 (white) - expected3 := "\033[97m123\033[0m" - result3 := colorizer(97, "123") - if result3 != expected3 { - t.Errorf("Expected: %s, but got: %s", expected3, result3) - } -} - -func TestNewHandler(t *testing.T) { - // Test case 1: Verify that the handler is created with the correct writer - writer := os.Stdout - handler := NewHandler(&slog.HandlerOptions{}) - if handler.writer != writer { - t.Errorf("Expected writer to be set to os.Stdout, but got: %v", &handler.writer) - } - - // Test case 2: Verify that the handler is created with colorization enabled - handler = NewHandler(&slog.HandlerOptions{}) - if !handler.colorize { - t.Error("Expected colorization to be enabled, but it is not") - } - - // Test case 3: Verify that the handler is created with output empty attributes enabled - handler = NewHandler(&slog.HandlerOptions{}) - if !handler.outputEmptyAttrs { - t.Error("Expected output empty attributes to be enabled, but it is not") - } -}