Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat performance monitoring #189

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
d8d08bb
Query performance monitoring
tharun0064 Dec 30, 2024
de0149a
Refactor : version specific blocking session annonamization (#35)
tharun0064 Jan 8, 2025
0059b78
refactor: resolved review comments (#36)
sjyothi54 Jan 9, 2025
6c367b1
Feat review comments (#37)
tharun0064 Jan 9, 2025
daef2fc
Feat review comments (#39)
sjyothi54 Jan 9, 2025
ab42094
compile issue - resolved
tharun0064 Jan 10, 2025
0a39766
compile issue - resolved
tharun0064 Jan 10, 2025
ac3db49
refactor : rename variable
tharun0064 Jan 10, 2025
92b77ec
Refactor : nil checks (#40)
tharun0064 Jan 10, 2025
47c0ede
Feat : resolved review comments (#42)
tharun0064 Jan 15, 2025
3195e15
resolved: review comments (#44)
tharun0064 Jan 16, 2025
97a13a6
Integration testing (#47)
rahulreddy15 Jan 16, 2025
f972996
Feat : resolved review comments (#48)
tharun0064 Jan 17, 2025
4c59d57
Feat : resolved review comments (#49)
tharun0064 Jan 17, 2025
a6b9816
Feat review comments (#51)
tharun0064 Jan 20, 2025
aea65db
Feat: resolved review comments (#52)
tharun0064 Jan 20, 2025
5c1e5c2
Refactored Integration Tests (#53)
rahulreddy15 Jan 20, 2025
de43d80
Feat review comments (#54)
tharun0064 Jan 21, 2025
86b4368
fix lint issues (#55)
tharun0064 Jan 21, 2025
7a3d042
Feat : resolved review comments (#56)
tharun0064 Jan 22, 2025
80d9b95
Feat: refactored unit test cases (#57)
tharun0064 Jan 22, 2025
65b36dd
Upgrade Go Version
rahulreddy15 Jan 23, 2025
206009d
Feat: resolved review comments (#58)
tharun0064 Jan 27, 2025
b69db10
Feat review comments (#59)
tharun0064 Jan 27, 2025
0d0f083
Feat: resolved review comments (#60)
tharun0064 Jan 27, 2025
9cfbb98
Feat resolved review comments (#61)
tharun0064 Jan 27, 2025
ffa77be
Feat : resolved review comments (#62)
tharun0064 Jan 28, 2025
d0de1a5
Feat review comments (#63)
tharun0064 Jan 28, 2025
473422a
Feat : resolved review comments (#64)
tharun0064 Jan 28, 2025
6928df0
Feat: resolved review comments (#65)
tharun0064 Jan 28, 2025
c5ddea4
Feat : resolved review comments (#66)
tharun0064 Jan 29, 2025
3668db1
remove has metrics (#69)
tharun0064 Feb 4, 2025
ae1612c
Feat review comments (#67)
tharun0064 Feb 5, 2025
6210f0b
resolved: review comments (#70)
tharun0064 Feb 5, 2025
971e12b
Execution plan datamodal fix (#71)
tharun0064 Feb 6, 2025
dace498
Add default description (#72)
tharun0064 Feb 7, 2025
14256a9
Feat review comments (#73)
tharun0064 Feb 11, 2025
a6a5acc
Oldest supprted postgres version is version 13
rahulreddy15 Feb 11, 2025
62f5ec5
Feat : resolved review comments (#74)
tharun0064 Feb 12, 2025
58e636e
Remove unnecasary list command
rahulreddy15 Feb 12, 2025
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
16 changes: 13 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,19 @@ 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 added to allow postgres with test data and extensions to start up
@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 added to allow postgres with test data and extensions to start up
@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"

# Enable query performance monitoring - Defaults to false
# ENABLE_QUERY_MONITORING : "false"

# Threshold in milliseconds for query response time to fetch individual query performance metrics - Defaults to 500
# QUERY_MONITORING_RESPONSE_TIME_THRESHOLD : "500"

# The number of records for each query performance metrics - Defaults to 20
# QUERY_MONITORING_COUNT_THRESHOLD : "20"

# 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
46 changes: 23 additions & 23 deletions src/args/argument_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,46 @@ package args

import (
"errors"

sdkArgs "github.com/newrelic/infra-integrations-sdk/v3/args"
"github.com/newrelic/infra-integrations-sdk/v3/log"
)

// ArgumentList struct that holds all PostgreSQL arguments
type ArgumentList struct {
sdkArgs.DefaultArgumentList
Username string `default:"" help:"The username for the PostgreSQL database"`
Password string `default:"" help:"The password for the specified username"`
Hostname string `default:"localhost" help:"The PostgreSQL hostname to connect to"`
Database string `default:"postgres" help:"The PostgreSQL database name to connect to"`
Port string `default:"5432" help:"The port to connect to the PostgreSQL database"`
CollectionList string `default:"{}" help:"A JSON object which defines the databases, schemas, tables, and indexes to collect. Can also be a JSON array that list databases to be collected. Can also be the string literal 'ALL' to collect everything. Collects nothing by default."`
CollectionIgnoreDatabaseList string `default:"[]" help:"A JSON array that list databases that will be excluded from collection. Nothing is excluded by default."`
CollectionIgnoreTableList string `default:"[]" help:"A JSON array that list tables that will be excluded from collection. Nothing is excluded by default."`
SSLRootCertLocation string `default:"" help:"Absolute path to PEM encoded root certificate file"`
SSLCertLocation string `default:"" help:"Absolute path to PEM encoded client cert file"`
SSLKeyLocation string `default:"" help:"Absolute path to PEM encoded client key file"`
Timeout string `default:"10" help:"Maximum wait for connection, in seconds. Set 0 for no timeout"`
CustomMetricsQuery string `default:"" help:"A SQL query to collect custom metrics. Must have the columns metric_name, metric_type, and metric_value. Additional columns are added as attributes"`
CustomMetricsConfig string `default:"" help:"YAML configuration with one or more custom SQL queries to collect"`
EnableSSL bool `default:"false" help:"If true will use SSL encryption, false will not use encryption"`
TrustServerCertificate bool `default:"false" help:"If true server certificate is not verified for SSL. If false certificate will be verified against supplied certificate"`
Pgbouncer bool `default:"false" help:"Collects metrics from PgBouncer instance. Assumes connection is through PgBouncer."`
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"`
Username string `default:"" help:"The username for the PostgreSQL database"`
Password string `default:"" help:"The password for the specified username"`
Hostname string `default:"localhost" help:"The PostgreSQL hostname to connect to"`
Database string `default:"postgres" help:"The PostgreSQL database name to connect to"`
Port string `default:"5432" help:"The port to connect to the PostgreSQL database"`
CollectionList string `default:"{}" help:"A JSON object which defines the databases, schemas, tables, and indexes to collect. Can also be a JSON array that list databases to be collected. Can also be the string literal 'ALL' to collect everything. Collects nothing by default."`
CollectionIgnoreDatabaseList string `default:"[]" help:"A JSON array that list databases that will be excluded from collection. Nothing is excluded by default."`
CollectionIgnoreTableList string `default:"[]" help:"A JSON array that list tables that will be excluded from collection. Nothing is excluded by default."`
SSLRootCertLocation string `default:"" help:"Absolute path to PEM encoded root certificate file"`
SSLCertLocation string `default:"" help:"Absolute path to PEM encoded client cert file"`
SSLKeyLocation string `default:"" help:"Absolute path to PEM encoded client key file"`
Timeout string `default:"10" help:"Maximum wait for connection, in seconds. Set 0 for no timeout"`
CustomMetricsQuery string `default:"" help:"A SQL query to collect custom metrics. Must have the columns metric_name, metric_type, and metric_value. Additional columns are added as attributes"`
CustomMetricsConfig string `default:"" help:"YAML configuration with one or more custom SQL queries to collect"`
EnableSSL bool `default:"false" help:"If true will use SSL encryption, false will not use encryption"`
TrustServerCertificate bool `default:"false" help:"If true server certificate is not verified for SSL. If false certificate will be verified against supplied certificate"`
Pgbouncer bool `default:"false" help:"Collects metrics from PgBouncer instance. Assumes connection is through PgBouncer."`
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:"Enable collection of detailed query performance metrics."`
QueryMonitoringResponseTimeThreshold int `default:"500" help:"Threshold in milliseconds for query response time. If response time for the individual query exceeds this threshold, the individual query is reported in metrics"`
QueryMonitoringCountThreshold int `default:"20" help:"The number of records for each query performance metrics"`
}

// Validate validates PostgreSQl arguments
func (al ArgumentList) Validate() error {
if al.Username == "" || al.Password == "" {
return errors.New("invalid configuration: must specify a username and password")
}

if err := al.validateSSL(); err != nil {
return err
}

return nil
}

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 {
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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package commonparameters

import (
"github.com/newrelic/infra-integrations-sdk/v3/log"
"github.com/newrelic/nri-postgresql/src/args"
)

// The maximum number records that can be fetched in a single metrics
const MaxQueryCountThreshold = 30

// DefaultQueryMonitoringCountThreshold is the default threshold for the number of queries to monitor.
const DefaultQueryMonitoringCountThreshold = 20

// DefaultQueryResponseTimeThreshold is the default threshold for the response time of a query.
const DefaultQueryResponseTimeThreshold = 500

type CommonParameters struct {
Version uint64
Databases string
QueryMonitoringCountThreshold int
QueryMonitoringResponseTimeThreshold int
Host string
Port string
}

func SetCommonParameters(args args.ArgumentList, version uint64, databases string) *CommonParameters {
return &CommonParameters{
Version: version,
Databases: databases, // comma separated database names
QueryMonitoringCountThreshold: validateAndGetQueryMonitoringCountThreshold(args),
QueryMonitoringResponseTimeThreshold: validateAndGetQueryMonitoringResponseTimeThreshold(args),
Host: args.Hostname,
Port: args.Port,
}
}

func validateAndGetQueryMonitoringResponseTimeThreshold(args args.ArgumentList) int {
if args.QueryMonitoringResponseTimeThreshold < 0 {
log.Warn("QueryResponseTimeThreshold should be greater than or equal to 0 but the input is %d, setting value to default which is %d", args.QueryMonitoringResponseTimeThreshold, DefaultQueryResponseTimeThreshold)
return DefaultQueryResponseTimeThreshold
}
return args.QueryMonitoringResponseTimeThreshold
}

func validateAndGetQueryMonitoringCountThreshold(args args.ArgumentList) int {
if args.QueryMonitoringCountThreshold < 0 {
log.Warn("QueryCountThreshold should be greater than 0 but the input is %d, setting value to default which is %d", args.QueryMonitoringCountThreshold, DefaultQueryMonitoringCountThreshold)
return DefaultQueryMonitoringCountThreshold
}
if args.QueryMonitoringCountThreshold > MaxQueryCountThreshold {
log.Warn("QueryCountThreshold should be less than or equal to max limit but the input is %d, setting value to max limit which is %d", args.QueryMonitoringCountThreshold, MaxQueryCountThreshold)
return MaxQueryCountThreshold
}
return args.QueryMonitoringCountThreshold
}
42 changes: 42 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,42 @@
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 GetDatabaseListInString(dbMap collection.DatabaseList) string {
if len(dbMap) == 0 {
return ""
}
var quotedNames = make([]string, 0)
for dbName := range dbMap {
quotedNames = append(quotedNames, fmt.Sprintf("'%s'", dbName))
}
return strings.Join(quotedNames, ",")
}

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() (string, error) {
randomInt, err := rand.Int(rand.Reader, big.NewInt(RandomIntRange))
if err != nil {
return "", ErrUnExpectedError
}
currentTime := time.Now().Format(TimeFormat)
result := fmt.Sprintf("%d-%s", randomInt.Int64(), currentTime)
return result, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package commonutils

import (
"sort"
"testing"

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

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 := GetDatabaseListInString(dbList)
assert.Equal(t, expected, result)

// Test with empty database list
dbList = collection.DatabaseList{}
expected = ""
result = 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 := 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 = AnonymizeQueryText(query)
assert.Equal(t, expected, result)
}
Loading
Loading