mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-06-05 23:03:48 +00:00
improvements mid may 2025 (#24)
* General improvements and bug fixes. * Improve tests coverage. * fixup! Improve tests coverage. * Update README.md with latest changes. * Fix the uint32 * Resolve issue with race condition for logging. * fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * Fix the test of the rate limiter * Add default ratelimit.json file * Update dependencies. * Significant refactor. * fixup! Significant refactor. * fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025 * fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! fixup! Merge remote-tracking branch 'origin/main' into improvements-mid-apr-2025
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Error codes for structured error responses
|
||||
const (
|
||||
ErrCodeConnectionRefused = "CONNECTION_REFUSED"
|
||||
ErrCodeConnectionReset = "CONNECTION_RESET"
|
||||
ErrCodeTimeout = "TIMEOUT"
|
||||
ErrCodeCircuitOpen = "CIRCUIT_OPEN"
|
||||
ErrCodeRateLimited = "RATE_LIMITED"
|
||||
ErrCodeInvalidRequest = "INVALID_REQUEST"
|
||||
ErrCodeBackendError = "BACKEND_ERROR"
|
||||
ErrCodeInternalError = "INTERNAL_ERROR"
|
||||
ErrCodeUnauthorized = "UNAUTHORIZED"
|
||||
ErrCodeForbidden = "FORBIDDEN"
|
||||
ErrCodeNotFound = "NOT_FOUND"
|
||||
ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
|
||||
ErrCodeBadGateway = "BAD_GATEWAY"
|
||||
ErrCodeInvalidResponse = "INVALID_RESPONSE"
|
||||
ErrCodeQueryTooComplex = "QUERY_TOO_COMPLEX"
|
||||
ErrCodeCacheFailed = "CACHE_FAILED"
|
||||
ErrCodeContextCanceled = "CONTEXT_CANCELED"
|
||||
)
|
||||
|
||||
// ProxyError represents a structured error response
|
||||
type ProxyError struct {
|
||||
Code string `json:"code"` // Machine-readable error code
|
||||
Message string `json:"message"` // Human-readable error message
|
||||
Details string `json:"details,omitempty"` // Additional error details
|
||||
Retryable bool `json:"retryable"` // Whether the request can be retried
|
||||
StatusCode int `json:"status_code"` // HTTP status code
|
||||
Timestamp time.Time `json:"timestamp"` // When the error occurred
|
||||
TraceID string `json:"trace_id,omitempty"` // Trace ID for correlation
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"` // Additional context
|
||||
Cause error `json:"-"` // Original error (not serialized)
|
||||
}
|
||||
|
||||
// Error implements the error interface
|
||||
func (e *ProxyError) Error() string {
|
||||
if e.Details != "" {
|
||||
return fmt.Sprintf("%s: %s (%s)", e.Code, e.Message, e.Details)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", e.Code, e.Message)
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying error
|
||||
func (e *ProxyError) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// MarshalJSON implements custom JSON marshaling
|
||||
func (e *ProxyError) MarshalJSON() ([]byte, error) {
|
||||
type Alias ProxyError
|
||||
return json.Marshal(&struct {
|
||||
*Alias
|
||||
CauseMessage string `json:"cause,omitempty"`
|
||||
}{
|
||||
Alias: (*Alias)(e),
|
||||
CauseMessage: func() string {
|
||||
if e.Cause != nil {
|
||||
return e.Cause.Error()
|
||||
}
|
||||
return ""
|
||||
}(),
|
||||
})
|
||||
}
|
||||
|
||||
// NewProxyError creates a new structured error
|
||||
func NewProxyError(code, message string, statusCode int, retryable bool) *ProxyError {
|
||||
return &ProxyError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
StatusCode: statusCode,
|
||||
Retryable: retryable,
|
||||
Timestamp: time.Now(),
|
||||
Metadata: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithDetails adds details to the error
|
||||
func (e *ProxyError) WithDetails(details string) *ProxyError {
|
||||
e.Details = details
|
||||
return e
|
||||
}
|
||||
|
||||
// WithCause adds the underlying cause
|
||||
func (e *ProxyError) WithCause(cause error) *ProxyError {
|
||||
e.Cause = cause
|
||||
return e
|
||||
}
|
||||
|
||||
// WithTraceID adds a trace ID
|
||||
func (e *ProxyError) WithTraceID(traceID string) *ProxyError {
|
||||
e.TraceID = traceID
|
||||
return e
|
||||
}
|
||||
|
||||
// WithMetadata adds metadata
|
||||
func (e *ProxyError) WithMetadata(key string, value interface{}) *ProxyError {
|
||||
e.Metadata[key] = value
|
||||
return e
|
||||
}
|
||||
|
||||
// Common error constructors
|
||||
|
||||
// NewConnectionError creates a connection-related error
|
||||
func NewConnectionError(err error) *ProxyError {
|
||||
code := ErrCodeConnectionRefused
|
||||
if err != nil {
|
||||
errStr := err.Error()
|
||||
if contains(errStr, "reset") {
|
||||
code = ErrCodeConnectionReset
|
||||
}
|
||||
}
|
||||
|
||||
return NewProxyError(code, "Failed to connect to backend", 502, true).
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
// NewTimeoutError creates a timeout error
|
||||
func NewTimeoutError(err error) *ProxyError {
|
||||
return NewProxyError(ErrCodeTimeout, "Request timed out", 504, false).
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
// NewCircuitOpenError creates a circuit breaker open error
|
||||
func NewCircuitOpenError() *ProxyError {
|
||||
return NewProxyError(ErrCodeCircuitOpen, "Service temporarily unavailable due to circuit breaker", 503, false).
|
||||
WithDetails("The backend service is currently experiencing issues. Please try again later.")
|
||||
}
|
||||
|
||||
// NewRateLimitError creates a rate limit error
|
||||
func NewRateLimitError(userID, role string) *ProxyError {
|
||||
return NewProxyError(ErrCodeRateLimited, "Rate limit exceeded", 429, false).
|
||||
WithDetails("You have exceeded the rate limit for your role").
|
||||
WithMetadata("user_id", userID).
|
||||
WithMetadata("role", role)
|
||||
}
|
||||
|
||||
// NewBackendError creates a backend error from status code
|
||||
func NewBackendError(statusCode int, body string) *ProxyError {
|
||||
code := ErrCodeBackendError
|
||||
message := "Backend returned an error"
|
||||
retryable := false
|
||||
|
||||
switch {
|
||||
case statusCode == 429:
|
||||
code = ErrCodeRateLimited
|
||||
message = "Backend rate limit exceeded"
|
||||
retryable = true
|
||||
case statusCode == 503:
|
||||
code = ErrCodeServiceUnavailable
|
||||
message = "Backend service unavailable"
|
||||
retryable = true
|
||||
case statusCode == 502 || statusCode == 504:
|
||||
code = ErrCodeBadGateway
|
||||
message = "Bad gateway"
|
||||
retryable = true
|
||||
case statusCode >= 500:
|
||||
code = ErrCodeBackendError
|
||||
message = "Backend server error"
|
||||
retryable = true
|
||||
case statusCode == 404:
|
||||
code = ErrCodeNotFound
|
||||
message = "Resource not found"
|
||||
case statusCode == 403:
|
||||
code = ErrCodeForbidden
|
||||
message = "Access forbidden"
|
||||
case statusCode == 401:
|
||||
code = ErrCodeUnauthorized
|
||||
message = "Unauthorized"
|
||||
case statusCode >= 400:
|
||||
code = ErrCodeInvalidRequest
|
||||
message = "Invalid request"
|
||||
}
|
||||
|
||||
return NewProxyError(code, message, statusCode, retryable).
|
||||
WithMetadata("backend_status", statusCode).
|
||||
WithMetadata("backend_body", truncateString(body, 500))
|
||||
}
|
||||
|
||||
// NewInvalidResponseError creates an invalid response error
|
||||
func NewInvalidResponseError(details string) *ProxyError {
|
||||
return NewProxyError(ErrCodeInvalidResponse, "Backend returned invalid response", 502, false).
|
||||
WithDetails(details)
|
||||
}
|
||||
|
||||
// NewInternalError creates an internal error
|
||||
func NewInternalError(err error) *ProxyError {
|
||||
return NewProxyError(ErrCodeInternalError, "Internal proxy error", 500, false).
|
||||
WithCause(err)
|
||||
}
|
||||
|
||||
// NewContextCanceledError creates a context canceled error
|
||||
func NewContextCanceledError() *ProxyError {
|
||||
return NewProxyError(ErrCodeContextCanceled, "Request canceled", 499, false).
|
||||
WithDetails("The request was canceled by the client")
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) > 0 && len(substr) > 0 && len(s) >= len(substr) && (s == substr || len(s) > len(substr) && (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || containsMiddle(s, substr)))
|
||||
}
|
||||
|
||||
func containsMiddle(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func truncateString(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen] + "..."
|
||||
}
|
||||
|
||||
// IsRetryable checks if an error is retryable
|
||||
func IsRetryable(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if proxyErr, ok := err.(*ProxyError); ok {
|
||||
return proxyErr.Retryable
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetStatusCode extracts the status code from an error
|
||||
func GetStatusCode(err error) int {
|
||||
if err == nil {
|
||||
return 200
|
||||
}
|
||||
|
||||
if proxyErr, ok := err.(*ProxyError); ok {
|
||||
return proxyErr.StatusCode
|
||||
}
|
||||
|
||||
return 500
|
||||
}
|
||||
Reference in New Issue
Block a user