Skip to content

Commit 5b0d8ed

Browse files
cuichenlidd-jasminesun
authored andcommitted
[receiver/postgresql] get query plan for top query collection (open-telemetry#39995)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description We enhanced the top query collection to get some query plan. <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes <!--Describe what testing was performed and which tests were added.--> #### Testing <!--Describe the documentation added.--> #### Documentation <!--Please delete paragraphs that you did not use before submitting.-->
1 parent 57b6b70 commit 5b0d8ed

28 files changed

+495
-295
lines changed
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: postgresqlreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: add the ability to obtain query plan for top n queries
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [39995]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext:
19+
20+
# If your change doesn't affect end users or the exported elements of any package,
21+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
22+
# Optional: The change log or logs in which this entry should be included.
23+
# e.g. '[user]' or '[user, api]'
24+
# Include 'user' if the change is relevant to end users.
25+
# Include 'api' if there is a change to a library API.
26+
# Default: '[user]'
27+
change_logs: [user]

receiver/postgresqlreceiver/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ We provide functionality to collect the most executed queries from postgresql. I
8383
...
8484
```
8585

86+
Along with those attributes, we will also report the query plan we gathered if it is possible.
87+
8688
By default, top query collection is disabled, also note, to use it, you will need
8789
to create the extension to every database. Take the example from `testdata/integration/02-create-extension.sh`
8890

@@ -95,6 +97,12 @@ The following options are available:
9597
- `max_rows_per_query`: (optional, default=1000) The max number of rows would return from the query
9698
against `pg_stat_statements`.
9799
- `top_n_query`: (optional, default=1000) The maximum number of active queries to report (to the next consumer) in a single run.
100+
- `max_explain_each_interval`: (optional, default=1000). The maximum number of explain query to be sent in each scrape interval. The top query
101+
collection would not get the query plan directly. Instead, we need to mimic the query in the database and get the query plan from database
102+
separately. This could lead some resources usage and limit this will reduce the impact on your database.
103+
- `query_plan_cache_size`: (optional, default=1000). The query plan cache size. Once we got explain for one query, we will store it in the cache.
104+
This defines the cache's size for query plan.
105+
- `query_plan_cache_ttl`: (optional, default=1h). How long before the query plan cache got expired. Example values: `1m`, `1h`.
98106

99107
### Example Configuration
100108

receiver/postgresqlreceiver/client.go

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,13 +66,55 @@ type client interface {
6666
getVersion(ctx context.Context) (string, error)
6767
getQuerySamples(ctx context.Context, limit int64, logger *zap.Logger) ([]map[string]any, error)
6868
getTopQuery(ctx context.Context, limit int64, logger *zap.Logger) ([]map[string]any, error)
69+
explainQuery(query string, queryID string, logger *zap.Logger) (string, error)
6970
}
7071

7172
type postgreSQLClient struct {
7273
client *sql.DB
7374
closeFn func() error
7475
}
7576

77+
// explainQuery implements client.
78+
func (c *postgreSQLClient) explainQuery(query string, queryID string, logger *zap.Logger) (string, error) {
79+
normalizedQueryID := strings.ReplaceAll(queryID, "-", "_")
80+
var queryBuilder strings.Builder
81+
var nulls []string
82+
counter := 1
83+
84+
for _, ch := range query {
85+
if ch == '?' {
86+
queryBuilder.WriteString(fmt.Sprintf("$%d", counter))
87+
counter++
88+
nulls = append(nulls, "null")
89+
} else {
90+
queryBuilder.WriteRune(ch)
91+
}
92+
}
93+
94+
preparedQuery := queryBuilder.String()
95+
96+
//nolint:errcheck
97+
defer c.client.Exec(fmt.Sprintf("/* otel-collector-ignore */ DEALLOCATE PREPARE otel_%s", normalizedQueryID))
98+
99+
// if there is no parameter needed, we can not put an empty bracket
100+
nullsString := ""
101+
if len(nulls) > 0 {
102+
nullsString = "(" + strings.Join(nulls, ", ") + ")"
103+
}
104+
setPlanCacheMode := "/* otel-collector-ignore */ SET plan_cache_mode = force_generic_plan;"
105+
prepareStatement := fmt.Sprintf("PREPARE otel_%s AS %s;", normalizedQueryID, preparedQuery)
106+
explainStatement := fmt.Sprintf("EXPLAIN(FORMAT JSON) EXECUTE otel_%s%s;", normalizedQueryID, nullsString)
107+
108+
wrappedDb := sqlquery.NewDbClient(sqlquery.DbWrapper{Db: c.client}, setPlanCacheMode+prepareStatement+explainStatement, logger, sqlquery.TelemetryConfig{})
109+
110+
result, err := wrappedDb.QueryRows(context.Background())
111+
if err != nil {
112+
logger.Error("failed to explain statement", zap.Error(err))
113+
return "", err
114+
}
115+
return obfuscateSQLExecPlan(result[0]["QUERY PLAN"])
116+
}
117+
76118
var _ client = (*postgreSQLClient)(nil)
77119

78120
type postgreSQLConfig struct {
@@ -892,7 +934,7 @@ func (c *postgreSQLClient) getTopQuery(ctx context.Context, limit int64, logger
892934
for _, row := range rows {
893935
hasConvention := map[string]string{
894936
"datname": "db.namespace",
895-
"query": "db.query.text",
937+
"query": QueryTextAttributeName,
896938
}
897939

898940
needConversion := map[string]func(string, string, *zap.Logger) (any, error){

receiver/postgresqlreceiver/config.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ const (
2828
)
2929

3030
type TopQueryCollection struct {
31-
Enabled bool `mapstructure:"enabled"`
32-
MaxRowsPerQuery int64 `mapstructure:"max_rows_per_query"`
33-
TopNQuery int64 `mapstructure:"top_n_query"`
31+
Enabled bool `mapstructure:"enabled"`
32+
MaxRowsPerQuery int64 `mapstructure:"max_rows_per_query"`
33+
TopNQuery int64 `mapstructure:"top_n_query"`
34+
MaxExplainEachInterval int64 `mapstructure:"max_explain_each_interval"`
35+
QueryPlanCacheSize int `mapstructure:"query_plan_cache_size"`
36+
QueryPlanCacheTTL time.Duration `mapstructure:"query_plan_cache_ttl"`
3437
}
3538

3639
type QuerySampleCollection struct {

receiver/postgresqlreceiver/config_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ func TestLoadConfig(t *testing.T) {
132132
expected.QuerySampleCollection.Enabled = true
133133
expected.TopNQuery = 1234
134134
expected.TopQueryCollection.Enabled = true
135+
expected.QueryPlanCacheTTL = time.Second * 123
135136
require.Equal(t, expected, cfg)
136137
})
137138

receiver/postgresqlreceiver/consts.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,8 @@ const (
1717
tempBlksReadColumnName = "temp_blks_read"
1818
tempBlksWrittenColumnName = "temp_blks_written"
1919
)
20+
21+
const (
22+
QueryTextAttributeName = "db.query.text"
23+
DatabaseAttributeName = "db.namespace"
24+
)

receiver/postgresqlreceiver/factory.go

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"time"
99

1010
lru "github.com/hashicorp/golang-lru/v2"
11+
"github.com/hashicorp/golang-lru/v2/expirable"
1112
"go.opentelemetry.io/collector/component"
1213
"go.opentelemetry.io/collector/config/confignet"
1314
"go.opentelemetry.io/collector/config/configtls"
@@ -27,11 +28,19 @@ func newCache(size int) *lru.Cache[string, float64] {
2728
if size <= 0 {
2829
size = 1
2930
}
30-
// lru will only returns error when the size is less than 0
31+
// lru will only return error when the size is less than 0
3132
cache, _ := lru.New[string, float64](size)
3233
return cache
3334
}
3435

36+
func newTTLCache[v any](size int, ttl time.Duration) *expirable.LRU[string, v] {
37+
if size <= 0 {
38+
size = 1
39+
}
40+
cache := expirable.NewLRU[string, v](size, nil, ttl)
41+
return cache
42+
}
43+
3544
func NewFactory() receiver.Factory {
3645
return receiver.NewFactory(
3746
metadata.Type,
@@ -61,9 +70,12 @@ func createDefaultConfig() component.Config {
6170
MaxRowsPerQuery: 1000,
6271
},
6372
TopQueryCollection: TopQueryCollection{
64-
Enabled: false,
65-
TopNQuery: 1000,
66-
MaxRowsPerQuery: 1000,
73+
Enabled: false,
74+
TopNQuery: 1000,
75+
MaxRowsPerQuery: 1000,
76+
MaxExplainEachInterval: 1000,
77+
QueryPlanCacheSize: 1000,
78+
QueryPlanCacheTTL: time.Hour,
6779
},
6880
}
6981
}
@@ -83,7 +95,7 @@ func createMetricsReceiver(
8395
clientFactory = newDefaultClientFactory(cfg)
8496
}
8597

86-
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(1))
98+
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(1), newTTLCache[string](1, time.Second))
8799
s, err := scraper.NewMetrics(ns.scrape, scraper.WithShutdown(ns.shutdown))
88100
if err != nil {
89101
return nil, err
@@ -116,7 +128,7 @@ func createLogsReceiver(
116128
if cfg.QuerySampleCollection.Enabled {
117129
// query sample collection does not need cache, but we do not want to make it
118130
// nil, so create one size 1 cache as a placeholder.
119-
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(1))
131+
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(1), newTTLCache[string](1, time.Second))
120132
s, err := scraper.NewLogs(func(ctx context.Context) (plog.Logs, error) {
121133
return ns.scrapeQuerySamples(ctx, cfg.QuerySampleCollection.MaxRowsPerQuery)
122134
}, scraper.WithShutdown(ns.shutdown))
@@ -133,9 +145,9 @@ func createLogsReceiver(
133145

134146
if cfg.TopQueryCollection.Enabled {
135147
// we have 10 updated only attributes. so we set the cache size accordingly.
136-
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(int(cfg.TopNQuery*10*2)))
148+
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(int(cfg.TopNQuery*10*2)), newTTLCache[string](cfg.QueryPlanCacheSize, cfg.QueryPlanCacheTTL))
137149
s, err := scraper.NewLogs(func(ctx context.Context) (plog.Logs, error) {
138-
return ns.scrapeTopQuery(ctx, cfg.TopQueryCollection.MaxRowsPerQuery, cfg.TopNQuery)
150+
return ns.scrapeTopQuery(ctx, cfg.TopQueryCollection.MaxRowsPerQuery, cfg.TopNQuery, cfg.MaxExplainEachInterval)
139151
}, scraper.WithShutdown(ns.shutdown))
140152
if err != nil {
141153
return nil, err

receiver/postgresqlreceiver/generated_package_test.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

receiver/postgresqlreceiver/go.mod

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ require (
3939

4040
require (
4141
dario.cat/mergo v1.0.1 // indirect
42-
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
42+
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
4343
github.com/DataDog/datadog-go/v5 v5.6.0 // indirect
44-
github.com/DataDog/go-sqllexer v0.1.3 // indirect
44+
github.com/DataDog/go-sqllexer v0.1.4 // indirect
4545
github.com/Microsoft/go-winio v0.6.2 // indirect
4646
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
4747
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -50,7 +50,7 @@ require (
5050
github.com/cpuguy83/dockercfg v0.3.2 // indirect
5151
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
5252
github.com/distribution/reference v0.6.0 // indirect
53-
github.com/docker/docker v28.0.1+incompatible // indirect
53+
github.com/docker/docker v28.1.1+incompatible // indirect
5454
github.com/docker/go-connections v0.5.0 // indirect
5555
github.com/docker/go-units v0.5.0 // indirect
5656
github.com/dustin/go-humanize v1.0.1 // indirect
@@ -60,28 +60,30 @@ require (
6060
github.com/fsnotify/fsnotify v1.9.0 // indirect
6161
github.com/go-logr/logr v1.4.2 // indirect
6262
github.com/go-logr/stdr v1.2.2 // indirect
63-
github.com/go-ole/go-ole v1.2.6 // indirect
63+
github.com/go-ole/go-ole v1.3.0 // indirect
6464
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
6565
github.com/gobwas/glob v0.2.3 // indirect
6666
github.com/gogo/protobuf v1.3.2 // indirect
6767
github.com/google/go-tpm v0.9.5 // indirect
6868
github.com/google/uuid v1.6.0 // indirect
6969
github.com/hashicorp/go-version v1.7.0 // indirect
7070
github.com/json-iterator/go v1.1.12 // indirect
71-
github.com/klauspost/compress v1.17.11 // indirect
71+
github.com/klauspost/compress v1.18.0 // indirect
7272
github.com/knadh/koanf/maps v0.1.2 // indirect
7373
github.com/knadh/koanf/providers/confmap v1.0.0 // indirect
7474
github.com/knadh/koanf/v2 v2.2.0 // indirect
75-
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
75+
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
7676
github.com/magiconair/properties v1.8.10 // indirect
7777
github.com/mitchellh/copystructure v1.2.0 // indirect
7878
github.com/mitchellh/reflectwalk v1.0.2 // indirect
7979
github.com/moby/docker-image-spec v1.3.1 // indirect
80+
github.com/moby/go-archive v0.1.0 // indirect
8081
github.com/moby/patternmatcher v0.6.0 // indirect
81-
github.com/moby/sys/sequential v0.5.0 // indirect
82-
github.com/moby/sys/user v0.1.0 // indirect
82+
github.com/moby/sys/atomicwriter v0.1.0 // indirect
83+
github.com/moby/sys/sequential v0.6.0 // indirect
84+
github.com/moby/sys/user v0.4.0 // indirect
8385
github.com/moby/sys/userns v0.1.0 // indirect
84-
github.com/moby/term v0.5.0 // indirect
86+
github.com/moby/term v0.5.2 // indirect
8587
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
8688
github.com/modern-go/reflect2 v1.0.2 // indirect
8789
github.com/morikuni/aec v1.0.0 // indirect
@@ -91,12 +93,12 @@ require (
9193
github.com/outcaste-io/ristretto v0.2.3 // indirect
9294
github.com/pkg/errors v0.9.1 // indirect
9395
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
94-
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
95-
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
96+
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
97+
github.com/shirou/gopsutil/v4 v4.25.3 // indirect
9698
github.com/sirupsen/logrus v1.9.3 // indirect
9799
github.com/stretchr/objx v0.5.2 // indirect
98-
github.com/tklauser/go-sysconf v0.3.12 // indirect
99-
github.com/tklauser/numcpus v0.6.1 // indirect
100+
github.com/tklauser/go-sysconf v0.3.15 // indirect
101+
github.com/tklauser/numcpus v0.10.0 // indirect
100102
github.com/yusufpapurcu/wmi v1.2.4 // indirect
101103
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
102104
go.opentelemetry.io/collector/consumer/consumererror v0.127.1-0.20250603105141-605011a1fea8 // indirect
@@ -107,7 +109,7 @@ require (
107109
go.opentelemetry.io/collector/receiver/receiverhelper v0.127.1-0.20250603105141-605011a1fea8 // indirect
108110
go.opentelemetry.io/collector/receiver/xreceiver v0.127.1-0.20250603105141-605011a1fea8 // indirect
109111
go.opentelemetry.io/contrib/bridges/otelzap v0.11.0 // indirect
110-
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
112+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect
111113
go.opentelemetry.io/otel v1.36.0 // indirect
112114
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
113115
go.opentelemetry.io/otel/log v0.12.2 // indirect
@@ -121,7 +123,7 @@ require (
121123
golang.org/x/net v0.39.0 // indirect
122124
golang.org/x/sys v0.33.0 // indirect
123125
golang.org/x/text v0.25.0 // indirect
124-
google.golang.org/genproto/googleapis/rpc v0.0.0-20250218202821-56aae31c358a // indirect
126+
google.golang.org/genproto/googleapis/rpc v0.0.0-20250414145226-207652e42e2e // indirect
125127
google.golang.org/grpc v1.72.2 // indirect
126128
google.golang.org/protobuf v1.36.6 // indirect
127129
gopkg.in/yaml.v3 v3.0.1 // indirect

0 commit comments

Comments
 (0)