Skip to content
Draft
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
9 changes: 5 additions & 4 deletions docs/content/metrics/domains/tls/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ This domain reports a single derived metric, `enabled`, that should be monitored
|**Metric Type**|bool|
|**Value Units**||

True (1) if `have_ssl = YES`, else false (0).
True (1) if the main MySQL connection interface supports encrypted connections, else false (0).
Metrics sinks that don't support bool report this metric as a gauge.

{{< hint type=note >}}
`have_ssl` is deprecated as of MySQL 8.0.26.
This domain does not currently support the [`tls_channel_status` table](https://dev.mysql.com/doc/refman/8.0/en/performance-schema-tls-channel-status-table.html) but there is an [open issue](https://github.com/cashapp/blip/issues/133) to fix this.
On MySQL 8.0.21 and newer, Blip reads the `mysql_main` channel's `Enabled` property from the [`tls_channel_status` table](https://dev.mysql.com/doc/refman/8.0/en/performance-schema-tls-channel-status-table.html).
For older MySQL versions and compatible distributions that do not have this table, or when the table cannot be read, Blip falls back to `have_ssl` when that variable is available.
{{< /hint >}}

## Options
Expand All @@ -46,7 +46,8 @@ None.

## MySQL Config

None.
On servers without the legacy `have_ssl` variable, including MySQL 8.4 and newer, the Blip database user needs `SELECT` access to `performance_schema.tls_channel_status`.
Earlier versions can use the legacy `have_ssl` fallback when this table is unavailable or inaccessible.

## Changelog

Expand Down
81 changes: 72 additions & 9 deletions metrics/tls/tls.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,42 @@ package tls
import (
"context"
"database/sql"
"errors"
"fmt"

myerr "github.com/go-mysql/errors"

"github.com/cashapp/blip/v2"
"github.com/cashapp/blip/v2/sqlutil"
)

const (
DOMAIN = "tls"

tlsChannelStatusQuery = "SELECT VALUE FROM performance_schema.tls_channel_status WHERE CHANNEL = ? AND PROPERTY = ?"
haveSSLQuery = "SELECT @@have_ssl"
)

// have_ssl is deprecated as of MySQL 8.0.26, so:
// @todo https://dev.mysql.com/doc/refman/8.0/en/performance-schema-tls-channel-status-table.html
type rowScanner interface {
Scan(dest ...interface{}) error
}

type queryRowFunc func(context.Context, string, ...interface{}) rowScanner

// TLS collects metrics for the tls domain.
type TLS struct {
db *sql.DB
queryRow queryRowFunc
query string
queryArgs []interface{}
}

var _ blip.Collector = &TLS{}

func NewTLS(db *sql.DB) *TLS {
return &TLS{
db: db,
queryRow: func(ctx context.Context, query string, args ...interface{}) rowScanner {
return db.QueryRowContext(ctx, query, args...)
},
}
}

Expand All @@ -44,7 +57,7 @@ func (c *TLS) Help() blip.CollectorHelp {
{
Name: "enabled",
Type: blip.BOOL,
Desc: "True (1) if have_ssl = YES, else false (0)",
Desc: "True (1) if the main MySQL connection interface supports encrypted connections, else false (0)",
},
},
}
Expand All @@ -54,12 +67,14 @@ func (c *TLS) Prepare(ctx context.Context, plan blip.Plan) (func(), error) {
// This domain only collects 1 metric (and there are no options),
// so we don't have to prepare anything per-level, just check that
// the only metric is specified correctly.
configured := false
LEVEL:
for _, level := range plan.Levels {
dom, ok := level.Collect[DOMAIN]
if !ok {
continue LEVEL // not collected at this level
}
configured = true
if len(dom.Metrics) == 0 {
return nil, fmt.Errorf("metric 'enabled' not specified; metrics to collect must be listed under 'metrics:' for each domain")
}
Expand All @@ -70,16 +85,64 @@ LEVEL:
return nil, fmt.Errorf("invalid metric: %s; this domain collects only 1 metric: enabled", dom.Metrics[0])
}
}

if !configured {
return nil, nil
}

// MySQL 8.0.21 added tls_channel_status, and MySQL 8.4 removed
// @@have_ssl. Probe the replacement table instead of relying on a version
// string because Blip supports multiple MySQL-compatible distributions.
var enabled string
err := c.queryRow(ctx, tlsChannelStatusQuery, "mysql_main", "Enabled").Scan(&enabled)
if err == nil {
c.query = tlsChannelStatusQuery
c.queryArgs = []interface{}{"mysql_main", "Enabled"}
return nil, nil
}

// A disabled Performance Schema returns no row, and older MySQL versions
// and compatible distributions do not have the table.
// Also preserve the old privilege behavior on MySQL 8.0: @@have_ssl does
// not require SELECT on Performance Schema, so it remains a valid fallback
// when the monitoring user cannot read tls_channel_status.
tlsChannelStatusErr := err
if !errors.Is(err, sql.ErrNoRows) {
switch myerr.MySQLErrorCode(err) {
case 1142, 1146: // SELECT denied, or table does not exist
// Try the legacy source below.
default:
return nil, fmt.Errorf("cannot read TLS status from performance_schema.tls_channel_status: %w", err)
}
}

err = c.queryRow(ctx, haveSSLQuery).Scan(&enabled)
if err != nil {
if myerr.MySQLErrorCode(tlsChannelStatusErr) == 1142 {
return nil, fmt.Errorf("cannot read TLS status: grant SELECT on performance_schema.tls_channel_status (%v); legacy @@have_ssl is unavailable (%v)", tlsChannelStatusErr, err)
}
return nil, fmt.Errorf("cannot read legacy TLS status from @@have_ssl: %w", err)
}
c.query = haveSSLQuery
c.queryArgs = nil

return nil, nil
}

func (c *TLS) Collect(ctx context.Context, levelName string) ([]blip.MetricValue, error) {
var haveSSL string
err := c.db.QueryRowContext(ctx, "SELECT @@have_ssl").Scan(&haveSSL)
if c.query == "" {
return nil, fmt.Errorf("tls.enabled failed: collector is not prepared")
}

var tlsEnabled string
err := c.queryRow(ctx, c.query, c.queryArgs...).Scan(&tlsEnabled)
if err != nil {
return nil, fmt.Errorf("tls.enabled failed: %s", err)
return nil, fmt.Errorf("tls.enabled failed: %w", err)
}
enabled, ok := sqlutil.Float64(tlsEnabled)
if !ok {
return nil, fmt.Errorf("tls.enabled failed: cannot convert TLS status %q to a boolean", tlsEnabled)
}
enabled, _ := sqlutil.Float64(haveSSL) // MySQL string value -> 1 or 0
metrics := []blip.MetricValue{
{
Name: "enabled",
Expand Down
Loading
Loading