Skip to content

Commit

Permalink
feat(configureable-logger): add support of configurable logger. (#81)
Browse files Browse the repository at this point in the history
Co-authored-by: Muhammad Haseeb <muhammmadhaseebisb@gmail.com>
  • Loading branch information
haseeb-mhr and Muhammad Haseeb committed May 16, 2024
1 parent 23fbdad commit 602e640
Show file tree
Hide file tree
Showing 11 changed files with 911 additions and 0 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ The https package provides logic related to HTTP requests, including building an
| [`Interceptors`](https/interceptors.go) | Provides handling to intercept requests. |
| [`Retryer`](https/retryer.go) | Provides handling to automatically retry for failed requests. |

### Logger
The logger package provides logic related to logging. It offers the Facade Design Pattern for configuring the Logger and SDK Logger. Additionally, it provides the LoggerConfiguration to customize logging behavior.

| File Name | Description |
|--------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------|
| [`Console Logger`](logger/defaultLogger.go) | Provides default implementation of [`Logger Interface`](logger/defaultLogger.go) to log messages. |
| [`Level`](logger/level.go) | Provides constants for log level like Level_ERROR, Level_INFO, etc. |
| [`Logger Configuration`](logger/loggerConfiguration.go) | Provides logging configurations for the Sdk Logger. |
| [`Response Logger Configuration`](logger/responseLoggerConfiguration.go) | Provides response logging configurations for the Sdk Logger. |
| [`Request Logger Configuration`](logger/requestLoggerConfiguration.go) | Provides request logging configurations for the Sdk Logger. |
| [`Sdk Logger`](logger/sdkLogger.go) | Provides default and null implementation of [` Sdk Logger Interface`](logger/sdkLogger.go) to log API requests and responses. |

### Test Helper
Package testHelper provides helper functions for testing purposes.

Expand Down
12 changes: 12 additions & 0 deletions https/callBuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
"strconv"
"strings"
"time"

"github.com/apimatic/go-core-runtime/logger"
)

// Constants for commonly used HTTP headers and content types.
Expand Down Expand Up @@ -71,6 +73,7 @@ type CallBuilder interface {
Authenticate(authGroup AuthGroup)
RequestRetryOption(option RequestRetryOption)
ArraySerializationOption(option ArraySerializationOption)
Logger(sdkLogger logger.SdkLoggerInterface)
}

// defaultCallBuilder is a struct that implements the CallBuilder interface for making API calls.
Expand Down Expand Up @@ -100,6 +103,7 @@ type defaultCallBuilder struct {
queryParams formParams
errors map[string]ErrorBuilder[error]
arraySerializationOption ArraySerializationOption
sdkLogger logger.SdkLoggerInterface
}

// newDefaultCallBuilder creates a new instance of defaultCallBuilder, which implements the CallBuilder interface.
Expand All @@ -124,6 +128,7 @@ func newDefaultCallBuilder(
retryConfig: retryConfig,
ctx: ctx,
arraySerializationOption: option,
sdkLogger: logger.NullSdkLogger{},
}
cb.addRetryInterceptor()
return &cb
Expand Down Expand Up @@ -160,6 +165,11 @@ func (cb *defaultCallBuilder) ArraySerializationOption(option ArraySerialization
cb.arraySerializationOption = option
}

// Logger sets the api logger interface instance for the API call.
func (cb *defaultCallBuilder) Logger(sdkLoggerInterface logger.SdkLoggerInterface) {
cb.sdkLogger = sdkLoggerInterface
}

// AppendPath appends the provided path to the existing path in the CallBuilder.
func (cb *defaultCallBuilder) AppendPath(path string) {
if cb.path != "" {
Expand Down Expand Up @@ -608,7 +618,9 @@ func (cb *defaultCallBuilder) Call() (*HttpContext, error) {
if err != nil {
return nil, err
}
cb.sdkLogger.LogRequest(request)
context := pipeline(request)
cb.sdkLogger.LogResponse(context.Response)

if cb.clientError != nil {
err = cb.clientError
Expand Down
19 changes: 19 additions & 0 deletions https/callBuilder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package https
import (
"context"
"errors"
"github.com/apimatic/go-core-runtime/logger"
"io"
"net/http"
"reflect"
Expand Down Expand Up @@ -48,6 +49,24 @@ func TestAppendPath(t *testing.T) {
}
}

func TestLogger(t *testing.T) {
request := GetCallBuilder(ctx, "GET", "//response/", nil)
request.AppendPath("/integer")
request.Logger(logger.NullSdkLogger{})
request.ArraySerializationOption(Indexed)

_, response, err := request.CallAsJson()
if err != nil {
t.Errorf("Error in CallAsJson: %v", err)
}

expected := 200

if response.StatusCode != expected {
t.Errorf("Failed:\nExpected: %v\nGot: %v", expected, response)
}
}

func TestAppendMultiplePath(t *testing.T) {
samplePath := "/number/integer/base64"
request := GetCallBuilder(ctx, "GET", "//response/", nil)
Expand Down
41 changes: 41 additions & 0 deletions logger/defaultLogger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package logger

import (
"fmt"
"regexp"
)

// LoggerInterface represents an interface for a generic logger.
type LoggerInterface interface {
// Log function provides a message string containing placeholders in the format '%{key}',
// along with the log level and a map of parameters that can be replaced in the message.
Log(level Level, message string, params map[string]any)
}

// ConsoleLogger represents a logger implementation that logs messages to the console.
type ConsoleLogger struct{}

// Log function provides a message string containing placeholders in the format '%{key}',
// along with the log level and a map of parameters that can be replaced in the message.
func (c ConsoleLogger) Log(level Level, message string, params map[string]any) {
fmt.Println(level, ": ", _formatMessage(message, params))
}

func _formatMessage(msg string, obj map[string]interface{}) string {
regex := regexp.MustCompile(`\%{([^}]+)}`)

formattedMsg := regex.ReplaceAllStringFunc(msg, func(match string) string {
key := match[2 : len(match)-1]
if value, ok := obj[key]; ok {
switch v := value.(type) {
case string:
return v
default:
return fmt.Sprintf("%v", v)
}
}
return match
})

return formattedMsg
}
58 changes: 58 additions & 0 deletions logger/level.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package logger

import (
"encoding/json"
"errors"
"fmt"
)

// Level is a string enum.
// An enum representing different log levels.
type Level string

// MarshalJSON implements the json.Marshaller interface for Level.
// It customizes the JSON marshaling process for Level objects.
func (e Level) MarshalJSON() (
[]byte,
error) {
if e.isValid() {
return []byte(fmt.Sprintf("\"%v\"", e)), nil
}
return nil, errors.New("the provided enum value is not allowed for Level")
}

// UnmarshalJSON implements the json.Unmarshaler interface for Level.
// It customizes the JSON unmarshalling process for Level objects.
func (e *Level) UnmarshalJSON(input []byte) error {
var enumValue string
err := json.Unmarshal(input, &enumValue)
if err != nil {
return err
}
*e = Level(enumValue)
if !e.isValid() {
return errors.New("the value " + string(input) + " cannot be unmarshalled to Level")
}
return nil
}

// Checks whether the value is actually a member of Level.
func (e *Level) isValid() bool {
switch *e {
case Level_ERROR,
Level_WARN,
Level_INFO,
Level_DEBUG,
Level_TRACE:
return true
}
return false
}

const (
Level_ERROR Level = "error" // Error log level.
Level_WARN Level = "warn" // Warning log level.
Level_INFO Level = "info" // Information log level.
Level_DEBUG Level = "debug" // Debug log level.
Level_TRACE Level = "trace" // Trace log level.
)
57 changes: 57 additions & 0 deletions logger/level_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package logger

import (
"encoding/json"
"reflect"
"testing"
)

func validateLevelEnumValues(level Level, t *testing.T) {
bytes, err := json.Marshal(level)
if err != nil {
t.Errorf("Unable to marshal level type : %v", err)
}
var newLevel Level
err = json.Unmarshal(bytes, &newLevel)
if err != nil {
t.Errorf("Unable to unmarshal bytes into level type : %v", err)
}

if !reflect.DeepEqual(level, newLevel) {
t.Errorf("Failed:\nExpected: %v\nGot: %v", level, newLevel)
}
}

func TestLevelEnumValueERROR(t *testing.T) {
level := Level(Level_ERROR)
validateLevelEnumValues(level, t)
}

func TestLevelEnumValueWARN(t *testing.T) {
level := Level(Level_WARN)
validateLevelEnumValues(level, t)
}

func TestLevelEnumValueINFO(t *testing.T) {
level := Level(Level_INFO)
validateLevelEnumValues(level, t)
}
func TestLevelEnumValueDEBUG(t *testing.T) {
level := Level(Level_DEBUG)
validateLevelEnumValues(level, t)
}

func TestLevelEnumValueTRACE(t *testing.T) {
level := Level(Level_TRACE)
validateLevelEnumValues(level, t)
}

func TestLevelEnumValueInvalid(t *testing.T) {
level := Level("Invalid")
validateLevelEnumValues(level, new(testing.T))
}

func TestLevelEnumValueInvalid2(t *testing.T) {
level := Level("nil")
validateLevelEnumValues(level, new(testing.T))
}
102 changes: 102 additions & 0 deletions logger/loggerConfiguration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package logger

// LoggerConfiguration represents options for configuring logging behavior.
type LoggerConfiguration struct {
// The logger to use for logging messages.
logger LoggerInterface
// The log level to determine which messages should be logged.
level Level
// Options for logging HTTP requests.
request RequestLoggerConfiguration
// Options for logging HTTP responses.
response ResponseLoggerConfiguration
// Indicates whether sensitive headers should be masked in logged messages.
maskSensitiveHeaders bool
}

func (l *LoggerConfiguration) isValid() bool {
return l.logger != nil && l.level.isValid()
}

// LoggerOptions represents a function type that can be used to apply configuration to the LoggerOptions struct.
type LoggerOptions func(*LoggerConfiguration)

// Default logger configuration
func defaultLoggerConfiguration() LoggerConfiguration {
return LoggerConfiguration{
logger: ConsoleLogger{},
level: Level_INFO,
request: NewHttpRequestLoggerConfiguration(),
response: NewResponseLoggerConfiguration(),
maskSensitiveHeaders: true,
}
}

// NewLoggerConfiguration creates default LoggingOptions with the provided options.
func NewLoggerConfiguration(options ...LoggerOptions) LoggerConfiguration {
config := defaultLoggerConfiguration()

for _, option := range options {
option(&config)
}
return config
}

// WithLogger is an option that sets the LoggerInterface in the LoggingOptions.
func WithLogger(logger LoggerInterface) LoggerOptions {
return func(l *LoggerConfiguration) {
l.logger = logger
}
}

// WithLevel is an option that sets the LogLevel in the LoggingOptions.
func WithLevel(level Level) LoggerOptions {
return func(l *LoggerConfiguration) {
l.level = level
}
}

// WithMaskSensitiveHeaders is an option that enable to mask Sensitive Headers in the LoggingOptions.
func WithMaskSensitiveHeaders(maskSensitiveHeaders bool) LoggerOptions {
return func(l *LoggerConfiguration) {
l.maskSensitiveHeaders = maskSensitiveHeaders
}
}

// WithRequestConfiguration is an option that sets that enable to log Request in the LoggingOptions.
func WithRequestConfiguration(options ...RequestLoggerOptions) LoggerOptions {
return func(l *LoggerConfiguration) {
l.request = NewHttpRequestLoggerConfiguration(options...)
}
}

// WithResponseConfiguration is an option that sets that enable to log Response in the LoggingOptions.
func WithResponseConfiguration(options ...ResponseLoggerOptions) LoggerOptions {
return func(l *LoggerConfiguration) {
l.response = NewResponseLoggerConfiguration(options...)
}
}

// messageLoggerConfiguration represents options for logging HTTP message details.
type messageLoggerConfiguration struct {
// Indicates whether the message body should be logged.
body bool
// Indicates whether the message headers should be logged.
headers bool
// Array of headers not to be displayed in logging.
excludeHeaders []string
// Array of headers to be displayed in logging.
includeHeaders []string
// Array of headers which values are non-sensitive to display in logging.
whitelistHeaders []string
}

func defaultMessageLoggerConfiguration() messageLoggerConfiguration {
return messageLoggerConfiguration{
body: false,
headers: false,
excludeHeaders: []string{},
includeHeaders: []string{},
whitelistHeaders: []string{},
}
}
Loading

0 comments on commit 602e640

Please sign in to comment.