From 47530eb7e89cf19cc0fbc17d1203a841d6c1bb7b Mon Sep 17 00:00:00 2001 From: Onasanya Tunde Date: Sun, 16 Mar 2025 04:02:53 +0100 Subject: [PATCH] Add logging functionality and update client initialization - Introduced a new logger implementation in logger.go, adhering to the Logger interface. - Updated the Client struct to use the new logger and modified the NewClient function to accept logging configuration. - Enhanced request handling functions to include context-aware logging. - Added indirect dependencies for go-faker and golang.org/x/text in go.mod and updated go.sum. - Updated example main.go to demonstrate the new logging capabilities. --- example/main.go | 16 ++++++-- go.mod | 5 +++ go.sum | 4 ++ logger.go | 102 ++++++++++++++++++++++++++++++++++++++++++++++++ paystack.go | 15 +++---- request.go | 27 ++++++++++--- 6 files changed, 153 insertions(+), 16 deletions(-) create mode 100644 logger.go diff --git a/example/main.go b/example/main.go index 1a4367e..fc9ecf6 100644 --- a/example/main.go +++ b/example/main.go @@ -2,14 +2,24 @@ package main import ( "context" + "log" "github.com/rammyblog/go-paystack" ) -const APIKey = "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +const APIKey = "x" func main() { ctx := context.Background() - newClient := paystack.NewClient(APIKey) - initializeTransaction(ctx, newClient) + newClient := paystack.NewClient(APIKey, &paystack.LogConfig{ + Level: paystack.LogLevelDebug, + JSONOutput: true, + }) + + resp, err := newClient.Transaction.Verify(ctx, "TX_Onas") + if err != nil { + log.Fatal(err) + return + } + log.Printf("\n Initialize transaction \n-%+v\n", resp) } diff --git a/go.mod b/go.mod index 9a6651b..6e102c7 100644 --- a/go.mod +++ b/go.mod @@ -3,3 +3,8 @@ module github.com/rammyblog/go-paystack go 1.21.0 require github.com/mitchellh/mapstructure v1.5.0 + +require ( + github.com/go-faker/faker/v4 v4.6.0 // indirect + golang.org/x/text v0.18.0 // indirect +) diff --git a/go.sum b/go.sum index 59f4b8e..b62b1b9 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,6 @@ +github.com/go-faker/faker/v4 v4.6.0 h1:6aOPzNptRiDwD14HuAnEtlTa+D1IfFuEHO8+vEFwjTs= +github.com/go-faker/faker/v4 v4.6.0/go.mod h1:ZmrHuVtTTm2Em9e0Du6CJ9CADaLEzGXW62z1YqFH0m0= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..963925b --- /dev/null +++ b/logger.go @@ -0,0 +1,102 @@ +package paystack + +import ( + "context" + "log/slog" + "os" +) + +// LogLevel represents the logging level +type LogLevel string + +const ( + LogLevelDebug LogLevel = "debug" + LogLevelInfo LogLevel = "info" + LogLevelWarn LogLevel = "warn" + LogLevelError LogLevel = "error" +) + +// LogConfig holds logging configuration +type LogConfig struct { + Level LogLevel + JSONOutput bool + Output *os.File +} + +// Logger interface defines logging behavior +type Logger interface { + Debug(msg string, args ...any) + Info(msg string, args ...any) + Warn(msg string, args ...any) + Error(msg string, args ...any) + WithContext(ctx context.Context) Logger +} + +// paystackLogger implements the Logger interface +type paystackLogger struct { + logger *slog.Logger + ctx context.Context +} + +// newLogger creates a new logger instance +func newLogger(config *LogConfig) Logger { + if config == nil { + config = &LogConfig{ + Level: LogLevelInfo, + JSONOutput: true, + Output: os.Stderr, + } + } + + var handler slog.Handler + opts := &slog.HandlerOptions{ + Level: getLogLevel(config.Level), + } + + if config.JSONOutput { + handler = slog.NewJSONHandler(os.Stderr, opts) + } else { + handler = slog.NewTextHandler(config.Output, opts) + } + + return &paystackLogger{ + logger: slog.New(handler), + ctx: context.Background(), + } +} + +func getLogLevel(level LogLevel) slog.Level { + switch level { + case LogLevelDebug: + return slog.LevelDebug + case LogLevelWarn: + return slog.LevelWarn + case LogLevelError: + return slog.LevelError + default: + return slog.LevelInfo + } +} + +func (l *paystackLogger) Debug(msg string, args ...any) { + l.logger.DebugContext(l.ctx, msg, args...) +} + +func (l *paystackLogger) Info(msg string, args ...any) { + l.logger.InfoContext(l.ctx, msg, args...) +} + +func (l *paystackLogger) Warn(msg string, args ...any) { + l.logger.WarnContext(l.ctx, msg, args...) +} + +func (l *paystackLogger) Error(msg string, args ...any) { + l.logger.ErrorContext(l.ctx, msg, args...) +} + +func (l *paystackLogger) WithContext(ctx context.Context) Logger { + return &paystackLogger{ + logger: l.logger, + ctx: ctx, + } +} diff --git a/paystack.go b/paystack.go index ce8f39e..41050b7 100644 --- a/paystack.go +++ b/paystack.go @@ -2,10 +2,8 @@ package paystack import ( "fmt" - "log/slog" "net/http" "net/url" - "os" "strings" "time" ) @@ -13,7 +11,7 @@ import ( type Client struct { APIKey string HttpClient *http.Client - log *slog.Logger + logger Logger Transaction *Transaction TransactionSplit *TransactionSplit Plan *Plans @@ -45,15 +43,18 @@ type PaginationMeta struct { } // NewClient creates a new Paystack API client with the given API key. -func NewClient(apiKey string) *Client { +func NewClient(apiKey string, config *LogConfig) *Client { httpClient := &http.Client{ Timeout: 5 * time.Second, } - logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) - parsedUrl, _ := url.Parse(BASE_URL) - c := &Client{APIKey: apiKey, HttpClient: httpClient, log: logger, BaseUrl: parsedUrl} + c := &Client{ + APIKey: apiKey, + HttpClient: httpClient, + logger: newLogger(config), + BaseUrl: parsedUrl, + } c.Transaction = newTransaction(c) c.TransactionSplit = newTransactionSplit(c) c.Plan = newPlans(c) diff --git a/request.go b/request.go index ec7b895..fc838c3 100644 --- a/request.go +++ b/request.go @@ -42,7 +42,7 @@ func postResource(ctx context.Context, c *Client, url string, body interface{}, return err } - return doReq(c, req, res) + return doReq(c, req, res, ctx) } func putResource(ctx context.Context, c *Client, url string, body interface{}, res interface{}) error { @@ -61,7 +61,7 @@ func putResource(ctx context.Context, c *Client, url string, body interface{}, r if err != nil { return err } - return doReq(c, req, res) + return doReq(c, req, res, ctx) } func getResource(ctx context.Context, c *Client, url string, res interface{}) error { @@ -72,7 +72,7 @@ func getResource(ctx context.Context, c *Client, url string, res interface{}) er return err } - return doReq(c, req, res) + return doReq(c, req, res, ctx) } func deleteResource(ctx context.Context, c *Client, url string, res interface{}) error { @@ -83,23 +83,38 @@ func deleteResource(ctx context.Context, c *Client, url string, res interface{}) return err } - return doReq(c, req, res) + return doReq(c, req, res, ctx) } -func doReq(c *Client, req *http.Request, res interface{}) error { - +func doReq(c *Client, req *http.Request, res interface{}, ctx context.Context) error { if req.Body != nil { req.Header.Set("Content-Type", "application/json") } req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey)) req.Header.Set("User-Agent", userAgent) + + c.logger.WithContext(ctx).Debug("sending request", + "method", req.Method, + "url", req.URL.String(), + "headers", req.Header, + ) + resp, err := c.HttpClient.Do(req) if err != nil { + c.logger.Error("request failed", + "error", err, + "method", req.Method, + "url", req.URL.String(), + ) return fmt.Errorf("error processing request - %+v", err) } err = parseAPIResponse(c, resp, res) if err != nil { + c.logger.Error("failed to parse response", + "error", err, + "status", resp.Status, + ) return err }