|
| 1 | +// http_client.go |
| 2 | +/* The `http_client` package provides a configurable HTTP client tailored for interacting with specific APIs. |
| 3 | +It supports different authentication methods, including "bearer" and "oauth". The client is designed with a |
| 4 | +focus on concurrency management, structured error handling, and flexible configuration options. |
| 5 | +The package offers a default timeout, custom backoff strategies, dynamic rate limiting, |
| 6 | +and detailed logging capabilities. The main `Client` structure encapsulates all necessary components, |
| 7 | +like the baseURL, authentication details, and an embedded standard HTTP client. */ |
| 8 | +package httpclient |
| 9 | + |
| 10 | +import ( |
| 11 | + "net/http" |
| 12 | + "sync" |
| 13 | + "time" |
| 14 | + |
| 15 | + "github.com/deploymenttheory/go-api-http-client/logger" |
| 16 | + "go.uber.org/zap" |
| 17 | +) |
| 18 | + |
| 19 | +// Client represents an HTTP client to interact with a specific API. |
| 20 | +type Client struct { |
| 21 | + APIHandler APIHandler // APIHandler interface used to define which API handler to use |
| 22 | + InstanceName string // Website Instance name without the root domain |
| 23 | + AuthMethod string // Specifies the authentication method: "bearer" or "oauth" |
| 24 | + Token string // Authentication Token |
| 25 | + OverrideBaseDomain string // Base domain override used when the default in the api handler isn't suitable |
| 26 | + OAuthCredentials OAuthCredentials // ClientID / Client Secret |
| 27 | + BearerTokenAuthCredentials BearerTokenAuthCredentials // Username and Password for Basic Authentication |
| 28 | + Expiry time.Time // Expiry time set for the auth token |
| 29 | + httpClient *http.Client |
| 30 | + tokenLock sync.Mutex |
| 31 | + clientConfig ClientConfig |
| 32 | + Logger logger.Logger |
| 33 | + ConcurrencyMgr *ConcurrencyManager |
| 34 | + PerfMetrics PerformanceMetrics |
| 35 | +} |
| 36 | + |
| 37 | +// Config holds configuration options for the HTTP Client. |
| 38 | +type ClientConfig struct { |
| 39 | + Auth AuthConfig // User can either supply these values manually or pass from LoadAuthConfig/Env vars |
| 40 | + Environment EnvironmentConfig // User can either supply these values manually or pass from LoadAuthConfig/Env vars |
| 41 | + ClientOptions ClientOptions // Optional configuration options for the HTTP Client |
| 42 | +} |
| 43 | + |
| 44 | +// EnvironmentConfig represents the structure to read authentication details from a JSON configuration file. |
| 45 | +type EnvironmentConfig struct { |
| 46 | + InstanceName string `json:"InstanceName,omitempty"` |
| 47 | + OverrideBaseDomain string `json:"OverrideBaseDomain,omitempty"` |
| 48 | + APIType string `json:"APIType,omitempty"` |
| 49 | +} |
| 50 | + |
| 51 | +// AuthConfig represents the structure to read authentication details from a JSON configuration file. |
| 52 | +type AuthConfig struct { |
| 53 | + Username string `json:"Username,omitempty"` |
| 54 | + Password string `json:"Password,omitempty"` |
| 55 | + ClientID string `json:"ClientID,omitempty"` |
| 56 | + ClientSecret string `json:"ClientSecret,omitempty"` |
| 57 | +} |
| 58 | + |
| 59 | +// ClientOptions holds optional configuration options for the HTTP Client. |
| 60 | +type ClientOptions struct { |
| 61 | + LogLevel string // Field for defining tiered logging level. |
| 62 | + LogOutputFormat string // Field for defining the output format of the logs. Use "JSON" for JSON format, "console" for human-readable format |
| 63 | + LogConsoleSeparator string // Field for defining the separator in console output format. |
| 64 | + HideSensitiveData bool // Field for defining whether sensitive fields should be hidden in logs. |
| 65 | + MaxRetryAttempts int // Config item defines the max number of retry request attempts for retryable HTTP methods. |
| 66 | + EnableDynamicRateLimiting bool // Field for defining whether dynamic rate limiting should be enabled. |
| 67 | + MaxConcurrentRequests int // Field for defining the maximum number of concurrent requests allowed in the semaphore |
| 68 | + TokenRefreshBufferPeriod time.Duration |
| 69 | + TotalRetryDuration time.Duration |
| 70 | + CustomTimeout time.Duration |
| 71 | +} |
| 72 | + |
| 73 | +// ClientPerformanceMetrics captures various metrics related to the client's |
| 74 | +// interactions with the API, providing insights into its performance and behavior. |
| 75 | +type PerformanceMetrics struct { |
| 76 | + TotalRequests int64 |
| 77 | + TotalRetries int64 |
| 78 | + TotalRateLimitErrors int64 |
| 79 | + TotalResponseTime time.Duration |
| 80 | + TokenWaitTime time.Duration |
| 81 | + lock sync.Mutex |
| 82 | +} |
| 83 | + |
| 84 | +// BuildClient creates a new HTTP client with the provided configuration. |
| 85 | +func BuildClient(config ClientConfig) (*Client, error) { |
| 86 | + // Parse the log level string to logger.LogLevel |
| 87 | + parsedLogLevel := logger.ParseLogLevelFromString(config.ClientOptions.LogLevel) |
| 88 | + |
| 89 | + // Set default value if none is provided |
| 90 | + if config.ClientOptions.LogConsoleSeparator == "" { |
| 91 | + config.ClientOptions.LogConsoleSeparator = "," |
| 92 | + } |
| 93 | + |
| 94 | + // Initialize the logger with parsed config values |
| 95 | + log := logger.BuildLogger(parsedLogLevel, config.ClientOptions.LogOutputFormat, config.ClientOptions.LogConsoleSeparator) |
| 96 | + |
| 97 | + // Set the logger's level (optional if BuildLogger already sets the level based on the input) |
| 98 | + log.SetLevel(parsedLogLevel) |
| 99 | + |
| 100 | + // Use the APIType from the config to determine which API handler to load |
| 101 | + apiHandler, err := LoadAPIHandler(config.Environment.APIType, log) |
| 102 | + if err != nil { |
| 103 | + return nil, log.Error("Failed to load API handler", zap.String("APIType", config.Environment.APIType), zap.Error(err)) |
| 104 | + } |
| 105 | + |
| 106 | + log.Info("Initializing new HTTP client with the provided configuration") |
| 107 | + |
| 108 | + // Validate and set default values for the configuration |
| 109 | + if config.Environment.APIType == "" { |
| 110 | + return nil, log.Error("InstanceName cannot be empty") |
| 111 | + } |
| 112 | + |
| 113 | + if config.ClientOptions.MaxRetryAttempts < 0 { |
| 114 | + config.ClientOptions.MaxRetryAttempts = DefaultMaxRetryAttempts |
| 115 | + log.Info("MaxRetryAttempts was negative, set to default value", zap.Int("MaxRetryAttempts", DefaultMaxRetryAttempts)) |
| 116 | + } |
| 117 | + |
| 118 | + if config.ClientOptions.MaxConcurrentRequests <= 0 { |
| 119 | + config.ClientOptions.MaxConcurrentRequests = DefaultMaxConcurrentRequests |
| 120 | + log.Info("MaxConcurrentRequests was negative or zero, set to default value", zap.Int("MaxConcurrentRequests", DefaultMaxConcurrentRequests)) |
| 121 | + } |
| 122 | + |
| 123 | + if config.ClientOptions.TokenRefreshBufferPeriod < 0 { |
| 124 | + config.ClientOptions.TokenRefreshBufferPeriod = DefaultTokenBufferPeriod |
| 125 | + log.Info("TokenRefreshBufferPeriod was negative, set to default value", zap.Duration("TokenRefreshBufferPeriod", DefaultTokenBufferPeriod)) |
| 126 | + } |
| 127 | + |
| 128 | + if config.ClientOptions.TotalRetryDuration <= 0 { |
| 129 | + config.ClientOptions.TotalRetryDuration = DefaultTotalRetryDuration |
| 130 | + log.Info("TotalRetryDuration was negative or zero, set to default value", zap.Duration("TotalRetryDuration", DefaultTotalRetryDuration)) |
| 131 | + } |
| 132 | + |
| 133 | + if config.ClientOptions.TokenRefreshBufferPeriod == 0 { |
| 134 | + config.ClientOptions.TokenRefreshBufferPeriod = DefaultTokenBufferPeriod |
| 135 | + log.Info("TokenRefreshBufferPeriod not set, set to default value", zap.Duration("TokenRefreshBufferPeriod", DefaultTokenBufferPeriod)) |
| 136 | + } |
| 137 | + |
| 138 | + if config.ClientOptions.TotalRetryDuration == 0 { |
| 139 | + config.ClientOptions.TotalRetryDuration = DefaultTotalRetryDuration |
| 140 | + log.Info("TotalRetryDuration not set, set to default value", zap.Duration("TotalRetryDuration", DefaultTotalRetryDuration)) |
| 141 | + } |
| 142 | + |
| 143 | + if config.ClientOptions.CustomTimeout == 0 { |
| 144 | + config.ClientOptions.CustomTimeout = DefaultTimeout |
| 145 | + log.Info("CustomTimeout not set, set to default value", zap.Duration("CustomTimeout", DefaultTimeout)) |
| 146 | + } |
| 147 | + |
| 148 | + // Determine the authentication method using the helper function |
| 149 | + authMethod, err := DetermineAuthMethod(config.Auth) |
| 150 | + if err != nil { |
| 151 | + log.Error("Failed to determine authentication method", zap.Error(err)) |
| 152 | + return nil, err |
| 153 | + } |
| 154 | + |
| 155 | + // Create a new HTTP client with the provided configuration. |
| 156 | + client := &Client{ |
| 157 | + APIHandler: apiHandler, |
| 158 | + InstanceName: config.Environment.InstanceName, |
| 159 | + AuthMethod: authMethod, |
| 160 | + OverrideBaseDomain: config.Environment.OverrideBaseDomain, |
| 161 | + httpClient: &http.Client{Timeout: config.ClientOptions.CustomTimeout}, |
| 162 | + clientConfig: config, |
| 163 | + Logger: log, |
| 164 | + ConcurrencyMgr: NewConcurrencyManager(config.ClientOptions.MaxConcurrentRequests, log, true), |
| 165 | + PerfMetrics: PerformanceMetrics{}, |
| 166 | + } |
| 167 | + |
| 168 | + // Log the client's configuration. |
| 169 | + log.Info("New API client initialized", |
| 170 | + zap.String("API Type", config.Environment.APIType), |
| 171 | + zap.String("Instance Name", client.InstanceName), |
| 172 | + zap.String("Override Base Domain", config.Environment.OverrideBaseDomain), |
| 173 | + zap.String("Authentication Method", authMethod), |
| 174 | + zap.String("Logging Level", config.ClientOptions.LogLevel), |
| 175 | + zap.String("Log Encoding Format", config.ClientOptions.LogOutputFormat), |
| 176 | + zap.String("Log Separator", config.ClientOptions.LogConsoleSeparator), |
| 177 | + zap.Bool("Hide Sensitive Data In Logs", config.ClientOptions.HideSensitiveData), |
| 178 | + zap.Int("Max Retry Attempts", config.ClientOptions.MaxRetryAttempts), |
| 179 | + zap.Int("Max Concurrent Requests", config.ClientOptions.MaxConcurrentRequests), |
| 180 | + zap.Bool("Enable Dynamic Rate Limiting", config.ClientOptions.EnableDynamicRateLimiting), |
| 181 | + zap.Duration("Token Refresh Buffer Period", config.ClientOptions.TokenRefreshBufferPeriod), |
| 182 | + zap.Duration("Total Retry Duration", config.ClientOptions.TotalRetryDuration), |
| 183 | + zap.Duration("Custom Timeout", config.ClientOptions.CustomTimeout), |
| 184 | + ) |
| 185 | + |
| 186 | + return client, nil |
| 187 | + |
| 188 | +} |
0 commit comments