feat: add logging and support for custom logger (#36)
* feat: add support for logging * fix linting * chore: remove old APIError in favor of ServerError
This commit is contained in:
committed by
GitHub
parent
95a4ff87ea
commit
e79dcb13f0
@@ -6,6 +6,7 @@ linters:
|
|||||||
- 'mnd'
|
- 'mnd'
|
||||||
- 'nlreturn'
|
- 'nlreturn'
|
||||||
- 'tagliatelle'
|
- 'tagliatelle'
|
||||||
|
- 'ireturn'
|
||||||
|
|
||||||
# Temporary
|
# Temporary
|
||||||
- 'cyclop'
|
- 'cyclop'
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
|
|
||||||
type {{ .Name }} interface {
|
type {{ .Name }} interface {
|
||||||
|
|
||||||
|
Logger
|
||||||
|
|
||||||
{{- range $k, $v := .Functions }}
|
{{- range $k, $v := .Functions }}
|
||||||
|
|
||||||
{{ if $v.Comment }}// {{ $v.Comment }}{{ end }}
|
{{ if $v.Comment }}// {{ $v.Comment }}{{ end }}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ type defaultUnifiVersionProvider struct {
|
|||||||
firmwareUpdateApi string
|
firmwareUpdateApi string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUnifiVersionProvider(firmwareUpdateApi string) UnifiVersionProvider { //nolint:ireturn
|
func NewUnifiVersionProvider(firmwareUpdateApi string) UnifiVersionProvider {
|
||||||
return &defaultUnifiVersionProvider{
|
return &defaultUnifiVersionProvider{
|
||||||
firmwareUpdateApi: firmwareUpdateApi,
|
firmwareUpdateApi: firmwareUpdateApi,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,8 +94,76 @@ if err != nil {
|
|||||||
|
|
||||||
## Debugging and Logging
|
## Debugging and Logging
|
||||||
|
|
||||||
For troubleshooting, it might be useful to enable verbose logging. You can implement an interceptor to log additional
|
The SDK provides flexible logging capabilities through the `Logger` interface. You can either use the default logger or implement your own custom logger.
|
||||||
details like headers, body content, and timings. This can be enabled conditionally in your application's debug mode.
|
|
||||||
|
### Using the Default Logger
|
||||||
|
|
||||||
|
The SDK includes a default logger based on [logrus](https://github.com/sirupsen/logrus). You can configure it with different logging levels:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Configure client with default logger at Debug level
|
||||||
|
config := &unifi.ClientConfig{
|
||||||
|
URL: "https://unifi.localdomain",
|
||||||
|
APIKey: "your-api-key",
|
||||||
|
Logger: unifi.NewDefaultLogger(unifi.DebugLevel),
|
||||||
|
}
|
||||||
|
client, err := unifi.NewClient(config)
|
||||||
|
```
|
||||||
|
|
||||||
|
Available logging levels are:
|
||||||
|
- `unifi.DisabledLevel` - no logging
|
||||||
|
- `unifi.TraceLevel` - most verbose level
|
||||||
|
- `unifi.DebugLevel` - debug information
|
||||||
|
- `unifi.InfoLevel` - default level, informational messages
|
||||||
|
- `unifi.WarnLevel` - warning messages
|
||||||
|
- `unifi.ErrorLevel` - error messages only
|
||||||
|
|
||||||
|
Then `Logger` methods are available to be used within the client:
|
||||||
|
|
||||||
|
```go
|
||||||
|
client.Logger.Trace("Trace message")
|
||||||
|
client.Logger.Tracef("Trace message with %s", "formatting")
|
||||||
|
client.Logger.Debug("Debug message")
|
||||||
|
client.Logger.Debugf("Debug message with %s", "formatting")
|
||||||
|
client.Logger.Info("Info message")
|
||||||
|
client.Logger.Infof("Info message with %s", "formatting")
|
||||||
|
client.Logger.Warn("Warn message")
|
||||||
|
client.Logger.Warnf("Warn message with %s", "formatting")
|
||||||
|
client.Logger.Error("Error message")
|
||||||
|
client.Logger.Errorf("Error message with %s", "formatting")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Logger Implementation
|
||||||
|
|
||||||
|
You can implement your own logger by implementing the `Logger` interface:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type MyCustomLogger struct {
|
||||||
|
// your logger fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement all required methods
|
||||||
|
func (l *MyCustomLogger) Trace(msg string) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Debug(msg string) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Info(msg string) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Error(msg string) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Warn(msg string) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Tracef(format string, args ...interface{}) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Debugf(format string, args ...interface{}) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Infof(format string, args ...interface{}) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Errorf(format string, args ...interface{}) { /* implementation */ }
|
||||||
|
func (l *MyCustomLogger) Warnf(format string, args ...interface{}) { /* implementation */ }
|
||||||
|
|
||||||
|
// Use custom logger in client configuration
|
||||||
|
config := &unifi.ClientConfig{
|
||||||
|
URL: "https://unifi.localdomain",
|
||||||
|
APIKey: "your-api-key",
|
||||||
|
Logger: &MyCustomLogger{},
|
||||||
|
}
|
||||||
|
client, err := unifi.NewClient(config)
|
||||||
|
```
|
||||||
|
|
||||||
|
If no logger is specified in the configuration, the SDK will use the default logger with `Info` level.
|
||||||
|
|
||||||
## Advanced Error Handling
|
## Advanced Error Handling
|
||||||
|
|
||||||
|
|||||||
@@ -71,6 +71,10 @@ if err != nil {
|
|||||||
- `HttpTransportCustomizer` for transport-level customization
|
- `HttpTransportCustomizer` for transport-level customization
|
||||||
- `HttpRoundTripperProvider` for complete HTTP client control
|
- `HttpRoundTripperProvider` for complete HTTP client control
|
||||||
|
|
||||||
|
4. **Removed unifi.APIError**:
|
||||||
|
- Old: `unifi.APIError` struct for API errors
|
||||||
|
- New: Standard `unifi.ServerError` struct for API errors
|
||||||
|
|
||||||
5. **Additional Features in filipowm/go-unifi**:
|
5. **Additional Features in filipowm/go-unifi**:
|
||||||
- Validation modes (Soft, Hard, Disabled)
|
- Validation modes (Soft, Hard, Disabled)
|
||||||
- Request/Response interceptors
|
- Request/Response interceptors
|
||||||
@@ -90,6 +94,7 @@ if err != nil {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
4. Remove explicit `Login()` calls as they are now handled automatically, unless you use [bare client initialization](./getting_started.md#BareClientInitialization)
|
4. Remove explicit `Login()` calls as they are now handled automatically, unless you use [bare client initialization](./getting_started.md#BareClientInitialization)
|
||||||
|
5. Replace usage of `unifi.APIError` with `unifi.ServerError`
|
||||||
|
|
||||||
The rest of your code using the client methods should continue to work as before, as the API methods remain the same.
|
The rest of your code using the client methods should continue to work as before, as the API methods remain the same.
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ var (
|
|||||||
|
|
||||||
// determineApiStyle checks the base URL to decide which API style to use and sets the apiPaths accordingly.
|
// determineApiStyle checks the base URL to decide which API style to use and sets the apiPaths accordingly.
|
||||||
func (c *client) determineApiStyle() error {
|
func (c *client) determineApiStyle() error {
|
||||||
|
c.Debug("Determining API style")
|
||||||
ctx, cancel := c.newRequestContext()
|
ctx, cancel := c.newRequestContext()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
@@ -85,8 +86,10 @@ func (c *client) determineApiStyle() error {
|
|||||||
|
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
|
c.Debug("Using new style API")
|
||||||
c.apiPaths = &NewStyleAPI
|
c.apiPaths = &NewStyleAPI
|
||||||
case http.StatusFound:
|
case http.StatusFound:
|
||||||
|
c.Debug("Using old style API")
|
||||||
c.apiPaths = &OldStyleAPI
|
c.apiPaths = &OldStyleAPI
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("expected 200 or 302 status code, but got: %d", resp.StatusCode)
|
return fmt.Errorf("expected 200 or 302 status code, but got: %d", resp.StatusCode)
|
||||||
|
|||||||
1
unifi/client.generated.go
generated
1
unifi/client.generated.go
generated
@@ -8,6 +8,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Client interface {
|
type Client interface {
|
||||||
|
Logger
|
||||||
|
|
||||||
// BaseURL returns the base URL of the controller.
|
// BaseURL returns the base URL of the controller.
|
||||||
BaseURL() string
|
BaseURL() string
|
||||||
|
|||||||
@@ -14,20 +14,17 @@ import (
|
|||||||
"golang.org/x/net/publicsuffix"
|
"golang.org/x/net/publicsuffix"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validationMode represents the mode for request validation.
|
// ValidationMode represents the mode for request validation.
|
||||||
// It may be set to "soft", "hard", or "disable". The default is "soft".
|
// It may be set to "soft", "hard", or "disable". The default is "soft".
|
||||||
type validationMode string
|
type ValidationMode int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// SoftValidation indicates that validation errors are logged as warnings but do not prevent the request from proceeding.
|
// SoftValidation indicates that validation errors are logged as warnings but do not prevent the request from proceeding.
|
||||||
SoftValidation validationMode = "soft"
|
SoftValidation ValidationMode = iota
|
||||||
// HardValidation indicates that validation errors are treated as fatal and will cause the request to be rejected.
|
// HardValidation indicates that validation errors are treated as fatal and will cause the request to be rejected.
|
||||||
HardValidation validationMode = "hard"
|
HardValidation
|
||||||
// DisableValidation indicates that no validation is performed on the request body.
|
// DisableValidation indicates that no validation is performed on the request body.
|
||||||
DisableValidation validationMode = "disable"
|
DisableValidation
|
||||||
// DefaultValidation is the default validation mode used if none is specified.
|
|
||||||
// Currently set to SoftValidation, but may change to HardValidation in a future major version.
|
|
||||||
DefaultValidation validationMode = SoftValidation // TODO: change to hard in next major version
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HttpTransportCustomizer is a function type for customizing the HTTP transport.
|
// HttpTransportCustomizer is a function type for customizing the HTTP transport.
|
||||||
@@ -73,7 +70,8 @@ type ClientConfig struct {
|
|||||||
UserAgent string
|
UserAgent string
|
||||||
ErrorHandler ResponseErrorHandler
|
ErrorHandler ResponseErrorHandler
|
||||||
UseLocking bool
|
UseLocking bool
|
||||||
ValidationMode validationMode `validate:"omitempty,oneof=soft hard disable"`
|
ValidationMode ValidationMode
|
||||||
|
Logger Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credentials abstracts authentication credentials.
|
// Credentials abstracts authentication credentials.
|
||||||
@@ -112,12 +110,13 @@ func (u UserPassCredentials) GetPass() string { return u.Password }
|
|||||||
|
|
||||||
// client represents a UniFi client.
|
// client represents a UniFi client.
|
||||||
type client struct {
|
type client struct {
|
||||||
|
Logger
|
||||||
baseURL *url.URL
|
baseURL *url.URL
|
||||||
sysInfo *SysInfo
|
sysInfo *SysInfo
|
||||||
apiPaths *APIPaths
|
apiPaths *APIPaths
|
||||||
timeout time.Duration
|
timeout time.Duration
|
||||||
credentials Credentials
|
credentials Credentials
|
||||||
validationMode validationMode
|
validationMode ValidationMode
|
||||||
useLocking bool
|
useLocking bool
|
||||||
|
|
||||||
http *http.Client
|
http *http.Client
|
||||||
@@ -168,10 +167,19 @@ func (c *client) Version() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
|
func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
|
||||||
|
var log Logger
|
||||||
|
if config.Logger != nil {
|
||||||
|
log = config.Logger
|
||||||
|
} else {
|
||||||
|
log = NewDefaultLogger(InfoLevel)
|
||||||
|
}
|
||||||
|
log.Info("Initializing new UniFi client")
|
||||||
var rt http.RoundTripper
|
var rt http.RoundTripper
|
||||||
var err error
|
var err error
|
||||||
config.URL = strings.TrimRight(config.URL, "/")
|
config.URL = strings.TrimRight(config.URL, "/")
|
||||||
|
log.Debugf("Connecting to UniFi controller at %s", config.URL)
|
||||||
if config.HttpRoundTripperProvider != nil {
|
if config.HttpRoundTripperProvider != nil {
|
||||||
|
log.Debug("Using custom HTTP round tripper provider")
|
||||||
rt = config.HttpRoundTripperProvider()
|
rt = config.HttpRoundTripperProvider()
|
||||||
}
|
}
|
||||||
if rt == nil {
|
if rt == nil {
|
||||||
@@ -180,6 +188,7 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
|
|||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.VerifySSL},
|
||||||
}
|
}
|
||||||
if config.HttpTransportCustomizer != nil {
|
if config.HttpTransportCustomizer != nil {
|
||||||
|
log.Debug("Customizing HTTP transport")
|
||||||
if transport, err = config.HttpTransportCustomizer(transport); err != nil {
|
if transport, err = config.HttpTransportCustomizer(transport); err != nil {
|
||||||
return nil, fmt.Errorf("failed customizing HTTP transport: %w", err)
|
return nil, fmt.Errorf("failed customizing HTTP transport: %w", err)
|
||||||
}
|
}
|
||||||
@@ -205,14 +214,18 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
|
|||||||
var credentials Credentials
|
var credentials Credentials
|
||||||
|
|
||||||
if config.APIKey != "" {
|
if config.APIKey != "" {
|
||||||
|
log.Debug("Using API key authentication")
|
||||||
credentials = APIKeyCredentials{APIKey: config.APIKey}
|
credentials = APIKeyCredentials{APIKey: config.APIKey}
|
||||||
interceptors = append(interceptors, &APIKeyAuthInterceptor{apiKey: config.APIKey})
|
interceptors = append(interceptors, &APIKeyAuthInterceptor{apiKey: config.APIKey})
|
||||||
} else {
|
} else {
|
||||||
|
log.Debug("Using user/pass authentication")
|
||||||
credentials = UserPassCredentials{User: config.User, Password: config.Password}
|
credentials = UserPassCredentials{User: config.User, Password: config.Password}
|
||||||
interceptors = append(interceptors, &CSRFInterceptor{})
|
interceptors = append(interceptors, &CSRFInterceptor{})
|
||||||
}
|
}
|
||||||
if len(config.UserAgent) == 0 {
|
if len(config.UserAgent) == 0 {
|
||||||
config.UserAgent = defaultUserAgent
|
config.UserAgent = defaultUserAgent
|
||||||
|
} else {
|
||||||
|
log.Debugf("Using custom User-Agent header: %s", config.UserAgent)
|
||||||
}
|
}
|
||||||
interceptors = append(interceptors, &DefaultHeadersInterceptor{headers: map[string]string{
|
interceptors = append(interceptors, &DefaultHeadersInterceptor{headers: map[string]string{
|
||||||
UserAgentHeader: config.UserAgent,
|
UserAgentHeader: config.UserAgent,
|
||||||
@@ -221,13 +234,13 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
|
|||||||
}})
|
}})
|
||||||
var errorHandler ResponseErrorHandler
|
var errorHandler ResponseErrorHandler
|
||||||
if config.ErrorHandler != nil {
|
if config.ErrorHandler != nil {
|
||||||
|
log.Debug("Using custom response error handler")
|
||||||
errorHandler = config.ErrorHandler
|
errorHandler = config.ErrorHandler
|
||||||
} else {
|
} else {
|
||||||
|
log.Debug("Using default response error handler")
|
||||||
errorHandler = &DefaultResponseErrorHandler{}
|
errorHandler = &DefaultResponseErrorHandler{}
|
||||||
}
|
}
|
||||||
if config.ValidationMode == "" {
|
log.Tracef("Validation mode: %d", config.ValidationMode)
|
||||||
config.ValidationMode = DefaultValidation
|
|
||||||
}
|
|
||||||
u := &client{
|
u := &client{
|
||||||
baseURL: baseURL,
|
baseURL: baseURL,
|
||||||
timeout: config.Timeout,
|
timeout: config.Timeout,
|
||||||
@@ -239,6 +252,7 @@ func newClientFromConfig(config *ClientConfig, v *validator) (*client, error) {
|
|||||||
errorHandler: errorHandler,
|
errorHandler: errorHandler,
|
||||||
lock: sync.Mutex{},
|
lock: sync.Mutex{},
|
||||||
validator: v,
|
validator: v,
|
||||||
|
Logger: log,
|
||||||
}
|
}
|
||||||
for _, interceptor := range config.Interceptors {
|
for _, interceptor := range config.Interceptors {
|
||||||
u.AddInterceptor(&interceptor)
|
u.AddInterceptor(&interceptor)
|
||||||
@@ -262,6 +276,7 @@ func NewClient(config *ClientConfig) (Client, error) { //nolint: ireturn
|
|||||||
return c, fmt.Errorf("failed getting server info: %w", err)
|
return c, fmt.Errorf("failed getting server info: %w", err)
|
||||||
} else {
|
} else {
|
||||||
c.sysInfo = sysInfo
|
c.sysInfo = sysInfo
|
||||||
|
c.Debugf("Connected to UniFi controller\nversion: %s; name: %s; build: %s; hostname: %s", sysInfo.Version, sysInfo.Name, sysInfo.Build, sysInfo.Hostname)
|
||||||
}
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
@@ -296,8 +311,10 @@ func newBareClient(config *ClientConfig) (*client, error) {
|
|||||||
// It returns an error if the authentication process fails.
|
// It returns an error if the authentication process fails.
|
||||||
func (c *client) Login() error {
|
func (c *client) Login() error {
|
||||||
if c.credentials.IsAPIKey() {
|
if c.credentials.IsAPIKey() {
|
||||||
|
c.Trace("API key authentication; skipping login")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
c.Trace("Logging in with user/pass credentials")
|
||||||
|
|
||||||
ctx, cancel := c.newRequestContext()
|
ctx, cancel := c.newRequestContext()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
115
unifi/logging.go
Normal file
115
unifi/logging.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package unifi
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger interface {
|
||||||
|
Trace(format string)
|
||||||
|
Debug(format string)
|
||||||
|
Info(format string)
|
||||||
|
Error(format string)
|
||||||
|
Warn(format string)
|
||||||
|
Tracef(format string, args ...interface{})
|
||||||
|
Debugf(format string, args ...interface{})
|
||||||
|
Infof(format string, args ...interface{})
|
||||||
|
Errorf(format string, args ...interface{})
|
||||||
|
Warnf(format string, args ...interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoggingLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DisabledLevel LoggingLevel = iota
|
||||||
|
TraceLevel
|
||||||
|
DebugLevel
|
||||||
|
InfoLevel
|
||||||
|
WarnLevel
|
||||||
|
ErrorLevel
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDefaultLogger(level LoggingLevel) Logger {
|
||||||
|
l := logrus.New()
|
||||||
|
var logrusLevel logrus.Level
|
||||||
|
switch level {
|
||||||
|
case DisabledLevel:
|
||||||
|
return &noopLogger{}
|
||||||
|
case TraceLevel:
|
||||||
|
logrusLevel = logrus.TraceLevel
|
||||||
|
case DebugLevel:
|
||||||
|
logrusLevel = logrus.DebugLevel
|
||||||
|
case InfoLevel:
|
||||||
|
logrusLevel = logrus.InfoLevel
|
||||||
|
case WarnLevel:
|
||||||
|
logrusLevel = logrus.WarnLevel
|
||||||
|
case ErrorLevel:
|
||||||
|
logrusLevel = logrus.ErrorLevel
|
||||||
|
default:
|
||||||
|
logrusLevel = logrus.InfoLevel
|
||||||
|
}
|
||||||
|
l.SetLevel(logrusLevel)
|
||||||
|
l.SetFormatter(&logrus.TextFormatter{
|
||||||
|
DisableTimestamp: true,
|
||||||
|
DisableLevelTruncation: true,
|
||||||
|
FullTimestamp: false,
|
||||||
|
ForceColors: true,
|
||||||
|
})
|
||||||
|
return &defaultLogger{l}
|
||||||
|
}
|
||||||
|
|
||||||
|
type noopLogger struct{}
|
||||||
|
|
||||||
|
func (l *noopLogger) Trace(msg string) {}
|
||||||
|
func (l *noopLogger) Debug(msg string) {}
|
||||||
|
func (l *noopLogger) Info(msg string) {}
|
||||||
|
func (l *noopLogger) Error(msg string) {}
|
||||||
|
func (l *noopLogger) Warn(msg string) {}
|
||||||
|
func (l *noopLogger) Tracef(format string, args ...interface{}) {}
|
||||||
|
func (l *noopLogger) Debugf(format string, args ...interface{}) {}
|
||||||
|
func (l *noopLogger) Infof(format string, args ...interface{}) {}
|
||||||
|
func (l *noopLogger) Errorf(format string, args ...interface{}) {}
|
||||||
|
func (l *noopLogger) Warnf(format string, args ...interface{}) {}
|
||||||
|
|
||||||
|
type defaultLogger struct {
|
||||||
|
*logrus.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Trace(msg string) {
|
||||||
|
l.Logger.Trace(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Debug(msg string) {
|
||||||
|
l.Logger.Debug(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Info(msg string) {
|
||||||
|
l.Logger.Info(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Error(msg string) {
|
||||||
|
l.Logger.Error(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Warn(msg string) {
|
||||||
|
l.Logger.Warn(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Tracef(format string, args ...interface{}) {
|
||||||
|
l.Logger.Tracef(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Debugf(format string, args ...interface{}) {
|
||||||
|
l.Logger.Debugf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Infof(format string, args ...interface{}) {
|
||||||
|
l.Logger.Infof(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Errorf(format string, args ...interface{}) {
|
||||||
|
l.Logger.Errorf(format, args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *defaultLogger) Warnf(format string, args ...interface{}) {
|
||||||
|
l.Logger.Warnf(format, args...)
|
||||||
|
}
|
||||||
@@ -39,12 +39,12 @@ func (c *client) buildRequestURL(apiPath string) (*url.URL, error) {
|
|||||||
// validateRequestBody validates the request body if validation is enabled.
|
// validateRequestBody validates the request body if validation is enabled.
|
||||||
func (c *client) validateRequestBody(reqBody interface{}) error {
|
func (c *client) validateRequestBody(reqBody interface{}) error {
|
||||||
if reqBody != nil && c.validationMode != DisableValidation {
|
if reqBody != nil && c.validationMode != DisableValidation {
|
||||||
|
c.Trace("Validating request body")
|
||||||
if err := c.validator.Validate(reqBody); err != nil {
|
if err := c.validator.Validate(reqBody); err != nil {
|
||||||
err = fmt.Errorf("failed validating request body: %w", err)
|
|
||||||
if c.validationMode == HardValidation {
|
if c.validationMode == HardValidation {
|
||||||
return err
|
return fmt.Errorf("failed validating request body: %w", err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Println(err)
|
c.Warnf("failed validating request body: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,7 @@ func (c *client) newRequestContext() (context.Context, context.CancelFunc) {
|
|||||||
// It validates the request body, applies interceptors, and decodes the HTTP response into respBody if provided.
|
// It validates the request body, applies interceptors, and decodes the HTTP response into respBody if provided.
|
||||||
// It returns an error if the request or response handling fails.
|
// It returns an error if the request or response handling fails.
|
||||||
func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interface{}, respBody interface{}) error {
|
func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interface{}, respBody interface{}) error {
|
||||||
|
c.Tracef("Performing request: %s %s", method, apiPath)
|
||||||
if err := c.validateRequestBody(reqBody); err != nil {
|
if err := c.validateRequestBody(reqBody); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -76,6 +77,7 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create request URL: %w", err)
|
return fmt.Errorf("unable to create request URL: %w", err)
|
||||||
}
|
}
|
||||||
|
c.Debugf("Executing request: %s %s", method, url.String())
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, method, url.String(), reqReader)
|
req, err := http.NewRequestWithContext(ctx, method, url.String(), reqReader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -84,8 +86,10 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
|
|||||||
|
|
||||||
if c.useLocking {
|
if c.useLocking {
|
||||||
c.lock.Lock()
|
c.lock.Lock()
|
||||||
|
c.Trace("Acquired lock fo request")
|
||||||
defer c.lock.Unlock()
|
defer c.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
c.Trace("Executing request interceptors")
|
||||||
for _, interceptor := range c.interceptors {
|
for _, interceptor := range c.interceptors {
|
||||||
if err := interceptor.InterceptRequest(req); err != nil {
|
if err := interceptor.InterceptRequest(req); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -98,20 +102,24 @@ func (c *client) Do(ctx context.Context, method, apiPath string, reqBody interfa
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
c.Trace("Executing response interceptors")
|
||||||
for _, interceptor := range c.interceptors {
|
for _, interceptor := range c.interceptors {
|
||||||
if err := interceptor.InterceptResponse(resp); err != nil {
|
if err := interceptor.InterceptResponse(resp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Trace("Checking for errors in response")
|
||||||
if err := c.errorHandler.HandleError(resp); err != nil {
|
if err := c.errorHandler.HandleError(resp); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if respBody == nil || resp.ContentLength == 0 {
|
if respBody == nil || resp.ContentLength == 0 {
|
||||||
|
c.Trace("No response body to decode")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Trace("Decoding response body")
|
||||||
err = json.NewDecoder(resp.Body).Decode(respBody)
|
err = json.NewDecoder(resp.Body).Decode(respBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to decode body: %s %s %w", method, apiPath, err)
|
return fmt.Errorf("unable to decode body: %s %s %w", method, apiPath, err)
|
||||||
|
|||||||
@@ -117,6 +117,7 @@ func (c *client) getOldSysInfo(ctx context.Context) (*SysInfo, error) {
|
|||||||
|
|
||||||
// GetSystemInformation retrieves system information, trying the new API first and falling back to the old API if necessary.
|
// GetSystemInformation retrieves system information, trying the new API first and falling back to the old API if necessary.
|
||||||
func (c *client) GetSystemInformation() (*SysInfo, error) {
|
func (c *client) GetSystemInformation() (*SysInfo, error) {
|
||||||
|
c.Trace("Reading system information")
|
||||||
ctx, cancel := c.newRequestContext()
|
ctx, cancel := c.newRequestContext()
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
|||||||
@@ -10,26 +10,6 @@ import (
|
|||||||
|
|
||||||
var ErrNotFound = errors.New("not found")
|
var ErrNotFound = errors.New("not found")
|
||||||
|
|
||||||
// TODO old-style error handling to be removed in future versions.
|
|
||||||
type APIError struct {
|
|
||||||
RC string
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *APIError) Error() string {
|
|
||||||
return err.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
func (err *APIError) Is(target error) bool {
|
|
||||||
var apiError *APIError
|
|
||||||
if errors.As(target, &apiError) {
|
|
||||||
if err.RC == apiError.RC && err.Message == apiError.Message {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
RC string `json:"rc"`
|
RC string `json:"rc"`
|
||||||
Message string `json:"msg"`
|
Message string `json:"msg"`
|
||||||
@@ -37,9 +17,9 @@ type Meta struct {
|
|||||||
|
|
||||||
func (m *Meta) error() error {
|
func (m *Meta) error() error {
|
||||||
if m.RC != "ok" {
|
if m.RC != "ok" {
|
||||||
return &APIError{
|
return &ServerError{
|
||||||
RC: m.RC,
|
ErrorCode: m.RC,
|
||||||
Message: m.Message,
|
Message: m.Message,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -488,17 +488,16 @@ func TestUrlValidation(t *testing.T) {
|
|||||||
func TestValidationModeValidation(t *testing.T) {
|
func TestValidationModeValidation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
validationMode validationMode
|
validationMode ValidationMode
|
||||||
expectedError string
|
|
||||||
}{
|
}{
|
||||||
{SoftValidation, ""},
|
{SoftValidation},
|
||||||
{HardValidation, ""},
|
{HardValidation},
|
||||||
{DisableValidation, ""},
|
{DisableValidation},
|
||||||
{"invalid", "must be one of"},
|
{99},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(string(tc.validationMode), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", tc.validationMode), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// given
|
// given
|
||||||
cc := &ClientConfig{
|
cc := &ClientConfig{
|
||||||
@@ -511,12 +510,6 @@ func TestValidationModeValidation(t *testing.T) {
|
|||||||
|
|
||||||
// when
|
// when
|
||||||
err = v.Validate(cc)
|
err = v.Validate(cc)
|
||||||
|
|
||||||
// then
|
|
||||||
if tc.expectedError != "" {
|
|
||||||
require.ErrorContains(t, err, tc.expectedError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -541,7 +534,7 @@ type validateableBody struct {
|
|||||||
func TestValidationModes(t *testing.T) {
|
func TestValidationModes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
validationMode validationMode
|
validationMode ValidationMode
|
||||||
expectedError string
|
expectedError string
|
||||||
expectRequest bool
|
expectRequest bool
|
||||||
}{
|
}{
|
||||||
@@ -551,7 +544,7 @@ func TestValidationModes(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(string(tc.validationMode), func(t *testing.T) {
|
t.Run(fmt.Sprintf("%d", tc.validationMode), func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
a := assert.New(t)
|
a := assert.New(t)
|
||||||
// given
|
// given
|
||||||
@@ -843,11 +836,14 @@ func TestMarshalRequestValid(t *testing.T) {
|
|||||||
func TestLoginWithAPIKeyDirect(t *testing.T) {
|
func TestLoginWithAPIKeyDirect(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
// Create a client manually with the APIKey set.
|
// Create a client manually with the APIKey set.
|
||||||
c := &client{
|
|
||||||
credentials: APIKeyCredentials{APIKey: "abc"},
|
c, err := newBareClient(&ClientConfig{
|
||||||
}
|
APIKey: "abc",
|
||||||
err := c.Login()
|
URL: testUrl,
|
||||||
assert.NoError(t, err)
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
err = c.Login()
|
||||||
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHttpTransportCustomizerError(t *testing.T) {
|
func TestHttpTransportCustomizerError(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user