diff --git a/internal/component/database_observability/mysql/component.go b/internal/component/database_observability/mysql/component.go index d446fc47e70..24252d55123 100644 --- a/internal/component/database_observability/mysql/component.go +++ b/internal/component/database_observability/mysql/component.go @@ -5,6 +5,7 @@ import ( "crypto/sha256" "database/sql" "fmt" + "log/slog" "net/http" "path" "regexp" @@ -17,6 +18,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/model" + mysqld_collector "github.com/prometheus/mysqld_exporter/collector" "go.uber.org/atomic" "github.com/grafana/alloy/internal/component" @@ -25,9 +27,12 @@ import ( "github.com/grafana/alloy/internal/component/database_observability" "github.com/grafana/alloy/internal/component/database_observability/mysql/collector" "github.com/grafana/alloy/internal/component/discovery" + exporter_mysql "github.com/grafana/alloy/internal/component/prometheus/exporter/mysql" "github.com/grafana/alloy/internal/featuregate" + "github.com/grafana/alloy/internal/runtime/logging" "github.com/grafana/alloy/internal/runtime/logging/level" http_service "github.com/grafana/alloy/internal/service/http" + "github.com/grafana/alloy/internal/static/integrations/mysqld_exporter" "github.com/grafana/alloy/syntax" "github.com/grafana/alloy/syntax/alloytypes" ) @@ -63,15 +68,16 @@ type Arguments struct { ExcludeSchemas []string `alloy:"exclude_schemas,attr,optional"` AllowUpdatePerfSchemaSettings bool `alloy:"allow_update_performance_schema_settings,attr,optional"` - CloudProvider *CloudProvider `alloy:"cloud_provider,block,optional"` - SetupConsumersArguments SetupConsumersArguments `alloy:"setup_consumers,block,optional"` - SetupActorsArguments SetupActorsArguments `alloy:"setup_actors,block,optional"` - QueryDetailsArguments QueryDetailsArguments `alloy:"query_details,block,optional"` - SchemaDetailsArguments SchemaDetailsArguments `alloy:"schema_details,block,optional"` - ExplainPlansArguments ExplainPlansArguments `alloy:"explain_plans,block,optional"` - LocksArguments LocksArguments `alloy:"locks,block,optional"` - QuerySamplesArguments QuerySamplesArguments `alloy:"query_samples,block,optional"` - HealthCheckArguments HealthCheckArguments `alloy:"health_check,block,optional"` + CloudProvider *CloudProvider `alloy:"cloud_provider,block,optional"` + SetupConsumersArguments SetupConsumersArguments `alloy:"setup_consumers,block,optional"` + SetupActorsArguments SetupActorsArguments `alloy:"setup_actors,block,optional"` + QueryDetailsArguments QueryDetailsArguments `alloy:"query_details,block,optional"` + SchemaDetailsArguments SchemaDetailsArguments `alloy:"schema_details,block,optional"` + ExplainPlansArguments ExplainPlansArguments `alloy:"explain_plans,block,optional"` + LocksArguments LocksArguments `alloy:"locks,block,optional"` + QuerySamplesArguments QuerySamplesArguments `alloy:"query_samples,block,optional"` + HealthCheckArguments HealthCheckArguments `alloy:"health_check,block,optional"` + PrometheusExporter *PrometheusExporterArguments `alloy:"prometheus_exporter,block,optional"` } type CloudProvider struct { @@ -132,6 +138,23 @@ type HealthCheckArguments struct { CollectInterval time.Duration `alloy:"collect_interval,attr,optional"` } +// PrometheusExporterArguments configures the embedded mysqld_exporter scrapers. +// When this block is present, mysqld_exporter metrics are served alongside the +// component's own metrics at the same /metrics endpoint. +// +// It is a distinct type (not an embedded struct) because the Alloy syntax +// system does not support anonymous/embedded fields. +type PrometheusExporterArguments exporter_mysql.Arguments + +func (a *PrometheusExporterArguments) SetToDefault() { + *a = PrometheusExporterArguments(exporter_mysql.DefaultArguments) +} + +func (a *PrometheusExporterArguments) Validate() error { + args := exporter_mysql.Arguments(*a) + return args.Validate() +} + var DefaultArguments = Arguments{ ExcludeSchemas: []string{}, AllowUpdatePerfSchemaSettings: false, @@ -188,6 +211,9 @@ func (a *Arguments) Validate() error { if err != nil { return err } + if a.PrometheusExporter != nil && len(a.Targets) > 0 { + return fmt.Errorf("prometheus_exporter and targets are mutually exclusive: use prometheus_exporter to embed the exporter, or targets to scrape an external one") + } return nil } @@ -209,18 +235,19 @@ type Collector interface { } type Component struct { - opts component.Options - args Arguments - mut sync.RWMutex - receivers []loki.LogsReceiver - handler loki.LogsReceiver - registry *prometheus.Registry - baseTarget discovery.Target - collectors []Collector - instanceKey string - dbConnection *sql.DB - healthErr *atomic.String - openSQL func(driverName, dataSourceName string) (*sql.DB, error) + opts component.Options + args Arguments + mut sync.RWMutex + receivers []loki.LogsReceiver + handler loki.LogsReceiver + registry *prometheus.Registry + baseTarget discovery.Target + collectors []Collector + instanceKey string + dbConnection *sql.DB + healthErr *atomic.String + openSQL func(driverName, dataSourceName string) (*sql.DB, error) + exporterCollector prometheus.Collector } func New(opts component.Options, args Arguments) (*Component, error) { @@ -425,6 +452,26 @@ func (c *Component) connectAndStartCollectors(ctx context.Context) error { cp = cloudProvider } + if c.exporterCollector != nil { + c.registry.Unregister(c.exporterCollector) + c.exporterCollector = nil + } + + if c.args.PrometheusExporter != nil { + exporterArgs := exporter_mysql.Arguments(*c.args.PrometheusExporter) + exporterCfg := exporterArgs.Convert() + scrapers := mysqld_exporter.GetScrapers(exporterCfg) + slogLogger := slog.New(logging.NewSlogGoKitHandler(c.opts.Logger)) + exporter := mysqld_collector.New(context.Background(), string(c.args.DataSourceName), scrapers, slogLogger, mysqld_collector.Config{ + LockTimeout: exporterCfg.LockWaitTimeout, + SlowLogFilter: exporterCfg.LogSlowFilter, + }) + if err := c.registry.Register(exporter); err != nil { + return fmt.Errorf("failed to register prometheus_exporter collector: %w", err) + } + c.exporterCollector = exporter + } + c.args.Targets = append([]discovery.Target{c.baseTarget}, c.args.Targets...) targets := make([]discovery.Target, 0, len(c.args.Targets)+1) for _, t := range c.args.Targets { diff --git a/internal/component/database_observability/mysql/component_test.go b/internal/component/database_observability/mysql/component_test.go index 90d05e11cc5..5e860968ea6 100644 --- a/internal/component/database_observability/mysql/component_test.go +++ b/internal/component/database_observability/mysql/component_test.go @@ -21,6 +21,7 @@ import ( "github.com/grafana/alloy/internal/component/database_observability" "github.com/grafana/alloy/internal/component/database_observability/mysql/collector" "github.com/grafana/alloy/internal/component/discovery" + exporter_mysql "github.com/grafana/alloy/internal/component/prometheus/exporter/mysql" http_service "github.com/grafana/alloy/internal/service/http" "github.com/grafana/alloy/syntax" "github.com/grafana/alloy/syntax/alloytypes" @@ -557,3 +558,62 @@ func TestMySQL_Reconnection(t *testing.T) { } }) } + +func Test_PrometheusExporterBlock(t *testing.T) { + t.Run("absent when not specified", func(t *testing.T) { + cfg := ` + data_source_name = "" + forward_to = [] + targets = [] + ` + var args Arguments + err := syntax.Unmarshal([]byte(cfg), &args) + require.NoError(t, err) + assert.Nil(t, args.PrometheusExporter) + }) + + t.Run("present with defaults when empty block", func(t *testing.T) { + cfg := ` + data_source_name = "" + forward_to = [] + targets = [] + prometheus_exporter {} + ` + var args Arguments + err := syntax.Unmarshal([]byte(cfg), &args) + require.NoError(t, err) + require.NotNil(t, args.PrometheusExporter) + exporterArgs := exporter_mysql.Arguments(*args.PrometheusExporter) + assert.Equal(t, 2, exporterArgs.LockWaitTimeout) // default value + }) + + t.Run("present with defaults when empty block", func(t *testing.T) { + cfg := ` + data_source_name = "" + forward_to = [] + targets = [] + prometheus_exporter { + enable_collectors = ["perf_schema.eventsstatements", "perf_schema.eventswaits"] + } + ` + var args Arguments + err := syntax.Unmarshal([]byte(cfg), &args) + require.NoError(t, err) + require.NotNil(t, args.PrometheusExporter) + exporterArgs := exporter_mysql.Arguments(*args.PrometheusExporter) + assert.Equal(t, 2, exporterArgs.LockWaitTimeout) // default value + assert.Equal(t, []string{"perf_schema.eventsstatements", "perf_schema.eventswaits"}, args.PrometheusExporter.EnableCollectors) + }) + + t.Run("error when both prometheus_exporter and targets are set", func(t *testing.T) { + cfg := ` + data_source_name = "" + forward_to = [] + targets = [{"__address__" = "localhost:9104"}] + prometheus_exporter {} + ` + var args Arguments + err := syntax.Unmarshal([]byte(cfg), &args) + require.ErrorContains(t, err, "prometheus_exporter and targets are mutually exclusive") + }) +}