Skip to content
Merged

Dev #85

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
2 changes: 1 addition & 1 deletion errors/http_error_handling.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ func TranslateStatusCode(resp *http.Response) string {
if message, exists := messages[resp.StatusCode]; exists {
return message
}
return "An unexpected error occurred. Please try again later."
return fmt.Sprintf("Unknown status code: %d", resp.StatusCode)
}

// IsNonRetryableStatusCode checks if the provided response indicates a non-retryable error.
Expand Down
53 changes: 11 additions & 42 deletions httpclient/httpclient_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ like the baseURL, authentication details, and an embedded standard HTTP client.
package httpclient

import (
"log"
"net/http"
"sync"
"time"
Expand Down Expand Up @@ -82,7 +83,15 @@ type PerformanceMetrics struct {
}

// BuildClient creates a new HTTP client with the provided configuration.
func BuildClient(config ClientConfig) (*Client, error) {
func BuildClient(configFilePath string) (*Client, error) {

// Load the configuration
config, err := SetClientConfiguration(configFilePath)
if err != nil {
log.Printf("Failed to set client configuration: %v", err) // This uses the standard log package before zap logger is initialized
return nil, err
}

// Parse the log level string to logger.LogLevel
parsedLogLevel := logger.ParseLogLevelFromString(config.ClientOptions.LogLevel)

Expand All @@ -105,46 +114,6 @@ func BuildClient(config ClientConfig) (*Client, error) {

log.Info("Initializing new HTTP client with the provided configuration")

// Validate and set default values for the configuration
if config.Environment.APIType == "" {
return nil, log.Error("InstanceName cannot be empty")
}

if config.ClientOptions.MaxRetryAttempts < 0 {
config.ClientOptions.MaxRetryAttempts = DefaultMaxRetryAttempts
log.Info("MaxRetryAttempts was negative, set to default value", zap.Int("MaxRetryAttempts", DefaultMaxRetryAttempts))
}

if config.ClientOptions.MaxConcurrentRequests <= 0 {
config.ClientOptions.MaxConcurrentRequests = DefaultMaxConcurrentRequests
log.Info("MaxConcurrentRequests was negative or zero, set to default value", zap.Int("MaxConcurrentRequests", DefaultMaxConcurrentRequests))
}

if config.ClientOptions.TokenRefreshBufferPeriod < 0 {
config.ClientOptions.TokenRefreshBufferPeriod = DefaultTokenBufferPeriod
log.Info("TokenRefreshBufferPeriod was negative, set to default value", zap.Duration("TokenRefreshBufferPeriod", DefaultTokenBufferPeriod))
}

if config.ClientOptions.TotalRetryDuration <= 0 {
config.ClientOptions.TotalRetryDuration = DefaultTotalRetryDuration
log.Info("TotalRetryDuration was negative or zero, set to default value", zap.Duration("TotalRetryDuration", DefaultTotalRetryDuration))
}

if config.ClientOptions.TokenRefreshBufferPeriod == 0 {
config.ClientOptions.TokenRefreshBufferPeriod = DefaultTokenBufferPeriod
log.Info("TokenRefreshBufferPeriod not set, set to default value", zap.Duration("TokenRefreshBufferPeriod", DefaultTokenBufferPeriod))
}

if config.ClientOptions.TotalRetryDuration == 0 {
config.ClientOptions.TotalRetryDuration = DefaultTotalRetryDuration
log.Info("TotalRetryDuration not set, set to default value", zap.Duration("TotalRetryDuration", DefaultTotalRetryDuration))
}

if config.ClientOptions.CustomTimeout == 0 {
config.ClientOptions.CustomTimeout = DefaultTimeout
log.Info("CustomTimeout not set, set to default value", zap.Duration("CustomTimeout", DefaultTimeout))
}

// Determine the authentication method using the helper function
authMethod, err := DetermineAuthMethod(config.Auth)
if err != nil {
Expand All @@ -159,7 +128,7 @@ func BuildClient(config ClientConfig) (*Client, error) {
AuthMethod: authMethod,
OverrideBaseDomain: config.Environment.OverrideBaseDomain,
httpClient: &http.Client{Timeout: config.ClientOptions.CustomTimeout},
clientConfig: config,
clientConfig: *config,
Logger: log,
ConcurrencyMgr: NewConcurrencyManager(config.ClientOptions.MaxConcurrentRequests, log, true),
PerfMetrics: PerformanceMetrics{},
Expand Down
188 changes: 188 additions & 0 deletions httpclient/httpclient_client.go.back
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// http_client.go
/* The `http_client` package provides a configurable HTTP client tailored for interacting with specific APIs.
It supports different authentication methods, including "bearer" and "oauth". The client is designed with a
focus on concurrency management, structured error handling, and flexible configuration options.
The package offers a default timeout, custom backoff strategies, dynamic rate limiting,
and detailed logging capabilities. The main `Client` structure encapsulates all necessary components,
like the baseURL, authentication details, and an embedded standard HTTP client. */
package httpclient

import (
"net/http"
"sync"
"time"

"github.com/deploymenttheory/go-api-http-client/logger"
"go.uber.org/zap"
)

// Client represents an HTTP client to interact with a specific API.
type Client struct {
APIHandler APIHandler // APIHandler interface used to define which API handler to use
InstanceName string // Website Instance name without the root domain
AuthMethod string // Specifies the authentication method: "bearer" or "oauth"
Token string // Authentication Token
OverrideBaseDomain string // Base domain override used when the default in the api handler isn't suitable
OAuthCredentials OAuthCredentials // ClientID / Client Secret
BearerTokenAuthCredentials BearerTokenAuthCredentials // Username and Password for Basic Authentication
Expiry time.Time // Expiry time set for the auth token
httpClient *http.Client
tokenLock sync.Mutex
clientConfig ClientConfig
Logger logger.Logger
ConcurrencyMgr *ConcurrencyManager
PerfMetrics PerformanceMetrics
}

// Config holds configuration options for the HTTP Client.
type ClientConfig struct {
Auth AuthConfig // User can either supply these values manually or pass from LoadAuthConfig/Env vars
Environment EnvironmentConfig // User can either supply these values manually or pass from LoadAuthConfig/Env vars
ClientOptions ClientOptions // Optional configuration options for the HTTP Client
}

// EnvironmentConfig represents the structure to read authentication details from a JSON configuration file.
type EnvironmentConfig struct {
InstanceName string `json:"InstanceName,omitempty"`
OverrideBaseDomain string `json:"OverrideBaseDomain,omitempty"`
APIType string `json:"APIType,omitempty"`
}

// AuthConfig represents the structure to read authentication details from a JSON configuration file.
type AuthConfig struct {
Username string `json:"Username,omitempty"`
Password string `json:"Password,omitempty"`
ClientID string `json:"ClientID,omitempty"`
ClientSecret string `json:"ClientSecret,omitempty"`
}

// ClientOptions holds optional configuration options for the HTTP Client.
type ClientOptions struct {
LogLevel string // Field for defining tiered logging level.
LogOutputFormat string // Field for defining the output format of the logs. Use "JSON" for JSON format, "console" for human-readable format
LogConsoleSeparator string // Field for defining the separator in console output format.
HideSensitiveData bool // Field for defining whether sensitive fields should be hidden in logs.
MaxRetryAttempts int // Config item defines the max number of retry request attempts for retryable HTTP methods.
EnableDynamicRateLimiting bool // Field for defining whether dynamic rate limiting should be enabled.
MaxConcurrentRequests int // Field for defining the maximum number of concurrent requests allowed in the semaphore
TokenRefreshBufferPeriod time.Duration
TotalRetryDuration time.Duration
CustomTimeout time.Duration
}

// ClientPerformanceMetrics captures various metrics related to the client's
// interactions with the API, providing insights into its performance and behavior.
type PerformanceMetrics struct {
TotalRequests int64
TotalRetries int64
TotalRateLimitErrors int64
TotalResponseTime time.Duration
TokenWaitTime time.Duration
lock sync.Mutex
}

// BuildClient creates a new HTTP client with the provided configuration.
func BuildClient(config ClientConfig) (*Client, error) {
// Parse the log level string to logger.LogLevel
parsedLogLevel := logger.ParseLogLevelFromString(config.ClientOptions.LogLevel)

// Set default value if none is provided
if config.ClientOptions.LogConsoleSeparator == "" {
config.ClientOptions.LogConsoleSeparator = ","
}

// Initialize the logger with parsed config values
log := logger.BuildLogger(parsedLogLevel, config.ClientOptions.LogOutputFormat, config.ClientOptions.LogConsoleSeparator)

// Set the logger's level (optional if BuildLogger already sets the level based on the input)
log.SetLevel(parsedLogLevel)

// Use the APIType from the config to determine which API handler to load
apiHandler, err := LoadAPIHandler(config.Environment.APIType, log)
if err != nil {
return nil, log.Error("Failed to load API handler", zap.String("APIType", config.Environment.APIType), zap.Error(err))
}

log.Info("Initializing new HTTP client with the provided configuration")

// Validate and set default values for the configuration
if config.Environment.APIType == "" {
return nil, log.Error("InstanceName cannot be empty")
}

if config.ClientOptions.MaxRetryAttempts < 0 {
config.ClientOptions.MaxRetryAttempts = DefaultMaxRetryAttempts
log.Info("MaxRetryAttempts was negative, set to default value", zap.Int("MaxRetryAttempts", DefaultMaxRetryAttempts))
}

if config.ClientOptions.MaxConcurrentRequests <= 0 {
config.ClientOptions.MaxConcurrentRequests = DefaultMaxConcurrentRequests
log.Info("MaxConcurrentRequests was negative or zero, set to default value", zap.Int("MaxConcurrentRequests", DefaultMaxConcurrentRequests))
}

if config.ClientOptions.TokenRefreshBufferPeriod < 0 {
config.ClientOptions.TokenRefreshBufferPeriod = DefaultTokenBufferPeriod
log.Info("TokenRefreshBufferPeriod was negative, set to default value", zap.Duration("TokenRefreshBufferPeriod", DefaultTokenBufferPeriod))
}

if config.ClientOptions.TotalRetryDuration <= 0 {
config.ClientOptions.TotalRetryDuration = DefaultTotalRetryDuration
log.Info("TotalRetryDuration was negative or zero, set to default value", zap.Duration("TotalRetryDuration", DefaultTotalRetryDuration))
}

if config.ClientOptions.TokenRefreshBufferPeriod == 0 {
config.ClientOptions.TokenRefreshBufferPeriod = DefaultTokenBufferPeriod
log.Info("TokenRefreshBufferPeriod not set, set to default value", zap.Duration("TokenRefreshBufferPeriod", DefaultTokenBufferPeriod))
}

if config.ClientOptions.TotalRetryDuration == 0 {
config.ClientOptions.TotalRetryDuration = DefaultTotalRetryDuration
log.Info("TotalRetryDuration not set, set to default value", zap.Duration("TotalRetryDuration", DefaultTotalRetryDuration))
}

if config.ClientOptions.CustomTimeout == 0 {
config.ClientOptions.CustomTimeout = DefaultTimeout
log.Info("CustomTimeout not set, set to default value", zap.Duration("CustomTimeout", DefaultTimeout))
}

// Determine the authentication method using the helper function
authMethod, err := DetermineAuthMethod(config.Auth)
if err != nil {
log.Error("Failed to determine authentication method", zap.Error(err))
return nil, err
}

// Create a new HTTP client with the provided configuration.
client := &Client{
APIHandler: apiHandler,
InstanceName: config.Environment.InstanceName,
AuthMethod: authMethod,
OverrideBaseDomain: config.Environment.OverrideBaseDomain,
httpClient: &http.Client{Timeout: config.ClientOptions.CustomTimeout},
clientConfig: config,
Logger: log,
ConcurrencyMgr: NewConcurrencyManager(config.ClientOptions.MaxConcurrentRequests, log, true),
PerfMetrics: PerformanceMetrics{},
}

// Log the client's configuration.
log.Info("New API client initialized",
zap.String("API Type", config.Environment.APIType),
zap.String("Instance Name", client.InstanceName),
zap.String("Override Base Domain", config.Environment.OverrideBaseDomain),
zap.String("Authentication Method", authMethod),
zap.String("Logging Level", config.ClientOptions.LogLevel),
zap.String("Log Encoding Format", config.ClientOptions.LogOutputFormat),
zap.String("Log Separator", config.ClientOptions.LogConsoleSeparator),
zap.Bool("Hide Sensitive Data In Logs", config.ClientOptions.HideSensitiveData),
zap.Int("Max Retry Attempts", config.ClientOptions.MaxRetryAttempts),
zap.Int("Max Concurrent Requests", config.ClientOptions.MaxConcurrentRequests),
zap.Bool("Enable Dynamic Rate Limiting", config.ClientOptions.EnableDynamicRateLimiting),
zap.Duration("Token Refresh Buffer Period", config.ClientOptions.TokenRefreshBufferPeriod),
zap.Duration("Total Retry Duration", config.ClientOptions.TotalRetryDuration),
zap.Duration("Custom Timeout", config.ClientOptions.CustomTimeout),
)

return client, nil

}
Loading