Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions aws/iam_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,15 @@ type AuthToken struct {
}

func NewAuthToken(username, hostname string, cfg aws.Config) AuthToken {
// RDS auth tokens require the :3306 suffix
return NewAuthTokenWithDefaultPort(username, hostname, "3306", cfg)
}

// NewAuthTokenWithDefaultPort constructs an RDS authentication token signer,
// adding defaultPort when hostname does not already include a port.
func NewAuthTokenWithDefaultPort(username, hostname, defaultPort string, cfg aws.Config) AuthToken {
// RDS auth tokens require the database port in the signed endpoint.
if !portRe.MatchString(hostname) {
hostname += ":3306"
hostname += ":" + defaultPort
}

return AuthToken{
Expand Down
6 changes: 3 additions & 3 deletions blip.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ const (
EVENT
)

// Metrics are metrics collected for one plan level, from one MySQL instance.
// Metrics are metrics collected for one plan level, from one monitor.
type Metrics struct {
Begin time.Time // when collection started
End time.Time // when collection completed
MonitorId string // ID of monitor (MySQL)
MonitorId string // ID of monitor
Plan string // plan name
Level string // level name
Interval uint // interval number
Expand Down Expand Up @@ -99,7 +99,7 @@ type SinkFactoryArgs struct {
Tags map[string]string // config.monitor.tags
}

// DbCredentials are MySQL credentials parsed or loaded for a connection.
// DbCredentials are database credentials parsed or loaded for a connection.
type DbCredentials struct {
Username string
Password string
Expand Down
3 changes: 2 additions & 1 deletion collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ type CollectorFactoryWithDBProvider interface {
//
// The domain argument allows one factory to serve domains with different
// compatibility, such as MySQL collectors and database-neutral cloud metrics.
// Implementations must return at least one database type.
// Implementations must return at least one database type. Return
// DatabaseTypeAny by itself for a database-neutral domain.
type CollectorFactoryDatabaseTypes interface {
CollectorFactory
DatabaseTypes(domain string) []DatabaseType
Expand Down
152 changes: 96 additions & 56 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -403,7 +403,8 @@ type ConfigMonitor struct {
// Empty values retain Blip's historical MySQL behavior.
DatabaseType DatabaseType `yaml:"database-type,omitempty"`

// ConfigMySQL:
// Shared connection identity and credentials. Socket and MyCnf are specific
// to Blip's built-in MySQL connection factory.
Socket string `yaml:"socket,omitempty"`
Hostname string `yaml:"hostname,omitempty"`
MyCnf string `yaml:"mycnf,omitempty"`
Expand All @@ -422,9 +423,12 @@ type ConfigMonitor struct {
Heartbeat ConfigHeartbeat `yaml:"heartbeat,omitempty"`
Plans ConfigPlans `yaml:"plans,omitempty"`
Plan string `yaml:"plan,omitempty"`
Postgres ConfigPostgres `yaml:"postgres,omitempty"`
Sinks ConfigSinks `yaml:"sinks,omitempty"`
TLS ConfigTLS `yaml:"tls,omitempty"`
// DatabaseConfig is opaque configuration owned by the external module
// selected by DatabaseType. MySQL continues to use the historical monitor
// fields above and rejects this section.
DatabaseConfig ConfigDatabase `yaml:"database-config,omitempty"`
Sinks ConfigSinks `yaml:"sinks,omitempty"`
TLS ConfigTLS `yaml:"tls,omitempty"`

Meta map[string]string `yaml:"meta,omitempty"`
}
Expand All @@ -434,13 +438,6 @@ const (
DEFAULT_MONITOR_TIMEOUT_CONNECT = "10s"
)

type DatabaseType string

const (
DatabaseTypeMySQL DatabaseType = "mysql"
DatabaseTypePostgres DatabaseType = "postgres"
)

func DefaultConfigMonitor() ConfigMonitor {
return ConfigMonitor{
Username: DEFAULT_MONITOR_USERNAME,
Expand All @@ -459,33 +456,55 @@ func DefaultConfigMonitor() ConfigMonitor {
}

// EffectiveDatabaseType returns the configured database type. An omitted
// value retains Blip's historical MySQL behavior. Environment interpolation is
// resolved here because monitor defaults are applied before the normal
// interpolation pass.
// value retains Blip's historical MySQL behavior. Direct environment-variable
// interpolation is resolved before defaults are selected; database type is a
// structural discriminator and does not support monitor-field interpolation.
func (c ConfigMonitor) EffectiveDatabaseType() DatabaseType {
databaseType := DatabaseType(interpolateEnv(string(c.DatabaseType)))
databaseType := interpolateEnv(string(c.DatabaseType))
if databaseType == "" {
return DatabaseTypeMySQL
}
return databaseType
return DatabaseType(databaseType)
}

func (c ConfigMonitor) Validate() error {
switch c.EffectiveDatabaseType() {
case DatabaseTypeMySQL:
if c.Postgres.Set() {
return fmt.Errorf("config.monitor.postgres requires database-type %q", DatabaseTypePostgres)
databaseType := c.EffectiveDatabaseType()
if databaseType == DatabaseTypeMySQL {
if len(c.DatabaseConfig) > 0 {
return fmt.Errorf("config.monitor.database-config requires an external database type")
}
case DatabaseTypePostgres:
if c.Socket != "" {
return fmt.Errorf("config.monitor.socket is only supported for database-type %q", DatabaseTypeMySQL)
}
if c.MyCnf != "" {
return fmt.Errorf("config.monitor.mycnf is only supported for database-type %q", DatabaseTypeMySQL)
}
return c.Postgres.Validate()
default:
return fmt.Errorf("config.monitor.database-type: invalid database type %q", c.DatabaseType)
return nil
}
if databaseType == DatabaseTypeAny {
return fmt.Errorf("config.monitor.database-type: %q is reserved for database-neutral collectors", databaseType)
}
if !ValidDatabaseType(databaseType) {
return fmt.Errorf("config.monitor.database-type: invalid database type %q", databaseType)
}
if c.Socket != "" {
return fmt.Errorf("config.monitor.socket is only supported for database-type %q", DatabaseTypeMySQL)
}
if c.MyCnf != "" {
return fmt.Errorf("config.monitor.mycnf is only supported for database-type %q", DatabaseTypeMySQL)
}
if c.Heartbeat.set() {
return fmt.Errorf("config.monitor.heartbeat is only supported for database-type %q", DatabaseTypeMySQL)
}
if c.Plans.Change.set() {
return fmt.Errorf("config.monitor.plans.change is only supported for database-type %q", DatabaseTypeMySQL)
}
if c.Plans.Table != "" {
return fmt.Errorf("config.monitor.plans.table is only supported for database-type %q", DatabaseTypeMySQL)
}
if c.Exporter.set() {
return fmt.Errorf("config.monitor.exporter is only supported for database-type %q", DatabaseTypeMySQL)
}
module, ok := registeredDatabaseModule(databaseType)
if !ok {
return fmt.Errorf("config.monitor.database-type: database module %q is not registered", databaseType)
}
if err := module.ValidateConfig(c); err != nil {
return fmt.Errorf("config.monitor.database-config for %q: %w", databaseType, err)
}
return nil
}
Expand All @@ -497,13 +516,17 @@ func (c ConfigMonitor) Redacted() ConfigMonitor {

func (c ConfigMonitor) redacted(seen map[*ConfigMonitor]*ConfigMonitor) ConfigMonitor {
c.Password = redactPassword(c.Password)
// External module configuration is opaque to Blip, so redact the entire
// section rather than guessing which module-owned fields might be secrets.
c.DatabaseConfig = nil
c.Sinks = c.Sinks.redacted()
c.Plans = c.Plans.redacted(seen)
return c
}

func (c *ConfigMonitor) ApplyDefaults(b Config) {
if c.EffectiveDatabaseType() == DatabaseTypeMySQL {
databaseType := c.EffectiveDatabaseType()
if databaseType == DatabaseTypeMySQL {
if c.Socket == "" {
c.Socket = b.MySQL.Socket
}
Expand Down Expand Up @@ -545,15 +568,16 @@ func (c *ConfigMonitor) ApplyDefaults(b Config) {
c.Sinks = ConfigSinks{}
}
c.AWS.ApplyDefaults(b)
c.Exporter.ApplyDefaults(b)
c.HA.ApplyDefaults(b)
c.Heartbeat.ApplyDefaults(b)
c.Plans.ApplyDefaults(b)
if c.EffectiveDatabaseType() == DatabaseTypePostgres {
postgresDefaults := DefaultConfigPostgres()
postgresDefaults.ConnectTimeout = c.TimeoutConnect
c.Postgres.ApplyDefaults(postgresDefaults)
}
// Exporter emulation, heartbeat writes, and plan state changes are
// MySQL-specific. Do not inherit their global defaults into external database
// monitors; explicit monitor values remain intact so Validate can report the
// unsupported configuration.
if databaseType == DatabaseTypeMySQL {
c.Exporter.ApplyDefaults(b)
c.Heartbeat.ApplyDefaults(b)
}
c.Plans.applyDefaults(b, databaseType == DatabaseTypeMySQL)
c.Sinks.ApplyDefaults(b)
c.TLS.ApplyDefaults(b)
}
Expand All @@ -580,7 +604,7 @@ func (c *ConfigMonitor) InterpolateEnvVars() {
c.Heartbeat.InterpolateEnvVars()
c.Plans.InterpolateEnvVars()
c.Plan = interpolateEnv(c.Plan)
c.Postgres.InterpolateEnvVars()
c.DatabaseConfig.interpolate(interpolateEnv)
c.Sinks.InterpolateEnvVars()
c.TLS.InterpolateEnvVars()
}
Expand All @@ -606,7 +630,7 @@ func (c *ConfigMonitor) InterpolateMonitor() {
c.Heartbeat.InterpolateMonitor(c)
c.Plans.InterpolateMonitor(c)
c.Plan = c.interpolateMon(c.Plan)
c.Postgres.InterpolateMonitor(c)
c.DatabaseConfig.interpolate(c.interpolateMon)
c.Sinks.InterpolateMonitor(c)
c.TLS.InterpolateMonitor(c)
}
Expand Down Expand Up @@ -658,20 +682,6 @@ func (c *ConfigMonitor) fieldValue(f string) string {
return c.PasswordFile
case "timeout-connect":
return c.TimeoutConnect
case "postgres.database":
return c.Postgres.Database
case "postgres.application-name":
return c.Postgres.ApplicationName
case "postgres.ssl-mode":
return c.Postgres.SSLMode
case "postgres.connect-timeout":
return c.Postgres.ConnectTimeout
case "postgres.statement-timeout":
return c.Postgres.StatementTimeout
case "postgres.lock-timeout":
return c.Postgres.LockTimeout
case "postgres.dial-address":
return c.Postgres.DialAddress
default:
return ""
}
Expand Down Expand Up @@ -735,6 +745,10 @@ type ConfigExporter struct {
Plan string `yaml:"plan,omitempty"`
}

func (c ConfigExporter) set() bool {
return c.Mode != "" || c.Plan != "" || len(c.Flags) > 0
}

func DefaultConfigExporter() ConfigExporter {
return ConfigExporter{}
}
Expand Down Expand Up @@ -798,6 +812,10 @@ type ConfigHeartbeat struct {
Table string `yaml:"table,omitempty"`
}

func (c ConfigHeartbeat) set() bool {
return c.Freq != "" || c.SourceId != "" || c.Role != "" || c.Table != ""
}

const (
DEFAULT_HEARTBEAT_TABLE = "blip.heartbeat"
)
Expand Down Expand Up @@ -966,6 +984,15 @@ func DefaultConfigPlans() ConfigPlans {
}

func (c ConfigPlans) Validate() error {
if c.Table == "" || c.Monitor == nil {
return nil
}
if c.Monitor.EffectiveDatabaseType() != DatabaseTypeMySQL {
return fmt.Errorf("config.plans.monitor.database-type: config.plans.table is only supported for database-type %q", DatabaseTypeMySQL)
}
if err := c.Monitor.Validate(); err != nil {
return fmt.Errorf("config.plans.monitor: %w", err)
}
return nil
}

Expand All @@ -986,11 +1013,17 @@ func (c ConfigPlans) redacted(seen map[*ConfigMonitor]*ConfigMonitor) ConfigPlan
}

func (c *ConfigPlans) ApplyDefaults(b Config) {
c.applyDefaults(b, true)
}

func (c *ConfigPlans) applyDefaults(b Config, applyChange bool) {
if len(c.Files) == 0 && len(b.Plans.Files) > 0 {
c.Files = make([]string, len(b.Plans.Files))
copy(c.Files, b.Plans.Files)
}
c.Change.ApplyDefaults(b)
if applyChange {
c.Change.ApplyDefaults(b)
}
}

func (c *ConfigPlans) InterpolateEnvVars() {
Expand Down Expand Up @@ -1079,6 +1112,13 @@ func (c ConfigPlanChange) Enabled() bool {
c.Active.Plan != ""
}

func (c ConfigPlanChange) set() bool {
return c.Offline.After != "" || c.Offline.Plan != "" ||
c.Standby.After != "" || c.Standby.Plan != "" ||
c.ReadOnly.After != "" || c.ReadOnly.Plan != "" ||
c.Active.After != "" || c.Active.Plan != ""
}

// --------------------------------------------------------------------------

type ConfigSinks map[string]map[string]string
Expand Down
Loading