Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[metricbeat][mysql] Add SSL support #37997

Merged
merged 17 commits into from
Mar 15, 2024
Merged
1 change: 1 addition & 0 deletions CHANGELOG.next.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ Setting environmental variable ELASTIC_NETINFO:false in Elastic Agent pod will d
- Add a `/inputs/` route to the HTTP monitoring endpoint that exposes metrics for each metricset instance. {pull}36971[36971]
- Add linux IO metrics to system/process {pull}37213[37213]
- Add new memory/cgroup metrics to Kibana module {pull}37232[37232]
- Add SSL support to mysql module {pull}37997[37997]


*Metricbeat*
Expand Down
13 changes: 12 additions & 1 deletion metricbeat/docs/modules/mysql.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,18 @@ metricbeat.modules:

# By setting raw to true, all raw fields from the status metricset will be added to the event.
#raw: false
----

# Optional SSL/TLS. By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"----

[float]
=== Metricsets
Expand Down
11 changes: 11 additions & 0 deletions metricbeat/metricbeat.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,17 @@ metricbeat.modules:
# By setting raw to true, all raw fields from the status metricset will be added to the event.
#raw: false

# Optional SSL/TLS. By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"
#--------------------------------- NATS Module ---------------------------------
- module: nats
metricsets:
Expand Down
12 changes: 12 additions & 0 deletions metricbeat/module/mysql/_meta/config.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@

# By setting raw to true, all raw fields from the status metricset will be added to the event.
#raw: false

# Optional SSL/TLS. By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pasted text should've had a new line character at the end. The missing character led to the broken documentation. See the fixing PR #38367

12 changes: 12 additions & 0 deletions metricbeat/module/mysql/_meta/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,15 @@

# Password of hosts. Empty by default.
#password: secret

# Optional SSL/TLS. By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"
32 changes: 32 additions & 0 deletions metricbeat/module/mysql/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

package mysql

import (
"crypto/tls"

"github.com/elastic/elastic-agent-libs/transport/tlscommon"
)

type Config struct {
Hosts []string `config:"hosts" validate:"required"`
Username string `config:"username"`
Password string `config:"password"`
TLS *tlscommon.Config `config:"ssl"`
TLSConfig *tls.Config
}
12 changes: 9 additions & 3 deletions metricbeat/module/mysql/galera_status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,23 +42,29 @@ func init() {

// MetricSet for fetching Galera-MySQL server status
type MetricSet struct {
mb.BaseMetricSet
*mysql.Metricset
db *sql.DB
}

// New create a new instance of the MetricSet
// Loads query_mode config setting from the config file
func New(base mb.BaseMetricSet) (mb.MetricSet, error) {
cfgwarn.Experimental("The galera_status metricset is experimental.")
return &MetricSet{BaseMetricSet: base}, nil

ms, err := mysql.NewMetricset(base)
if err != nil {
return nil, err
}

return &MetricSet{Metricset: ms, db: nil}, nil
}

// Fetch methods implements the data gathering and data conversion to the right format
// It returns the event which is then forward to the output.
func (m *MetricSet) Fetch(reporter mb.ReporterV2) error {
if m.db == nil {
var err error
m.db, err = mysql.NewDB(m.HostData().URI)
m.db, err = mysql.NewDB(m.HostData().URI, m.Metricset.Config.TLSConfig)
if err != nil {
return fmt.Errorf("Galera-status fetch failed: %w", err)
}
Expand Down
53 changes: 46 additions & 7 deletions metricbeat/module/mysql/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,18 @@ Package mysql is Metricbeat module for MySQL server.
package mysql

import (
"crypto/tls"
"database/sql"
"fmt"

"github.com/elastic/beats/v7/metricbeat/mb"
"github.com/elastic/elastic-agent-libs/transport/tlscommon"

"github.com/go-sql-driver/mysql"
)

const TLSConfigKey = "custom"

func init() {
// Register the ModuleFactory function for the "mysql" module.
if err := mb.Registry.AddModule("mysql", NewModule); err != nil {
Expand All @@ -38,26 +42,49 @@ func init() {

func NewModule(base mb.BaseModule) (mb.Module, error) {
// Validate that at least one host has been specified.
config := struct {
Hosts []string `config:"hosts" validate:"required"`
}{}
if err := base.UnpackConfig(&config); err != nil {
var c Config
if err := base.UnpackConfig(&c); err != nil {
return nil, err
}

return &base, nil
}

type Metricset struct {
mb.BaseMetricSet
Config Config
}

func NewMetricset(base mb.BaseMetricSet) (*Metricset, error) {
var c Config
if err := base.Module().UnpackConfig(&c); err != nil {
return nil, fmt.Errorf("could not read config: %w", err)
}

if c.TLS.IsEnabled() {
tlsConfig, err := tlscommon.LoadTLSConfig(c.TLS)
if err != nil {
return nil, fmt.Errorf("could not load provided TLS configuration: %w", err)
}

c.TLSConfig = tlsConfig.ToConfig()
}

return &Metricset{Config: c, BaseMetricSet: base}, nil
}

// ParseDSN creates a DSN (data source name) string by parsing the host.
// It validates the resulting DSN and returns an error if the DSN is invalid.
//
// Format: [username[:password]@][protocol[(address)]]/
// Example: root:test@tcp(127.0.0.1:3306)/
func ParseDSN(mod mb.Module, host string) (mb.HostData, error) {
c := struct {
Username string `config:"username"`
Password string `config:"password"`
Username string `config:"username"`
Password string `config:"password"`
TLS *tlscommon.Config `config:"ssl"`
}{}

if err := mod.UnpackConfig(&c); err != nil {
return mb.HostData{}, err
}
Expand Down Expand Up @@ -86,6 +113,10 @@ func ParseDSN(mod mb.Module, host string) (mb.HostData, error) {
noCredentialsConfig.User = ""
noCredentialsConfig.Passwd = ""

if c.TLS.IsEnabled() {
config.TLSConfig = TLSConfigKey
}
shmsr marked this conversation as resolved.
Show resolved Hide resolved

return mb.HostData{
URI: config.FormatDSN(),
SanitizedURI: noCredentialsConfig.FormatDSN(),
Expand All @@ -99,10 +130,18 @@ func ParseDSN(mod mb.Module, host string) (mb.HostData, error) {
// must be valid, otherwise an error will be returned.
//
// DSN Format: [username[:password]@][protocol[(address)]]/
func NewDB(dsn string) (*sql.DB, error) {
func NewDB(dsn string, tlsConfig *tls.Config) (*sql.DB, error) {
if tlsConfig != nil {
err := mysql.RegisterTLSConfig(TLSConfigKey, tlsConfig)
if err != nil {
return nil, fmt.Errorf("registering custom tls config failed: %w", err)
}
}

db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("sql open failed: %w", err)
}

return db, nil
}
2 changes: 1 addition & 1 deletion metricbeat/module/mysql/mysql_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import (
func TestNewDB(t *testing.T) {
service := compose.EnsureUp(t, "mysql")

db, err := NewDB(GetMySQLEnvDSN(service.Host()))
db, err := NewDB(GetMySQLEnvDSN(service.Host()), nil)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we introduce a test that validates the added SSL support?

assert.NoError(t, err)

err = db.Ping()
Expand Down
27 changes: 24 additions & 3 deletions metricbeat/module/mysql/query/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,17 @@ package query

import (
"context"
"crypto/tls"
"fmt"

mysqlDriver "github.com/go-sql-driver/mysql"

"github.com/elastic/beats/v7/libbeat/common/cfgwarn"
"github.com/elastic/beats/v7/metricbeat/helper/sql"
"github.com/elastic/beats/v7/metricbeat/mb"
"github.com/elastic/beats/v7/metricbeat/module/mysql"
"github.com/elastic/elastic-agent-libs/mapstr"
"github.com/elastic/elastic-agent-libs/transport/tlscommon"
)

func init() {
Expand All @@ -57,8 +61,10 @@ type MetricSet struct {
mb.BaseMetricSet
db *sql.DbClient
Config struct {
Queries []query `config:"queries" validate:"nonzero,required"`
Namespace string `config:"namespace" validate:"nonzero,required"`
Queries []query `config:"queries" validate:"nonzero,required"`
Namespace string `config:"namespace" validate:"nonzero,required"`
TLS *tlscommon.Config `config:"ssl"`
TLSConfig *tls.Config
}
}

Expand All @@ -72,16 +78,31 @@ func New(base mb.BaseMetricSet) (mb.MetricSet, error) {
return nil, err
}

if b.Config.TLS.IsEnabled() {
tlsConfig, err := tlscommon.LoadTLSConfig(b.Config.TLS)
if err != nil {
return nil, fmt.Errorf("could not load provided TLS configuration: %w", err)
}

b.Config.TLSConfig = tlsConfig.ToConfig()
}

return b, nil
}

// Fetch fetches status messages from a mysql host.
func (m *MetricSet) Fetch(ctx context.Context, reporter mb.ReporterV2) error {
if m.db == nil {
if m.Config.TLSConfig != nil {
err := mysqlDriver.RegisterTLSConfig(mysql.TLSConfigKey, m.Config.TLSConfig)
if err != nil {
return fmt.Errorf("registering custom tls config failed: %w", err)
}
}
var err error
m.db, err = sql.NewDBClient("mysql", m.HostData().URI, m.Logger())
if err != nil {
return fmt.Errorf("mysql-status fetch failed: %w", err)
return fmt.Errorf("mysql-query fetch failed: %w", err)
}
}

Expand Down
11 changes: 8 additions & 3 deletions metricbeat/module/mysql/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,20 +40,25 @@ func init() {

// MetricSet for fetching MySQL server status.
type MetricSet struct {
mb.BaseMetricSet
*mysql.Metricset
db *sql.DB
}

// New creates and returns a new MetricSet instance.
func New(base mb.BaseMetricSet) (mb.MetricSet, error) {
return &MetricSet{BaseMetricSet: base}, nil
ms, err := mysql.NewMetricset(base)
if err != nil {
return nil, err
}

return &MetricSet{Metricset: ms, db: nil}, nil
}

// Fetch fetches status messages from a mysql host.
func (m *MetricSet) Fetch(reporter mb.ReporterV2) error {
if m.db == nil {
var err error
m.db, err = mysql.NewDB(m.HostData().URI)
m.db, err = mysql.NewDB(m.HostData().URI, m.Metricset.Config.TLSConfig)
if err != nil {
return fmt.Errorf("mysql-status fetch failed: %w", err)
}
Expand Down
12 changes: 12 additions & 0 deletions metricbeat/modules.d/mysql.yml.disabled
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,15 @@

# Password of hosts. Empty by default.
#password: secret

# Optional SSL/TLS. By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"
11 changes: 11 additions & 0 deletions x-pack/metricbeat/metricbeat.reference.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1172,6 +1172,17 @@ metricbeat.modules:
# By setting raw to true, all raw fields from the status metricset will be added to the event.
#raw: false

# Optional SSL/TLS. By default is false.
#ssl.enabled: true

# List of root certificates for SSL/TLS server verification
#ssl.certificate_authorities: ["/etc/pki/root/ca.crt"]

# Certificate for SSL/TLS client authentication
#ssl.certificate: "/etc/pki/client/cert.crt"

# Client certificate key file
#ssl.key: "/etc/pki/client/cert.key"
#--------------------------------- NATS Module ---------------------------------
- module: nats
metricsets:
Expand Down
Loading