Skip to content

Commit

Permalink
Add support for collecting metrics from sys.user_summary (#628)
Browse files Browse the repository at this point in the history
* Add support for collecting metrics from sys.user_summary

Signed-off-by: Eduardo J. Ortega U <5791035+ejortegau@users.noreply.github.com>
Signed-off-by: Eduardo J. Ortega U. <5791035+ejortegau@users.noreply.github.com>
Co-authored-by: Ben Kochie <superq@gmail.com>
  • Loading branch information
ejortegau and SuperQ authored Mar 28, 2023
1 parent 2fabbb3 commit fad2c0c
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 4 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@ collect.engine_innodb_status | 5.1 | C
collect.engine_tokudb_status | 5.6 | Collect from SHOW ENGINE TOKUDB STATUS.
collect.global_status | 5.1 | Collect from SHOW GLOBAL STATUS (Enabled by default)
collect.global_variables | 5.1 | Collect from SHOW GLOBAL VARIABLES (Enabled by default)
collect.heartbeat | 5.1 | Collect from [heartbeat](#heartbeat).
collect.heartbeat.database | 5.1 | Database from where to collect heartbeat data. (default: heartbeat)
collect.heartbeat.table | 5.1 | Table from where to collect heartbeat data. (default: heartbeat)
collect.heartbeat.utc | 5.1 | Use UTC for timestamps of the current server (`pt-heartbeat` is called with `--utc`). (default: false)
collect.info_schema.clientstats | 5.5 | If running with userstat=1, set to true to collect client statistics.
collect.info_schema.innodb_metrics | 5.6 | Collect metrics from information_schema.innodb_metrics.
collect.info_schema.innodb_tablespaces | 5.7 | Collect metrics from information_schema.innodb_sys_tablespaces.
Expand Down Expand Up @@ -128,10 +132,7 @@ collect.perf_schema.replication_group_member_stats | 5.7 | C
collect.perf_schema.replication_applier_status_by_worker | 5.7 | Collect metrics from performance_schema.replication_applier_status_by_worker.
collect.slave_status | 5.1 | Collect from SHOW SLAVE STATUS (Enabled by default)
collect.slave_hosts | 5.1 | Collect from SHOW SLAVE HOSTS
collect.heartbeat | 5.1 | Collect from [heartbeat](#heartbeat).
collect.heartbeat.database | 5.1 | Database from where to collect heartbeat data. (default: heartbeat)
collect.heartbeat.table | 5.1 | Table from where to collect heartbeat data. (default: heartbeat)
collect.heartbeat.utc | 5.1 | Use UTC for timestamps of the current server (`pt-heartbeat` is called with `--utc`). (default: false)
collect.sys.user_summary | 5.7 | Collect metrics from sys.x$user_summary (disabled by default).


### General Flags
Expand Down
16 changes: 16 additions & 0 deletions collector/sys.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2022 The Prometheus Authors.
// Licensed 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 collector

const sysSchema = "sys"
156 changes: 156 additions & 0 deletions collector/sys_user_summary.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Copyright 2022 The Prometheus Authors
// Licensed 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 collector

import (
"context"
"database/sql"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
)

const sysUserSummaryQuery = `
SELECT
user,
statements,
statement_latency,
table_scans,
file_ios,
file_io_latency,
current_connections,
total_connections,
unique_hosts,
current_memory,
total_memory_allocated
FROM
` + sysSchema + `.x$user_summary
`

var (
sysUserSummaryStatements = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "statements_total"),
" The total number of statements for the user",
[]string{"user"}, nil)
sysUserSummaryStatementLatency = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "statement_latency"),
"The total wait time of timed statements for the user",
[]string{"user"}, nil)
sysUserSummaryTableScans = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "table_scans_total"),
"The total number of table scans for the user",
[]string{"user"}, nil)
sysUserSummaryFileIOs = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "file_ios_total"),
"The total number of file I/O events for the user",
[]string{"user"}, nil)
sysUserSummaryFileIOLatency = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "file_io_seconds_total"),
"The total wait time of timed file I/O events for the user",
[]string{"user"}, nil)
sysUserSummaryCurrentConnections = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "current_connections"),
"The current number of connections for the user",
[]string{"user"}, nil)
sysUserSummaryTotalConnections = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "connections_total"),
"The total number of connections for the user",
[]string{"user"}, nil)
sysUserSummaryUniqueHosts = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "unique_hosts_total"),
"The number of distinct hosts from which connections for the user have originated",
[]string{"user"}, nil)
sysUserSummaryCurrentMemory = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "current_memory_bytes"),
"The current amount of allocated memory for the user",
[]string{"user"}, nil)
sysUserSummaryTotalMemoryAllocated = prometheus.NewDesc(
prometheus.BuildFQName(namespace, sysSchema, "memory_allocated_bytes_total"),
"The total amount of allocated memory for the user",
[]string{"user"}, nil)
)

type ScrapeSysUserSummary struct{}

// Name of the Scraper. Should be unique.
func (ScrapeSysUserSummary) Name() string {
return sysSchema + ".user_summary"
}

// Help describes the role of the Scraper.
func (ScrapeSysUserSummary) Help() string {
return "Collect per user metrics from sys.x$user_summary. See https://dev.mysql.com/doc/refman/5.7/en/sys-user-summary.html for details"
}

// Version of MySQL from which scraper is available.
func (ScrapeSysUserSummary) Version() float64 {
return 5.7
}

// Scrape the information from sys.user_summary, creating a metric for each value of each row, labeled with the user
func (ScrapeSysUserSummary) Scrape(ctx context.Context, db *sql.DB, ch chan<- prometheus.Metric, logger log.Logger) error {

userSummaryRows, err := db.QueryContext(ctx, sysUserSummaryQuery)
if err != nil {
return err
}
defer userSummaryRows.Close()

var (
user string
statements uint64
statement_latency uint64
table_scans uint64
file_ios uint64
file_io_latency uint64
current_connections uint64
total_connections uint64
unique_hosts uint64
current_memory uint64
total_memory_allocated uint64
)

for userSummaryRows.Next() {
err = userSummaryRows.Scan(
&user,
&statements,
&statement_latency,
&table_scans,
&file_ios,
&file_io_latency,
&current_connections,
&total_connections,
&unique_hosts,
&current_memory,
&total_memory_allocated,
)
if err != nil {
return err
}

ch <- prometheus.MustNewConstMetric(sysUserSummaryStatements, prometheus.CounterValue, float64(statements), user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryStatementLatency, prometheus.CounterValue, float64(statement_latency)/picoSeconds, user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryTableScans, prometheus.CounterValue, float64(table_scans), user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryFileIOs, prometheus.CounterValue, float64(file_ios), user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryFileIOLatency, prometheus.CounterValue, float64(file_io_latency)/picoSeconds, user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryCurrentConnections, prometheus.GaugeValue, float64(current_connections), user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryTotalConnections, prometheus.CounterValue, float64(total_connections), user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryUniqueHosts, prometheus.CounterValue, float64(unique_hosts), user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryCurrentMemory, prometheus.GaugeValue, float64(current_memory), user)
ch <- prometheus.MustNewConstMetric(sysUserSummaryTotalMemoryAllocated, prometheus.CounterValue, float64(total_memory_allocated), user)

}
return nil
}

var _ Scraper = ScrapeSysUserSummary{}
132 changes: 132 additions & 0 deletions collector/sys_user_summary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2022 The Prometheus Authors
// Licensed 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 collector

import (
"context"
"database/sql/driver"
"github.com/DATA-DOG/go-sqlmock"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
dto "github.com/prometheus/client_model/go"
"github.com/smartystreets/goconvey/convey"
"regexp"
"strconv"
"testing"
)

func TestScrapeSysUserSummary(t *testing.T) {

db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("error opening a stub database connection: %s", err)
}
defer db.Close()

columns := []string{
"user",
"statemets",
"statement_latency",
"table_scans",
"file_ios",
"file_io_latency",
"current_connections",
"total_connections",
"unique_hosts",
"current_memory",
"total_memory_allocated",
}
rows := sqlmock.NewRows(columns)
queryResults := [][]driver.Value{
{
"user1",
"110",
"120",
"140",
"150",
"160",
"170",
"180",
"190",
"110",
"111",
},
{
"user2",
"210",
"220",
"240",
"250",
"260",
"270",
"280",
"290",
"210",
"211",
},
}
expectedMetrics := []MetricResult{}
// Register the query results with mock SQL driver and assemble expected metric results list
for _, row := range queryResults {
rows.AddRow(row...)
user := row[0]
for i, metricsValue := range row {
if i == 0 {
continue
}
metricType := dto.MetricType_COUNTER
// Current Connections and Current Memory are gauges
if i == 6 || i == 9 {
metricType = dto.MetricType_GAUGE
}
value, err := strconv.ParseFloat(metricsValue.(string), 64)
if err != nil {
t.Errorf("Failed to parse result value as float64: %+v", err)
}
// Statement latency & IO latency are latencies in picoseconds, convert them to seconds
if i == 2 || i == 5 {
value = value / picoSeconds
}
expectedMetrics = append(expectedMetrics, MetricResult{
labels: labelMap{"user": user.(string)},
value: value,
metricType: metricType,
})
}
}

mock.ExpectQuery(sanitizeQuery(regexp.QuoteMeta(sysUserSummaryQuery))).WillReturnRows(rows)

ch := make(chan prometheus.Metric)

go func() {
if err = (ScrapeSysUserSummary{}).Scrape(context.Background(), db, ch, log.NewNopLogger()); err != nil {
t.Errorf("error calling function on test: %s", err)
}
close(ch)
}()

// Ensure metrics look OK
convey.Convey("Metrics comparison", t, func() {
for _, expect := range expectedMetrics {
got := readMetric(<-ch)
convey.So(expect, convey.ShouldResemble, got)
}
})

// Ensure all SQL queries were executed
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled exceptions: %s", err)
}
}
1 change: 1 addition & 0 deletions mysqld_exporter.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ var scrapers = map[collector.Scraper]bool{
collector.ScrapePerfReplicationGroupMembers{}: false,
collector.ScrapePerfReplicationGroupMemberStats{}: false,
collector.ScrapePerfReplicationApplierStatsByWorker{}: false,
collector.ScrapeSysUserSummary{}: false,
collector.ScrapeUserStat{}: false,
collector.ScrapeClientStat{}: false,
collector.ScrapeTableStat{}: false,
Expand Down

0 comments on commit fad2c0c

Please sign in to comment.