1
0
mirror of https://github.com/redis/go-redis.git synced 2025-12-02 06:22:31 +03:00

chore: refactor everything so slog.Logger can satisfy LoggerWithLevel interface

This commit is contained in:
ccoVeille
2025-11-08 00:30:06 +01:00
parent 14e25d740b
commit 92148e72b1
16 changed files with 325 additions and 137 deletions

View File

@@ -77,66 +77,3 @@ func (l LogLevelT) InfoOrAbove() bool {
func (l LogLevelT) DebugOrAbove() bool {
return l >= LogLevelDebug
}
// LoggerWithLevel is a logger interface with leveled logging methods.
//
// This interface can be implemented by custom loggers to provide leveled logging.
type LoggerWithLevel interface {
// Infof logs an info level message
Infof(ctx context.Context, format string, v ...interface{})
// Warnf logs a warning level message
Warnf(ctx context.Context, format string, v ...interface{})
// Debugf logs a debug level message
Debugf(ctx context.Context, format string, v ...interface{})
// Errorf logs an error level message
Errorf(ctx context.Context, format string, v ...interface{})
// Enabled reports whether the given log level is enabled in the logger
Enabled(ctx context.Context, level LogLevelT) bool
}
// legacyLoggerAdapter is a logger that implements LoggerWithLevel interface
// using the global [Logger] and [LogLevel] variables.
type legacyLoggerAdapter struct{}
func (l *legacyLoggerAdapter) Infof(ctx context.Context, format string, v ...interface{}) {
if LogLevel.InfoOrAbove() {
Logger.Printf(ctx, format, v...)
}
}
func (l *legacyLoggerAdapter) Warnf(ctx context.Context, format string, v ...interface{}) {
if LogLevel.WarnOrAbove() {
Logger.Printf(ctx, format, v...)
}
}
func (l *legacyLoggerAdapter) Debugf(ctx context.Context, format string, v ...interface{}) {
if LogLevel.DebugOrAbove() {
Logger.Printf(ctx, format, v...)
}
}
func (l legacyLoggerAdapter) Errorf(ctx context.Context, format string, v ...interface{}) {
Logger.Printf(ctx, format, v...)
}
func (l legacyLoggerAdapter) Enabled(_ context.Context, level LogLevelT) bool {
switch level {
case LogLevelWarn:
return LogLevel.WarnOrAbove()
case LogLevelInfo:
return LogLevel.InfoOrAbove()
case LogLevelDebug:
return LogLevel.DebugOrAbove()
case LogLevelError:
fallthrough
default:
return true
}
}
var LegacyLoggerWithLevel LoggerWithLevel = &legacyLoggerAdapter{}

View File

@@ -11,6 +11,7 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/internal/util"
"github.com/redis/go-redis/v9/logging"
)
var (
@@ -121,7 +122,7 @@ type Options struct {
DialerRetryTimeout time.Duration
// Optional logger for connection pool operations.
Logger internal.LoggerWithLevel
Logger *logging.CustomLogger
}
type lastDialErrorWrap struct {
@@ -1055,10 +1056,10 @@ func (p *ConnPool) isHealthyConn(cn *Conn, nowNs int64) bool {
return true
}
func (p *ConnPool) logger() internal.LoggerWithLevel {
if p.cfg.Logger != nil {
return p.cfg.Logger
func (p *ConnPool) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if p.cfg != nil && p.cfg.Logger != nil {
logger = p.cfg.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}

144
logging/custom.go Normal file
View File

@@ -0,0 +1,144 @@
package logging
import (
"context"
"fmt"
)
// CustomLogger is a logger interface with leveled logging methods.
//
// This interface can be implemented by custom loggers to provide leveled logging.
type CustomLogger struct {
logger LoggerWithLevel
loggerLevel *LogLevelT
printfAdapter PrintfAdapter
}
func NewCustomLogger(logger LoggerWithLevel, opts ...CustomLoggerOption) *CustomLogger {
cl := &CustomLogger{
logger: logger,
}
for _, opt := range opts {
opt(cl)
}
return cl
}
type CustomLoggerOption func(*CustomLogger)
func WithPrintfAdapter(adapter PrintfAdapter) CustomLoggerOption {
return func(cl *CustomLogger) {
cl.printfAdapter = adapter
}
}
func WithLoggerLevel(level LogLevelT) CustomLoggerOption {
return func(cl *CustomLogger) {
cl.loggerLevel = &level
}
}
// PrintfAdapter is a function that converts Printf-style log messages into structured log messages.
// It can be used to extract key-value pairs from the formatted message.
type PrintfAdapter func(ctx context.Context, format string, v ...any) (context.Context, string, []any)
// Error is a structured error level logging method with context and arguments.
func (cl *CustomLogger) Error(ctx context.Context, msg string, args ...any) {
if cl == nil || cl.logger == nil {
legacyLoggerWithLevel.Errorf(ctx, msg, args...)
return
}
cl.logger.ErrorContext(ctx, msg, args...)
}
func (cl *CustomLogger) Errorf(ctx context.Context, format string, v ...any) {
if cl == nil || cl.logger == nil {
legacyLoggerWithLevel.Errorf(ctx, format, v...)
return
}
cl.logger.ErrorContext(ctx, format, v...)
}
// Warn is a structured warning level logging method with context and arguments.
func (cl *CustomLogger) Warn(ctx context.Context, msg string, args ...any) {
if cl == nil || cl.logger == nil {
legacyLoggerWithLevel.Warnf(ctx, msg, args...)
return
}
cl.logger.WarnContext(ctx, msg, args...)
}
func (cl *CustomLogger) Warnf(ctx context.Context, format string, v ...any) {
if cl == nil || cl.logger == nil {
legacyLoggerWithLevel.Warnf(ctx, format, v...)
return
}
cl.logger.WarnContext(cl.printfToStructured(ctx, format, v...))
}
// Info is a structured info level logging method with context and arguments.
func (cl *CustomLogger) Info(ctx context.Context, msg string, args ...any) {
if cl == nil || cl.logger == nil {
legacyLoggerWithLevel.Infof(ctx, msg, args...)
return
}
cl.logger.InfoContext(ctx, msg, args...)
}
// Debug is a structured debug level logging method with context and arguments.
func (cl *CustomLogger) Debug(ctx context.Context, msg string, args ...any) {
if cl == nil || cl.logger == nil {
legacyLoggerWithLevel.Debugf(ctx, msg, args...)
return
}
cl.logger.DebugContext(ctx, msg, args...)
}
func (cl *CustomLogger) Infof(ctx context.Context, format string, v ...any) {
if cl == nil || cl.logger == nil {
legacyLoggerWithLevel.Infof(ctx, format, v...)
return
}
cl.logger.InfoContext(cl.printfToStructured(ctx, format, v...))
}
func (cl *CustomLogger) Debugf(ctx context.Context, format string, v ...any) {
if cl == nil || cl.logger == nil {
legacyLoggerWithLevel.Debugf(ctx, format, v...)
return
}
cl.logger.DebugContext(cl.printfToStructured(ctx, format, v...))
}
func (cl *CustomLogger) printfToStructured(ctx context.Context, format string, v ...any) (context.Context, string, []any) {
if cl != nil && cl.printfAdapter != nil {
return cl.printfAdapter(ctx, format, v...)
}
return ctx, fmt.Sprintf(format, v...), nil
}
func (cl *CustomLogger) Enabled(ctx context.Context, level LogLevelT) bool {
if cl != nil && cl.loggerLevel != nil {
return level >= *cl.loggerLevel
}
return legacyLoggerWithLevel.Enabled(ctx, level)
}
// LoggerWithLevel is a logger interface with leveled logging methods.
//
// [slog.Logger] from the standard library satisfies this interface.
type LoggerWithLevel interface {
// InfoContext logs an info level message
InfoContext(ctx context.Context, format string, v ...any)
// WarnContext logs a warning level message
WarnContext(ctx context.Context, format string, v ...any)
// Debugf logs a debug level message
DebugContext(ctx context.Context, format string, v ...any)
// Errorf logs an error level message
ErrorContext(ctx context.Context, format string, v ...any)
}

91
logging/legacy.go Normal file
View File

@@ -0,0 +1,91 @@
package logging
import (
"context"
"github.com/redis/go-redis/v9/internal"
)
// legacyLoggerAdapter is a logger that implements [LoggerWithLevel] interface
// using the global [internal.Logger] and [internal.LogLevel] variables.
type legacyLoggerAdapter struct{}
var _ LoggerWithLevel = (*legacyLoggerAdapter)(nil)
// structuredToPrintf converts a structured log message and key-value pairs into something a Printf-style logger can understand.
func (l *legacyLoggerAdapter) structuredToPrintf(msg string, v ...any) (string, []any) {
format := msg
var args []any
for i := 0; i < len(v); i += 2 {
if i+1 >= len(v) {
break
}
format += " %v=%v"
args = append(args, v[i], v[i+1])
}
return format, args
}
func (l legacyLoggerAdapter) Errorf(ctx context.Context, format string, v ...any) {
internal.Logger.Printf(ctx, format, v...)
}
func (l *legacyLoggerAdapter) ErrorContext(ctx context.Context, msg string, args ...any) {
format, v := l.structuredToPrintf(msg, args...)
l.Errorf(ctx, format, v...)
}
func (l *legacyLoggerAdapter) WarnContext(ctx context.Context, msg string, args ...any) {
format, v := l.structuredToPrintf(msg, args...)
l.Warnf(ctx, format, v...)
}
func (l *legacyLoggerAdapter) Warnf(ctx context.Context, format string, v ...any) {
if !internal.LogLevel.WarnOrAbove() {
// Skip logging
return
}
internal.Logger.Printf(ctx, format, v...)
}
func (l *legacyLoggerAdapter) InfoContext(ctx context.Context, msg string, args ...any) {
format, v := l.structuredToPrintf(msg, args...)
l.Infof(ctx, format, v...)
}
func (l *legacyLoggerAdapter) Infof(ctx context.Context, format string, v ...any) {
if !internal.LogLevel.InfoOrAbove() {
// Skip logging
return
}
internal.Logger.Printf(ctx, format, v...)
}
func (l *legacyLoggerAdapter) DebugContext(ctx context.Context, msg string, args ...any) {
format, v := l.structuredToPrintf(msg, args...)
l.Debugf(ctx, format, v...)
}
func (l *legacyLoggerAdapter) Debugf(ctx context.Context, format string, v ...any) {
if !internal.LogLevel.DebugOrAbove() {
// Skip logging
return
}
internal.Logger.Printf(ctx, format, v...)
}
func (l *legacyLoggerAdapter) Enabled(ctx context.Context, level LogLevelT) bool {
switch level {
case LogLevelDebug:
return internal.LogLevel.DebugOrAbove()
case LogLevelWarn:
return internal.LogLevel.WarnOrAbove()
case LogLevelInfo:
return internal.LogLevel.InfoOrAbove()
}
return true
}
var legacyLoggerWithLevel = &legacyLoggerAdapter{}

View File

@@ -6,8 +6,8 @@ import (
"sync/atomic"
"time"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
"github.com/redis/go-redis/v9/logging"
)
// CircuitBreakerState represents the state of a circuit breaker
@@ -194,11 +194,12 @@ func (cb *CircuitBreaker) GetStats() CircuitBreakerStats {
}
}
func (cb *CircuitBreaker) logger() internal.LoggerWithLevel {
func (cb *CircuitBreaker) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if cb.config != nil && cb.config.Logger != nil {
return cb.config.Logger
logger = cb.config.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}
// CircuitBreakerStats provides statistics about a circuit breaker
@@ -351,9 +352,10 @@ func (cbm *CircuitBreakerManager) Reset() {
})
}
func (cbm *CircuitBreakerManager) logger() internal.LoggerWithLevel {
func (cbm *CircuitBreakerManager) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if cbm.config != nil && cbm.config.Logger != nil {
return cbm.config.Logger
logger = cbm.config.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}

View File

@@ -7,9 +7,9 @@ import (
"strings"
"time"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
"github.com/redis/go-redis/v9/internal/util"
"github.com/redis/go-redis/v9/logging"
)
// Mode represents the maintenance notifications mode
@@ -130,7 +130,7 @@ type Config struct {
MaxHandoffRetries int
// Logger is an optional custom logger for maintenance notifications.
Logger internal.LoggerWithLevel
Logger *logging.CustomLogger
}
func (c *Config) IsEnabled() bool {
@@ -369,11 +369,12 @@ func (c *Config) applyWorkerDefaults(poolSize int) {
}
}
func (c *Config) logger() internal.LoggerWithLevel {
func (c *Config) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if c.Logger != nil {
return c.Logger
logger = c.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}
// DetectEndpointType automatically detects the appropriate endpoint type

View File

@@ -11,6 +11,7 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/logging"
)
// handoffWorkerManager manages background workers and queue for connection handoffs
@@ -492,9 +493,10 @@ func (hwm *handoffWorkerManager) closeConnFromRequest(ctx context.Context, reque
}
}
func (hwm *handoffWorkerManager) logger() internal.LoggerWithLevel {
func (hwm *handoffWorkerManager) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if hwm.config != nil && hwm.config.Logger != nil {
return hwm.config.Logger
logger = hwm.config.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}

View File

@@ -9,10 +9,10 @@ import (
"sync/atomic"
"time"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/interfaces"
"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/logging"
"github.com/redis/go-redis/v9/push"
)
@@ -311,9 +311,10 @@ func (hm *Manager) AddNotificationHook(notificationHook NotificationHook) {
hm.hooks = append(hm.hooks, notificationHook)
}
func (hm *Manager) logger() internal.LoggerWithLevel {
func (hm *Manager) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if hm.config != nil && hm.config.Logger != nil {
return hm.config.Logger
logger = hm.config.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}

View File

@@ -6,9 +6,9 @@ import (
"sync"
"time"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/logging"
)
// OperationsManagerInterface defines the interface for completing handoff operations
@@ -181,9 +181,10 @@ func (ph *PoolHook) Shutdown(ctx context.Context) error {
return ph.workerManager.shutdownWorkers(ctx)
}
func (ph *PoolHook) logger() internal.LoggerWithLevel {
if ph.config.Logger != nil {
return ph.config.Logger
func (ph *PoolHook) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if ph.config != nil && ph.config.Logger != nil {
logger = ph.config.Logger
}
return internal.LegacyLoggerWithLevel
}
return logger
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/maintnotifications/logs"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/logging"
"github.com/redis/go-redis/v9/push"
)
@@ -273,9 +274,10 @@ func (snh *NotificationHandler) handleFailedOver(ctx context.Context, handlerCtx
return nil
}
func (snh *NotificationHandler) logger() internal.LoggerWithLevel {
func (snh *NotificationHandler) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if snh.manager != nil && snh.manager.config != nil && snh.manager.config.Logger != nil {
return snh.manager.config.Logger
logger = snh.manager.config.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}

View File

@@ -14,10 +14,10 @@ import (
"time"
"github.com/redis/go-redis/v9/auth"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/internal/util"
"github.com/redis/go-redis/v9/logging"
"github.com/redis/go-redis/v9/maintnotifications"
"github.com/redis/go-redis/v9/push"
)
@@ -271,7 +271,7 @@ type Options struct {
// Logger is the logger used by the client for logging.
// If none is provided, the global logger [internal.LegacyLoggerWithLevel] is used.
Logger internal.LoggerWithLevel
Logger *logging.CustomLogger
}
func (opt *Options) init() {

View File

@@ -20,6 +20,7 @@ import (
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/internal/rand"
"github.com/redis/go-redis/v9/logging"
"github.com/redis/go-redis/v9/maintnotifications"
"github.com/redis/go-redis/v9/push"
)
@@ -150,7 +151,7 @@ type ClusterOptions struct {
MaintNotificationsConfig *maintnotifications.Config
// Logger is an optional logger for logging cluster-related messages.
Logger internal.LoggerWithLevel
Logger *logging.CustomLogger
}
func (opt *ClusterOptions) init() {
@@ -708,12 +709,12 @@ func (c *clusterNodes) Random() (*clusterNode, error) {
return c.GetOrCreate(addrs[n])
}
func (c *clusterNodes) logger() internal.LoggerWithLevel {
if c.opt.Logger != nil {
return c.opt.Logger
} else {
return internal.LegacyLoggerWithLevel
func (c *clusterNodes) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if c.opt != nil && c.opt.Logger != nil {
logger = c.opt.Logger
}
return logger
}
//------------------------------------------------------------------------------
@@ -2139,12 +2140,12 @@ func (c *ClusterClient) context(ctx context.Context) context.Context {
return context.Background()
}
func (c *ClusterClient) logger() internal.LoggerWithLevel {
if c.opt.Logger != nil {
return c.opt.Logger
} else {
return internal.LegacyLoggerWithLevel
func (c *ClusterClient) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if c.opt != nil && c.opt.Logger != nil {
logger = c.opt.Logger
}
return logger
}
func appendIfNotExist[T comparable](vals []T, newVal T) []T {

View File

@@ -10,6 +10,7 @@ import (
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/logging"
"github.com/redis/go-redis/v9/push"
)
@@ -144,12 +145,12 @@ func mapKeys(m map[string]struct{}) []string {
// logger is a wrapper around the logger to log messages with context.
//
// it uses the client logger if set, otherwise it uses the global logger.
func (c *PubSub) logger() internal.LoggerWithLevel {
if c.opt.Logger != nil {
return c.opt.Logger
} else {
return internal.LegacyLoggerWithLevel
func (c *PubSub) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if c.opt != nil && c.opt.Logger != nil {
logger = c.opt.Logger
}
return logger
}
func (c *PubSub) _subscribe(
@@ -646,7 +647,7 @@ type channel struct {
pubSub *PubSub
// Optional logger for logging channel-related messages.
Logger internal.LoggerWithLevel
Logger *logging.CustomLogger
msgCh chan *Message
allCh chan interface{}
@@ -809,10 +810,10 @@ func (c *channel) initAllChan() {
}()
}
func (c *channel) logger() internal.LoggerWithLevel {
func (c *channel) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if c.Logger != nil {
return c.Logger
} else {
return internal.LegacyLoggerWithLevel
logger = c.Logger
}
return logger
}

View File

@@ -15,6 +15,7 @@ import (
"github.com/redis/go-redis/v9/internal/hscan"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/logging"
"github.com/redis/go-redis/v9/maintnotifications"
"github.com/redis/go-redis/v9/push"
)
@@ -230,7 +231,7 @@ type baseClient struct {
streamingCredentialsManager *streaming.Manager
// loggerWithLevel is used for logging
loggerWithLevel internal.LoggerWithLevel
loggerWithLevel *logging.CustomLogger
}
func (c *baseClient) clone() *baseClient {
@@ -754,12 +755,12 @@ func (c *baseClient) context(ctx context.Context) context.Context {
// logger is a wrapper around the logger to log messages with context.
// it uses the client logger if set, otherwise it uses the global logger.
func (c *baseClient) logger() internal.LoggerWithLevel {
if c.opt.Logger != nil {
return c.opt.Logger
} else {
return internal.LegacyLoggerWithLevel
func (c *baseClient) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if c.opt != nil && c.opt.Logger != nil {
logger = c.opt.Logger
}
return logger
}
// createInitConnFunc creates a connection initialization function that can be used for reconnections.

12
ring.go
View File

@@ -20,6 +20,7 @@ import (
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/proto"
"github.com/redis/go-redis/v9/internal/rand"
"github.com/redis/go-redis/v9/logging"
)
var errRingShardsDown = errors.New("redis: all ring shards are down")
@@ -155,7 +156,7 @@ type RingOptions struct {
IdentitySuffix string
UnstableResp3 bool
Logger internal.LoggerWithLevel
Logger *logging.CustomLogger
}
func (opt *RingOptions) init() {
@@ -561,11 +562,12 @@ func (c *ringSharding) Close() error {
return firstErr
}
func (c *ringSharding) logger() internal.LoggerWithLevel {
if c.opt.Logger != nil {
return c.opt.Logger
func (c *ringSharding) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if c.opt != nil && c.opt.Logger != nil {
logger = c.opt.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}
//------------------------------------------------------------------------------

View File

@@ -13,10 +13,10 @@ import (
"time"
"github.com/redis/go-redis/v9/auth"
"github.com/redis/go-redis/v9/internal"
"github.com/redis/go-redis/v9/internal/pool"
"github.com/redis/go-redis/v9/internal/rand"
"github.com/redis/go-redis/v9/internal/util"
"github.com/redis/go-redis/v9/logging"
"github.com/redis/go-redis/v9/maintnotifications"
"github.com/redis/go-redis/v9/push"
)
@@ -151,7 +151,7 @@ type FailoverOptions struct {
//MaintNotificationsConfig *maintnotifications.Config
// Optional logger for logging
Logger internal.LoggerWithLevel
Logger *logging.CustomLogger
}
func (opt *FailoverOptions) clientOptions() *Options {
@@ -1132,11 +1132,12 @@ func (c *sentinelFailover) listen(pubsub *PubSub) {
}
}
func (c *sentinelFailover) logger() internal.LoggerWithLevel {
if c.opt.Logger != nil {
return c.opt.Logger
func (c *sentinelFailover) logger() *logging.CustomLogger {
var logger *logging.CustomLogger
if c.opt != nil && c.opt.Logger != nil {
logger = c.opt.Logger
}
return internal.LegacyLoggerWithLevel
return logger
}
func contains(slice []string, str string) bool {