Skip to content

Commit 741af2c

Browse files
committed
Added the mariadb backup metrics integration tests.
1 parent fa4b1ee commit 741af2c

7 files changed

Lines changed: 602 additions & 1 deletion

File tree

src/backup_metrics.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,15 @@ package main
22

33
import (
44
"database/sql"
5+
"sync"
6+
57
"github.com/newrelic/infra-integrations-sdk/v3/data/metric"
68
"github.com/newrelic/infra-integrations-sdk/v3/log"
79
)
810

11+
// mdlWarningOnce ensures the MDL instrument warning is logged at most once per process run.
12+
var mdlWarningOnce sync.Once
13+
914
// backupMetrics defines metrics for detecting active backup operations
1015
var backupMetrics = map[string][]interface{}{
1116
"db.backupActive.total": {"total_backup_active", metric.GAUGE},
@@ -188,9 +193,38 @@ func supportsMetadataLocks(db *sql.DB) bool {
188193
return count > 0
189194
}
190195

196+
// warnIfMDLInstrumentDisabled logs a warning if the MDL instrument is disabled in performance_schema.
197+
// On MariaDB the instrument is off by default, which leaves metadata_locks empty and causes
198+
// active backup metrics to report 0. Customers must enable it in their database configuration:
199+
//
200+
// performance-schema-instrument='wait/lock/metadata/sql/mdl=ON'
201+
func warnIfMDLInstrumentDisabled(db *sql.DB) {
202+
var enabled int
203+
err := db.QueryRow(`
204+
SELECT COUNT(*)
205+
FROM performance_schema.setup_instruments
206+
WHERE NAME = 'wait/lock/metadata/sql/mdl'
207+
AND ENABLED = 'YES'
208+
`).Scan(&enabled)
209+
if err != nil {
210+
log.Debug("Could not check MDL instrument status: %v", err)
211+
return
212+
}
213+
if enabled > 0 {
214+
return
215+
}
216+
mdlWarningOnce.Do(func() {
217+
log.Warn("MDL instrument 'wait/lock/metadata/sql/mdl' is disabled in performance_schema. " +
218+
"Active backup metrics (db.backupActive.*) will always report 0. " +
219+
"To enable accurate detection, add to your database configuration: " +
220+
"performance-schema-instrument='wait/lock/metadata/sql/mdl=ON'")
221+
})
222+
}
223+
191224
// getBackupMetricsQuery returns the appropriate backup metrics query based on database version
192225
func getBackupMetricsQuery(db *sql.DB) string {
193226
if supportsMetadataLocks(db) {
227+
warnIfMDLInstrumentDisabled(db)
194228
log.Debug("Using metadata_locks-based backup detection (MySQL 8.0+/MariaDB 10.5+)")
195229
return backupMetricsQuery
196230
}

src/backup_metrics_test.go

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
package main
22

33
import (
4+
"fmt"
5+
"sync"
46
"testing"
57

8+
sqlmock "github.com/DATA-DOG/go-sqlmock"
69
"github.com/newrelic/infra-integrations-sdk/v3/data/metric"
710
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
812
)
913

1014
func TestBackupMetricsMap(t *testing.T) {
@@ -149,3 +153,147 @@ func TestBackupMetricsCount(t *testing.T) {
149153
assert.Equal(t, 4, len(backupMetrics), "Should have 4 active backup metrics")
150154
assert.Equal(t, 10, len(backupHistoryMetrics), "Should have 10 historical backup metrics")
151155
}
156+
157+
// supportsMetadataLocks tests
158+
159+
func TestSupportsMetadataLocks_TableExists(t *testing.T) {
160+
db, mock, err := sqlmock.New()
161+
require.NoError(t, err)
162+
defer db.Close()
163+
164+
mock.ExpectQuery("metadata_locks").
165+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
166+
167+
assert.True(t, supportsMetadataLocks(db))
168+
assert.NoError(t, mock.ExpectationsWereMet())
169+
}
170+
171+
func TestSupportsMetadataLocks_TableNotExists(t *testing.T) {
172+
db, mock, err := sqlmock.New()
173+
require.NoError(t, err)
174+
defer db.Close()
175+
176+
mock.ExpectQuery("metadata_locks").
177+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))
178+
179+
assert.False(t, supportsMetadataLocks(db))
180+
assert.NoError(t, mock.ExpectationsWereMet())
181+
}
182+
183+
func TestSupportsMetadataLocks_QueryError(t *testing.T) {
184+
db, mock, err := sqlmock.New()
185+
require.NoError(t, err)
186+
defer db.Close()
187+
188+
mock.ExpectQuery("metadata_locks").
189+
WillReturnError(fmt.Errorf("connection refused"))
190+
191+
assert.False(t, supportsMetadataLocks(db))
192+
assert.NoError(t, mock.ExpectationsWereMet())
193+
}
194+
195+
// warnIfMDLInstrumentDisabled tests
196+
197+
func TestWarnIfMDLInstrumentDisabled_InstrumentEnabled(t *testing.T) {
198+
db, mock, err := sqlmock.New()
199+
require.NoError(t, err)
200+
defer db.Close()
201+
202+
mock.ExpectQuery("setup_instruments").
203+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
204+
205+
warnIfMDLInstrumentDisabled(db) // no warning expected
206+
assert.NoError(t, mock.ExpectationsWereMet())
207+
}
208+
209+
func TestWarnIfMDLInstrumentDisabled_InstrumentDisabled(t *testing.T) {
210+
mdlWarningOnce = sync.Once{} // reset so warning can fire
211+
defer func() { mdlWarningOnce = sync.Once{} }()
212+
213+
db, mock, err := sqlmock.New()
214+
require.NoError(t, err)
215+
defer db.Close()
216+
217+
mock.ExpectQuery("setup_instruments").
218+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))
219+
220+
warnIfMDLInstrumentDisabled(db) // warning fires once
221+
assert.NoError(t, mock.ExpectationsWereMet())
222+
}
223+
224+
func TestWarnIfMDLInstrumentDisabled_WarnsOnlyOnce(t *testing.T) {
225+
mdlWarningOnce = sync.Once{} // reset so first call fires
226+
defer func() { mdlWarningOnce = sync.Once{} }()
227+
228+
db, mock, err := sqlmock.New()
229+
require.NoError(t, err)
230+
defer db.Close()
231+
232+
// Both calls query the DB; only the first triggers the log.Warn via sync.Once
233+
mock.ExpectQuery("setup_instruments").
234+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))
235+
mock.ExpectQuery("setup_instruments").
236+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))
237+
238+
warnIfMDLInstrumentDisabled(db) // fires warning
239+
warnIfMDLInstrumentDisabled(db) // warning already consumed by Once, skipped
240+
assert.NoError(t, mock.ExpectationsWereMet())
241+
}
242+
243+
func TestWarnIfMDLInstrumentDisabled_QueryError(t *testing.T) {
244+
db, mock, err := sqlmock.New()
245+
require.NoError(t, err)
246+
defer db.Close()
247+
248+
mock.ExpectQuery("setup_instruments").
249+
WillReturnError(fmt.Errorf("permission denied"))
250+
251+
warnIfMDLInstrumentDisabled(db) // should not panic; error logged at Debug level
252+
assert.NoError(t, mock.ExpectationsWereMet())
253+
}
254+
255+
// getBackupMetricsQuery tests
256+
257+
func TestGetBackupMetricsQuery_UsesMetadataLocksWhenAvailable(t *testing.T) {
258+
mdlWarningOnce = sync.Once{}
259+
defer func() { mdlWarningOnce = sync.Once{} }()
260+
261+
db, mock, err := sqlmock.New()
262+
require.NoError(t, err)
263+
defer db.Close()
264+
265+
// supportsMetadataLocks → table exists
266+
mock.ExpectQuery("metadata_locks").
267+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
268+
// warnIfMDLInstrumentDisabled → instrument enabled, no warning
269+
mock.ExpectQuery("setup_instruments").
270+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(1))
271+
272+
assert.Equal(t, backupMetricsQuery, getBackupMetricsQuery(db))
273+
assert.NoError(t, mock.ExpectationsWereMet())
274+
}
275+
276+
func TestGetBackupMetricsQuery_UsesFallbackWhenTableMissing(t *testing.T) {
277+
db, mock, err := sqlmock.New()
278+
require.NoError(t, err)
279+
defer db.Close()
280+
281+
// supportsMetadataLocks → table not found
282+
mock.ExpectQuery("metadata_locks").
283+
WillReturnRows(sqlmock.NewRows([]string{"COUNT(*)"}).AddRow(0))
284+
285+
assert.Equal(t, backupMetricsQueryFallback, getBackupMetricsQuery(db))
286+
assert.NoError(t, mock.ExpectationsWereMet())
287+
}
288+
289+
func TestGetBackupMetricsQuery_UsesFallbackOnQueryError(t *testing.T) {
290+
db, mock, err := sqlmock.New()
291+
require.NoError(t, err)
292+
defer db.Close()
293+
294+
mock.ExpectQuery("metadata_locks").
295+
WillReturnError(fmt.Errorf("table unavailable"))
296+
297+
assert.Equal(t, backupMetricsQueryFallback, getBackupMetricsQuery(db))
298+
assert.NoError(t, mock.ExpectationsWereMet())
299+
}

src/metrics_parse.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,11 @@ func getRawData(db dataSource) (map[string]interface{}, map[string]interface{},
127127
}
128128
}
129129

130-
// Collect historical backup metrics if enabled
130+
// Collect historical backup metrics if enabled.
131+
// backupHistoryMetricsQuery uses performance_schema.events_statements_history which is
132+
// available on all supported versions (MySQL 5.6+ / MariaDB 10.0+), so no version guard
133+
// is needed. If performance_schema is disabled or the table is inaccessible, the error
134+
// is handled gracefully below.
131135
if args.ExtendedBackupHistoryMetrics {
132136
backupHistoryData, err := db.query(backupHistoryMetricsQuery)
133137
if err != nil {

tests/integration/integration_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,28 @@ func TestMySQLIntegrationOnlySlaveMetrics(t *testing.T) {
292292
}
293293
}
294294

295+
func testMySQLIntegrationBackupMetrics(t *testing.T, mysqlConfig MysqlConfig) {
296+
testName := helpers.GetTestName(t)
297+
stdout := runIntegration(t, mysqlConfig.MasterHostname,
298+
"METRICS=true",
299+
"EXTENDED_BACKUP_METRICS=true",
300+
"EXTENDED_BACKUP_HISTORY_METRICS=true",
301+
fmt.Sprintf("NRIA_CACHE_PATH=/tmp/%v.json", testName),
302+
)
303+
schemaDir := fmt.Sprintf("json-schema-files-%s", mysqlConfig.Version)
304+
schemaPath := filepath.Join(schemaDir, "mysql-schema-backup-metrics.json")
305+
err := jsonschema.Validate(schemaPath, stdout)
306+
require.NoError(t, err, "The output of MySQL integration doesn't have expected backup metrics format")
307+
}
308+
309+
func TestMySQLIntegrationBackupMetrics(t *testing.T) {
310+
for _, mysqlConfig := range MysqlConfigs {
311+
t.Run(mysqlConfig.Version, func(t *testing.T) {
312+
testMySQLIntegrationBackupMetrics(t, mysqlConfig)
313+
})
314+
}
315+
}
316+
295317
func runUnconfiguredMysqlPerfConfigTest(t *testing.T, args []string, outputMetricsFile string, expectedError string, testName string) {
296318
for _, mysqlUnconfiguredPerfConfig := range MysqlConfigs {
297319
if isDBVersionLessThan8(mysqlUnconfiguredPerfConfig.Version) {
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"description": "Schema for MySQL backup metrics integration output",
4+
"properties": {
5+
"integration_version": {
6+
"minLength": 1,
7+
"pattern": "^[0-9]+.[0-9]+.[0-9]+$",
8+
"type": "string"
9+
},
10+
"data": {
11+
"type": "array",
12+
"items": [
13+
{
14+
"type": "object",
15+
"properties": {
16+
"events": {
17+
"items": {
18+
"properties": {},
19+
"required": []
20+
},
21+
"type": "array"
22+
},
23+
"inventory": {
24+
"properties": {},
25+
"required": [],
26+
"type": "object"
27+
},
28+
"metrics": {
29+
"items": [
30+
{
31+
"type": "object",
32+
"properties": {
33+
"event_type": {
34+
"minLength": 1,
35+
"pattern": "^MysqlSample$",
36+
"type": "string"
37+
},
38+
"db.backupActive.total": {
39+
"type": "number"
40+
},
41+
"db.backupActive.logical": {
42+
"type": "number"
43+
},
44+
"db.backupActive.physical": {
45+
"type": "number"
46+
},
47+
"db.backupActive.tableLock": {
48+
"type": "number"
49+
},
50+
"db.backupHistory.total": {
51+
"type": "number"
52+
},
53+
"db.backupHistory.logical.count": {
54+
"type": "number"
55+
},
56+
"db.backupHistory.logical.avgDuration": {
57+
"type": "number"
58+
},
59+
"db.backupHistory.logical.maxDuration": {
60+
"type": "number"
61+
},
62+
"db.backupHistory.physical.count": {
63+
"type": "number"
64+
},
65+
"db.backupHistory.physical.avgDuration": {
66+
"type": "number"
67+
},
68+
"db.backupHistory.physical.maxDuration": {
69+
"type": "number"
70+
},
71+
"db.backupHistory.tableLock.count": {
72+
"type": "number"
73+
},
74+
"db.backupHistory.tableLock.avgDuration": {
75+
"type": "number"
76+
},
77+
"db.backupHistory.tableLock.maxDuration": {
78+
"type": "number"
79+
}
80+
},
81+
"required": [
82+
"event_type",
83+
"db.backupActive.total",
84+
"db.backupActive.logical",
85+
"db.backupActive.physical",
86+
"db.backupActive.tableLock",
87+
"db.backupHistory.total",
88+
"db.backupHistory.logical.count",
89+
"db.backupHistory.logical.avgDuration",
90+
"db.backupHistory.logical.maxDuration",
91+
"db.backupHistory.physical.count",
92+
"db.backupHistory.physical.avgDuration",
93+
"db.backupHistory.physical.maxDuration",
94+
"db.backupHistory.tableLock.count",
95+
"db.backupHistory.tableLock.avgDuration",
96+
"db.backupHistory.tableLock.maxDuration"
97+
]
98+
}
99+
],
100+
"minItems": 1,
101+
"type": "array",
102+
"uniqueItems": true
103+
}
104+
}
105+
}
106+
],
107+
"required": [
108+
"metrics",
109+
"inventory",
110+
"events"
111+
]
112+
},
113+
"name": {
114+
"minLength": 1,
115+
"pattern": "^com.newrelic.mysql$",
116+
"type": "string"
117+
},
118+
"protocol_version": {
119+
"minLength": 1,
120+
"pattern": "^3$",
121+
"type": "string"
122+
}
123+
},
124+
"required": [
125+
"name",
126+
"protocol_version",
127+
"integration_version",
128+
"data"
129+
],
130+
"type": "object"
131+
}

0 commit comments

Comments
 (0)