first commit

This commit is contained in:
meninjar
2026-04-14 01:23:34 +00:00
commit edfaa886ff
443 changed files with 1245931 additions and 0 deletions
View File
+329
View File
@@ -0,0 +1,329 @@
package errors
import (
"net/http"
"time"
"google.golang.org/grpc/codes"
)
// Builder provides fluent API for building errors
type Builder struct {
code string
message string
category string
httpStatus int
grpcCode codes.Code
metadata Metadata
cause error
}
// NewBuilder creates a new error builder
func NewBuilder() *Builder {
return &Builder{
metadata: make(Metadata),
}
}
// Code sets error code
func (b *Builder) Code(code string) *Builder {
b.code = code
info := GetErrorInfo(code)
b.category = info.Category
b.httpStatus = info.HTTPStatus
b.grpcCode = info.GRPCCode
return b
}
// Message sets error message
func (b *Builder) Message(message string) *Builder {
b.message = message
return b
}
// Category sets error category
func (b *Builder) Category(category string) *Builder {
b.category = category
return b
}
// HTTPStatus sets HTTP status code
func (b *Builder) HTTPStatus(status int) *Builder {
b.httpStatus = status
return b
}
// GRPCCode sets gRPC code
func (b *Builder) GRPCCode(code codes.Code) *Builder {
b.grpcCode = code
return b
}
// Metadata adds metadata
func (b *Builder) Metadata(key string, value interface{}) *Builder {
if b.metadata == nil {
b.metadata = make(Metadata)
}
b.metadata[key] = value
return b
}
// Metadatas adds multiple metadata
func (b *Builder) Metadatas(metadata Metadata) *Builder {
if b.metadata == nil {
b.metadata = make(Metadata)
}
for k, v := range metadata {
b.metadata[k] = v
}
return b
}
// Cause sets underlying error
func (b *Builder) Cause(err error) *Builder {
b.cause = err
return b
}
// Timestamp adds timestamp
func (b *Builder) Timestamp(t time.Time) *Builder {
b.Metadata("timestamp", t.Format(time.RFC3339))
return b
}
// RequestID adds request ID
func (b *Builder) RequestID(id string) *Builder {
b.Metadata("request_id", id)
return b
}
// UserID adds user ID
func (b *Builder) UserID(id string) *Builder {
b.Metadata("user_id", id)
return b
}
// Service adds service name
func (b *Builder) Service(name string) *Builder {
b.Metadata("service", name)
return b
}
// Operation adds operation name
func (b *Builder) Operation(name string) *Builder {
b.Metadata("operation", name)
return b
}
// Retryable sets retryable flag
func (b *Builder) Retryable(retryable bool) *Builder {
b.Metadata("retryable", retryable)
return b
}
// Severity sets error severity
func (b *Builder) Severity(severity string) *Builder {
b.Metadata("severity", severity)
return b
}
// Component adds component name
func (b *Builder) Component(name string) *Builder {
b.Metadata("component", name)
return b
}
// Build creates the final error
func (b *Builder) Build() Error {
// Use defaults if not set
if b.code == "" {
b.code = ErrCodeInternalError
}
if b.message == "" {
b.message = "An error occurred"
}
if b.category == "" {
b.category = CategoryInternal
}
if b.httpStatus == 0 {
b.httpStatus = http.StatusInternalServerError
}
if b.grpcCode == codes.OK {
b.grpcCode = codes.Internal
}
if b.metadata == nil {
b.metadata = make(Metadata)
}
if _, exists := b.metadata["timestamp"]; !exists {
b.Metadata("timestamp", time.Now().Format(time.RFC3339))
}
return &AppError{
code: b.code,
message: b.message,
category: b.category,
httpStatus: b.httpStatus,
grpcCode: b.grpcCode,
metadata: b.metadata,
cause: b.cause,
timestamp: time.Now(),
stackTrace: captureStackTrace(),
}
}
// NewValidationError creates validation error builder
func NewValidationError() *Builder {
return NewBuilder().
Code(ErrCodeValidationFailed).
Category(CategoryValidation).
HTTPStatus(http.StatusBadRequest).
GRPCCode(codes.InvalidArgument).
Severity("warning")
}
// NotFoundError creates not found error builder
func NotFoundError() *Builder {
return NewBuilder().
Code(ErrCodeNotFound).
Category(CategoryNotFound).
HTTPStatus(http.StatusNotFound).
GRPCCode(codes.NotFound).
Severity("info")
}
// UnauthorizedError creates unauthorized error builder
func UnauthorizedError() *Builder {
return NewBuilder().
Code(ErrCodeUnauthorized).
Category(CategoryUnauthorized).
HTTPStatus(http.StatusUnauthorized).
GRPCCode(codes.Unauthenticated).
Severity("warning")
}
// ForbiddenError creates forbidden error builder
func ForbiddenError() *Builder {
return NewBuilder().
Code(ErrCodeForbidden).
Category(CategoryForbidden).
HTTPStatus(http.StatusForbidden).
GRPCCode(codes.PermissionDenied).
Severity("warning")
}
// AlreadyExistsError creates already exists error builder
func AlreadyExistsError() *Builder {
return NewBuilder().
Code(ErrCodeConflict).
Category(CategoryConflict).
HTTPStatus(http.StatusConflict).
GRPCCode(codes.AlreadyExists).
Severity("warning")
}
// DuplicateError creates duplicate entry error builder
func DuplicateError() *Builder {
return NewBuilder().
Code(ErrCodeDuplicateEntry).
Category(CategoryConflict).
HTTPStatus(http.StatusConflict).
GRPCCode(codes.AlreadyExists).
Severity("warning")
}
// ConflictError creates conflict error builder
func ConflictError() *Builder {
return NewBuilder().
Code(ErrCodeConflict).
Category(CategoryConflict).
HTTPStatus(http.StatusConflict).
GRPCCode(codes.AlreadyExists).
Severity("warning")
}
// InternalError creates internal error builder
func InternalError() *Builder {
return NewBuilder().
Code(ErrCodeInternalError).
Category(CategoryInternal).
HTTPStatus(http.StatusInternalServerError).
GRPCCode(codes.Internal).
Severity("error")
}
// ExternalError creates external error builder
func ExternalError() *Builder {
return NewBuilder().
Code(ErrCodeExternalError).
Category(CategoryExternal).
HTTPStatus(http.StatusBadGateway).
GRPCCode(codes.Unavailable).
Severity("error").
Retryable(true)
}
// TimeoutError creates timeout error builder
func TimeoutError() *Builder {
return NewBuilder().
Code(ErrCodeTimeout).
Category(CategoryTimeout).
HTTPStatus(http.StatusRequestTimeout).
GRPCCode(codes.DeadlineExceeded).
Severity("warning").
Retryable(true)
}
// BusinessError creates business error builder
func BusinessError() *Builder {
return NewBuilder().
Code(ErrCodeBusinessRule).
Category(CategoryBusiness).
HTTPStatus(http.StatusBadRequest).
GRPCCode(codes.FailedPrecondition).
Severity("warning")
}
// DatabaseError creates database error builder
func DatabaseError() *Builder {
return NewBuilder().
Code(ErrCodeDatabaseError).
Category(CategoryDatabase).
HTTPStatus(http.StatusInternalServerError).
GRPCCode(codes.Internal).
Severity("error")
}
// NetworkError creates network error builder
func NetworkError() *Builder {
return NewBuilder().
Code(ErrCodeNetworkError).
Category(CategoryNetwork).
HTTPStatus(http.StatusBadGateway).
GRPCCode(codes.Unavailable).
Severity("error").
Retryable(true)
}
// CustomError creates custom error builder
func CustomError(code, message string) *Builder {
return NewBuilder().
Code(code).
Message(message)
}
// FromError creates builder from existing error
func FromErrorBuilder(err error) *Builder {
appErr := FromError(err)
if appErr == nil {
return NewBuilder()
}
return NewBuilder().
Code(appErr.Code()).
Message(appErr.Error()).
Category(appErr.Category()).
HTTPStatus(appErr.HTTPStatus()).
GRPCCode(appErr.GRPCCode()).
Metadatas(appErr.Metadata()).
Cause(appErr.Cause())
}
+294
View File
@@ -0,0 +1,294 @@
package errors
import (
"net/http"
"strings"
"sync"
"google.golang.org/grpc/codes"
)
// Error categories
const (
CategoryValidation = "validation"
CategoryNotFound = "not_found"
CategoryUnauthorized = "unauthorized"
CategoryForbidden = "forbidden"
CategoryConflict = "conflict"
CategoryRateLimit = "rate_limit"
CategoryInternal = "internal"
CategoryExternal = "external"
CategoryBusiness = "business"
CategoryTimeout = "timeout"
CategoryNetwork = "network"
CategoryDatabase = "database"
)
// Standard error codes
const (
// Validation errors
ErrCodeValidationFailed = "VALIDATION_FAILED"
ErrCodeInvalidInput = "INVALID_INPUT"
ErrCodeMissingField = "MISSING_FIELD"
ErrCodeInvalidFormat = "INVALID_FORMAT"
ErrCodeInvalidLength = "INVALID_LENGTH"
ErrCodeInvalidRange = "INVALID_RANGE"
// Not found errors
ErrCodeNotFound = "NOT_FOUND"
ErrCodeUserNotFound = "USER_NOT_FOUND"
ErrCodeResourceNotFound = "RESOURCE_NOT_FOUND"
ErrCodeDataNotFound = "DATA_NOT_FOUND"
// Authorization errors
ErrCodeUnauthorized = "UNAUTHORIZED"
ErrCodeInvalidToken = "INVALID_TOKEN"
ErrCodeTokenExpired = "TOKEN_EXPIRED"
ErrCodeInvalidCredentials = "INVALID_CREDENTIALS"
// Forbidden errors
ErrCodeForbidden = "FORBIDDEN"
ErrCodeInsufficientRights = "INSUFFICIENT_RIGHTS"
ErrCodeAccessDenied = "ACCESS_DENIED"
// Conflict errors
ErrCodeConflict = "CONFLICT"
ErrCodeDuplicateEntry = "DUPLICATE_ENTRY"
ErrCodeResourceLocked = "RESOURCE_LOCKED"
ErrCodeConcurrentUpdate = "CONCURRENT_UPDATE"
// Rate limit errors
ErrCodeRateLimitExceeded = "RATE_LIMIT_EXCEEDED"
ErrCodeTooManyRequests = "TOO_MANY_REQUESTS"
ErrCodeQuotaExceeded = "QUOTA_EXCEEDED"
// Internal errors
ErrCodeInternalError = "INTERNAL_ERROR"
ErrCodeUnexpectedError = "UNEXPECTED_ERROR"
ErrCodeServiceUnavailable = "SERVICE_UNAVAILABLE"
ErrCodeConfigurationError = "CONFIGURATION_ERROR"
// External errors
ErrCodeExternalError = "EXTERNAL_ERROR"
ErrCodeThirdPartyError = "THIRD_PARTY_ERROR"
ErrCodeAPIError = "API_ERROR"
// Business errors
ErrCodeBusinessRule = "BUSINESS_RULE"
ErrCodeInsufficientBalance = "INSUFFICIENT_BALANCE"
ErrCodeAccountSuspended = "ACCOUNT_SUSPENDED"
// Timeout errors
ErrCodeTimeout = "TIMEOUT"
ErrCodeRequestTimeout = "REQUEST_TIMEOUT"
ErrCodeConnectionTimeout = "CONNECTION_TIMEOUT"
// Network errors
ErrCodeNetworkError = "NETWORK_ERROR"
ErrCodeConnectionFailed = "CONNECTION_FAILED"
ErrCodeConnectionLost = "CONNECTION_LOST"
// Database errors
ErrCodeDatabaseError = "DATABASE_ERROR"
ErrCodeQueryFailed = "QUERY_FAILED"
ErrCodeTransactionFailed = "TRANSACTION_FAILED"
)
// ErrorInfo contains error metadata
type ErrorInfo struct {
Code string
Category string
HTTPStatus int
GRPCCode codes.Code
}
// ErrorTemplate for custom error types
type ErrorTemplate struct {
Code string
Message string
Category string
HTTPStatus int
GRPCCode codes.Code
Retryable bool
}
var (
errorCodes = make(map[string]ErrorInfo)
errorCodesMu sync.RWMutex
errorTemplates = make(map[string]*ErrorTemplate)
errorTemplatesMu sync.RWMutex
)
func init() {
// Initialize standard error codes
initializeErrorCodes()
}
// initializeErrorCodes sets up standard error codes
func initializeErrorCodes() {
// Validation errors
registerErrorCode(ErrCodeValidationFailed, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeInvalidInput, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeMissingField, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeInvalidFormat, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeInvalidLength, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
registerErrorCode(ErrCodeInvalidRange, CategoryValidation, http.StatusBadRequest, codes.InvalidArgument)
// Not found errors
registerErrorCode(ErrCodeNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
registerErrorCode(ErrCodeUserNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
registerErrorCode(ErrCodeResourceNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
registerErrorCode(ErrCodeDataNotFound, CategoryNotFound, http.StatusNotFound, codes.NotFound)
// Authorization errors
registerErrorCode(ErrCodeUnauthorized, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
registerErrorCode(ErrCodeInvalidToken, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
registerErrorCode(ErrCodeTokenExpired, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
registerErrorCode(ErrCodeInvalidCredentials, CategoryUnauthorized, http.StatusUnauthorized, codes.Unauthenticated)
// Forbidden errors
registerErrorCode(ErrCodeForbidden, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
registerErrorCode(ErrCodeInsufficientRights, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
registerErrorCode(ErrCodeAccessDenied, CategoryForbidden, http.StatusForbidden, codes.PermissionDenied)
// Conflict errors
registerErrorCode(ErrCodeConflict, CategoryConflict, http.StatusConflict, codes.AlreadyExists)
registerErrorCode(ErrCodeDuplicateEntry, CategoryConflict, http.StatusConflict, codes.AlreadyExists)
registerErrorCode(ErrCodeResourceLocked, CategoryConflict, http.StatusLocked, codes.Aborted)
registerErrorCode(ErrCodeConcurrentUpdate, CategoryConflict, http.StatusConflict, codes.Aborted)
// Rate limit errors
registerErrorCode(ErrCodeRateLimitExceeded, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
registerErrorCode(ErrCodeTooManyRequests, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
registerErrorCode(ErrCodeQuotaExceeded, CategoryRateLimit, http.StatusTooManyRequests, codes.ResourceExhausted)
// Internal errors
registerErrorCode(ErrCodeInternalError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
registerErrorCode(ErrCodeUnexpectedError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
registerErrorCode(ErrCodeServiceUnavailable, CategoryInternal, http.StatusServiceUnavailable, codes.Unavailable)
registerErrorCode(ErrCodeConfigurationError, CategoryInternal, http.StatusInternalServerError, codes.Internal)
// External errors
registerErrorCode(ErrCodeExternalError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
registerErrorCode(ErrCodeThirdPartyError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
registerErrorCode(ErrCodeAPIError, CategoryExternal, http.StatusBadGateway, codes.Unavailable)
// Business errors
registerErrorCode(ErrCodeBusinessRule, CategoryBusiness, http.StatusBadRequest, codes.FailedPrecondition)
registerErrorCode(ErrCodeInsufficientBalance, CategoryBusiness, http.StatusBadRequest, codes.FailedPrecondition)
registerErrorCode(ErrCodeAccountSuspended, CategoryBusiness, http.StatusForbidden, codes.PermissionDenied)
// Timeout errors
registerErrorCode(ErrCodeTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
registerErrorCode(ErrCodeRequestTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
registerErrorCode(ErrCodeConnectionTimeout, CategoryTimeout, http.StatusRequestTimeout, codes.DeadlineExceeded)
// Network errors
registerErrorCode(ErrCodeNetworkError, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
registerErrorCode(ErrCodeConnectionFailed, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
registerErrorCode(ErrCodeConnectionLost, CategoryNetwork, http.StatusBadGateway, codes.Unavailable)
// Database errors
registerErrorCode(ErrCodeDatabaseError, CategoryDatabase, http.StatusInternalServerError, codes.Internal)
registerErrorCode(ErrCodeQueryFailed, CategoryDatabase, http.StatusInternalServerError, codes.Internal)
registerErrorCode(ErrCodeTransactionFailed, CategoryDatabase, http.StatusInternalServerError, codes.Aborted)
}
// registerErrorCode registers an error code
func registerErrorCode(code, category string, httpStatus int, grpcCode codes.Code) {
errorCodesMu.Lock()
defer errorCodesMu.Unlock()
errorCodes[code] = ErrorInfo{
Code: code,
Category: category,
HTTPStatus: httpStatus,
GRPCCode: grpcCode,
}
}
// RegisterErrorCode registers a custom error code
func RegisterErrorCode(code, category string, httpStatus int, grpcCode codes.Code) {
registerErrorCode(code, category, httpStatus, grpcCode)
}
// GetErrorInfo returns error information for a code
func GetErrorInfo(code string) ErrorInfo {
errorCodesMu.RLock()
defer errorCodesMu.RUnlock()
if info, exists := errorCodes[code]; exists {
return info
}
return ErrorInfo{
Code: code,
Category: CategoryInternal,
HTTPStatus: http.StatusInternalServerError,
GRPCCode: codes.Internal,
}
}
// RegisterErrorTemplate registers a custom error template
func RegisterErrorTemplate(name string, template *ErrorTemplate) {
errorTemplatesMu.Lock()
defer errorTemplatesMu.Unlock()
errorTemplates[name] = template
}
// GetErrorTemplate returns error template by name
func GetErrorTemplate(name string) *ErrorTemplate {
errorTemplatesMu.RLock()
defer errorTemplatesMu.RUnlock()
return errorTemplates[name]
}
// GetAllErrorCodes returns all registered error codes
func GetAllErrorCodes() map[string]ErrorInfo {
errorCodesMu.RLock()
defer errorCodesMu.RUnlock()
result := make(map[string]ErrorInfo)
for k, v := range errorCodes {
result[k] = v
}
return result
}
// GetErrorCodesByCategory returns error codes by category
func GetErrorCodesByCategory(category string) map[string]ErrorInfo {
errorCodesMu.RLock()
defer errorCodesMu.RUnlock()
result := make(map[string]ErrorInfo)
for k, v := range errorCodes {
if v.Category == category {
result[k] = v
}
}
return result
}
// IsRetryable checks if error is retryable
func IsRetryable(code string) bool {
info := GetErrorInfo(code)
switch info.Category {
case CategoryNetwork, CategoryTimeout, CategoryExternal:
return true
case CategoryDatabase:
return strings.Contains(code, "TIMEOUT") || strings.Contains(code, "CONNECTION")
default:
return false
}
}
// IsClientError checks if error is client-side (4xx)
func IsClientError(code string) bool {
info := GetErrorInfo(code)
return info.HTTPStatus >= 400 && info.HTTPStatus < 500
}
// IsServerError checks if error is server-side (5xx)
func IsServerError(code string) bool {
info := GetErrorInfo(code)
return info.HTTPStatus >= 500
}
+266
View File
@@ -0,0 +1,266 @@
package errors
import (
"encoding/json"
"fmt"
"net/http"
"runtime"
"time"
"google.golang.org/grpc/codes"
)
// Core error interface
type Error interface {
Error() string
Code() string
Category() string
HTTPStatus() int
GRPCCode() codes.Code
Metadata() map[string]interface{}
Cause() error
GetLocalizedMessage(lang string) string
WithMetadata(key string, value interface{}) Error
WithCause(err error) Error
}
// Metadata type for error context
type Metadata map[string]interface{}
// Core error implementation
type AppError struct {
code string
message string
category string
httpStatus int
grpcCode codes.Code
metadata Metadata
cause error
timestamp time.Time
stackTrace []string
}
// New creates a new error
func New(message string) Error {
return &AppError{
code: ErrCodeInternalError,
message: message,
category: CategoryInternal,
httpStatus: http.StatusInternalServerError,
grpcCode: codes.Internal,
metadata: make(Metadata),
timestamp: time.Now(),
stackTrace: captureStackTrace(),
}
}
// NewWithCode creates a new error with code
func NewWithCode(code, message string) Error {
info := GetErrorInfo(code)
if info.Code == "" {
info = ErrorInfo{
Code: code,
Category: CategoryInternal,
HTTPStatus: http.StatusInternalServerError,
GRPCCode: codes.Internal,
}
}
return &AppError{
code: code,
message: message,
category: info.Category,
httpStatus: info.HTTPStatus,
grpcCode: info.GRPCCode,
metadata: make(Metadata),
timestamp: time.Now(),
stackTrace: captureStackTrace(),
}
}
// NewWithMetadata creates a new error with metadata
func NewWithMetadata(code, message string, metadata Metadata) Error {
err := NewWithCode(code, message)
err.(*AppError).metadata = metadata
return err
}
// NewWithType creates a new error with custom type
func NewWithType(errorType string, metadata Metadata) Error {
template := GetErrorTemplate(errorType)
if template == nil {
return NewWithMetadata(ErrCodeInternalError, "Unknown error type", metadata)
}
err := &AppError{
code: template.Code,
message: template.Message,
category: template.Category,
httpStatus: template.HTTPStatus,
grpcCode: template.GRPCCode,
metadata: make(Metadata),
timestamp: time.Now(),
stackTrace: captureStackTrace(),
}
for k, v := range metadata {
err.metadata[k] = v
}
return err
}
// Wrap wraps an existing error
func Wrap(err error, code, message string) Error {
if err == nil {
return nil
}
if IsAppError(err) {
return err.(Error)
}
appErr := NewWithCode(code, message)
appErr.(*AppError).cause = err
return appErr
}
// FromError converts any error to AppError
func FromError(err error) Error {
if err == nil {
return nil
}
if appErr, ok := err.(Error); ok {
return appErr
}
return Wrap(err, ErrCodeInternalError, err.Error())
}
// IsAppError checks if error is AppError
func IsAppError(err error) bool {
_, ok := err.(Error)
return ok
}
// Error implements error interface
func (e *AppError) Error() string {
if e.cause != nil {
return fmt.Sprintf("%s: %v", e.message, e.cause)
}
return e.message
}
// Code returns error code
func (e *AppError) Code() string {
return e.code
}
// Category returns error category
func (e *AppError) Category() string {
return e.category
}
// HTTPStatus returns HTTP status code
func (e *AppError) HTTPStatus() int {
return e.httpStatus
}
// GRPCCode returns gRPC code
func (e *AppError) GRPCCode() codes.Code {
return e.grpcCode
}
// Metadata returns error metadata
func (e *AppError) Metadata() map[string]interface{} {
return e.metadata
}
// Cause returns underlying error
func (e *AppError) Cause() error {
return e.cause
}
// Unwrap implements the standard library unwrap interface for Go 1.13+ error chains
func (e *AppError) Unwrap() error {
return e.cause
}
// GetLocalizedMessage returns localized message
func (e *AppError) GetLocalizedMessage(lang string) string {
return GetLocalizedMessage(e.code, lang, e.message)
}
// WithMetadata adds metadata to error
func (e *AppError) WithMetadata(key string, value interface{}) Error {
if e.metadata == nil {
e.metadata = make(Metadata)
}
e.metadata[key] = value
return e
}
// WithCause adds cause to error
func (e *AppError) WithCause(err error) Error {
e.cause = err
return e
}
// ToJSON converts error to JSON
func (e *AppError) ToJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"code": e.code,
"message": e.message,
"category": e.category,
"http_status": e.httpStatus,
"grpc_code": e.grpcCode.String(),
"metadata": e.metadata,
"timestamp": e.timestamp,
"stack_trace": e.stackTrace,
})
}
// captureStackTrace captures current stack trace
func captureStackTrace() []string {
var stack []string
for i := 2; i < 15; i++ {
pc, file, line, ok := runtime.Caller(i)
if !ok {
break
}
fn := runtime.FuncForPC(pc)
stack = append(stack, fmt.Sprintf("%s:%d %s", file, line, fn.Name()))
}
return stack
}
// Is checks if error matches target
func Is(err, target error) bool {
if err == target {
return true
}
if appErr, ok := err.(Error); ok {
if targetErr, ok := target.(Error); ok {
return appErr.Code() == targetErr.Code()
}
}
return false
}
// As finds the first error in err's chain that matches target
func As(err error, target interface{}) bool {
if appErr, ok := err.(Error); ok {
switch t := target.(type) {
case **AppError:
*t = appErr.(*AppError)
return true
case *Error:
*t = appErr
return true
}
}
return false
}
+261
View File
@@ -0,0 +1,261 @@
package errors
import (
"context"
"net/http"
"service/pkg/logger"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// GRPCErrorInterceptor creates unary interceptor for error handling
func GRPCUnaryInterceptor() grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
if err != nil {
return nil, HandleGRPCError(err)
}
return resp, nil
}
}
// GRPCStreamInterceptor creates stream interceptor for error handling
func GRPCStreamInterceptor() grpc.StreamServerInterceptor {
return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
err := handler(srv, ss)
if err != nil {
return HandleGRPCError(err)
}
return nil
}
}
// HandleGRPCError converts error to gRPC error
func HandleGRPCError(err error) error {
appErr := FromError(err)
if appErr == nil {
return status.Error(codes.Internal, "Internal server error")
}
// Log gRPC error
LogGRPCError(appErr)
// Convert to gRPC status
return status.Error(appErr.GRPCCode(), appErr.GetLocalizedMessage("en"))
}
// LogGRPCError logs gRPC error
func LogGRPCError(err Error) {
log := logger.Default()
log.Error("gRPC error occurred",
logger.String("error_code", err.Code()),
logger.String("category", err.Category()),
logger.String("grpc_code", err.GRPCCode().String()),
logger.Any("metadata", err.Metadata()),
)
}
// FromGRPCError converts gRPC error to AppError
func FromGRPCError(err error) Error {
if err == nil {
return nil
}
st, ok := status.FromError(err)
if !ok {
return NewWithCode(ErrCodeInternalError, err.Error())
}
// Map gRPC codes to error codes
code := mapGRPCToErrorCode(st.Code())
appErr := NewWithCode(code, st.Message())
// Add gRPC metadata
appErr = appErr.WithMetadata("grpc_code", st.Code().String())
return appErr
}
// mapGRPCToErrorCode maps gRPC codes to error codes
func mapGRPCToErrorCode(grpcCode codes.Code) string {
switch grpcCode {
case codes.OK:
return ""
case codes.Canceled:
return ErrCodeTimeout
case codes.Unknown:
return ErrCodeInternalError
case codes.InvalidArgument:
return ErrCodeValidationFailed
case codes.DeadlineExceeded:
return ErrCodeTimeout
case codes.NotFound:
return ErrCodeNotFound
case codes.AlreadyExists:
return ErrCodeDuplicateEntry
case codes.PermissionDenied:
return ErrCodeForbidden
case codes.ResourceExhausted:
return ErrCodeRateLimitExceeded
case codes.FailedPrecondition:
return ErrCodeBusinessRule
case codes.Aborted:
return ErrCodeConflict
case codes.OutOfRange:
return ErrCodeInvalidRange
case codes.Unimplemented:
return ErrCodeServiceUnavailable
case codes.Internal:
return ErrCodeInternalError
case codes.Unavailable:
return ErrCodeExternalError
case codes.DataLoss:
return ErrCodeInternalError
case codes.Unauthenticated:
return ErrCodeUnauthorized
default:
return ErrCodeInternalError
}
}
// GRPCMiddlewareConfig configures gRPC middleware
type GRPCMiddlewareConfig struct {
SkipMethods []string
LogAllErrors bool
SanitizeErrors bool
DefaultLanguage string
EnableMetrics bool
CustomCodeMapping map[codes.Code]string
}
// DefaultGRPCMiddlewareConfig returns default gRPC configuration
func DefaultGRPCMiddlewareConfig() *GRPCMiddlewareConfig {
return &GRPCMiddlewareConfig{
SkipMethods: []string{"/grpc.health.v1.Health/Check"},
LogAllErrors: true,
SanitizeErrors: true,
DefaultLanguage: "en",
EnableMetrics: true,
}
}
// GRPCUnaryInterceptorWithConfig creates interceptor with custom configuration
func GRPCUnaryInterceptorWithConfig(config *GRPCMiddlewareConfig) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
// Skip specified methods
for _, method := range config.SkipMethods {
if info.FullMethod == method {
return handler(ctx, req)
}
}
resp, err := handler(ctx, req)
if err != nil {
return nil, handleGRPCErrorWithConfig(err, config)
}
return resp, nil
}
}
// handleGRPCErrorWithConfig handles gRPC error with custom configuration
func handleGRPCErrorWithConfig(err error, config *GRPCMiddlewareConfig) error {
appErr := FromError(err)
if appErr == nil {
return status.Error(codes.Internal, "Internal server error")
}
// Apply custom code mapping
if customCode, exists := config.CustomCodeMapping[appErr.GRPCCode()]; exists {
appErr = NewWithCode(customCode, appErr.Error())
}
// Sanitize error if needed
if config.SanitizeErrors {
appErr = sanitizeError(appErr).(Error)
}
// Log error
if config.LogAllErrors {
LogGRPCError(appErr)
}
return status.Error(appErr.GRPCCode(), appErr.GetLocalizedMessage(config.DefaultLanguage))
}
// ConvertToHTTPStatus converts gRPC code to HTTP status
func ConvertToHTTPStatus(grpcCode codes.Code) int {
switch grpcCode {
case codes.OK:
return http.StatusOK
case codes.Canceled:
return http.StatusRequestTimeout
case codes.Unknown:
return http.StatusInternalServerError
case codes.InvalidArgument:
return http.StatusBadRequest
case codes.DeadlineExceeded:
return http.StatusRequestTimeout
case codes.NotFound:
return http.StatusNotFound
case codes.AlreadyExists:
return http.StatusConflict
case codes.PermissionDenied:
return http.StatusForbidden
case codes.ResourceExhausted:
return http.StatusTooManyRequests
case codes.FailedPrecondition:
return http.StatusBadRequest
case codes.Aborted:
return http.StatusConflict
case codes.OutOfRange:
return http.StatusBadRequest
case codes.Unimplemented:
return http.StatusNotImplemented
case codes.Internal:
return http.StatusInternalServerError
case codes.Unavailable:
return http.StatusServiceUnavailable
case codes.DataLoss:
return http.StatusInternalServerError
case codes.Unauthenticated:
return http.StatusUnauthorized
default:
return http.StatusInternalServerError
}
}
// ConvertToGRPCCode converts HTTP status to gRPC code
func ConvertToGRPCCode(httpStatus int) codes.Code {
switch httpStatus {
case http.StatusOK:
return codes.OK
case http.StatusBadRequest:
return codes.InvalidArgument
case http.StatusUnauthorized:
return codes.Unauthenticated
case http.StatusForbidden:
return codes.PermissionDenied
case http.StatusNotFound:
return codes.NotFound
case http.StatusConflict:
return codes.AlreadyExists
case http.StatusTooManyRequests:
return codes.ResourceExhausted
case http.StatusRequestTimeout:
return codes.DeadlineExceeded
case http.StatusNotImplemented:
return codes.Unimplemented
case http.StatusInternalServerError:
return codes.Internal
case http.StatusServiceUnavailable:
return codes.Unavailable
case http.StatusBadGateway:
return codes.Unavailable
default:
return codes.Unknown
}
}
+206
View File
@@ -0,0 +1,206 @@
package errors
import (
"encoding/json"
"fmt"
"net/http"
"service/pkg/logger"
"strings"
"time"
"github.com/gin-gonic/gin"
)
// HTTPErrorResponse represents standardized HTTP error response
type HTTPErrorResponse struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
Details map[string]interface{} `json:"details,omitempty"`
RequestID string `json:"request_id,omitempty"`
Timestamp string `json:"timestamp"`
Retryable bool `json:"retryable,omitempty"`
} `json:"error"`
Meta struct {
HTTPStatus int `json:"http_status"`
Category string `json:"category"`
} `json:"meta,omitempty"`
}
// HTTPMiddleware creates error handling middleware for Gin
func HTTPMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
// Check if there are any errors
if len(c.Errors) > 0 {
// Get the last error
err := c.Errors.Last().Err
HandleHTTPError(c, err)
}
}
}
// HandleHTTPError handles HTTP error response
func HandleHTTPError(c *gin.Context, err error) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
}
// Create error response
response := &HTTPErrorResponse{}
response.Error.Code = appErr.Code()
response.Error.Message = appErr.GetLocalizedMessage(getLanguageFromContext(c))
response.Error.Details = appErr.Metadata()
response.Error.RequestID = c.GetString("request_id")
response.Error.Timestamp = appErr.Metadata()["timestamp"].(string)
response.Error.Retryable = IsRetryable(appErr.Code())
response.Meta.HTTPStatus = appErr.HTTPStatus()
response.Meta.Category = appErr.Category()
// Log error
LogHTTPError(c, appErr)
// Send response
c.JSON(appErr.HTTPStatus(), response)
}
// getLanguageFromContext extracts language from context
func getLanguageFromContext(c *gin.Context) string {
// Try Accept-Language header
lang := c.GetHeader("Accept-Language")
if lang != "" {
return parseAcceptLanguage(lang)
}
// Try query parameter
lang = c.Query("lang")
if lang != "" {
return lang
}
// Default to English
return "en"
}
// parseAcceptLanguage parses Accept-Language header
func parseAcceptLanguage(header string) string {
// Simple parsing - take first language
for idx := 0; idx < len(header); idx++ {
if header[idx] == ',' || header[idx] == ';' {
return header[:idx]
}
}
return header
}
// LogHTTPError logs HTTP error with context
func LogHTTPError(c *gin.Context, err Error) {
log := logger.Default()
log.Error("HTTP error occurred",
logger.String("error_code", err.Code()),
logger.String("category", err.Category()),
logger.Int("http_status", err.HTTPStatus()),
logger.Any("metadata", err.Metadata()),
logger.String("path", c.Request.URL.Path),
logger.String("method", c.Request.Method),
)
}
// ValidationErrorHandler handles validation errors specifically
func ValidationErrorHandler(c *gin.Context, err error) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeValidationFailed, err.Error())
}
// Enhance validation errors with field details
if appErr.Category() == CategoryValidation {
if details, ok := appErr.Metadata()["validation_errors"]; ok {
response := gin.H{
"error": gin.H{
"code": appErr.Code(),
"message": appErr.GetLocalizedMessage(getLanguageFromContext(c)),
"fields": details,
},
}
c.JSON(http.StatusBadRequest, response)
return
}
}
HandleHTTPError(c, appErr)
}
// PanicHandler handles panics
func PanicHandler(c *gin.Context, recovered interface{}) {
var err error
switch x := recovered.(type) {
case string:
err = NewWithCode(ErrCodeInternalError, x)
case error:
err = FromError(x)
default:
err = NewWithCode(ErrCodeUnexpectedError, fmt.Sprintf("Unknown panic: %v", x))
}
HandleHTTPError(c, err)
}
// WriteErrorResponse writes error response directly
func WriteErrorResponse(w http.ResponseWriter, err error) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
}
response := &HTTPErrorResponse{}
response.Error.Code = appErr.Code()
response.Error.Message = appErr.GetLocalizedMessage("en")
response.Error.Details = appErr.Metadata()
response.Error.Timestamp = appErr.Metadata()["timestamp"].(string)
response.Error.Retryable = IsRetryable(appErr.Code())
response.Meta.HTTPStatus = appErr.HTTPStatus()
response.Meta.Category = appErr.Category()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(appErr.HTTPStatus())
json.NewEncoder(w).Encode(response)
}
// sanitizeError removes sensitive information from errors
func sanitizeError(err error) error {
appErr := FromError(err)
if appErr == nil {
return err
}
// Remove sensitive metadata
sanitized := make(Metadata)
for k, v := range appErr.Metadata() {
if !isSensitiveField(k) {
sanitized[k] = v
}
}
return NewWithMetadata(appErr.Code(), appErr.Error(), sanitized)
}
// isSensitiveField checks if field contains sensitive information
func isSensitiveField(field string) bool {
sensitiveFields := []string{"password", "token", "secret", "key", "auth"}
for _, sensitive := range sensitiveFields {
if strings.Contains(strings.ToLower(field), sensitive) {
return true
}
}
return false
}
// generateRequestID generates a unique request ID
func generateRequestID() string {
return fmt.Sprintf("req_%d", time.Now().UnixNano())
}
+300
View File
@@ -0,0 +1,300 @@
package errors
import (
"encoding/json"
"fmt"
"strings"
"sync"
)
// MessageStore stores localized messages
type MessageStore struct {
messages map[string]map[string]string // language -> code -> message
mu sync.RWMutex
}
var (
messageStore = &MessageStore{
messages: make(map[string]map[string]string),
}
)
func init() {
// Initialize default messages
initializeDefaultMessages()
}
// initializeDefaultMessages sets up default error messages
func initializeDefaultMessages() {
// English messages
englishMessages := map[string]string{
ErrCodeValidationFailed: "Validation failed",
ErrCodeInvalidInput: "Invalid input provided",
ErrCodeMissingField: "Required field is missing",
ErrCodeInvalidFormat: "Invalid format",
ErrCodeInvalidLength: "Invalid length",
ErrCodeInvalidRange: "Value out of range",
ErrCodeNotFound: "Resource not found",
ErrCodeUserNotFound: "User not found",
ErrCodeResourceNotFound: "Resource not found",
ErrCodeDataNotFound: "Data not found",
ErrCodeUnauthorized: "Unauthorized access",
ErrCodeInvalidToken: "Invalid token provided",
ErrCodeTokenExpired: "Token has expired",
ErrCodeInvalidCredentials: "Invalid credentials",
ErrCodeForbidden: "Access forbidden",
ErrCodeInsufficientRights: "Insufficient permissions",
ErrCodeAccessDenied: "Access denied",
ErrCodeConflict: "Conflict occurred",
ErrCodeDuplicateEntry: "Duplicate entry",
ErrCodeResourceLocked: "Resource is locked",
ErrCodeConcurrentUpdate: "Concurrent update detected",
ErrCodeRateLimitExceeded: "Rate limit exceeded",
ErrCodeTooManyRequests: "Too many requests",
ErrCodeQuotaExceeded: "Quota exceeded",
ErrCodeInternalError: "Internal server error",
ErrCodeUnexpectedError: "An unexpected error occurred",
ErrCodeServiceUnavailable: "Service is unavailable",
ErrCodeConfigurationError: "Configuration error",
ErrCodeExternalError: "External service error",
ErrCodeThirdPartyError: "Third party service error",
ErrCodeAPIError: "API error occurred",
ErrCodeBusinessRule: "Business rule violation",
ErrCodeInsufficientBalance: "Insufficient balance",
ErrCodeAccountSuspended: "Account suspended",
ErrCodeTimeout: "Request timeout",
ErrCodeRequestTimeout: "Request timeout",
ErrCodeConnectionTimeout: "Connection timeout",
ErrCodeNetworkError: "Network error",
ErrCodeConnectionFailed: "Connection failed",
ErrCodeConnectionLost: "Connection lost",
ErrCodeDatabaseError: "Database error",
ErrCodeQueryFailed: "Query execution failed",
ErrCodeTransactionFailed: "Transaction failed",
}
// Indonesian messages
indonesianMessages := map[string]string{
ErrCodeValidationFailed: "Validasi gagal",
ErrCodeInvalidInput: "Input tidak valid",
ErrCodeMissingField: "Field wajib tidak ada",
ErrCodeInvalidFormat: "Format tidak valid",
ErrCodeInvalidLength: "Panjang tidak valid",
ErrCodeInvalidRange: "Nilai di luar jangkauan",
ErrCodeNotFound: "Sumber daya tidak ditemukan",
ErrCodeUserNotFound: "Pengguna tidak ditemukan",
ErrCodeResourceNotFound: "Sumber daya tidak ditemukan",
ErrCodeDataNotFound: "Data tidak ditemukan",
ErrCodeUnauthorized: "Akses tidak sah",
ErrCodeInvalidToken: "Token tidak valid",
ErrCodeTokenExpired: "Token telah kadaluarsa",
ErrCodeInvalidCredentials: "Kredensial tidak valid",
ErrCodeForbidden: "Akses dilarang",
ErrCodeInsufficientRights: "Izin tidak mencukupi",
ErrCodeAccessDenied: "Akses ditolak",
ErrCodeConflict: "Terjadi konflik",
ErrCodeDuplicateEntry: "Entri duplikat",
ErrCodeResourceLocked: "Sumber daya terkunci",
ErrCodeConcurrentUpdate: "Pembaruan bersamaan terdeteksi",
ErrCodeRateLimitExceeded: "Batas laju terlampaui",
ErrCodeTooManyRequests: "Terlalu banyak permintaan",
ErrCodeQuotaExceeded: "Kuota terlampaui",
ErrCodeInternalError: "Kesalahan server internal",
ErrCodeUnexpectedError: "Terjadi kesalahan tak terduga",
ErrCodeServiceUnavailable: "Layanan tidak tersedia",
ErrCodeConfigurationError: "Kesalahan konfigurasi",
ErrCodeExternalError: "Kesalahan layanan eksternal",
ErrCodeThirdPartyError: "Kesalahan layanan pihak ketiga",
ErrCodeAPIError: "Terjadi kesalahan API",
ErrCodeBusinessRule: "Pelanggaran aturan bisnis",
ErrCodeInsufficientBalance: "Saldo tidak mencukupi",
ErrCodeAccountSuspended: "Akun ditangguhkan",
ErrCodeTimeout: "Permintaan timeout",
ErrCodeRequestTimeout: "Permintaan timeout",
ErrCodeConnectionTimeout: "Koneksi timeout",
ErrCodeNetworkError: "Kesalahan jaringan",
ErrCodeConnectionFailed: "Koneksi gagal",
ErrCodeConnectionLost: "Koneksi terputus",
ErrCodeDatabaseError: "Kesalahan database",
ErrCodeQueryFailed: "Eksekusi query gagal",
ErrCodeTransactionFailed: "Transaksi gagal",
}
// Add messages to store
SetMessages("en", englishMessages)
SetMessages("id", indonesianMessages)
}
// SetMessages sets messages for a language
func SetMessages(language string, messages map[string]string) {
messageStore.mu.Lock()
defer messageStore.mu.Unlock()
if messageStore.messages[language] == nil {
messageStore.messages[language] = make(map[string]string)
}
for code, message := range messages {
messageStore.messages[language][code] = message
}
}
// SetMessage sets a single message for a language
func SetMessage(language, code, message string) {
messageStore.mu.Lock()
defer messageStore.mu.Unlock()
if messageStore.messages[language] == nil {
messageStore.messages[language] = make(map[string]string)
}
messageStore.messages[language][code] = message
}
// GetLocalizedMessage returns localized message for error code
func GetLocalizedMessage(code, language, defaultMessage string) string {
messageStore.mu.RLock()
defer messageStore.mu.RUnlock()
// Try specific language
if langMessages, exists := messageStore.messages[language]; exists {
if message, exists := langMessages[code]; exists {
return message
}
}
// Try English as fallback
if langMessages, exists := messageStore.messages["en"]; exists {
if message, exists := langMessages[code]; exists {
return message
}
}
// Return default message
return defaultMessage
}
// GetSupportedLanguages returns all supported languages
func GetSupportedLanguages() []string {
messageStore.mu.RLock()
defer messageStore.mu.RUnlock()
languages := make([]string, 0, len(messageStore.messages))
for lang := range messageStore.messages {
languages = append(languages, lang)
}
return languages
}
// LoadMessagesFromJSON loads messages from JSON file
func LoadMessagesFromJSON(language, jsonData string) error {
var messages map[string]string
if err := json.Unmarshal([]byte(jsonData), &messages); err != nil {
return fmt.Errorf("failed to parse JSON messages: %w", err)
}
SetMessages(language, messages)
return nil
}
// FormatMessage formats message with parameters
func FormatMessage(template string, params map[string]interface{}) string {
result := template
for key, value := range params {
placeholder := fmt.Sprintf("{{%s}}", key)
result = strings.ReplaceAll(result, placeholder, fmt.Sprintf("%v", value))
}
return result
}
// GetLocalizedMessageWithParams returns localized message with parameters
func GetLocalizedMessageWithParams(code, language, defaultMessage string, params map[string]interface{}) string {
message := GetLocalizedMessage(code, language, defaultMessage)
if params != nil {
message = FormatMessage(message, params)
}
return message
}
// MessageBuilder builds localized messages
type MessageBuilder struct {
language string
code string
params map[string]interface{}
}
// NewMessageBuilder creates a new message builder.
// Parameter 'lang' dianjurkan untuk dikirim berdasarkan context HTTP request
// misalnya dari header Accept-Language dari klien per request.
func NewMessageBuilder(lang string) *MessageBuilder {
if lang == "" {
lang = "en" // Fallback language default
}
return &MessageBuilder{
language: lang,
params: make(map[string]interface{}),
}
}
// Language sets language
func (mb *MessageBuilder) Language(lang string) *MessageBuilder {
mb.language = lang
return mb
}
// Code sets error code
func (mb *MessageBuilder) Code(code string) *MessageBuilder {
mb.code = code
return mb
}
// Param adds parameter
func (mb *MessageBuilder) Param(key string, value interface{}) *MessageBuilder {
mb.params[key] = value
return mb
}
// Params adds multiple parameters
func (mb *MessageBuilder) Params(params map[string]interface{}) *MessageBuilder {
for k, v := range params {
mb.params[k] = v
}
return mb
}
// Build builds the final message
func (mb *MessageBuilder) Build(defaultMessage string) string {
return GetLocalizedMessageWithParams(mb.code, mb.language, defaultMessage, mb.params)
}
// ValidationMessages provides common validation messages
type ValidationMessages struct {
Required string
Invalid string
TooShort string
TooLong string
InvalidEmail string
InvalidPhone string
}
// GetValidationMessages returns validation messages for language
func GetValidationMessages(language string) ValidationMessages {
return ValidationMessages{
Required: GetLocalizedMessage("VALIDATION_REQUIRED", language, "This field is required"),
Invalid: GetLocalizedMessage("VALIDATION_INVALID", language, "Invalid value"),
TooShort: GetLocalizedMessage("VALIDATION_TOO_SHORT", language, "Value is too short"),
TooLong: GetLocalizedMessage("VALIDATION_TOO_LONG", language, "Value is too long"),
InvalidEmail: GetLocalizedMessage("VALIDATION_INVALID_EMAIL", language, "Invalid email format"),
InvalidPhone: GetLocalizedMessage("VALIDATION_INVALID_PHONE", language, "Invalid phone format"),
}
}
// AddValidationMessages adds validation messages for language
func AddValidationMessages(language string, messages ValidationMessages) {
SetMessage(language, "VALIDATION_REQUIRED", messages.Required)
SetMessage(language, "VALIDATION_INVALID", messages.Invalid)
SetMessage(language, "VALIDATION_TOO_SHORT", messages.TooShort)
SetMessage(language, "VALIDATION_TOO_LONG", messages.TooLong)
SetMessage(language, "VALIDATION_INVALID_EMAIL", messages.InvalidEmail)
SetMessage(language, "VALIDATION_INVALID_PHONE", messages.InvalidPhone)
}
+60
View File
@@ -0,0 +1,60 @@
package errors
import (
"sync"
)
// Metrics holds error metrics
type Metrics struct {
TotalErrors int
ErrorsByCode map[string]int
ErrorsByCategory map[string]int
}
// ErrorMetrics represents error metrics for external use
type ErrorMetrics struct {
TotalErrors int `json:"total_errors"`
ErrorsByCode map[string]int `json:"errors_by_code"`
ErrorsByCategory map[string]int `json:"errors_by_category"`
}
var (
globalMetrics = &Metrics{
ErrorsByCode: make(map[string]int),
ErrorsByCategory: make(map[string]int),
}
metricsMutex sync.RWMutex
)
// RecordError records an error in metrics
func RecordError(err Error) {
metricsMutex.Lock()
defer metricsMutex.Unlock()
globalMetrics.TotalErrors++
globalMetrics.ErrorsByCode[err.Code()]++
globalMetrics.ErrorsByCategory[err.Category()]++
}
// GetMetrics returns a copy of current metrics
func GetMetrics() *Metrics {
metricsMutex.RLock()
defer metricsMutex.RUnlock()
// Create a deep copy
copy := &Metrics{
TotalErrors: globalMetrics.TotalErrors,
ErrorsByCode: make(map[string]int),
ErrorsByCategory: make(map[string]int),
}
for k, v := range globalMetrics.ErrorsByCode {
copy.ErrorsByCode[k] = v
}
for k, v := range globalMetrics.ErrorsByCategory {
copy.ErrorsByCategory[k] = v
}
return copy
}
+441
View File
@@ -0,0 +1,441 @@
package errors
import (
"context"
"fmt"
"net/http"
"service/pkg/logger"
"time"
"github.com/gin-gonic/gin"
"google.golang.org/grpc"
"google.golang.org/grpc/status"
)
// ErrorMiddlewareConfig configures error middleware
type ErrorMiddlewareConfig struct {
// SkipRoutes defines routes to skip error handling
SkipRoutes []string
// CustomHandlers defines custom error handlers
CustomHandlers map[string]ErrorHandlerFunc
// LogAllErrors enables logging all errors
LogAllErrors bool
// SanitizeErrors enables error sanitization
SanitizeErrors bool
// DefaultLanguage sets default language for error messages
DefaultLanguage string
// RequestIDHeader sets header name for request ID
RequestIDHeader string
// EnableMetrics enables error metrics collection
EnableMetrics bool
// Timeout sets timeout for error handling
Timeout time.Duration
// RecoveryEnabled enables panic recovery
RecoveryEnabled bool
// CircuitBreakerEnabled enables circuit breaker
CircuitBreakerEnabled bool
}
// ErrorHandlerFunc handles errors
type ErrorHandlerFunc func(*gin.Context, error)
// DefaultMiddlewareConfig returns default configuration
func DefaultMiddlewareConfig() *ErrorMiddlewareConfig {
return &ErrorMiddlewareConfig{
SkipRoutes: []string{"/health", "/metrics", "/ping"},
LogAllErrors: true,
SanitizeErrors: true,
DefaultLanguage: "en",
RequestIDHeader: "X-Request-ID",
EnableMetrics: true,
Timeout: 30 * time.Second,
RecoveryEnabled: true,
CircuitBreakerEnabled: false,
}
}
// ErrorMiddleware creates Gin middleware for error handling
func ErrorMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
var cfg *ErrorMiddlewareConfig
if len(config) > 0 {
cfg = config[0]
} else {
cfg = DefaultMiddlewareConfig()
}
return func(c *gin.Context) {
// Skip specified routes
for _, route := range cfg.SkipRoutes {
if c.Request.URL.Path == route {
c.Next()
return
}
}
// Generate request ID if not present
requestID := c.GetHeader(cfg.RequestIDHeader)
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
// Set language
lang := getLanguageFromContext(c)
if lang == "" {
lang = cfg.DefaultLanguage
}
c.Set("language", lang)
// Create context with timeout
if cfg.Timeout > 0 {
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
}
// Process request
c.Next()
// Handle errors
if len(c.Errors) > 0 {
handleErrors(c, c.Errors.Last().Err, cfg)
}
}
}
// RecoveryMiddleware creates recovery middleware
func RecoveryMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
var cfg *ErrorMiddlewareConfig
if len(config) > 0 {
cfg = config[0]
} else {
cfg = DefaultMiddlewareConfig()
}
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
var err error
switch x := recovered.(type) {
case string:
err = NewWithCode(ErrCodeInternalError, x)
case error:
err = FromError(x)
default:
err = NewWithCode(ErrCodeUnexpectedError,
fmt.Sprintf("Unknown panic: %v", x))
}
handleErrors(c, err, cfg)
})
}
// CombinedMiddleware creates combined error and recovery middleware
func CombinedMiddleware(config ...*ErrorMiddlewareConfig) gin.HandlerFunc {
cfg := DefaultMiddlewareConfig()
if len(config) > 0 {
cfg = config[0]
}
return func(c *gin.Context) {
// Skip specified routes
for _, route := range cfg.SkipRoutes {
if c.Request.URL.Path == route {
c.Next()
return
}
}
// Generate request ID if not present
requestID := c.GetHeader(cfg.RequestIDHeader)
if requestID == "" {
requestID = generateRequestID()
}
c.Set("request_id", requestID)
// Set language
lang := getLanguageFromContext(c)
if lang == "" {
lang = cfg.DefaultLanguage
}
c.Set("language", lang)
// Create context with timeout
if cfg.Timeout > 0 {
ctx, cancel := context.WithTimeout(c.Request.Context(), cfg.Timeout)
defer cancel()
c.Request = c.Request.WithContext(ctx)
}
// Process request with recovery
defer func() {
if r := recover(); r != nil {
var err error
switch x := r.(type) {
case string:
err = NewWithCode(ErrCodeInternalError, x)
case error:
err = FromError(x)
default:
err = NewWithCode(ErrCodeUnexpectedError,
fmt.Sprintf("Unknown panic: %v", x))
}
handleErrors(c, err, cfg)
}
}()
c.Next()
// Handle errors
if len(c.Errors) > 0 {
handleErrors(c, c.Errors.Last().Err, cfg)
}
}
}
// handleErrors handles errors with configuration
func handleErrors(c *gin.Context, err error, cfg *ErrorMiddlewareConfig) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, "Unknown error")
}
// Apply custom handlers
if handler, exists := cfg.CustomHandlers[appErr.Code()]; exists {
handler(c, appErr)
return
}
// Apply registered handlers
appErr = ApplyHandlers(appErr)
// Sanitize error if needed
if cfg.SanitizeErrors {
appErr = sanitizeError(appErr).(Error)
}
// Record metrics
if cfg.EnableMetrics {
RecordError(appErr)
}
// Log error
if cfg.LogAllErrors {
log := logger.Default()
log.Error("HTTP error occurred",
logger.String("error_code", appErr.Code()),
logger.String("category", appErr.Category()),
logger.Int("http_status", appErr.HTTPStatus()),
logger.Any("metadata", appErr.Metadata()),
logger.String("method", c.Request.Method),
logger.String("path", c.Request.URL.Path),
logger.String("request_id", c.GetString("request_id")),
)
}
// Create error response
response := createErrorResponse(c, appErr, cfg)
// Send response
c.JSON(appErr.HTTPStatus(), response)
}
// createErrorResponse creates error response
func createErrorResponse(c *gin.Context, err Error, cfg *ErrorMiddlewareConfig) gin.H {
lang := c.GetString("language")
requestID := c.GetString("request_id")
response := gin.H{
"error": gin.H{
"code": err.Code(),
"message": err.GetLocalizedMessage(lang),
"details": err.Metadata(),
"request_id": requestID,
"timestamp": time.Now().UTC().Format(time.RFC3339),
"retryable": IsRetryable(err.Code()),
},
"meta": gin.H{
"http_status": err.HTTPStatus(),
"category": err.Category(),
},
}
// Add stack trace in debug mode
if gin.Mode() == gin.DebugMode {
response["error"].(gin.H)["stack_trace"] = err.Metadata()["stack_trace"]
}
return response
}
// CustomErrorHandler creates custom error handler
func CustomErrorHandler(code string, handler ErrorHandlerFunc) ErrorHandlerFunc {
return func(c *gin.Context, err error) {
appErr := FromError(err)
if appErr != nil && appErr.Code() == code {
handler(c, appErr)
return
}
// Fallback to default handling
HandleHTTPError(c, err)
}
}
// ValidationHandler creates validation error handler
func ValidationHandler(c *gin.Context, err error) {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeValidationFailed, err.Error())
}
// Enhance validation errors with field details
if appErr.Category() == CategoryValidation {
if details, ok := appErr.Metadata()["validation_errors"]; ok {
response := gin.H{
"error": gin.H{
"code": appErr.Code(),
"message": appErr.GetLocalizedMessage(getLanguageFromContext(c)),
"fields": details,
},
}
c.JSON(http.StatusBadRequest, response)
return
}
}
HandleHTTPError(c, appErr)
}
// TimeoutHandler creates timeout error handler
func TimeoutHandler(c *gin.Context, err error) {
appErr := NewWithCode(ErrCodeTimeout, "Request timeout").
WithMetadata("timeout", c.GetHeader("X-Timeout")).
WithMetadata("endpoint", c.Request.URL.Path)
HandleHTTPError(c, appErr)
}
// RateLimitHandler creates rate limit error handler
func RateLimitHandler(c *gin.Context, err error) {
appErr := NewWithCode(ErrCodeRateLimitExceeded, "Rate limit exceeded").
WithMetadata("limit", c.GetHeader("X-Rate-Limit-Limit")).
WithMetadata("remaining", c.GetHeader("X-Rate-Limit-Remaining")).
WithMetadata("reset", c.GetHeader("X-Rate-Limit-Reset"))
HandleHTTPError(c, appErr)
}
// NotFoundHandler creates 404 error handler
func NotFoundHandler(c *gin.Context) {
err := NewWithCode(ErrCodeNotFound,
fmt.Sprintf("Route %s %s not found", c.Request.Method, c.Request.URL.Path)).
WithMetadata("method", c.Request.Method).
WithMetadata("path", c.Request.URL.Path).
WithMetadata("query", c.Request.URL.RawQuery)
HandleHTTPError(c, err)
}
// MethodNotAllowedHandler creates 405 error handler
func MethodNotAllowedHandler(c *gin.Context) {
err := NewWithCode(ErrCodeInvalidInput,
fmt.Sprintf("Method %s not allowed for route %s", c.Request.Method, c.Request.URL.Path)).
WithMetadata("method", c.Request.Method).
WithMetadata("path", c.Request.URL.Path).
WithMetadata("allowed_methods", c.GetHeader("Allow"))
HandleHTTPError(c, err)
}
// GRPCErrorInterceptor creates gRPC error interceptor
func GRPCErrorInterceptor(config ...*ErrorMiddlewareConfig) grpc.UnaryServerInterceptor {
cfg := DefaultMiddlewareConfig()
if len(config) > 0 {
cfg = config[0]
}
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
resp, err := handler(ctx, req)
if err != nil {
return nil, handleGRPCError(err, cfg)
}
return resp, nil
}
}
// handleGRPCError handles gRPC error with configuration
func handleGRPCError(err error, cfg *ErrorMiddlewareConfig) error {
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, "Internal server error")
}
// Apply registered handlers
appErr = ApplyHandlers(appErr)
// Sanitize error if needed
if cfg.SanitizeErrors {
appErr = sanitizeError(appErr).(Error)
}
// Record metrics
if cfg.EnableMetrics {
RecordError(appErr)
}
// Log error
if cfg.LogAllErrors {
log := logger.Default()
log.Error("gRPC error occurred",
logger.String("error_code", appErr.Code()),
logger.String("category", appErr.Category()),
logger.String("grpc_code", appErr.GRPCCode().String()),
logger.Any("metadata", appErr.Metadata()),
)
}
// Convert to gRPC status
return status.Error(appErr.GRPCCode(), appErr.GetLocalizedMessage(cfg.DefaultLanguage))
}
// GetErrorMetrics returns current error metrics
func GetErrorMetrics() ErrorMetrics {
m := GetMetrics()
return ErrorMetrics{
TotalErrors: m.TotalErrors,
ErrorsByCode: m.ErrorsByCode,
ErrorsByCategory: m.ErrorsByCategory,
}
}
// MetricsHandler creates metrics endpoint handler
func MetricsHandler() gin.HandlerFunc {
return func(c *gin.Context) {
metrics := GetErrorMetrics()
c.JSON(http.StatusOK, gin.H{
"error_metrics": metrics,
"timestamp": time.Now().UTC().Format(time.RFC3339),
})
}
}
// HealthHandler creates health endpoint handler
func HealthHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"service": "error-handler",
})
}
}
+383
View File
@@ -0,0 +1,383 @@
package errors
import (
"context"
"fmt"
"math"
"math/rand"
"service/pkg/logger"
"sync"
"time"
)
// RetryConfig configures retry behavior
type RetryConfig struct {
MaxAttempts int
Delay time.Duration
MaxDelay time.Duration
Backoff BackoffStrategy
RetryIf func(Error) bool
OnRetry func(attempt int, err Error)
}
// BackoffStrategy defines backoff strategy
type BackoffStrategy int
const (
LinearBackoff BackoffStrategy = iota
ExponentialBackoff
ExponentialBackoffWithJitter
FixedBackoff
)
// DefaultRetryConfig returns default retry configuration
func DefaultRetryConfig() *RetryConfig {
return &RetryConfig{
MaxAttempts: 3,
Delay: time.Second,
MaxDelay: 30 * time.Second,
Backoff: ExponentialBackoff,
RetryIf: DefaultRetryCondition,
OnRetry: nil,
}
}
// DefaultRetryCondition determines if error is retryable
func DefaultRetryCondition(err Error) bool {
return IsRetryable(err.Code())
}
// Retry executes function with retry logic
func Retry(fn func() (interface{}, error), config *RetryConfig) (interface{}, error) {
if config == nil {
config = DefaultRetryConfig()
}
var lastErr Error
log := logger.Default()
for attempt := 0; attempt < config.MaxAttempts; attempt++ {
result, err := fn()
if err == nil {
return result, nil
}
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, err.Error())
}
// Check if error is retryable
if config.RetryIf != nil && !config.RetryIf(appErr) {
return nil, appErr
}
lastErr = appErr
// Don't wait after last attempt
if attempt < config.MaxAttempts-1 {
delay := calculateDelay(attempt, config)
if config.OnRetry != nil {
config.OnRetry(attempt+1, appErr)
}
log.Warn(appErr.Error(),
logger.Int("attempt", attempt+1),
logger.Int("max_attempts", config.MaxAttempts),
logger.String("retry_delay", delay.String()),
logger.Bool("will_retry", attempt < config.MaxAttempts-1),
)
time.Sleep(delay)
}
}
return nil, lastErr
}
// RetryWithContext executes function with retry logic and context
func RetryWithContext(ctx context.Context, fn func() (interface{}, error), config *RetryConfig) (interface{}, error) {
if config == nil {
config = DefaultRetryConfig()
}
var lastErr Error
log := logger.Default()
for attempt := 0; attempt < config.MaxAttempts; attempt++ {
// Check context cancellation
select {
case <-ctx.Done():
return nil, Wrap(ctx.Err(), ErrCodeTimeout, "Context cancelled during retry")
default:
}
result, err := fn()
if err == nil {
return result, nil
}
appErr := FromError(err)
if appErr == nil {
appErr = NewWithCode(ErrCodeInternalError, err.Error())
}
// Check if error is retryable
if config.RetryIf != nil && !config.RetryIf(appErr) {
return nil, appErr
}
lastErr = appErr
// Don't wait after last attempt
if attempt < config.MaxAttempts-1 {
delay := calculateDelay(attempt, config)
if config.OnRetry != nil {
config.OnRetry(attempt+1, appErr)
}
log.Warn(appErr.Error(),
logger.Int("attempt", attempt+1),
logger.Int("max_attempts", config.MaxAttempts),
logger.String("retry_delay", delay.String()),
logger.Bool("will_retry", attempt < config.MaxAttempts-1),
)
// Wait with context cancellation
select {
case <-ctx.Done():
return nil, Wrap(ctx.Err(), ErrCodeTimeout, "Context cancelled during retry delay")
case <-time.After(delay):
}
}
}
return nil, lastErr
}
// calculateDelay calculates delay based on backoff strategy
func calculateDelay(attempt int, config *RetryConfig) time.Duration {
var delay time.Duration
switch config.Backoff {
case LinearBackoff:
delay = time.Duration(attempt+1) * config.Delay
case ExponentialBackoff:
delay = config.Delay * time.Duration(math.Pow(2, float64(attempt)))
case ExponentialBackoffWithJitter:
delay = config.Delay * time.Duration(math.Pow(2, float64(attempt)))
// Add jitter
jitter := time.Duration(rand.Float64() * float64(delay) * 0.1)
delay += jitter
case FixedBackoff:
delay = config.Delay
default:
delay = config.Delay
}
// Apply max delay limit
if delay > config.MaxDelay {
delay = config.MaxDelay
}
return delay
}
// CircuitBreaker implements circuit breaker pattern
type CircuitBreaker struct {
name string
maxFailures int
resetTimeout time.Duration
state CircuitState
failures int
lastFailTime time.Time
mu sync.RWMutex
}
// CircuitState represents circuit breaker state
type CircuitState int
const (
CircuitClosed CircuitState = iota
CircuitOpen
CircuitHalfOpen
)
// NewCircuitBreaker creates new circuit breaker
func NewCircuitBreaker(name string, maxFailures int, resetTimeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
name: name,
maxFailures: maxFailures,
resetTimeout: resetTimeout,
state: CircuitClosed,
}
}
// Execute executes function with circuit breaker protection
func (cb *CircuitBreaker) Execute(fn func() (interface{}, error)) (interface{}, error) {
cb.mu.Lock()
defer cb.mu.Unlock()
log := logger.Default()
// Check if circuit is open
if cb.state == CircuitOpen {
if time.Since(cb.lastFailTime) > cb.resetTimeout {
cb.state = CircuitHalfOpen
log.Info("Circuit breaker transitioning to half-open", logger.String("circuit", cb.name))
} else {
return nil, NewWithCode(ErrCodeServiceUnavailable,
fmt.Sprintf("Circuit breaker '%s' is open", cb.name))
}
}
// Execute function
result, err := fn()
if err != nil {
cb.onFailure()
return nil, err
}
cb.onSuccess()
return result, nil
}
// onSuccess handles successful execution
func (cb *CircuitBreaker) onSuccess() {
cb.failures = 0
if cb.state == CircuitHalfOpen {
cb.state = CircuitClosed
log := logger.Default()
log.Info("Circuit breaker closed", logger.String("circuit", cb.name))
}
}
// onFailure handles failed execution
func (cb *CircuitBreaker) onFailure() {
cb.failures++
cb.lastFailTime = time.Now()
if cb.failures >= cb.maxFailures {
cb.state = CircuitOpen
log := logger.Default()
log.Warn("Circuit breaker opened",
logger.String("circuit", cb.name),
logger.Int("failures", cb.failures),
logger.Int("max_failures", cb.maxFailures),
)
}
}
// GetState returns current circuit breaker state
func (cb *CircuitBreaker) GetState() CircuitState {
cb.mu.RLock()
defer cb.mu.RUnlock()
return cb.state
}
// GetFailures returns current failure count
func (cb *CircuitBreaker) GetFailures() int {
cb.mu.RLock()
defer cb.mu.RUnlock()
return cb.failures
}
// Reset resets circuit breaker
func (cb *CircuitBreaker) Reset() {
cb.mu.Lock()
defer cb.mu.Unlock()
cb.state = CircuitClosed
cb.failures = 0
cb.lastFailTime = time.Time{}
log := logger.Default()
log.Info("Circuit breaker reset", logger.String("circuit", cb.name))
}
// RecoveryManager manages error recovery strategies
type RecoveryManager struct {
circuitBreakers map[string]*CircuitBreaker
retryConfigs map[string]*RetryConfig
mu sync.RWMutex
}
// NewRecoveryManager creates new recovery manager
func NewRecoveryManager() *RecoveryManager {
return &RecoveryManager{
circuitBreakers: make(map[string]*CircuitBreaker),
retryConfigs: make(map[string]*RetryConfig),
}
}
// RegisterCircuitBreaker registers circuit breaker
func (rm *RecoveryManager) RegisterCircuitBreaker(name string, maxFailures int, resetTimeout time.Duration) {
rm.mu.Lock()
defer rm.mu.Unlock()
rm.circuitBreakers[name] = NewCircuitBreaker(name, maxFailures, resetTimeout)
}
// RegisterRetryConfig registers retry configuration
func (rm *RecoveryManager) RegisterRetryConfig(name string, config *RetryConfig) {
rm.mu.Lock()
defer rm.mu.Unlock()
rm.retryConfigs[name] = config
}
// ExecuteWithRecovery executes function with recovery strategies
func (rm *RecoveryManager) ExecuteWithRecovery(operationName string, fn func() (interface{}, error)) (interface{}, error) {
rm.mu.RLock()
cb, hasCB := rm.circuitBreakers[operationName]
config, hasConfig := rm.retryConfigs[operationName]
rm.mu.RUnlock()
// Execute with circuit breaker if available
if hasCB {
return cb.Execute(func() (interface{}, error) {
// Execute with retry if available
if hasConfig {
return Retry(fn, config)
}
return fn()
})
}
// Execute with retry only
if hasConfig {
return Retry(fn, config)
}
// Execute normally
return fn()
}
// GetCircuitBreaker returns circuit breaker by name
func (rm *RecoveryManager) GetCircuitBreaker(name string) (*CircuitBreaker, bool) {
rm.mu.RLock()
defer rm.mu.RUnlock()
cb, exists := rm.circuitBreakers[name]
return cb, exists
}
// GetRetryConfig returns retry config by name
func (rm *RecoveryManager) GetRetryConfig(name string) (*RetryConfig, bool) {
rm.mu.RLock()
defer rm.mu.RUnlock()
config, exists := rm.retryConfigs[name]
return config, exists
}
// Global recovery manager
var globalRecoveryManager = NewRecoveryManager()
// GetRecoveryManager returns global recovery manager
func GetRecoveryManager() *RecoveryManager {
return globalRecoveryManager
}
// ExecuteWithGlobalRecovery executes with global recovery manager
func ExecuteWithGlobalRecovery(operationName string, fn func() (interface{}, error)) (interface{}, error) {
return globalRecoveryManager.ExecuteWithRecovery(operationName, fn)
}
+336
View File
@@ -0,0 +1,336 @@
package errors
import (
"fmt"
"sync"
"time"
"google.golang.org/grpc/codes"
)
// ErrorType represents a custom error type
type ErrorType interface {
Name() string
New(message string, metadata Metadata) Error
FromError(err error) Error
Validate(metadata Metadata) error
}
// ErrorRegistry manages custom error types
type ErrorRegistry struct {
types map[string]ErrorType
handlers map[string]func(Error) Error
mu sync.RWMutex
}
var (
registry = &ErrorRegistry{
types: make(map[string]ErrorType),
handlers: make(map[string]func(Error) Error),
}
)
// RegisterErrorType registers a custom error type
func RegisterErrorType(name string, errorType ErrorType) {
registry.mu.Lock()
defer registry.mu.Unlock()
registry.types[name] = errorType
}
// GetErrorType returns error type by name
func GetErrorType(name string) (ErrorType, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
errorType, exists := registry.types[name]
return errorType, exists
}
// RegisterErrorHandler registers custom error handler
func RegisterErrorHandler(code string, handler func(Error) Error) {
registry.mu.Lock()
defer registry.mu.Unlock()
registry.handlers[code] = handler
}
// GetErrorHandler returns error handler for code
func GetErrorHandler(code string) (func(Error) Error, bool) {
registry.mu.RLock()
defer registry.mu.RUnlock()
handler, exists := registry.handlers[code]
return handler, exists
}
// ApplyHandlers applies registered handlers to error
func ApplyHandlers(err Error) Error {
if err == nil {
return nil
}
// Apply specific handler for error code
if handler, exists := GetErrorHandler(err.Code()); exists {
err = handler(err)
}
// Apply category handlers
categoryHandlers := GetCategoryHandlers(err.Category())
for _, handler := range categoryHandlers {
err = handler(err)
}
return err
}
// CategoryHandler handles errors by category
type CategoryHandler struct {
Category string
Handler func(Error) Error
}
var (
categoryHandlers = make(map[string][]func(Error) Error)
categoryMu sync.RWMutex
)
// RegisterCategoryHandler registers handler for error category
func RegisterCategoryHandler(category string, handler func(Error) Error) {
categoryMu.Lock()
defer categoryMu.Unlock()
if categoryHandlers[category] == nil {
categoryHandlers[category] = make([]func(Error) Error, 0)
}
categoryHandlers[category] = append(categoryHandlers[category], handler)
}
// GetCategoryHandlers returns handlers for category
func GetCategoryHandlers(category string) []func(Error) Error {
categoryMu.RLock()
defer categoryMu.RUnlock()
handlers := make([]func(Error) Error, len(categoryHandlers[category]))
copy(handlers, categoryHandlers[category])
return handlers
}
// BaseErrorType provides base implementation for custom error types
type BaseErrorType struct {
name string
defaultCode string
category string
httpStatus int
grpcCode int
validator func(Metadata) error
}
// NewBaseErrorType creates new base error type
func NewBaseErrorType(name, defaultCode, category string, httpStatus int, grpcCode int) *BaseErrorType {
return &BaseErrorType{
name: name,
defaultCode: defaultCode,
category: category,
httpStatus: httpStatus,
grpcCode: grpcCode,
}
}
// Name returns error type name
func (bet *BaseErrorType) Name() string {
return bet.name
}
// New creates new error of this type
func (bet *BaseErrorType) New(message string, metadata Metadata) Error {
code := bet.defaultCode
if c, exists := metadata["code"].(string); exists {
code = c
}
return &AppError{
code: code,
message: message,
category: bet.category,
httpStatus: bet.httpStatus,
grpcCode: codes.Code(bet.grpcCode),
metadata: metadata,
timestamp: time.Now(),
stackTrace: captureStackTrace(),
}
}
// FromError converts existing error to this type
func (bet *BaseErrorType) FromError(err error) Error {
appErr := FromError(err)
if appErr == nil {
return bet.New(err.Error(), make(Metadata))
}
return bet.New(appErr.Error(), appErr.Metadata())
}
// Validate validates metadata
func (bet *BaseErrorType) Validate(metadata Metadata) error {
if bet.validator != nil {
return bet.validator(metadata)
}
return nil
}
// SetValidator sets validator function
func (bet *BaseErrorType) SetValidator(validator func(Metadata) error) {
bet.validator = validator
}
// BusinessErrorType represents business logic errors
type BusinessErrorType struct {
*BaseErrorType
ruleName string
}
// NewBusinessErrorType creates new business error type
func NewBusinessErrorType(name, ruleName string) *BusinessErrorType {
return &BusinessErrorType{
BaseErrorType: NewBaseErrorType(name, ErrCodeBusinessRule, CategoryBusiness, 400, int(codes.FailedPrecondition)),
ruleName: ruleName,
}
}
// RuleName returns business rule name
func (bet *BusinessErrorType) RuleName() string {
return bet.ruleName
}
// ValidationErrorType represents validation errors
type ValidationErrorType struct {
*BaseErrorType
field string
rule string
}
// NewValidationErrorType creates new validation error type
func NewValidationErrorType(name, field, rule string) *ValidationErrorType {
return &ValidationErrorType{
BaseErrorType: NewBaseErrorType(name, ErrCodeValidationFailed, CategoryValidation, 400, int(codes.InvalidArgument)),
field: field,
rule: rule,
}
}
// Field returns field name
func (vet *ValidationErrorType) Field() string {
return vet.field
}
// Rule returns validation rule
func (vet *ValidationErrorType) Rule() string {
return vet.rule
}
// ExternalErrorType represents external service errors
type ExternalErrorType struct {
*BaseErrorType
serviceName string
retryable bool
}
// NewExternalErrorType creates new external error type
func NewExternalErrorType(name, serviceName string, retryable bool) *ExternalErrorType {
return &ExternalErrorType{
BaseErrorType: NewBaseErrorType(name, ErrCodeExternalError, CategoryExternal, 502, int(codes.Unavailable)),
serviceName: serviceName,
retryable: retryable,
}
}
// ServiceName returns external service name
func (eet *ExternalErrorType) ServiceName() string {
return eet.serviceName
}
// Retryable returns if error is retryable
func (eet *ExternalErrorType) Retryable() bool {
return eet.retryable
}
// ErrorFactory creates errors from configuration
type ErrorFactory struct {
config map[string]interface{}
mu sync.RWMutex
}
// NewErrorFactory creates new error factory
func NewErrorFactory() *ErrorFactory {
return &ErrorFactory{
config: make(map[string]interface{}),
}
}
// Configure configures error factory
func (ef *ErrorFactory) Configure(config map[string]interface{}) {
ef.mu.Lock()
defer ef.mu.Unlock()
ef.config = config
}
// CreateError creates error from configuration
func (ef *ErrorFactory) CreateError(errorName string, message string, metadata Metadata) Error {
ef.mu.RLock()
defer ef.mu.RUnlock()
// Try to get error type from configuration
if errorTypeConfig, exists := ef.config[errorName]; exists {
if configMap, ok := errorTypeConfig.(map[string]interface{}); ok {
name, _ := configMap["name"].(string)
if errorType, exists := GetErrorType(name); exists {
return errorType.New(message, metadata)
}
}
}
// Fallback to creating standard error
return NewWithMetadata(ErrCodeInternalError, message, metadata)
}
// GetRegisteredTypes returns all registered error types
func GetRegisteredTypes() map[string]ErrorType {
registry.mu.RLock()
defer registry.mu.RUnlock()
types := make(map[string]ErrorType)
for k, v := range registry.types {
types[k] = v
}
return types
}
// GetRegisteredHandlers returns all registered handlers
func GetRegisteredHandlers() map[string]func(Error) Error {
registry.mu.RLock()
defer registry.mu.RUnlock()
handlers := make(map[string]func(Error) Error)
for k, v := range registry.handlers {
handlers[k] = v
}
return handlers
}
// ValidateErrorType validates error type configuration
func ValidateErrorType(errorType ErrorType) error {
if errorType.Name() == "" {
return fmt.Errorf("error type name cannot be empty")
}
// Test error creation
testErr := errorType.New("test", make(Metadata))
if testErr == nil {
return fmt.Errorf("error type failed to create error")
}
if testErr.Code() == "" {
return fmt.Errorf("error type must produce error with code")
}
return nil
}
+429
View File
@@ -0,0 +1,429 @@
package errors
import (
"fmt"
"reflect"
"regexp"
"strconv"
"strings"
)
// ValidationError represents a validation error
type ValidationError struct {
Field string `json:"field"`
Value interface{} `json:"value"`
Rule string `json:"rule"`
Message string `json:"message"`
}
// ValidationErrors represents multiple validation errors
type ValidationErrors []ValidationError
// Error implements error interface
func (ve ValidationErrors) Error() string {
if len(ve) == 0 {
return "validation failed"
}
if len(ve) == 1 {
return fmt.Sprintf("validation failed: %s", ve[0].Message)
}
return fmt.Sprintf("validation failed: %d errors", len(ve))
}
// ToMap converts validation errors to map
func (ve ValidationErrors) ToMap() map[string]interface{} {
result := make(map[string]interface{})
for _, err := range ve {
result[err.Field] = map[string]interface{}{
"value": err.Value,
"rule": err.Rule,
"message": err.Message,
}
}
return result
}
// Validator provides validation functionality
type Validator struct {
rules map[string]ValidationRule
}
// ValidationRule represents a validation rule
type ValidationRule struct {
Name string
Validator func(interface{}) error
Message string
}
// NewValidator creates new validator
func NewValidator() *Validator {
return &Validator{
rules: make(map[string]ValidationRule),
}
}
// AddRule adds validation rule
func (v *Validator) AddRule(name string, validator func(interface{}) error, message string) {
v.rules[name] = ValidationRule{
Name: name,
Validator: validator,
Message: message,
}
}
// Validate validates value against rules
func (v *Validator) Validate(value interface{}, rules []string) ValidationErrors {
var errors ValidationErrors
for _, ruleName := range rules {
if rule, exists := v.rules[ruleName]; exists {
if err := rule.Validator(value); err != nil {
errors = append(errors, ValidationError{
Field: "",
Value: value,
Rule: ruleName,
Message: rule.Message,
})
}
}
}
return errors
}
// ValidateField validates a field with rules
func (v *Validator) ValidateField(field string, value interface{}, rules []string) ValidationErrors {
var errors ValidationErrors
for _, ruleName := range rules {
if rule, exists := v.rules[ruleName]; exists {
if err := rule.Validator(value); err != nil {
errors = append(errors, ValidationError{
Field: field,
Value: value,
Rule: ruleName,
Message: fmt.Sprintf(rule.Message, field),
})
}
}
}
return errors
}
// ValidateStruct validates struct fields
func (v *Validator) ValidateStruct(obj interface{}) ValidationErrors {
var errors ValidationErrors
val := reflect.ValueOf(obj)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
if val.Kind() != reflect.Struct {
return errors
}
typ := val.Type()
for i := 0; i < val.NumField(); i++ {
field := typ.Field(i)
fieldValue := val.Field(i)
// Get validation tags
tag := field.Tag.Get("validate")
if tag == "" {
continue
}
rules := strings.Split(tag, ",")
fieldErrors := v.ValidateField(field.Name, fieldValue.Interface(), rules)
errors = append(errors, fieldErrors...)
}
return errors
}
// Common validation rules
var (
// Required rule
Required = ValidationRule{
Name: "required",
Validator: func(value interface{}) error {
if value == nil || value == "" {
return fmt.Errorf("value is required")
}
return nil
},
Message: "%s is required",
}
// Email rule
Email = ValidationRule{
Name: "email",
Validator: func(value interface{}) error {
str, ok := value.(string)
if !ok {
return fmt.Errorf("value must be string")
}
emailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
if !emailRegex.MatchString(str) {
return fmt.Errorf("invalid email format")
}
return nil
},
Message: "%s must be a valid email",
}
// Min length rule
MinLength = func(min int) ValidationRule {
return ValidationRule{
Name: fmt.Sprintf("min_%d", min),
Validator: func(value interface{}) error {
str, ok := value.(string)
if !ok {
return fmt.Errorf("value must be string")
}
if len(str) < min {
return fmt.Errorf("value must be at least %d characters", min)
}
return nil
},
Message: fmt.Sprintf("%%s must be at least %d characters", min),
}
}
// Max length rule
MaxLength = func(max int) ValidationRule {
return ValidationRule{
Name: fmt.Sprintf("max_%d", max),
Validator: func(value interface{}) error {
str, ok := value.(string)
if !ok {
return fmt.Errorf("value must be string")
}
if len(str) > max {
return fmt.Errorf("value must be at most %d characters", max)
}
return nil
},
Message: fmt.Sprintf("%%s must be at most %d characters", max),
}
}
// Numeric rule
Numeric = ValidationRule{
Name: "numeric",
Validator: func(value interface{}) error {
switch v := value.(type) {
case int, int8, int16, int32, int64:
return nil
case uint, uint8, uint16, uint32, uint64:
return nil
case float32, float64:
return nil
case string:
if _, err := strconv.ParseFloat(v, 64); err != nil {
return fmt.Errorf("value must be numeric")
}
return nil
default:
return fmt.Errorf("value must be numeric")
}
},
Message: "%s must be numeric",
}
// Positive rule
Positive = ValidationRule{
Name: "positive",
Validator: func(value interface{}) error {
var num float64
switch v := value.(type) {
case int:
num = float64(v)
case int64:
num = float64(v)
case float64:
num = v
case string:
parsed, err := strconv.ParseFloat(v, 64)
if err != nil {
return fmt.Errorf("value must be numeric")
}
num = parsed
default:
return fmt.Errorf("value must be numeric")
}
if num <= 0 {
return fmt.Errorf("value must be positive")
}
return nil
},
Message: "%s must be positive",
}
// Range rule
Range = func(min, max float64) ValidationRule {
return ValidationRule{
Name: fmt.Sprintf("range_%.1f_%.1f", min, max),
Validator: func(value interface{}) error {
var num float64
switch v := value.(type) {
case int:
num = float64(v)
case int64:
num = float64(v)
case float64:
num = v
case string:
parsed, err := strconv.ParseFloat(v, 64)
if err != nil {
return fmt.Errorf("value must be numeric")
}
num = parsed
default:
return fmt.Errorf("value must be numeric")
}
if num < min || num > max {
return fmt.Errorf("value must be between %.1f and %.1f", min, max)
}
return nil
},
Message: fmt.Sprintf("%%s must be between %.1f and %.1f", min, max),
}
}
)
// DefaultValidator returns validator with common rules
func DefaultValidator() *Validator {
v := NewValidator()
v.AddRule("required", Required.Validator, Required.Message)
v.AddRule("email", Email.Validator, Email.Message)
v.AddRule("numeric", Numeric.Validator, Numeric.Message)
v.AddRule("positive", Positive.Validator, Positive.Message)
return v
}
// ValidateStruct validates struct using default validator
func ValidateStruct(obj interface{}) ValidationErrors {
v := DefaultValidator()
return v.ValidateStruct(obj)
}
// CreateValidationError creates validation error
func CreateValidationError(field string, value interface{}, rule, message string) Error {
metadata := Metadata{
"field": field,
"value": value,
"rule": rule,
"validation_errors": map[string]interface{}{
field: map[string]interface{}{
"value": value,
"rule": rule,
"message": message,
},
},
}
return NewWithMetadata(ErrCodeValidationFailed,
fmt.Sprintf("Validation failed for field %s", field), metadata)
}
// CreateValidationErrors creates error from multiple validation errors
func CreateValidationErrors(errors ValidationErrors) Error {
if len(errors) == 0 {
return nil
}
if len(errors) == 1 {
err := errors[0]
return CreateValidationError(err.Field, err.Value, err.Rule, err.Message)
}
metadata := Metadata{
"validation_errors": errors.ToMap(),
"error_count": len(errors),
}
return NewWithMetadata(ErrCodeValidationFailed,
fmt.Sprintf("Validation failed with %d errors", len(errors)), metadata)
}
// FieldValidation provides field-level validation
type FieldValidation struct {
Field string
Value interface{}
Rules []string
}
// ValidateFields validates multiple fields
func ValidateFields(validations []FieldValidation) ValidationErrors {
v := DefaultValidator()
var allErrors ValidationErrors
for _, validation := range validations {
errors := v.ValidateField(validation.Field, validation.Value, validation.Rules)
allErrors = append(allErrors, errors...)
}
return allErrors
}
// ValidationBuilder provides fluent API for validation
type ValidationBuilder struct {
validations []FieldValidation
}
// NewValidationBuilder creates new validation builder
func NewValidationBuilder() *ValidationBuilder {
return &ValidationBuilder{
validations: make([]FieldValidation, 0),
}
}
// Field adds field validation
func (vb *ValidationBuilder) Field(field string, value interface{}) *FieldValidationBuilder {
return &FieldValidationBuilder{
builder: vb,
field: field,
value: value,
}
}
// FieldValidationBuilder builds field validation
type FieldValidationBuilder struct {
builder *ValidationBuilder
field string
value interface{}
}
// Rules adds validation rules
func (fvb *FieldValidationBuilder) Rules(rules ...string) *ValidationBuilder {
fvb.builder.validations = append(fvb.builder.validations, FieldValidation{
Field: fvb.field,
Value: fvb.value,
Rules: rules,
})
return fvb.builder
}
// Validate executes all validations
func (vb *ValidationBuilder) Validate() ValidationErrors {
return ValidateFields(vb.validations)
}
// ValidateWithError executes validations and returns error
func (vb *ValidationBuilder) ValidateWithError() Error {
errors := vb.Validate()
if len(errors) > 0 {
return CreateValidationErrors(errors)
}
return nil
}