diff --git a/database/db.go b/database/db.go index d024e42..cfc2025 100644 --- a/database/db.go +++ b/database/db.go @@ -18,6 +18,7 @@ import ( "github.com/lib/pq" "github.com/pkg/errors" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" "net" @@ -36,7 +37,7 @@ type DB struct { Options *Options - addr string + addrDescription string columnMap ColumnMap logger *logging.Logger tableSemaphores map[string]*semaphore.Weighted @@ -94,8 +95,8 @@ func (o *Options) Validate() error { // NewDbFromConfig returns a new DB from Config. func NewDbFromConfig(c *Config, logger *logging.Logger, connectorCallbacks RetryConnectorCallbacks) (*DB, error) { - var addr string var db *sqlx.DB + var typeAddr string switch c.Type { case "mysql": @@ -144,8 +145,8 @@ func NewDbFromConfig(c *Config, logger *logging.Logger, connectorCallbacks Retry return setGaleraOpts(ctx, conn, int64(c.Options.WsrepSyncWait)) } - addr = config.Addr db = sqlx.NewDb(sql.OpenDB(NewConnector(connector, logger, connectorCallbacks)), MySQL) + typeAddr = config.Addr case "pgsql": uri := &url.URL{ Scheme: "postgres", @@ -202,12 +203,18 @@ func NewDbFromConfig(c *Config, logger *logging.Logger, connectorCallbacks Retry return nil, errors.Wrap(err, "can't open pgsql database") } - addr = utils.JoinHostPort(c.Host, port) db = sqlx.NewDb(sql.OpenDB(NewConnector(connector, logger, connectorCallbacks)), PostgreSQL) + typeAddr = utils.JoinHostPort(c.Host, port) default: return nil, unknownDbType(c.Type) } + addrDescription := c.Type + if c.TlsOptions.Enable { + addrDescription += "+tls" + } + addrDescription += fmt.Sprintf("://%s@%s/%s", c.User, typeAddr, c.Database) + db.SetMaxIdleConns(c.Options.MaxConnections / 3) db.SetMaxOpenConns(c.Options.MaxConnections) @@ -217,15 +224,21 @@ func NewDbFromConfig(c *Config, logger *logging.Logger, connectorCallbacks Retry DB: db, Options: &c.Options, columnMap: NewColumnMap(db.Mapper), - addr: addr, + addrDescription: addrDescription, logger: logger, tableSemaphores: make(map[string]*semaphore.Weighted), }, nil } -// GetAddr returns the database host:port or Unix socket address. +// GetAddr returns the Redis connection address in a technical form. func (db *DB) GetAddr() string { - return db.addr + return db.addrDescription +} + +// MarshalLogObject implements zapcore.ObjectMarshaler, adding the database address to each log message. +func (db *DB) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddString("database_address", db.addrDescription) + return nil } // BuildColumns returns all columns of the given struct. diff --git a/database/db_test.go b/database/db_test.go new file mode 100644 index 0000000..5808f8c --- /dev/null +++ b/database/db_test.go @@ -0,0 +1,103 @@ +package database + +import ( + "github.com/icinga/icinga-go-library/config" + "github.com/icinga/icinga-go-library/logging" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + "testing" +) + +func TestNewDbFromConfig_GetAddr(t *testing.T) { + tests := []struct { + name string + conf *Config + addr string + }{ + { + name: "mysql-simple", + conf: &Config{ + Type: "mysql", + Host: "example.com", + Database: "db", + User: "user", + }, + addr: "mysql://user@example.com:3306/db", + }, + { + name: "mysql-custom-port", + conf: &Config{ + Type: "mysql", + Host: "example.com", + Port: 1234, + Database: "db", + User: "user", + }, + addr: "mysql://user@example.com:1234/db", + }, + { + name: "mysql-tls", + conf: &Config{ + Type: "mysql", + Host: "example.com", + Database: "db", + User: "user", + TlsOptions: config.TLS{Enable: true}, + }, + addr: "mysql+tls://user@example.com:3306/db", + }, + { + name: "mysql-unix-domain-socket", + conf: &Config{ + Type: "mysql", + Host: "/var/empty/mysql.sock", + Database: "db", + User: "user", + }, + addr: "mysql://user@/var/empty/mysql.sock/db", + }, + { + name: "pgsql-simple", + conf: &Config{ + Type: "pgsql", + Host: "example.com", + Database: "db", + User: "user", + }, + addr: "pgsql://user@example.com:5432/db", + }, + { + name: "pgsql-custom-port", + conf: &Config{ + Type: "pgsql", + Host: "example.com", + Port: 1234, + Database: "db", + User: "user", + }, + addr: "pgsql://user@example.com:1234/db", + }, + { + name: "pgsql-tls", + conf: &Config{ + Type: "pgsql", + Host: "example.com", + Database: "db", + User: "user", + TlsOptions: config.TLS{Enable: true}, + }, + addr: "pgsql+tls://user@example.com:5432/db", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + db, err := NewDbFromConfig( + test.conf, + logging.NewLogger(zaptest.NewLogger(t).Sugar(), 0), + RetryConnectorCallbacks{}) + require.NoError(t, err) + require.Equal(t, test.addr, db.GetAddr()) + }) + } +} diff --git a/redis/client.go b/redis/client.go index 5110ae4..b57fc6f 100644 --- a/redis/client.go +++ b/redis/client.go @@ -13,6 +13,7 @@ import ( "github.com/pkg/errors" "github.com/redis/go-redis/v9" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "golang.org/x/sync/errgroup" "golang.org/x/sync/semaphore" "net" @@ -79,9 +80,27 @@ func NewClientFromConfig(c *Config, logger *logging.Logger) (*Client, error) { return NewClient(redis.NewClient(options), logger, &c.Options), nil } -// GetAddr returns the Redis host:port or Unix socket address. +// GetAddr returns the Redis connection address in a technical form. func (c *Client) GetAddr() string { - return c.Client.Options().Addr + description := "redis" + if c.Client.Options().TLSConfig != nil { + description += "+tls" + } + description += "://" + if username := c.Client.Options().Username; username != "" { + description += username + "@" + } + description += c.Client.Options().Addr + if db := c.Client.Options().DB; db != 0 { + description += fmt.Sprintf("/%d", db) + } + return description +} + +// MarshalLogObject implements zapcore.ObjectMarshaler, adding the redis connection string to each log message. +func (c *Client) MarshalLogObject(encoder zapcore.ObjectEncoder) error { + encoder.AddString("redis_address", c.GetAddr()) + return nil } // HPair defines Redis hashes field-value pairs. diff --git a/redis/client_test.go b/redis/client_test.go new file mode 100644 index 0000000..223b2ec --- /dev/null +++ b/redis/client_test.go @@ -0,0 +1,87 @@ +package redis + +import ( + "github.com/icinga/icinga-go-library/config" + "github.com/icinga/icinga-go-library/logging" + "github.com/stretchr/testify/require" + "go.uber.org/zap/zaptest" + "testing" +) + +func TestNewClientFromConfig_GetAddr(t *testing.T) { + tests := []struct { + name string + conf *Config + addr string + }{ + { + name: "redis-simple", + conf: &Config{ + Host: "example.com", + }, + addr: "redis://example.com:6379", + }, + { + name: "redis-custom-port", + conf: &Config{ + Host: "example.com", + Port: 6380, + }, + addr: "redis://example.com:6380", + }, + { + name: "redis-acl", + conf: &Config{ + Host: "example.com", + Username: "user", + Password: "pass", + }, + addr: "redis://user@example.com:6379", + }, + { + name: "redis-custom-database", + conf: &Config{ + Host: "example.com", + Database: 23, + }, + addr: "redis://example.com:6379/23", + }, + { + name: "redis-tls", + conf: &Config{ + Host: "example.com", + TlsOptions: config.TLS{Enable: true}, + }, + addr: "redis+tls://example.com:6379", + }, + { + name: "redis-with-everything", + conf: &Config{ + Host: "example.com", + Port: 6380, + Username: "user", + Password: "pass", + Database: 23, + TlsOptions: config.TLS{Enable: true}, + }, + addr: "redis+tls://user@example.com:6380/23", + }, + { + name: "redis-unix-domain-socket", + conf: &Config{ + Host: "/var/empty/redis.sock", + }, + addr: "redis:///var/empty/redis.sock", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + redis, err := NewClientFromConfig( + test.conf, + logging.NewLogger(zaptest.NewLogger(t).Sugar(), 0)) + require.NoError(t, err) + require.Equal(t, test.addr, redis.GetAddr()) + }) + } +}