Skip to content

Commit f990e6e

Browse files
authored
feat: add query level monitoring support for RDS (#189)
feat: add query level monitoring support for RDS (#189)
1 parent b96a8dc commit f990e6e

16 files changed

Lines changed: 573 additions & 116 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ Unreleased section should follow [Release Toolkit](https://github.com/newrelic/r
99

1010
## Unreleased
1111

12+
### enhancements
13+
- Added Query Performance Monitoring support for RDS
14+
- Added QueryMonitoringOnly flag to enable only query monitoring feature
15+
- Made individual query filtering case-insensitive
16+
1217
## v1.15.0 - 2025-04-21
1318

1419
### 🚀 Enhancements

mysql-config.yml.sample

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ integrations:
4040
role: write-replica
4141
inventory_source: config/mysql
4242

43-
# Example configuration for enabling performance monitoring
43+
# Example configuration for enabling query performance monitoring
4444
- name: nri-mysql
4545
env:
4646
HOSTNAME: localhost
@@ -69,6 +69,7 @@ integrations:
6969

7070
# Enable query performance monitoring
7171
ENABLE_QUERY_MONITORING: true
72+
7273
# Fetch interval in seconds for grouped slow queries. Should match the interval in mysql-config.yml
7374
# SLOW_QUERY_MONITORING_FETCH_INTERVAL: 30
7475
# Threshold in milliseconds for query response time to fetch individual query performance metrics
@@ -82,4 +83,48 @@ integrations:
8283
labels:
8384
env: production
8485
role: performance-monitoring
86+
inventory_source: config/mysql-performance
87+
88+
# Example configuration for enabling query performance monitoring only
89+
- name: nri-mysql
90+
env:
91+
HOSTNAME: localhost
92+
PORT: 3306
93+
# ENABLE_TLS: false
94+
# INSECURE_SKIP_VERIFY: false
95+
# Specify extra connection parameters as attr1=val1&attr2=val2.
96+
# EXTRA_CONNECTION_URL_ARGS: ""
97+
98+
# If not empty `socket` parameter will discard `port` parameter
99+
# SOCKET: <PATH_TO_LOCAL_SOCKET_FILE_NAME>
100+
101+
USERNAME: newrelic
102+
PASSWORD: <YOUR_SELECTED_PASSWORD>
103+
# Allow old password https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_old_passwords
104+
# OLD_PASSWORDS: false
105+
106+
# New users should leave this property as `true`, to identify the
107+
# monitored entities as `remote`. Setting this property to `false` (the
108+
# default value) is deprecated and will be removed soon, disallowing
109+
# entities that are identified as `local`.
110+
# Please check the documentation to get more information about local
111+
# versus remote entities:
112+
# https://github.com/newrelic/infra-integrations-sdk/blob/master/docs/entity-definition.md
113+
REMOTE_MONITORING: true
114+
115+
# Enable collection of only detailed query performance metrics, excluding other metrics. - Defaults to false
116+
QUERY_MONITORING_ONLY : true
117+
# Fetch interval in seconds for grouped slow queries. Should match the interval in mysql-config.yml
118+
# SLOW_QUERY_MONITORING_FETCH_INTERVAL: 30
119+
# Threshold in milliseconds for query response time to fetch individual query performance metrics
120+
# QUERY_MONITORING_RESPONSE_TIME_THRESHOLD: 500
121+
# Query count limit for fetching grouped slow and individual query performance metrics
122+
# QUERY_MONITORING_COUNT_THRESHOLD: 20
123+
# Provide any necessary database exclusions as a JSON array
124+
# EXCLUDED_PERFORMANCE_DATABASES: '["employees","azure_sys"]'
125+
# Note: System databases (mysql, information_schema, performance_schema, sys) are always excluded.
126+
interval: 30s
127+
labels:
128+
env: production
129+
role: performance-monitoring-only
85130
inventory_source: config/mysql-performance

src/args/argument_list.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type ArgumentList struct {
2020
OldPasswords bool `default:"false" help:"Allow the use of old passwords: https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_old_passwords"`
2121
ShowVersion bool `default:"false" help:"Display build information and exit."`
2222
EnableQueryMonitoring bool `default:"false" help:"Enable collection of detailed query performance metrics."`
23+
QueryMonitoringOnly bool `default:"false" help:"Enable collection of only detailed query performance metrics, excluding other metrics."`
2324
SlowQueryMonitoringFetchInterval int `default:"30" help:"Fetch interval in seconds for grouped slow queries. Should match the interval in mysql-config.yml."`
2425
QueryMonitoringResponseTimeThreshold int `default:"500" help:"Threshold in milliseconds for query response time to fetch individual query performance metrics."`
2526
QueryMonitoringCountThreshold int `default:"20" help:"Query count limit for fetching grouped slow and individual query performance metrics."`

src/mysql.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ func main() {
4949
e, err := infrautils.CreateNodeEntity(i, args.RemoteMonitoring, args.Hostname, args.Port)
5050
infrautils.FatalIfErr(err)
5151

52+
/*
53+
If the QueryMonitoringOnly flag is set, only populate query performance metrics
54+
and return from main function without proceeding further.
55+
*/
56+
if args.QueryMonitoringOnly {
57+
queryperformancemonitoring.PopulateQueryPerformanceMetrics(args, e, i)
58+
return
59+
}
60+
5261
db, err := openSQLDB(dbutils.GenerateDSN(args, ""))
5362
infrautils.FatalIfErr(err)
5463
defer db.close()

src/query-performance-monitoring/constants/constants.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,6 @@ const (
2222
*/
2323
ExplainQueryFormat = "EXPLAIN FORMAT=JSON %s"
2424

25-
/*
26-
SupportedStatements defines the SQL statements for which this integration fetches query execution plans.
27-
Restricting the supported statements improves compatibility and reduces the complexity of plan analysis.
28-
*/
29-
SupportedStatements = "SELECT WITH"
30-
3125
/*
3226
QueryPlanTimeoutDuration sets the timeout for fetching query execution plans.
3327
This prevents indefinite waits when a query plan retrieval takes too long, ensuring system responsiveness.
@@ -81,6 +75,17 @@ const (
8175
This ensures version strings are formatted correctly and allows for proper version comparison.
8276
*/
8377
MinVersionParts = 2
78+
79+
/*
80+
EssentialConsumersCount defines the number of essential consumers that must be enabled
81+
in the performance schema to ensure that the necessary performance data is available.
82+
83+
The EssentialConsumersCount is set to 5 because two of the consumers (events_statements_cpu and events_waits_history_long)
84+
are optional and may not be available in Aurora RDS environments. Therefore, we consider only the remaining five consumers
85+
(events_waits_current, events_waits_history, events_statements_current, events_statements_history, and events_statements_history_long) as essential and
86+
consistently available across all supported MySQL environments.
87+
*/
88+
EssentialConsumersCount = 5
8489
)
8590

8691
/*

src/query-performance-monitoring/performance-metrics-collectors/query_execution_plan.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -341,8 +341,14 @@ func SetExecutionPlanMetrics(i *integration.Integration, args arguments.Argument
341341

342342
// isSupportedStatement checks if the given query is a supported statement.
343343
func isSupportedStatement(query string) bool {
344-
for _, stmt := range strings.Split(constants.SupportedStatements, " ") {
345-
if strings.HasPrefix(query, stmt) {
344+
upperCaseQuery := strings.ToUpper(strings.TrimSpace(query))
345+
/*
346+
SupportedStatements defines the SQL statements for which this integration fetches query execution plans.
347+
Restricting the supported statements improves compatibility and reduces the complexity of plan analysis.
348+
*/
349+
supportedStatements := []string{"SELECT", "WITH"}
350+
for _, stmt := range supportedStatements {
351+
if strings.HasPrefix(upperCaseQuery, stmt) {
346352
return true
347353
}
348354
}

src/query-performance-monitoring/performance-metrics-collectors/query_execution_plan_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,11 @@ func TestIsSupportedStatement(t *testing.T) {
354354
t.Run("Supported Statement", func(t *testing.T) {
355355
assert.True(t, isSupportedStatement("SELECT * FROM test"))
356356
assert.True(t, isSupportedStatement("WITH cte AS (SELECT * FROM test) SELECT * FROM cte"))
357+
assert.True(t, isSupportedStatement("select * from users"))
358+
assert.True(t, isSupportedStatement(" SELECT * FROM users"))
359+
assert.True(t, isSupportedStatement("with cte as (select * from users) select * from cte"))
360+
assert.True(t, isSupportedStatement("Select * from test"))
361+
assert.True(t, isSupportedStatement("With cte as (Select * from test) Select * from cte"))
357362
})
358363

359364
t.Run("Unsupported Statement", func(t *testing.T) {
@@ -362,6 +367,9 @@ func TestIsSupportedStatement(t *testing.T) {
362367
assert.False(t, isSupportedStatement("INSERT INTO test VALUES (1)"))
363368
assert.False(t, isSupportedStatement("UPDATE test SET value = 1"))
364369
assert.False(t, isSupportedStatement("DELETE FROM test"))
370+
assert.False(t, isSupportedStatement("CREATE TABLE users (id INT, name VARCHAR(255))"))
371+
assert.False(t, isSupportedStatement(""))
372+
assert.False(t, isSupportedStatement(" "))
365373
})
366374
}
367375

src/query-performance-monitoring/utils/helpers.go

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,12 @@ import (
1616

1717
// Dynamic error
1818
var (
19-
ErrEssentialConsumerNotEnabled = errors.New("essential consumer is not enabled")
20-
ErrEssentialInstrumentNotEnabled = errors.New("essential instrument is not fully enabled")
21-
ErrMySQLVersion = errors.New("failed to determine MySQL version")
22-
ErrModelIsNotValid = errors.New("model is not a valid struct")
23-
ErrNoRowsReturned = errors.New("no rows returned from EXPLAIN")
24-
ErrQueryTextNil = errors.New("query text is nil")
25-
ErrQueryTextEmpty = errors.New("query text is empty")
26-
ErrQueryIDNil = errors.New("query ID is nil")
19+
ErrMySQLVersion = errors.New("failed to determine MySQL version")
20+
ErrModelIsNotValid = errors.New("model is not a valid struct")
21+
ErrNoRowsReturned = errors.New("no rows returned from EXPLAIN")
22+
ErrQueryTextNil = errors.New("query text is nil")
23+
ErrQueryTextEmpty = errors.New("query text is empty")
24+
ErrQueryIDNil = errors.New("query ID is nil")
2725
)
2826

2927
func CreateMetricSet(e *integration.Entity, sampleName string, args arguments.ArgumentList) *metric.Set {

0 commit comments

Comments
 (0)