From a9a0af6795f47714ee3b33a6d17cdd19873890c9 Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Fri, 6 Mar 2026 14:29:26 -0800 Subject: [PATCH 1/4] [receiver/sqlserver] Use shared resource builder for logs and metrics --- receiver/sqlserverreceiver/scraper.go | 27 ++--- receiver/sqlserverreceiver/scraper_test.go | 122 ++++++++++++++++++++- 2 files changed, 129 insertions(+), 20 deletions(-) diff --git a/receiver/sqlserverreceiver/scraper.go b/receiver/sqlserverreceiver/scraper.go index 7eb6a4cd39c70..dc60e495131cd 100644 --- a/receiver/sqlserverreceiver/scraper.go +++ b/receiver/sqlserverreceiver/scraper.go @@ -169,9 +169,8 @@ func (s *sqlServerScraperHelper) Shutdown(context.Context) error { return nil } -// setupResourceBuilder configures common resource attributes for metrics -func (s *sqlServerScraperHelper) setupResourceBuilder(row sqlquery.StringMap) *metadata.ResourceBuilder { - rb := s.mb.NewResourceBuilder() +// setupResourceBuilder configures common resource attributes for metrics and logs. +func (s *sqlServerScraperHelper) setupResourceBuilder(rb *metadata.ResourceBuilder, row sqlquery.StringMap) *metadata.ResourceBuilder { rb.SetSqlserverComputerName(row[computerNameKey]) rb.SetSqlserverInstanceName(row[instanceNameKey]) @@ -224,7 +223,7 @@ func (s *sqlServerScraperHelper) recordDatabaseIOMetrics(ctx context.Context) er now := pcommon.NewTimestampFromTime(time.Now()) var val any for i, row := range rows { - rb := s.setupResourceBuilder(row) + rb := s.setupResourceBuilder(s.mb.NewResourceBuilder(), row) rb.SetSqlserverDatabaseName(row[databaseNameKey]) val, err = retrieveFloat(row, readLatencyMsKey) @@ -308,7 +307,7 @@ func (s *sqlServerScraperHelper) recordDatabasePerfCounterMetrics(ctx context.Co now := pcommon.NewTimestampFromTime(time.Now()) for i, row := range rows { - rb := s.setupResourceBuilder(row) + rb := s.setupResourceBuilder(s.mb.NewResourceBuilder(), row) switch row[counterKey] { case activeTempTables: @@ -579,7 +578,7 @@ func (s *sqlServerScraperHelper) recordDatabaseStatusMetrics(ctx context.Context var errs []error now := pcommon.NewTimestampFromTime(time.Now()) for _, row := range rows { - rb := s.setupResourceBuilder(row) + rb := s.setupResourceBuilder(s.mb.NewResourceBuilder(), row) errs = append(errs, s.mb.RecordSqlserverDatabaseCountDataPoint(now, row[dbOnline], metadata.AttributeDatabaseStatusOnline), @@ -618,7 +617,7 @@ func (s *sqlServerScraperHelper) recordDatabaseWaitMetrics(ctx context.Context) now := pcommon.NewTimestampFromTime(time.Now()) var val any for i, row := range rows { - rb := s.setupResourceBuilder(row) + rb := s.setupResourceBuilder(s.mb.NewResourceBuilder(), row) rb.SetSqlserverDatabaseName(row[databaseNameKey]) val, err = retrieveFloat(row, waitTimeMs) @@ -779,12 +778,7 @@ func (s *sqlServerScraperHelper) recordDatabaseQueryTextAndPlan(ctx context.Cont s.logger.Debug(fmt.Sprintf("QueryHash: %v, PlanHash: %v, DataRow: %v", queryHashVal, queryPlanHashVal, row)) if !resourcesAdded { - resourceAttributes := resources.Attributes() - resourceAttributes.PutStr("host.name", s.config.Server) - resourceAttributes.PutStr("sqlserver.computer.name", row[computerNameKey]) - resourceAttributes.PutStr("sqlserver.instance.name", row[instanceNameKey]) - resourceAttributes.PutStr("service.instance.id", s.serviceInstanceID) - + resources = s.setupResourceBuilder(s.lb.NewResourceBuilder(), row).Emit() resourcesAdded = true } s.lb.RecordDbServerTopQueryEvent( @@ -1073,12 +1067,7 @@ func (s *sqlServerScraperHelper) recordDatabaseSampleQuery(ctx context.Context) ) if !resourcesAdded { - resourceAttributes := resources.Attributes() - resourceAttributes.PutStr("host.name", s.config.Server) - resourceAttributes.PutStr("sqlserver.computer.name", row[computerNameKey]) - resourceAttributes.PutStr("sqlserver.instance.name", row[instanceNameKey]) - resourceAttributes.PutStr("service.instance.id", s.serviceInstanceID) - + resources = s.setupResourceBuilder(s.lb.NewResourceBuilder(), row).Emit() resourcesAdded = true } } diff --git a/receiver/sqlserverreceiver/scraper_test.go b/receiver/sqlserverreceiver/scraper_test.go index 69c402b38ca92..7685925ec766c 100644 --- a/receiver/sqlserverreceiver/scraper_test.go +++ b/receiver/sqlserverreceiver/scraper_test.go @@ -89,6 +89,11 @@ func configureAllScraperMetricsAndEvents(cfg *Config, enabled bool) { // cfg.QuerySample.Enabled = enabled } +func enableLogResourceAttributesForGoldenTests(cfg *Config) { + cfg.LogsBuilderConfig.ResourceAttributes.SqlserverComputerName.Enabled = true + cfg.LogsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true +} + func TestEmptyScrape(t *testing.T) { cfg := createDefaultConfig().(*Config) cfg.Username = "sa" @@ -379,6 +384,7 @@ func TestQueryTextAndPlanQueryMetricsShouldBeCachedSinceFirstCollection(t *testi cfg.Port = 1433 cfg.Server = "0.0.0.0" cfg.MetricsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true + enableLogResourceAttributesForGoldenTests(cfg) cfg.Events.DbServerTopQuery.Enabled = true assert.NoError(t, cfg.Validate()) @@ -454,6 +460,7 @@ func TestQueryTextAndPlanQuery(t *testing.T) { cfg.Port = 1433 cfg.Server = "0.0.0.0" cfg.MetricsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true + enableLogResourceAttributesForGoldenTests(cfg) cfg.Events.DbServerTopQuery.Enabled = true assert.NoError(t, cfg.Validate()) @@ -603,6 +610,7 @@ func TestRecordDatabaseSampleQuery(t *testing.T) { cfg.Port = 1433 cfg.Server = "0.0.0.0" cfg.MetricsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true + enableLogResourceAttributesForGoldenTests(cfg) assert.NoError(t, cfg.Validate()) configureAllScraperMetricsAndEvents(cfg, false) @@ -634,6 +642,118 @@ func TestRecordDatabaseSampleQuery(t *testing.T) { } } +func TestRecordDatabaseSampleQueryUsesResourceBuilderForLogs(t *testing.T) { + cfg := createDefaultConfig().(*Config) + cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" + cfg.LogsBuilderConfig.ResourceAttributes.HostName.Enabled = true + cfg.LogsBuilderConfig.ResourceAttributes.ServerAddress.Enabled = true + cfg.LogsBuilderConfig.ResourceAttributes.ServerPort.Enabled = true + cfg.Events.DbServerQuerySample.Enabled = true + assert.NoError(t, cfg.Validate()) + + configureAllScraperMetricsAndEvents(cfg, false) + cfg.Events.DbServerQuerySample.Enabled = true + + scrapers := setupSQLServerLogsScrapers(receivertest.NewNopSettings(metadata.Type), cfg) + assert.Len(t, scrapers, 1) + + scraper := scrapers[0] + scraper.client = mockClient{ + instanceName: scraper.config.InstanceName, + SQL: scraper.sqlQuery, + maxRowsPerQuery: 100, + } + + actualLogs, err := scraper.ScrapeLogs(t.Context()) + assert.NoError(t, err) + assert.Equal(t, 1, actualLogs.ResourceLogs().Len()) + + resourceAttributes := actualLogs.ResourceLogs().At(0).Resource().Attributes() + hostName, exists := resourceAttributes.Get("host.name") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com", hostName.AsString()) + + serviceInstanceID, exists := resourceAttributes.Get("service.instance.id") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com:1434", serviceInstanceID.AsString()) + + serverAddress, exists := resourceAttributes.Get("server.address") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com", serverAddress.AsString()) + + serverPort, exists := resourceAttributes.Get("server.port") + assert.True(t, exists) + assert.Equal(t, int64(1434), serverPort.Int()) +} + +func TestRecordDatabaseQueryTextAndPlanUsesResourceBuilderForLogs(t *testing.T) { + cfg := createDefaultConfig().(*Config) + cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" + cfg.LogsBuilderConfig.ResourceAttributes.HostName.Enabled = true + cfg.LogsBuilderConfig.ResourceAttributes.ServerAddress.Enabled = true + cfg.LogsBuilderConfig.ResourceAttributes.ServerPort.Enabled = true + cfg.Events.DbServerTopQuery.Enabled = true + assert.NoError(t, cfg.Validate()) + + configureAllScraperMetricsAndEvents(cfg, false) + cfg.Events.DbServerTopQuery.Enabled = true + cfg.TopQueryCollection.CollectionInterval = cfg.ControllerConfig.CollectionInterval + + scrapers := setupSQLServerLogsScrapers(receivertest.NewNopSettings(metadata.Type), cfg) + assert.Len(t, scrapers, 1) + + scraper := scrapers[0] + const totalElapsedTime = "total_elapsed_time" + const rowsReturned = "total_rows" + const totalWorkerTime = "total_worker_time" + const logicalReads = "total_logical_reads" + const logicalWrites = "total_logical_writes" + const physicalReads = "total_physical_reads" + const executionCount = "execution_count" + const totalGrant = "total_grant_kb" + + queryHash := hex.EncodeToString([]byte("0x37849E874171E3F3")) + queryPlanHash := hex.EncodeToString([]byte("0xD3112909429A1B50")) + procedureID := "0" + scraper.cacheAndDiff(queryHash, queryPlanHash, procedureID, totalElapsedTime, 846) + scraper.cacheAndDiff(queryHash, queryPlanHash, procedureID, rowsReturned, 1) + scraper.cacheAndDiff(queryHash, queryPlanHash, procedureID, logicalReads, 1) + scraper.cacheAndDiff(queryHash, queryPlanHash, procedureID, logicalWrites, 1) + scraper.cacheAndDiff(queryHash, queryPlanHash, procedureID, physicalReads, 1) + scraper.cacheAndDiff(queryHash, queryPlanHash, procedureID, executionCount, 1) + scraper.cacheAndDiff(queryHash, queryPlanHash, procedureID, totalWorkerTime, 845) + scraper.cacheAndDiff(queryHash, queryPlanHash, procedureID, totalGrant, 1) + + scraper.client = mockClient{ + instanceName: scraper.config.InstanceName, + SQL: scraper.sqlQuery, + maxQuerySampleCount: 1000, + lookbackTime: 20, + topQueryCount: 200, + } + + actualLogs, err := scraper.ScrapeLogs(t.Context()) + assert.NoError(t, err) + assert.Equal(t, 1, actualLogs.ResourceLogs().Len()) + + resourceAttributes := actualLogs.ResourceLogs().At(0).Resource().Attributes() + hostName, exists := resourceAttributes.Get("host.name") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com", hostName.AsString()) + + serviceInstanceID, exists := resourceAttributes.Get("service.instance.id") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com:1434", serviceInstanceID.AsString()) + + serverAddress, exists := resourceAttributes.Get("server.address") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com", serverAddress.AsString()) + + serverPort, exists := resourceAttributes.Get("server.port") + assert.True(t, exists) + assert.Equal(t, int64(1434), serverPort.Int()) +} + func TestSetupResourceBuilder(t *testing.T) { tests := []struct { name string @@ -695,7 +815,7 @@ func TestSetupResourceBuilder(t *testing.T) { instanceNameKey: "test-instance", } - rb := scraper.setupResourceBuilder(row) + rb := scraper.setupResourceBuilder(scraper.mb.NewResourceBuilder(), row) resource := rb.Emit() hostName, exists := resource.Attributes().Get("host.name") From f8cb045876fe701a846344bf0a146f37e8350331 Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Fri, 6 Mar 2026 16:29:01 -0800 Subject: [PATCH 2/4] Simplify tests --- receiver/sqlserverreceiver/scraper_test.go | 90 +++++++++++++++---- .../expectedQueryTextAndPlanQuery.yaml | 6 ++ .../expectedRecordDatabaseSampleQuery.yaml | 6 ++ ...ordDatabaseSampleQueryWithInvalidData.yaml | 6 ++ 4 files changed, 89 insertions(+), 19 deletions(-) diff --git a/receiver/sqlserverreceiver/scraper_test.go b/receiver/sqlserverreceiver/scraper_test.go index 7685925ec766c..81ef0f20ea668 100644 --- a/receiver/sqlserverreceiver/scraper_test.go +++ b/receiver/sqlserverreceiver/scraper_test.go @@ -89,9 +89,11 @@ func configureAllScraperMetricsAndEvents(cfg *Config, enabled bool) { // cfg.QuerySample.Enabled = enabled } -func enableLogResourceAttributesForGoldenTests(cfg *Config) { - cfg.LogsBuilderConfig.ResourceAttributes.SqlserverComputerName.Enabled = true - cfg.LogsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true +func enableSQLServerResourceAttributesForTests(resourceAttributes *metadata.ResourceAttributesConfig) { + resourceAttributes.SqlserverComputerName.Enabled = true + resourceAttributes.SqlserverInstanceName.Enabled = true + resourceAttributes.ServerAddress.Enabled = true + resourceAttributes.ServerPort.Enabled = true } func TestEmptyScrape(t *testing.T) { @@ -383,8 +385,7 @@ func TestQueryTextAndPlanQueryMetricsShouldBeCachedSinceFirstCollection(t *testi cfg.Password = "password" cfg.Port = 1433 cfg.Server = "0.0.0.0" - cfg.MetricsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true - enableLogResourceAttributesForGoldenTests(cfg) + enableSQLServerResourceAttributesForTests(&cfg.LogsBuilderConfig.ResourceAttributes) cfg.Events.DbServerTopQuery.Enabled = true assert.NoError(t, cfg.Validate()) @@ -459,8 +460,7 @@ func TestQueryTextAndPlanQuery(t *testing.T) { cfg.Password = "password" cfg.Port = 1433 cfg.Server = "0.0.0.0" - cfg.MetricsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true - enableLogResourceAttributesForGoldenTests(cfg) + enableSQLServerResourceAttributesForTests(&cfg.LogsBuilderConfig.ResourceAttributes) cfg.Events.DbServerTopQuery.Enabled = true assert.NoError(t, cfg.Validate()) @@ -609,8 +609,7 @@ func TestRecordDatabaseSampleQuery(t *testing.T) { cfg.Password = "password" cfg.Port = 1433 cfg.Server = "0.0.0.0" - cfg.MetricsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true - enableLogResourceAttributesForGoldenTests(cfg) + enableSQLServerResourceAttributesForTests(&cfg.LogsBuilderConfig.ResourceAttributes) assert.NoError(t, cfg.Validate()) configureAllScraperMetricsAndEvents(cfg, false) @@ -645,9 +644,7 @@ func TestRecordDatabaseSampleQuery(t *testing.T) { func TestRecordDatabaseSampleQueryUsesResourceBuilderForLogs(t *testing.T) { cfg := createDefaultConfig().(*Config) cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" - cfg.LogsBuilderConfig.ResourceAttributes.HostName.Enabled = true - cfg.LogsBuilderConfig.ResourceAttributes.ServerAddress.Enabled = true - cfg.LogsBuilderConfig.ResourceAttributes.ServerPort.Enabled = true + enableSQLServerResourceAttributesForTests(&cfg.LogsBuilderConfig.ResourceAttributes) cfg.Events.DbServerQuerySample.Enabled = true assert.NoError(t, cfg.Validate()) @@ -677,21 +674,19 @@ func TestRecordDatabaseSampleQueryUsesResourceBuilderForLogs(t *testing.T) { assert.True(t, exists) assert.Equal(t, "datasource-host.example.com:1434", serviceInstanceID.AsString()) - serverAddress, exists := resourceAttributes.Get("server.address") + computerName, exists := resourceAttributes.Get("sqlserver.computer.name") assert.True(t, exists) - assert.Equal(t, "datasource-host.example.com", serverAddress.AsString()) + assert.Equal(t, "DESKTOP-GHAEGRD", computerName.AsString()) - serverPort, exists := resourceAttributes.Get("server.port") + instanceName, exists := resourceAttributes.Get("sqlserver.instance.name") assert.True(t, exists) - assert.Equal(t, int64(1434), serverPort.Int()) + assert.Equal(t, "sqlserver", instanceName.AsString()) } func TestRecordDatabaseQueryTextAndPlanUsesResourceBuilderForLogs(t *testing.T) { cfg := createDefaultConfig().(*Config) cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" - cfg.LogsBuilderConfig.ResourceAttributes.HostName.Enabled = true - cfg.LogsBuilderConfig.ResourceAttributes.ServerAddress.Enabled = true - cfg.LogsBuilderConfig.ResourceAttributes.ServerPort.Enabled = true + enableSQLServerResourceAttributesForTests(&cfg.LogsBuilderConfig.ResourceAttributes) cfg.Events.DbServerTopQuery.Enabled = true assert.NoError(t, cfg.Validate()) @@ -745,6 +740,63 @@ func TestRecordDatabaseQueryTextAndPlanUsesResourceBuilderForLogs(t *testing.T) assert.True(t, exists) assert.Equal(t, "datasource-host.example.com:1434", serviceInstanceID.AsString()) + computerName, exists := resourceAttributes.Get("sqlserver.computer.name") + assert.True(t, exists) + assert.Equal(t, "DESKTOP-GHAEGRD", computerName.AsString()) + + instanceName, exists := resourceAttributes.Get("sqlserver.instance.name") + assert.True(t, exists) + assert.Equal(t, "sqlserver", instanceName.AsString()) + + serverAddress, exists := resourceAttributes.Get("server.address") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com", serverAddress.AsString()) + + serverPort, exists := resourceAttributes.Get("server.port") + assert.True(t, exists) + assert.Equal(t, int64(1434), serverPort.Int()) +} + +func TestRecordDatabaseStatusMetricsUsesResourceBuilderForMetrics(t *testing.T) { + cfg := createDefaultConfig().(*Config) + cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" + enableSQLServerResourceAttributesForTests(&cfg.MetricsBuilderConfig.ResourceAttributes) + cfg.Metrics.SqlserverCPUCount.Enabled = true + assert.NoError(t, cfg.Validate()) + + configureAllScraperMetricsAndEvents(cfg, false) + cfg.Metrics.SqlserverCPUCount.Enabled = true + + scrapers := setupSQLServerScrapers(receivertest.NewNopSettings(metadata.Type), cfg) + assert.Len(t, scrapers, 1) + + scraper := scrapers[0] + scraper.client = mockClient{ + instanceName: scraper.config.InstanceName, + SQL: scraper.sqlQuery, + } + + actualMetrics, err := scraper.ScrapeMetrics(t.Context()) + assert.NoError(t, err) + assert.Equal(t, 1, actualMetrics.ResourceMetrics().Len()) + + resourceAttributes := actualMetrics.ResourceMetrics().At(0).Resource().Attributes() + hostName, exists := resourceAttributes.Get("host.name") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com", hostName.AsString()) + + serviceInstanceID, exists := resourceAttributes.Get("service.instance.id") + assert.True(t, exists) + assert.Equal(t, "datasource-host.example.com:1434", serviceInstanceID.AsString()) + + computerName, exists := resourceAttributes.Get("sqlserver.computer.name") + assert.True(t, exists) + assert.Equal(t, "abcde", computerName.AsString()) + + instanceName, exists := resourceAttributes.Get("sqlserver.instance.name") + assert.True(t, exists) + assert.Equal(t, "ad8fb2b53dce", instanceName.AsString()) + serverAddress, exists := resourceAttributes.Get("server.address") assert.True(t, exists) assert.Equal(t, "datasource-host.example.com", serverAddress.AsString()) diff --git a/receiver/sqlserverreceiver/testdata/expectedQueryTextAndPlanQuery.yaml b/receiver/sqlserverreceiver/testdata/expectedQueryTextAndPlanQuery.yaml index b68c276bed762..94a536da65d70 100644 --- a/receiver/sqlserverreceiver/testdata/expectedQueryTextAndPlanQuery.yaml +++ b/receiver/sqlserverreceiver/testdata/expectedQueryTextAndPlanQuery.yaml @@ -4,6 +4,12 @@ resourceLogs: - key: host.name value: stringValue: 0.0.0.0 + - key: server.address + value: + stringValue: 0.0.0.0 + - key: server.port + value: + intValue: "1433" - key: service.instance.id value: stringValue: 0.0.0.0:1433 diff --git a/receiver/sqlserverreceiver/testdata/expectedRecordDatabaseSampleQuery.yaml b/receiver/sqlserverreceiver/testdata/expectedRecordDatabaseSampleQuery.yaml index b0babe6d17b09..0214732dd6266 100644 --- a/receiver/sqlserverreceiver/testdata/expectedRecordDatabaseSampleQuery.yaml +++ b/receiver/sqlserverreceiver/testdata/expectedRecordDatabaseSampleQuery.yaml @@ -4,6 +4,12 @@ resourceLogs: - key: host.name value: stringValue: 0.0.0.0 + - key: server.address + value: + stringValue: 0.0.0.0 + - key: server.port + value: + intValue: "1433" - key: service.instance.id value: stringValue: 0.0.0.0:1433 diff --git a/receiver/sqlserverreceiver/testdata/expectedRecordDatabaseSampleQueryWithInvalidData.yaml b/receiver/sqlserverreceiver/testdata/expectedRecordDatabaseSampleQueryWithInvalidData.yaml index c91eabef757bc..e00bf0f4e742e 100644 --- a/receiver/sqlserverreceiver/testdata/expectedRecordDatabaseSampleQueryWithInvalidData.yaml +++ b/receiver/sqlserverreceiver/testdata/expectedRecordDatabaseSampleQueryWithInvalidData.yaml @@ -4,6 +4,12 @@ resourceLogs: - key: host.name value: stringValue: 0.0.0.0 + - key: server.address + value: + stringValue: 0.0.0.0 + - key: server.port + value: + intValue: "1433" - key: service.instance.id value: stringValue: 0.0.0.0:1433 From 63e24a69df859fe061abe4269d39d7d49e8d3526 Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Fri, 6 Mar 2026 16:40:37 -0800 Subject: [PATCH 3/4] Add changelog --- .chloggen/42355-fix-hostname.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .chloggen/42355-fix-hostname.yaml diff --git a/.chloggen/42355-fix-hostname.yaml b/.chloggen/42355-fix-hostname.yaml new file mode 100644 index 0000000000000..6d8691d9ce5d4 --- /dev/null +++ b/.chloggen/42355-fix-hostname.yaml @@ -0,0 +1,5 @@ +change_type: bug_fix +component: receiver/sqlserver +note: Add missing `host.name` for logs when using `datasource` configuration +issues: [46740] +change_logs: [user] From 49594a43b091c37b09e8460d113a20a9e7d452a8 Mon Sep 17 00:00:00 2001 From: Sam Xie Date: Mon, 9 Mar 2026 11:32:29 -0700 Subject: [PATCH 4/4] Resolve conflict --- receiver/sqlserverreceiver/scraper_test.go | 297 +++++++++++---------- 1 file changed, 153 insertions(+), 144 deletions(-) diff --git a/receiver/sqlserverreceiver/scraper_test.go b/receiver/sqlserverreceiver/scraper_test.go index c38726ef65a2f..cc828e1a6abb1 100644 --- a/receiver/sqlserverreceiver/scraper_test.go +++ b/receiver/sqlserverreceiver/scraper_test.go @@ -657,6 +657,159 @@ func TestRecordDatabaseSampleQuery(t *testing.T) { } } +// TestMultiStatementProcNoDuplicateRows validates that a stored procedure +// containing multiple SELECT statements (each with a distinct query_hash / +// query_plan_hash but sharing the same plan_handle) produces exactly one +// log record per statement -- not duplicated rows caused by a 1:N join on +// plan_handle alone. +func TestMultiStatementProcNoDuplicateRows(t *testing.T) { + cfg := createDefaultConfig().(*Config) + cfg.Username = "sa" + cfg.Password = "password" + cfg.Port = 1433 + cfg.Server = "0.0.0.0" + cfg.MetricsBuilderConfig.ResourceAttributes.SqlserverInstanceName.Enabled = true + cfg.Events.DbServerTopQuery.Enabled = true + assert.NoError(t, cfg.Validate()) + + configureAllScraperMetricsAndEvents(cfg, false) + cfg.Events.DbServerTopQuery.Enabled = true + cfg.TopQueryCollection.CollectionInterval = cfg.ControllerConfig.CollectionInterval + + scrapers := setupSQLServerLogsScrapers(receivertest.NewNopSettings(metadata.Type), cfg) + assert.NotNil(t, scrapers) + + scraper := scrapers[0] + assert.NotNil(t, scraper.cache) + + // Seed the cache so that cacheAndDiff returns non-zero diffs (simulates a + // prior scrape). Use the hex-encoded query_hash values from the mock data. + stmt1Hash := hex.EncodeToString([]byte("0xAAAAAAAAAAAAAAAA")) + stmt1PlanHash := hex.EncodeToString([]byte("0xBBBBBBBBBBBBBBBB")) + stmt2Hash := hex.EncodeToString([]byte("0xCCCCCCCCCCCCCCCC")) + stmt2PlanHash := hex.EncodeToString([]byte("0xDDDDDDDDDDDDDDDD")) + procID := "1431676148" + + for _, pair := range [][2]string{{stmt1Hash, stmt1PlanHash}, {stmt2Hash, stmt2PlanHash}} { + scraper.cacheAndDiff(pair[0], pair[1], procID, "execution_count", 1) + scraper.cacheAndDiff(pair[0], pair[1], procID, "total_elapsed_time", 1) + scraper.cacheAndDiff(pair[0], pair[1], procID, "total_grant_kb", 1) + scraper.cacheAndDiff(pair[0], pair[1], procID, "total_logical_reads", 1) + scraper.cacheAndDiff(pair[0], pair[1], procID, "total_logical_writes", 1) + scraper.cacheAndDiff(pair[0], pair[1], procID, "total_physical_reads", 1) + scraper.cacheAndDiff(pair[0], pair[1], procID, "total_rows", 1) + scraper.cacheAndDiff(pair[0], pair[1], procID, "total_worker_time", 1) + } + + scraper.client = mockMultiStatementProcClient{ + mockClient: mockClient{ + instanceName: scraper.config.InstanceName, + SQL: scraper.sqlQuery, + maxQuerySampleCount: 1000, + lookbackTime: 20, + topQueryCount: 200, + }, + } + + actualLogs, err := scraper.ScrapeLogs(t.Context()) + assert.NoError(t, err) + + // The mock data contains exactly 2 rows (two distinct statements inside one + // stored procedure sharing a single plan_handle). Before the fix, a join on + // plan_handle alone would fan these into 4 rows. After the fix the join + // additionally matches on query_hash + query_plan_hash, keeping the count + // at 2. Verify we get exactly 2 log records. + assert.Equal(t, 2, actualLogs.LogRecordCount(), + "Expected exactly 2 log records for 2 distinct statements; duplicates indicate the plan_handle join is too broad") + + // Verify both records are top_query events. + scopeLogs := actualLogs.ResourceLogs().At(0).ScopeLogs().At(0) + for i := 0; i < scopeLogs.LogRecords().Len(); i++ { + assert.Equal(t, "db.server.top_query", scopeLogs.LogRecords().At(i).EventName()) + } + + // Collect query_hash attribute values and verify they are distinct. + seenHashes := make(map[string]bool) + for i := 0; i < scopeLogs.LogRecords().Len(); i++ { + qh, ok := scopeLogs.LogRecords().At(i).Attributes().Get("sqlserver.query_hash") + assert.True(t, ok) + seenHashes[qh.Str()] = true + } + assert.Len(t, seenHashes, 2, + "Expected 2 distinct query_hash values, got duplicates") +} + +func TestSetupResourceBuilder(t *testing.T) { + tests := []struct { + name string + config *Config + expectedHostName string + }{ + { + name: "with server configuration", + config: func() *Config { + cfg := createDefaultConfig().(*Config) + cfg.Server = "testserver.example.com" + cfg.Port = 1433 + cfg.MetricsBuilderConfig.ResourceAttributes.HostName.Enabled = true + return cfg + }(), + expectedHostName: "testserver.example.com", + }, + { + name: "with datasource configuration", + config: func() *Config { + cfg := createDefaultConfig().(*Config) + cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" + cfg.MetricsBuilderConfig.ResourceAttributes.HostName.Enabled = true + return cfg + }(), + expectedHostName: "datasource-host.example.com", + }, + { + name: "with datasource default port", + config: func() *Config { + cfg := createDefaultConfig().(*Config) + cfg.DataSource = "sqlserver://testuser:testpass@datasource-host2.example.com?database=testdb" + cfg.MetricsBuilderConfig.ResourceAttributes.HostName.Enabled = true + return cfg + }(), + expectedHostName: "datasource-host2.example.com", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + settings := receivertest.NewNopSettings(metadata.Type) + scraper := newSQLServerScraper( + settings.ID, + "SELECT 1", + sqlquery.TelemetryConfig{}, + func() (*sql.DB, error) { return nil, nil }, + func(_ sqlquery.Db, _ string, _ *zap.Logger, _ sqlquery.TelemetryConfig) sqlquery.DbClient { + return nil + }, + settings, + tt.config, + nil, + ) + scraper.mb = metadata.NewMetricsBuilder(tt.config.MetricsBuilderConfig, settings) + + row := sqlquery.StringMap{ + computerNameKey: "test-computer", + instanceNameKey: "test-instance", + } + + rb := scraper.setupResourceBuilder(scraper.mb.NewResourceBuilder(), row) + resource := rb.Emit() + + hostName, exists := resource.Attributes().Get("host.name") + assert.True(t, exists) + assert.Equal(t, tt.expectedHostName, hostName.AsString()) + }) + } +} + func TestRecordDatabaseSampleQueryUsesResourceBuilderForLogs(t *testing.T) { cfg := createDefaultConfig().(*Config) cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" @@ -773,79 +926,6 @@ func TestRecordDatabaseQueryTextAndPlanUsesResourceBuilderForLogs(t *testing.T) assert.Equal(t, int64(1434), serverPort.Int()) } -// TestMultiStatementProcNoDuplicateRows validates that a stored procedure -// containing multiple SELECT statements (each with a distinct query_hash / -// query_plan_hash but sharing the same plan_handle) produces exactly one -// log record per statement, without duplicate rows from a broad join. -func TestMultiStatementProcNoDuplicateRows(t *testing.T) { - cfg := createDefaultConfig().(*Config) - cfg.Username = "sa" - cfg.Password = "password" - cfg.Port = 1433 - cfg.Server = "0.0.0.0" - enableSQLServerResourceAttributesForTests(&cfg.LogsBuilderConfig.ResourceAttributes) - cfg.Events.DbServerTopQuery.Enabled = true - assert.NoError(t, cfg.Validate()) - - configureAllScraperMetricsAndEvents(cfg, false) - cfg.Events.DbServerTopQuery.Enabled = true - cfg.TopQueryCollection.CollectionInterval = cfg.ControllerConfig.CollectionInterval - - scrapers := setupSQLServerLogsScrapers(receivertest.NewNopSettings(metadata.Type), cfg) - assert.NotNil(t, scrapers) - - scraper := scrapers[0] - assert.NotNil(t, scraper.cache) - - // Seed the cache so cacheAndDiff returns non-zero diffs, simulating a prior scrape. - stmt1Hash := hex.EncodeToString([]byte("0xAAAAAAAAAAAAAAAA")) - stmt1PlanHash := hex.EncodeToString([]byte("0xBBBBBBBBBBBBBBBB")) - stmt2Hash := hex.EncodeToString([]byte("0xCCCCCCCCCCCCCCCC")) - stmt2PlanHash := hex.EncodeToString([]byte("0xDDDDDDDDDDDDDDDD")) - procID := "1431676148" - - for _, pair := range [][2]string{{stmt1Hash, stmt1PlanHash}, {stmt2Hash, stmt2PlanHash}} { - scraper.cacheAndDiff(pair[0], pair[1], procID, "execution_count", 1) - scraper.cacheAndDiff(pair[0], pair[1], procID, "total_elapsed_time", 1) - scraper.cacheAndDiff(pair[0], pair[1], procID, "total_grant_kb", 1) - scraper.cacheAndDiff(pair[0], pair[1], procID, "total_logical_reads", 1) - scraper.cacheAndDiff(pair[0], pair[1], procID, "total_logical_writes", 1) - scraper.cacheAndDiff(pair[0], pair[1], procID, "total_physical_reads", 1) - scraper.cacheAndDiff(pair[0], pair[1], procID, "total_rows", 1) - scraper.cacheAndDiff(pair[0], pair[1], procID, "total_worker_time", 1) - } - - scraper.client = mockMultiStatementProcClient{ - mockClient: mockClient{ - instanceName: scraper.config.InstanceName, - SQL: scraper.sqlQuery, - maxQuerySampleCount: 1000, - lookbackTime: 20, - topQueryCount: 200, - }, - } - - actualLogs, err := scraper.ScrapeLogs(t.Context()) - assert.NoError(t, err) - - // Two distinct statements share one plan_handle in the mock data. - assert.Equal(t, 2, actualLogs.LogRecordCount(), - "expected exactly 2 log records for 2 distinct statements; duplicates indicate the plan_handle join is too broad") - - scopeLogs := actualLogs.ResourceLogs().At(0).ScopeLogs().At(0) - for i := 0; i < scopeLogs.LogRecords().Len(); i++ { - assert.Equal(t, "db.server.top_query", scopeLogs.LogRecords().At(i).EventName()) - } - - seenHashes := make(map[string]bool) - for i := 0; i < scopeLogs.LogRecords().Len(); i++ { - qh, ok := scopeLogs.LogRecords().At(i).Attributes().Get("sqlserver.query_hash") - assert.True(t, ok) - seenHashes[qh.Str()] = true - } - assert.Len(t, seenHashes, 2, "expected 2 distinct query_hash values") -} - func TestRecordDatabaseStatusMetricsUsesResourceBuilderForMetrics(t *testing.T) { cfg := createDefaultConfig().(*Config) cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" @@ -894,74 +974,3 @@ func TestRecordDatabaseStatusMetricsUsesResourceBuilderForMetrics(t *testing.T) assert.True(t, exists) assert.Equal(t, int64(1434), serverPort.Int()) } - -func TestSetupResourceBuilder(t *testing.T) { - tests := []struct { - name string - config *Config - expectedHostName string - }{ - { - name: "with server configuration", - config: func() *Config { - cfg := createDefaultConfig().(*Config) - cfg.Server = "testserver.example.com" - cfg.Port = 1433 - cfg.MetricsBuilderConfig.ResourceAttributes.HostName.Enabled = true - return cfg - }(), - expectedHostName: "testserver.example.com", - }, - { - name: "with datasource configuration", - config: func() *Config { - cfg := createDefaultConfig().(*Config) - cfg.DataSource = "sqlserver://testuser:testpass@datasource-host.example.com:1434?database=testdb" - cfg.MetricsBuilderConfig.ResourceAttributes.HostName.Enabled = true - return cfg - }(), - expectedHostName: "datasource-host.example.com", - }, - { - name: "with datasource default port", - config: func() *Config { - cfg := createDefaultConfig().(*Config) - cfg.DataSource = "sqlserver://testuser:testpass@datasource-host2.example.com?database=testdb" - cfg.MetricsBuilderConfig.ResourceAttributes.HostName.Enabled = true - return cfg - }(), - expectedHostName: "datasource-host2.example.com", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - settings := receivertest.NewNopSettings(metadata.Type) - scraper := newSQLServerScraper( - settings.ID, - "SELECT 1", - sqlquery.TelemetryConfig{}, - func() (*sql.DB, error) { return nil, nil }, - func(_ sqlquery.Db, _ string, _ *zap.Logger, _ sqlquery.TelemetryConfig) sqlquery.DbClient { - return nil - }, - settings, - tt.config, - nil, - ) - scraper.mb = metadata.NewMetricsBuilder(tt.config.MetricsBuilderConfig, settings) - - row := sqlquery.StringMap{ - computerNameKey: "test-computer", - instanceNameKey: "test-instance", - } - - rb := scraper.setupResourceBuilder(scraper.mb.NewResourceBuilder(), row) - resource := rb.Emit() - - hostName, exists := resource.Attributes().Get("host.name") - assert.True(t, exists) - assert.Equal(t, tt.expectedHostName, hostName.AsString()) - }) - } -}