Skip to content

Commit

Permalink
feat(query_perf_monitoring): Implement query performance monitoring m…
Browse files Browse the repository at this point in the history
…etrics (#191)

feat(query_perf_monitoring): Implement query performance monitoring metrics (#191)
  • Loading branch information
sairaj18 authored Jan 23, 2025
1 parent 14cfcec commit b243aa1
Show file tree
Hide file tree
Showing 52 changed files with 3,278 additions and 117 deletions.
14 changes: 11 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,17 @@ test:

integration-test:
@echo "=== $(INTEGRATION) === [ test ]: running integration tests..."
@docker compose -f tests/docker-compose.yml pull
@go test -v -tags=integration -count 1 ./tests/. || (ret=$$?; docker compose -f tests/docker-compose.yml down && exit $$ret)
@docker compose -f tests/docker-compose.yml down
@docker compose -f tests/docker-compose.yml up -d
@sleep 10
@go test -v -tags=integration -count 1 ./tests/postgresql_test.go -timeout 300s || (ret=$$?; docker compose -f tests/docker-compose.yml down -v && exit $$ret)
@docker compose -f tests/docker-compose.yml down -v
@echo "=== $(INTEGRATION) === [ test ]: running integration tests for query performance monitoring..."
@echo "Starting containers for performance tests..."
@docker compose -f tests/docker-compose-performance.yml up -d
@sleep 30
@go test -v -tags=query_performance ./tests/postgresqlperf_test.go -timeout 600s || (ret=$$?; docker compose -f tests/docker-compose-performance.yml down -v && exit $$ret)
@echo "Stopping performance test containers..."
@docker compose -f tests/docker-compose-performance.yml down -v

install: compile
@echo "=== $(INTEGRATION) === [ install ]: installing bin/$(BINARY_NAME)..."
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ require (
github.com/xeipuuv/gojsonschema v1.2.0
gopkg.in/DATA-DOG/go-sqlmock.v1 v1.3.0
gopkg.in/yaml.v3 v3.0.1
github.com/go-viper/mapstructure/v2 v2.2.1
)

require (
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down
9 changes: 9 additions & 0 deletions postgresql-config.yml.sample
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ integrations:
# True if SSL is to be used. Defaults to false.
ENABLE_SSL: "false"

# True is query monitoring should be enabled - defaults to false
# ENABLE_QUERY_MONITORING : "false"

# The threshold in ms which a query must exceed response time to be considered for fetching execution plan - defaults to 1000ms
# QUERY_RESPONSE_TIME_THRESHOLD : "500"

# The no of records for each metric in a interval (max of slow queries can be 20 and for execution plan metrics it can be 30)
# QUERY_COUNT_THRESHOLD : "10"

# True if the SSL certificate should be trusted without validating.
# Setting this to true may open up the monitoring service to MITM attacks.
# Defaults to false.
Expand Down
3 changes: 3 additions & 0 deletions src/args/argument_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type ArgumentList struct {
CollectDbLockMetrics bool `default:"false" help:"If true, enables collection of lock metrics for the specified database. (Note: requires that the 'tablefunc' extension is installed)"` //nolint: stylecheck
CollectBloatMetrics bool `default:"true" help:"Enable collecting bloat metrics which can be performance intensive"`
ShowVersion bool `default:"false" help:"Print build information and exit"`
EnableQueryMonitoring bool `default:"false" help:"Query monitoring is disabled by default. Set to true to enable."`
QueryResponseTimeThreshold int `default:"500" help:"Threshold in milliseconds for query response time to fetch individual query performance metrics."`
QueryCountThreshold int `default:"20" help:"Query count limit for fetch query monitoring level metrics."`
}

// Validate validates PostgreSQl arguments
Expand Down
3 changes: 1 addition & 2 deletions src/connection/pgsql_connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ func (p PGSQLConnection) HaveExtensionInSchema(extensionName, schemaName string)
return true
}

// createConnectionURL creates the connection string. A list of paramters
// createConnectionURL creates the connection string. A list of parameters
// can be found here https://godoc.org/github.com/lib/pq#hdr-Connection_String_Parameters
func createConnectionURL(ci *connectionInfo, database string) string {
connectionURL := &url.URL{
Expand All @@ -170,7 +170,6 @@ func createConnectionURL(ci *connectionInfo, database string) string {
}

connectionURL.RawQuery = query.Encode()

return connectionURL.String()
}

Expand Down
9 changes: 8 additions & 1 deletion src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"runtime"
"strings"

queryperformancemonitoring "github.com/newrelic/nri-postgresql/src/query-performance-monitoring"

"github.com/newrelic/infra-integrations-sdk/v3/integration"
"github.com/newrelic/infra-integrations-sdk/v3/log"
"github.com/newrelic/nri-postgresql/src/args"
Expand All @@ -27,6 +29,7 @@ var (
)

func main() {

var args args.ArgumentList
// Create Integration
pgIntegration, err := integration.New(integrationName, integrationVersion, integration.Args(&args))
Expand Down Expand Up @@ -62,7 +65,6 @@ func main() {
log.Error("Error creating list of entities to collect: %s", err)
os.Exit(1)
}

instance, err := pgIntegration.Entity(fmt.Sprintf("%s:%s", args.Hostname, args.Port), "pg-instance")
if err != nil {
log.Error("Error creating instance entity: %s", err.Error())
Expand All @@ -89,4 +91,9 @@ func main() {
if err = pgIntegration.Publish(); err != nil {
log.Error(err.Error())
}

if args.EnableQueryMonitoring && args.HasMetrics() {
queryperformancemonitoring.QueryPerformanceMain(args, pgIntegration, collectionList)
}

}
5 changes: 2 additions & 3 deletions src/metrics/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func PopulateMetrics(
}
defer con.Close()

version, err := collectVersion(con)
version, err := CollectVersion(con)
if err != nil {
log.Error("Metrics collection failed: error collecting version number: %s", err.Error())
return
Expand Down Expand Up @@ -223,15 +223,14 @@ type serverVersionRow struct {
Version string `db:"server_version"`
}

func collectVersion(connection *connection.PGSQLConnection) (*semver.Version, error) {
func CollectVersion(connection *connection.PGSQLConnection) (*semver.Version, error) {
var versionRows []*serverVersionRow
if err := connection.Query(&versionRows, versionQuery); err != nil {
return nil, err
}

re := regexp.MustCompile(`[0-9]+\.[0-9]+(\.[0-9])?`)
version := re.FindString(versionRows[0].Version)

// special cases for ubuntu/debian parsing
//version := versionRows[0].Version
//if strings.Contains(version, "Ubuntu") {
Expand Down
10 changes: 5 additions & 5 deletions src/metrics/version_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func Test_collectVersion(t *testing.T) {
Minor: 3,
}

version, err := collectVersion(testConnection)
version, err := CollectVersion(testConnection)

assert.Nil(t, err)
assert.Equal(t, expected, version)
Expand All @@ -42,7 +42,7 @@ func Test_collectVersion_EnterpriseDB(t *testing.T) {
Patch: 7,
}

version, err := collectVersion(testConnection)
version, err := CollectVersion(testConnection)

assert.Nil(t, err)
assert.Equal(t, expected, version)
Expand All @@ -61,7 +61,7 @@ func Test_collectVersion_Ubuntu(t *testing.T) {
Minor: 4,
}

version, err := collectVersion(testConnection)
version, err := CollectVersion(testConnection)

assert.Nil(t, err)
assert.Equal(t, expected, version)
Expand All @@ -80,7 +80,7 @@ func Test_collectVersion_Debian(t *testing.T) {
Minor: 4,
}

version, err := collectVersion(testConnection)
version, err := CollectVersion(testConnection)

assert.Nil(t, err)
assert.Equal(t, expected, version)
Expand All @@ -94,7 +94,7 @@ func Test_collectVersion_Err(t *testing.T) {

mock.ExpectQuery(versionQuery).WillReturnRows(versionRows)

_, err := collectVersion(testConnection)
_, err := CollectVersion(testConnection)

assert.NotNil(t, err)
}
50 changes: 50 additions & 0 deletions src/query-performance-monitoring/common-utils/common_helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package commonutils

import (
"crypto/rand"
"fmt"
"math/big"
"regexp"
"strings"
"time"

"github.com/newrelic/nri-postgresql/src/collection"
)

// re is a regular expression that matches single-quoted strings, numbers, or double-quoted strings
var re = regexp.MustCompile(`'[^']*'|\d+|".*?"`)

func GetQuotedStringFromArray(array []string) string {
var quotedNames = make([]string, 0)
for _, name := range array {
quotedNames = append(quotedNames, fmt.Sprintf("'%s'", name))
}
return strings.Join(quotedNames, ",")
}

func GetDatabaseListInString(dbList collection.DatabaseList) string {
var databaseNames = make([]string, 0)
for dbName := range dbList {
databaseNames = append(databaseNames, dbName)
}
if len(databaseNames) == 0 {
return ""
}
return GetQuotedStringFromArray(databaseNames)
}

func AnonymizeQueryText(query string) string {
anonymizedQuery := re.ReplaceAllString(query, "?")
return anonymizedQuery
}

// This function is used to generate a unique plan ID for a query
func GeneratePlanID(queryID string) *string {
randomInt, err := rand.Int(rand.Reader, big.NewInt(RandomIntRange))
if err != nil {
return nil
}
currentTime := time.Now().Format(TimeFormat)
result := fmt.Sprintf("%s-%d-%s", queryID, randomInt.Int64(), currentTime)
return &result
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package commonutils_test

import (
"sort"
"testing"
"time"

"github.com/newrelic/nri-postgresql/src/collection"
commonutils "github.com/newrelic/nri-postgresql/src/query-performance-monitoring/common-utils"
"github.com/stretchr/testify/assert"
)

func TestGetQuotedStringFromArray(t *testing.T) {
input := []string{"db1", "db2", "db3"}
expected := "'db1','db2','db3'"
result := commonutils.GetQuotedStringFromArray(input)
assert.Equal(t, expected, result)
}

func TestGetDatabaseListInString(t *testing.T) {
dbListKeys := []string{"db1"}
sort.Strings(dbListKeys) // Sort the keys to ensure consistent order
dbList := collection.DatabaseList{}
for _, key := range dbListKeys {
dbList[key] = collection.SchemaList{}
}
expected := "'db1'"
result := commonutils.GetDatabaseListInString(dbList)
assert.Equal(t, expected, result)

// Test with empty database list
dbList = collection.DatabaseList{}
expected = ""
result = commonutils.GetDatabaseListInString(dbList)
assert.Equal(t, expected, result)
}

func TestAnonymizeQueryText(t *testing.T) {
query := "SELECT * FROM users WHERE id = 1 AND name = 'John'"
expected := "SELECT * FROM users WHERE id = ? AND name = ?"
result := commonutils.AnonymizeQueryText(query)
assert.Equal(t, expected, result)
query = "SELECT * FROM employees WHERE id = 10 OR name <> 'John Doe' OR name != 'John Doe' OR age < 30 OR age <= 30 OR salary > 50000OR salary >= 50000 OR department LIKE 'Sales%' OR department ILIKE 'sales%'OR join_date BETWEEN '2023-01-01' AND '2023-12-31' OR department IN ('HR', 'Engineering', 'Marketing') OR department IS NOT NULL OR department IS NULL;"
expected = "SELECT * FROM employees WHERE id = ? OR name <> ? OR name != ? OR age < ? OR age <= ? OR salary > ?OR salary >= ? OR department LIKE ? OR department ILIKE ?OR join_date BETWEEN ? AND ? OR department IN (?, ?, ?) OR department IS NOT NULL OR department IS NULL;"
result = commonutils.AnonymizeQueryText(query)
assert.Equal(t, expected, result)
}

func TestGeneratePlanID(t *testing.T) {
queryID := "query123"
result := commonutils.GeneratePlanID(queryID)
assert.NotNil(t, result)
assert.Contains(t, *result, queryID)
assert.Contains(t, *result, "-")
assert.Contains(t, *result, time.Now().Format(commonutils.TimeFormat)[:8]) // Check date part
}
24 changes: 24 additions & 0 deletions src/query-performance-monitoring/common-utils/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package commonutils

import "errors"

const MaxQueryThreshold = 30
const MaxIndividualQueryThreshold = 10
const PublishThreshold = 100
const RandomIntRange = 1000000
const TimeFormat = "20060102150405"
const VersionRegex = "PostgreSQL (\\d+)\\."

var ErrParseVersion = errors.New("unable to parse PostgreSQL version from string")
var ErrUnsupportedVersion = errors.New("unsupported PostgreSQL version")
var ErrUnExpectedError = errors.New("unexpected error")

var ErrVersionFetchError = errors.New("no rows returned from version query")
var ErrInvalidModelType = errors.New("invalid model type")
var ErrNotEligible = errors.New("not Eligible to fetch metrics")

const PostgresVersion12 = 12
const PostgresVersion11 = 11
const PostgresVersion13 = 13
const PostgresVersion14 = 14
const VersionIndex = 2
Loading

0 comments on commit b243aa1

Please sign in to comment.