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
2 changes: 1 addition & 1 deletion app/services/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ func (mp *metricspusher) Push(cred credential.Credential) error {
}
}

// ── Traefik metrics (aggregate per entrypoint) ──────────────────────────────
// ── Traefik metrics (HTTP requests / response time / error rate per entrypoint) ──

traefikSets, err := mp.traefikcollector.Collect(ctx)
if err != nil {
Expand Down
27 changes: 15 additions & 12 deletions domain/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,16 +104,19 @@ type StorageAttributes struct {
}

type MySQLDatabaseMetrics struct {
Up bool `json:"up"`
ThreadsConnected int `json:"threads_connected"`
ThreadsRunning int `json:"threads_running"`
MaxConnections int `json:"max_connections"`
MaxUsedConnections int64 `json:"max_used_connections"`
QueriesPerSecond float64 `json:"queries_per_second"`
SlowQueries int64 `json:"slow_queries"`
InnoDBCacheHitRatio float64 `json:"innodb_cache_hit_ratio"`
ReplicationLagSeconds *int `json:"replication_lag_seconds,omitempty"`
ReplicationConnected *bool `json:"replication_connected,omitempty"`
Up bool `json:"up"`
ConnectionsTotal int `json:"connections_total"`
ConnectionsAborted int64 `json:"connections_aborted"`
MaxConnections int `json:"max_connections"`
ThreadsRunning int `json:"threads_running"`
QueriesPerSecond float64 `json:"queries_per_second"`
SlowQueriesPerSecond float64 `json:"slow_queries_per_second"`
InnoDBBufferPoolHitRatio float64 `json:"innodb_buffer_pool_hit_ratio"`
InnoDBRowLockWaitsPerSecond float64 `json:"innodb_row_lock_waits_per_second"`
TmpDiskTablesPerSecond float64 `json:"tmp_disk_tables_per_second"`
SelectFullScansPerSecond float64 `json:"select_full_scans_per_second"`
ReplicationLagSeconds *int `json:"replication_lag_seconds,omitempty"`
ReplicationConnected *bool `json:"replication_connected,omitempty"`
}

type MongoDBMetrics struct {
Expand Down Expand Up @@ -204,9 +207,9 @@ type TraefikRouterMetrics struct {
}

type TraefikRouterAttributes struct {
RouterName string `json:"router_name"`
RouterName string `json:"router_name"`
EntrypointName string `json:"entrypoint_name"`
Service string `json:"service,omitempty"`
Service string `json:"service,omitempty"`
}

type ContainerAttributes struct {
Expand Down
38 changes: 27 additions & 11 deletions internal/mysqlmetrics/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ type Collector interface {
}

type mysqlCollector struct {
queryTimeout time.Duration
lastQueries *int64
lastTime time.Time
queryTimeout time.Duration
lastQueries *int64
lastSlowQueries *int64
lastRowLockWaits *int64
lastTmpDiskTables *int64
lastSelectFullJoins *int64
lastTime time.Time
}

func New() Collector {
Expand Down Expand Up @@ -73,9 +77,10 @@ func (mc *mysqlCollector) Collect(cred credential.Credential) (metrics.MySQLData
func (mc *mysqlCollector) collectGlobalStatus(ctx context.Context, db *sql.DB, m *metrics.MySQLDatabaseMetrics) error {
rows, err := db.QueryContext(ctx, `
SHOW GLOBAL STATUS WHERE Variable_name IN (
'Threads_connected','Threads_running','Max_used_connections',
'Queries','Slow_queries',
'Innodb_buffer_pool_read_requests','Innodb_buffer_pool_reads'
'Threads_connected','Threads_running',
'Queries','Slow_queries','Aborted_connects',
'Innodb_buffer_pool_read_requests','Innodb_buffer_pool_reads',
'Innodb_row_lock_waits','Created_tmp_disk_tables','Select_full_join'
)
`)
if err != nil {
Expand All @@ -95,27 +100,38 @@ func (mc *mysqlCollector) collectGlobalStatus(ctx context.Context, db *sql.DB, m
return err
}

m.ThreadsConnected = parseInt(status["Threads_connected"])
m.ConnectionsTotal = parseInt(status["Threads_connected"])
m.ThreadsRunning = parseInt(status["Threads_running"])
m.MaxUsedConnections = parseInt64(status["Max_used_connections"])
m.SlowQueries = parseInt64(status["Slow_queries"])
m.ConnectionsAborted = parseInt64(status["Aborted_connects"])

readReqs := parseFloat64(status["Innodb_buffer_pool_read_requests"])
diskReads := parseFloat64(status["Innodb_buffer_pool_reads"])
if readReqs > 0 {
m.InnoDBCacheHitRatio = (readReqs - diskReads) / readReqs * 100
m.InnoDBBufferPoolHitRatio = (readReqs - diskReads) / readReqs * 100
}

// Delta-based QPS using cumulative Queries counter
// Delta-based rates using cumulative counters
currentQueries := parseInt64(status["Queries"])
currentSlowQueries := parseInt64(status["Slow_queries"])
currentRowLockWaits := parseInt64(status["Innodb_row_lock_waits"])
currentTmpDiskTables := parseInt64(status["Created_tmp_disk_tables"])
currentSelectFullJoins := parseInt64(status["Select_full_join"])
now := time.Now()
if mc.lastQueries != nil {
elapsed := now.Sub(mc.lastTime).Seconds()
if elapsed > 0 {
m.QueriesPerSecond = float64(currentQueries-*mc.lastQueries) / elapsed
m.SlowQueriesPerSecond = float64(currentSlowQueries-*mc.lastSlowQueries) / elapsed
m.InnoDBRowLockWaitsPerSecond = float64(currentRowLockWaits-*mc.lastRowLockWaits) / elapsed
m.TmpDiskTablesPerSecond = float64(currentTmpDiskTables-*mc.lastTmpDiskTables) / elapsed
m.SelectFullScansPerSecond = float64(currentSelectFullJoins-*mc.lastSelectFullJoins) / elapsed
}
}
mc.lastQueries = &currentQueries
mc.lastSlowQueries = &currentSlowQueries
mc.lastRowLockWaits = &currentRowLockWaits
mc.lastTmpDiskTables = &currentTmpDiskTables
mc.lastSelectFullJoins = &currentSelectFullJoins
mc.lastTime = now

return nil
Expand Down
89 changes: 89 additions & 0 deletions internal/mysqlmetrics/collector_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package mysqlmetrics

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestDeltaRates_FirstCollectionReturnsZero(t *testing.T) {
mc := &mysqlCollector{queryTimeout: 10 * time.Second}

assert.Nil(t, mc.lastQueries, "lastQueries should be nil before first collection")
assert.Nil(t, mc.lastSlowQueries, "lastSlowQueries should be nil before first collection")
}

func TestDeltaRates_SecondCollectionReturnsRates(t *testing.T) {
baseTime := time.Now()
initialQueries := int64(1000)
initialSlowQueries := int64(10)

mc := &mysqlCollector{
queryTimeout: 10 * time.Second,
lastQueries: &initialQueries,
lastSlowQueries: &initialSlowQueries,
lastTime: baseTime,
}

// Simulate 10 seconds elapsed, 500 new queries, 5 new slow queries
elapsed := 10 * time.Second
currentQueries := int64(1500)
currentSlowQueries := int64(15)
now := baseTime.Add(elapsed)

elapsedSec := now.Sub(mc.lastTime).Seconds()
qps := float64(currentQueries-*mc.lastQueries) / elapsedSec
sqps := float64(currentSlowQueries-*mc.lastSlowQueries) / elapsedSec

assert.Equal(t, 50.0, qps, "QPS should be 500 queries / 10s = 50")
assert.Equal(t, 0.5, sqps, "slow QPS should be 5 queries / 10s = 0.5")
}

func TestDeltaRates_ZeroElapsedReturnsZero(t *testing.T) {
baseTime := time.Now()
initialQueries := int64(1000)
initialSlowQueries := int64(10)

mc := &mysqlCollector{
queryTimeout: 10 * time.Second,
lastQueries: &initialQueries,
lastSlowQueries: &initialSlowQueries,
lastTime: baseTime,
}

elapsed := mc.lastTime.Sub(baseTime).Seconds()
assert.Equal(t, 0.0, elapsed, "elapsed should be 0 when time hasn't advanced")
}

func TestInnoDBBufferPoolHitRatio_Calculation(t *testing.T) {
readReqs := 10000.0
diskReads := 100.0

ratio := (readReqs - diskReads) / readReqs * 100
assert.Equal(t, 99.0, ratio, "hit ratio should be 99% when 100 of 10000 reads hit disk")
}

func TestInnoDBBufferPoolHitRatio_ZeroRequests(t *testing.T) {
readReqs := 0.0
diskReads := 0.0

var ratio float64
if readReqs > 0 {
ratio = (readReqs - diskReads) / readReqs * 100
}
assert.Equal(t, 0.0, ratio, "hit ratio should be 0 when there are no read requests")
}

func TestParseInt_ValidValues(t *testing.T) {
assert.Equal(t, 42, parseInt("42"))
assert.Equal(t, 0, parseInt("0"))
assert.Equal(t, 0, parseInt(""))
assert.Equal(t, 0, parseInt("invalid"))
}

func TestParseInt64_ValidValues(t *testing.T) {
assert.Equal(t, int64(1234567890), parseInt64("1234567890"))
assert.Equal(t, int64(0), parseInt64(""))
assert.Equal(t, int64(0), parseInt64("invalid"))
}
Loading