Skip to content

Commit 3f73f14

Browse files
authored
[receiver/postgresql] Add receiver.postgresql.useOTelSemconv feature gate for OTel semconv resource model (#45345)
#### Description Adds a new **`receiver.postgresql.useOTelSemconv`** feature gate (alpha, disabled by default) that aligns the PostgreSQL receiver's resource model with [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/database/postgresql/). ##### Feature gate: `receiver.postgresql.useOTelSemconv` **When disabled (default):** Legacy per-entity resource model is preserved. Each database, table, and index emits metrics under a separate resource with `postgresql.database.name`, `postgresql.table.name`, `postgresql.index.name`, and `postgresql.schema.name` as resource attributes. **When enabled:** All metrics from a single PostgreSQL server are emitted under one resource identified by: - **`service.instance.id`**: Deterministic UUID v5 derived from server address and port, using the [OTel namespace UUID](https://opentelemetry.io/docs/specs/semconv/registry/attributes/service/). This ensures the `(service.namespace, service.name, service.instance.id)` triplet is globally unique, which is required for correct Prometheus OTLP conversion. - **`server.address`**: PostgreSQL hostname, IP address, or Unix socket path per semconv. - **`server.port`**: PostgreSQL server port per semconv. Database, schema, table, and index are differentiated through metric-level attributes instead of resource attributes. This gate is mutually exclusive with `receiver.postgresql.separateSchemaAttr` — both cannot be enabled simultaneously. ##### Added metric-level attributes when feature gate is enabled The following metric-level attributes are added to applicable metrics when the feature gate is enabled: - **`db.namespace`**: Database name for database-level metrics; `{database}|{schema}` for table/index/function metrics - **`db.collection.name`**: Table name on table-level metrics #### Link to tracking issue Fixes #45347. #### Testing - Focused semconv and feature-gate tests are added - A semconv integration test is added - Three semconv goldens are added #### Documentation - `documentation.md` regenerated via `mdatagen` to reflect new attributes. - `README.md` updated with new "OpenTelemetry semantic conventions feature gate" section and mutual exclusivity note. - Changelog entry added in `.chloggen/postgresql-service-name.yaml`. <!--Authorship attestation. See AGENTS.md for details. AI agents must not check this box on behalf of the user; the human author must check it themselves before the PR is ready for review.--> #### Authorship - [x] I, a human, wrote this pull request description myself. --------- Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
1 parent c7da9fd commit 3f73f14

23 files changed

Lines changed: 8695 additions & 763 deletions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
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. receiver/filelog)
7+
component: receiver/postgresql
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: |
11+
Align resource and metric attributes with OpenTelemetry semantic conventions behind the
12+
receiver.postgresql.useOTelSemconv feature gate (alpha, disabled by default).
13+
14+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
15+
issues: [45347]
16+
17+
# (Optional) One or more lines of additional information to render under the primary note.
18+
# These lines will be padded with 2 spaces and then inserted directly into the document.
19+
# Use pipe (|) for multiline entries.
20+
subtext: |
21+
- When gate enabled: single resource per server with server.address, server.port, service.instance.id (UUID v5)
22+
- When gate enabled: db.namespace, db.collection.name, postgresql.index.name added as metric-level attributes
23+
- When gate disabled (default): legacy per-entity resource model with postgresql.database.name, etc.
24+
- receiver.postgresql.separateSchemaAttr kept at StageAlpha; mutually exclusive with useOTelSemconv
25+
26+
# If your change doesn't affect end users or the exported elements of any package,
27+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
28+
# Optional: The change log or logs in which this entry should be included.
29+
# e.g. '[user]' or '[user, api]'
30+
# Include 'user' if the change is relevant to end users.
31+
# Include 'api' if there is a change to a library API.
32+
# Default: '[user]'
33+
change_logs: [user]

receiver/postgresqlreceiver/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ The monitoring user must be granted `SELECT` on `pg_stat_database`.
4848
> otelcol-contrib --feature-gates=receiver.postgresql.separateSchemaAttr
4949
> ```
5050
>
51+
> **Note:** This gate is mutually exclusive with `receiver.postgresql.useOTelSemconv`. Both cannot be
52+
> enabled at the same time.
53+
>
5154
> See https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/29559 for more details.
5255
5356
## Configuration
@@ -191,6 +194,15 @@ receivers:
191194
max_open: 5
192195
```
193196
197+
## OpenTelemetry semantic conventions feature gate
198+
199+
The feature gate `receiver.postgresql.useOTelSemconv` (alpha, disabled by default) controls the resource model used by this receiver:
200+
201+
- **Gate disabled (default):** Legacy per-entity resource model. Each database, table, and index emits metrics under a separate resource with `postgresql.database.name`, `postgresql.table.name`, `postgresql.index.name`, and `postgresql.schema.name` as resource attributes. `service.instance.id` is in `host:port` format.
202+
- **Gate enabled:** Single resource per server. All metrics are emitted under one resource with `server.address`, `server.port`, and `service.instance.id` (UUID v5) as resource attributes, aligning with OpenTelemetry semantic conventions. Metric-level attributes `db.namespace`, `db.collection.name`, and `postgresql.index.name` are present on applicable metrics.
203+
204+
This gate is mutually exclusive with `receiver.postgresql.separateSchemaAttr` — both cannot be enabled simultaneously.
205+
194206
## Metrics
195207
196208
Details about the metrics produced by this receiver can be found in [metadata.yaml](./metadata.yaml)

receiver/postgresqlreceiver/documentation.md

Lines changed: 141 additions & 3 deletions
Large diffs are not rendered by default.

receiver/postgresqlreceiver/factory.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,10 @@ func createMetricsReceiver(
9595
clientFactory = newDefaultClientFactory(cfg)
9696
}
9797

98-
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(1), newTTLCache[string](1, time.Second))
98+
ns, err := newPostgreSQLScraper(params, cfg, clientFactory, newCache(1), newTTLCache[string](1, time.Second))
99+
if err != nil {
100+
return nil, err
101+
}
99102
s, err := scraper.NewMetrics(ns.scrape, scraper.WithShutdown(ns.shutdown))
100103
if err != nil {
101104
return nil, err
@@ -128,7 +131,10 @@ func createLogsReceiver(
128131
if cfg.Events.DbServerQuerySample.Enabled {
129132
// query sample collection does not need cache, but we do not want to make it
130133
// nil, so create one size 1 cache as a placeholder.
131-
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(1), newTTLCache[string](1, time.Second))
134+
ns, err := newPostgreSQLScraper(params, cfg, clientFactory, newCache(1), newTTLCache[string](1, time.Second))
135+
if err != nil {
136+
return nil, err
137+
}
132138
s, err := scraper.NewLogs(func(ctx context.Context) (plog.Logs, error) {
133139
return ns.scrapeQuerySamples(ctx, cfg.QuerySampleCollection.MaxRowsPerQuery)
134140
}, scraper.WithShutdown(ns.shutdown))
@@ -145,7 +151,10 @@ func createLogsReceiver(
145151

146152
if cfg.Events.DbServerTopQuery.Enabled {
147153
// we have 10 updated only attributes. so we set the cache size accordingly.
148-
ns := newPostgreSQLScraper(params, cfg, clientFactory, newCache(int(cfg.TopNQuery*10*2)), newTTLCache[string](cfg.QueryPlanCacheSize, cfg.QueryPlanCacheTTL))
154+
ns, err := newPostgreSQLScraper(params, cfg, clientFactory, newCache(int(cfg.TopNQuery*10*2)), newTTLCache[string](cfg.QueryPlanCacheSize, cfg.QueryPlanCacheTTL))
155+
if err != nil {
156+
return nil, err
157+
}
149158
s, err := scraper.NewLogs(func(ctx context.Context) (plog.Logs, error) {
150159
return ns.scrapeTopQuery(ctx, cfg.TopQueryCollection.MaxRowsPerQuery, cfg.TopNQuery, cfg.MaxExplainEachInterval, cfg.TopQueryCollection.CollectionInterval)
151160
}, scraper.WithShutdown(ns.shutdown))

receiver/postgresqlreceiver/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/DATA-DOG/go-sqlmock v1.5.2
77
github.com/DataDog/datadog-agent/pkg/obfuscate v0.77.0-devel.0.20260213154712-e02b9359151a
88
github.com/google/go-cmp v0.7.0
9+
github.com/google/uuid v1.6.0
910
github.com/hashicorp/golang-lru/v2 v2.0.7
1011
github.com/lib/pq v1.12.3
1112
github.com/open-telemetry/opentelemetry-collector-contrib/internal/common v0.156.0
@@ -67,7 +68,6 @@ require (
6768
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
6869
github.com/gobwas/glob v0.2.3 // indirect
6970
github.com/google/go-tpm v0.9.8 // indirect
70-
github.com/google/uuid v1.6.0 // indirect
7171
github.com/hashicorp/go-version v1.9.0 // indirect
7272
github.com/json-iterator/go v1.1.12 // indirect
7373
github.com/klauspost/compress v1.18.5 // indirect

receiver/postgresqlreceiver/integration_test.go

Lines changed: 68 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,67 @@ func TestIntegrationWithConnectionPool(t *testing.T) {
6565
t.Run("all_db_connpool", integrationTest("all_db_connpool", []string{}, pre17TestVersion))
6666
}
6767

68-
func integrationTest(name string, databases []string, pgVersion string) func(*testing.T) {
68+
func TestIntegrationSemconv(t *testing.T) {
69+
defer testutil.SetFeatureGateForTest(t, metadata.ReceiverPostgresqlUseOTelSemconvFeatureGate, true)()
70+
defer testutil.SetFeatureGateForTest(t, metadata.ReceiverPostgresqlConnectionPoolFeatureGate, false)()
71+
t.Run("multi_db", integrationTestSemconv("multi_db_semconv", []string{"otel", "otel2"}, pre17TestVersion))
72+
}
73+
74+
func integrationTest(
75+
name string,
76+
databases []string,
77+
pgVersion string,
78+
additionalIgnoredResourceAttributeValues ...string,
79+
) func(*testing.T) {
80+
compareOptions := []pmetrictest.CompareMetricsOption{
81+
pmetrictest.IgnoreResourceAttributeValue("service.instance.id"),
82+
}
83+
for _, attribute := range additionalIgnoredResourceAttributeValues {
84+
compareOptions = append(compareOptions, pmetrictest.IgnoreResourceAttributeValue(attribute))
85+
}
86+
compareOptions = append(compareOptions,
87+
pmetrictest.IgnoreResourceMetricsOrder(),
88+
pmetrictest.IgnoreMetricValues(
89+
"postgresql.backends",
90+
"postgresql.bgwriter.buffers.allocated",
91+
"postgresql.bgwriter.buffers.writes",
92+
"postgresql.bgwriter.checkpoint.count",
93+
"postgresql.bgwriter.duration",
94+
"postgresql.bgwriter.maxwritten",
95+
"postgresql.blks_hit",
96+
"postgresql.blks_read",
97+
"postgresql.blocks_read",
98+
"postgresql.commits",
99+
"postgresql.connection.max",
100+
"postgresql.database.count",
101+
"postgresql.database.locks",
102+
"postgresql.db_size",
103+
"postgresql.deadlocks",
104+
"postgresql.index.scans",
105+
"postgresql.index.size",
106+
"postgresql.operations",
107+
"postgresql.replication.data_delay",
108+
"postgresql.rollbacks",
109+
"postgresql.rows",
110+
"postgresql.sequential_scans",
111+
"postgresql.table.count",
112+
"postgresql.table.size",
113+
"postgresql.table.vacuum.count",
114+
"postgresql.tup_deleted",
115+
"postgresql.tup_fetched",
116+
"postgresql.tup_inserted",
117+
"postgresql.tup_returned",
118+
"postgresql.tup_updated",
119+
"postgresql.wal.age",
120+
"postgresql.wal.delay",
121+
"postgresql.wal.lag",
122+
),
123+
pmetrictest.IgnoreSubsequentDataPoints("postgresql.backends"),
124+
pmetrictest.IgnoreMetricDataPointsOrder(),
125+
pmetrictest.IgnoreStartTimestamp(),
126+
pmetrictest.IgnoreTimestamp(),
127+
)
128+
69129
expectedFile := filepath.Join("testdata", "integration", "expected_"+name+".yaml")
70130
return scraperinttest.NewIntegrationTest(
71131
NewFactory(),
@@ -110,52 +170,14 @@ func integrationTest(name string, databases []string, pgVersion string) func(*te
110170
rCfg.Metrics.PostgresqlDatabaseLocks.Enabled = true
111171
}),
112172
scraperinttest.WithExpectedFile(expectedFile),
113-
scraperinttest.WithCompareOptions(
114-
pmetrictest.IgnoreResourceAttributeValue("service.instance.id"),
115-
pmetrictest.IgnoreResourceMetricsOrder(),
116-
pmetrictest.IgnoreMetricValues(
117-
"postgresql.backends",
118-
"postgresql.bgwriter.buffers.allocated",
119-
"postgresql.bgwriter.buffers.writes",
120-
"postgresql.bgwriter.checkpoint.count",
121-
"postgresql.bgwriter.duration",
122-
"postgresql.bgwriter.maxwritten",
123-
"postgresql.blks_hit",
124-
"postgresql.blks_read",
125-
"postgresql.blocks_read",
126-
"postgresql.commits",
127-
"postgresql.connection.max",
128-
"postgresql.database.count",
129-
"postgresql.database.locks",
130-
"postgresql.db_size",
131-
"postgresql.deadlocks",
132-
"postgresql.index.scans",
133-
"postgresql.index.size",
134-
"postgresql.operations",
135-
"postgresql.replication.data_delay",
136-
"postgresql.rollbacks",
137-
"postgresql.rows",
138-
"postgresql.sequential_scans",
139-
"postgresql.table.count",
140-
"postgresql.table.size",
141-
"postgresql.table.vacuum.count",
142-
"postgresql.tup_deleted",
143-
"postgresql.tup_fetched",
144-
"postgresql.tup_inserted",
145-
"postgresql.tup_returned",
146-
"postgresql.tup_updated",
147-
"postgresql.wal.age",
148-
"postgresql.wal.delay",
149-
"postgresql.wal.lag",
150-
),
151-
pmetrictest.IgnoreSubsequentDataPoints("postgresql.backends"),
152-
pmetrictest.IgnoreMetricDataPointsOrder(),
153-
pmetrictest.IgnoreStartTimestamp(),
154-
pmetrictest.IgnoreTimestamp(),
155-
),
173+
scraperinttest.WithCompareOptions(compareOptions...),
156174
).Run
157175
}
158176

177+
func integrationTestSemconv(name string, databases []string, pgVersion string) func(*testing.T) {
178+
return integrationTest(name, databases, pgVersion, "server.address", "server.port")
179+
}
180+
159181
func TestGetDatabaseTableMetricsIgnoresAccessExclusiveLocks(t *testing.T) {
160182
ci, err := testcontainers.GenericContainer(
161183
t.Context(),
@@ -367,11 +389,12 @@ func TestScrapeLogsFromContainer(t *testing.T) {
367389
}
368390
clientFactory := newDefaultClientFactory(&cfg)
369391

370-
ns := newPostgreSQLScraper(receiver.Settings{
392+
ns, err := newPostgreSQLScraper(receiver.Settings{
371393
TelemetrySettings: component.TelemetrySettings{
372394
Logger: zap.Must(zap.NewProduction()),
373395
},
374396
}, &cfg, clientFactory, newCache(1), newTTLCache[string](1000, time.Second))
397+
assert.NoError(t, err)
375398
plogs, err := ns.scrapeQuerySamples(t.Context(), 30)
376399
assert.NoError(t, err)
377400
logRecords := plogs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords()

0 commit comments

Comments
 (0)