Skip to content

Commit 30fcd9e

Browse files
rgeyerblewis12
authored andcommitted
fix: improved postgres version parsing for explain plans in database_observability component (#5131)
* Move semver parsing of postgres version into explain plan collector to facilitate better testing * More guardrails for parsing the postgres versions with semver to support more possible/probable version strings * More useful comments * Add percona version string test
1 parent f45519b commit 30fcd9e

File tree

3 files changed

+71
-38
lines changed

3 files changed

+71
-38
lines changed

internal/component/database_observability/postgres/collector/explain_plan.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ var unrecoverablePostgresSQLErrors = []string{
5050
}
5151

5252
var paramCountRegex = regexp.MustCompile(`\$\d+`)
53+
var versSanitizeRegex = regexp.MustCompile(`^v?[0-9]+\.?[0-9]+`)
5354

5455
type PgSQLExplainplan struct {
5556
Plan PlanNode `json:"Plan"`
@@ -214,7 +215,7 @@ type ExplainPlanArguments struct {
214215
ExcludeSchemas []string
215216
EntryHandler loki.EntryHandler
216217
InitialLookback time.Time
217-
DBVersion semver.Version
218+
DBVersion string
218219

219220
Logger log.Logger
220221
}
@@ -239,10 +240,9 @@ type ExplainPlan struct {
239240
}
240241

241242
func NewExplainPlan(args ExplainPlanArguments) (*ExplainPlan, error) {
242-
return &ExplainPlan{
243+
ep := &ExplainPlan{
243244
dbConnection: args.DB,
244245
dbDSN: args.DSN,
245-
dbVersion: args.DBVersion,
246246
dbConnectionFactory: defaultDbConnectionFactory,
247247
scrapeInterval: args.ScrapeInterval,
248248
queryCache: make(map[string]*queryInfo),
@@ -253,7 +253,16 @@ func NewExplainPlan(args ExplainPlanArguments) (*ExplainPlan, error) {
253253
entryHandler: args.EntryHandler,
254254
logger: log.With(args.Logger, "collector", ExplainPlanCollector),
255255
running: atomic.NewBool(false),
256-
}, nil
256+
}
257+
// Pre-sanitize the version by removing any trailing characters before semver gets it
258+
foundVers := versSanitizeRegex.FindString(args.DBVersion)
259+
engineSemver, err := semver.ParseTolerant(foundVers)
260+
if err != nil {
261+
return ep, fmt.Errorf("failed to parse database engine version: %s: %w", args.DBVersion, err)
262+
}
263+
ep.dbVersion = engineSemver
264+
265+
return ep, nil
257266
}
258267

259268
func (c *ExplainPlan) sendExplainPlansOutput(schemaName string, digest string, generatedAt string, result database_observability.ExplainProcessingResult, reason string, plan *database_observability.ExplainPlanNode) error {

internal/component/database_observability/postgres/collector/explain_plan_test.go

Lines changed: 57 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2255,37 +2255,66 @@ func TestNewExplainPlan(t *testing.T) {
22552255
entryHandler := loki.NewCollectingHandler()
22562256
defer entryHandler.Stop()
22572257

2258-
pre17ver, err := semver.ParseTolerant("14.1")
2259-
require.NoError(t, err)
2258+
t.Run("pre17 version", func(t *testing.T) {
2259+
pre17ver := "14.1"
2260+
pre17semver, err := semver.ParseTolerant(pre17ver)
2261+
require.NoError(t, err)
22602262

2261-
args := ExplainPlanArguments{
2262-
DB: db,
2263-
DSN: "postgres://user:pass@localhost:5432/testdb",
2264-
ScrapeInterval: time.Minute,
2265-
PerScrapeRatio: 0.1,
2266-
ExcludeSchemas: []string{"information_schema", "pg_catalog"},
2267-
EntryHandler: entryHandler,
2268-
InitialLookback: time.Now().Add(-time.Hour),
2269-
DBVersion: pre17ver,
2270-
Logger: logger,
2271-
}
2263+
args := ExplainPlanArguments{
2264+
DB: db,
2265+
DSN: "postgres://user:pass@localhost:5432/testdb",
2266+
ScrapeInterval: time.Minute,
2267+
PerScrapeRatio: 0.1,
2268+
ExcludeSchemas: []string{"information_schema", "pg_catalog"},
2269+
EntryHandler: entryHandler,
2270+
InitialLookback: time.Now().Add(-time.Hour),
2271+
DBVersion: pre17ver,
2272+
Logger: logger,
2273+
}
22722274

2273-
explainPlan, err := NewExplainPlan(args)
2275+
explainPlan, err := NewExplainPlan(args)
22742276

2275-
require.NoError(t, err)
2276-
require.NotNil(t, explainPlan)
2277-
assert.Equal(t, db, explainPlan.dbConnection)
2278-
assert.Equal(t, args.DSN, explainPlan.dbDSN)
2279-
assert.Equal(t, args.DBVersion, explainPlan.dbVersion)
2280-
assert.Equal(t, args.ScrapeInterval, explainPlan.scrapeInterval)
2281-
assert.Equal(t, args.PerScrapeRatio, explainPlan.perScrapeRatio)
2282-
assert.Equal(t, args.ExcludeSchemas, explainPlan.excludeSchemas)
2283-
assert.Equal(t, entryHandler, explainPlan.entryHandler)
2284-
assert.NotNil(t, explainPlan.queryCache)
2285-
assert.NotNil(t, explainPlan.queryDenylist)
2286-
assert.NotNil(t, explainPlan.finishedQueryCache)
2287-
assert.NotNil(t, explainPlan.running)
2288-
assert.False(t, explainPlan.running.Load())
2277+
require.NoError(t, err)
2278+
require.NotNil(t, explainPlan)
2279+
assert.Equal(t, db, explainPlan.dbConnection)
2280+
assert.Equal(t, args.DSN, explainPlan.dbDSN)
2281+
assert.Equal(t, pre17semver, explainPlan.dbVersion)
2282+
assert.Equal(t, args.ScrapeInterval, explainPlan.scrapeInterval)
2283+
assert.Equal(t, args.PerScrapeRatio, explainPlan.perScrapeRatio)
2284+
assert.Equal(t, args.ExcludeSchemas, explainPlan.excludeSchemas)
2285+
assert.Equal(t, entryHandler, explainPlan.entryHandler)
2286+
assert.NotNil(t, explainPlan.queryCache)
2287+
assert.NotNil(t, explainPlan.queryDenylist)
2288+
assert.NotNil(t, explainPlan.finishedQueryCache)
2289+
assert.NotNil(t, explainPlan.running)
2290+
assert.False(t, explainPlan.running.Load())
2291+
})
2292+
2293+
t.Run("version with trailing characters from docker image", func(t *testing.T) {
2294+
args := ExplainPlanArguments{
2295+
DBVersion: "16.10 (Debian 16.10-1.pgdg13+1)",
2296+
}
2297+
2298+
ep, err := NewExplainPlan(args)
2299+
require.NoError(t, err)
2300+
2301+
assert.Equal(t, ep.dbVersion.Major, uint64(16))
2302+
assert.Equal(t, ep.dbVersion.Minor, uint64(10))
2303+
assert.Equal(t, ep.dbVersion.Patch, uint64(0))
2304+
})
2305+
2306+
t.Run("version with trailing characters from percona helm", func(t *testing.T) {
2307+
args := ExplainPlanArguments{
2308+
DBVersion: "17.7 - Percona Server for PostgreSQL 17.7.1",
2309+
}
2310+
2311+
ep, err := NewExplainPlan(args)
2312+
require.NoError(t, err)
2313+
2314+
assert.Equal(t, ep.dbVersion.Major, uint64(17))
2315+
assert.Equal(t, ep.dbVersion.Minor, uint64(7))
2316+
assert.Equal(t, ep.dbVersion.Patch, uint64(0))
2317+
})
22892318
}
22902319

22912320
func TestExplainPlan_Name(t *testing.T) {

internal/component/database_observability/postgres/component.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"sync"
1212
"time"
1313

14-
"github.com/blang/semver/v4"
1514
"github.com/lib/pq"
1615
"github.com/prometheus/client_golang/prometheus"
1716
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -447,17 +446,13 @@ func (c *Component) startCollectors(systemID string, engineVersion string, cloud
447446
c.collectors = append(c.collectors, ciCollector)
448447

449448
if collectors[collector.ExplainPlanCollector] {
450-
engineSemver, err := semver.ParseTolerant(engineVersion)
451-
if err != nil {
452-
logStartError(collector.ExplainPlanCollector, "parse version", err)
453-
}
454449
epCollector, err := collector.NewExplainPlan(collector.ExplainPlanArguments{
455450
DB: c.dbConnection,
456451
DSN: string(c.args.DataSourceName),
457452
ScrapeInterval: c.args.ExplainPlanArguments.CollectInterval,
458453
PerScrapeRatio: c.args.ExplainPlanArguments.PerCollectRatio,
459454
Logger: c.opts.Logger,
460-
DBVersion: engineSemver,
455+
DBVersion: engineVersion,
461456
EntryHandler: entryHandler,
462457
})
463458
if err != nil {

0 commit comments

Comments
 (0)