Skip to content

Commit 781c48c

Browse files
authored
[receiver/oracledb] Oracle top query collection interval (#44505)
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue. Ex. Adding a feature - Explain what this achieves.--> #### Description At present, only the SQLServer receiver allows an independent collection interval to be configured for top query metrics. Because the Oracle receiver lacks this option, users must configure it twice—separately for top queries and samples—whenever different collection intervals are required. This PR is to do a similar implementation in Oracle receiver. With this change now the `collection_interval` for top_query_collection can be configured as below. ``` receivers: oracledb: datasource: "oracle://system:[email protected]:1521/XEPDB1" top_query_collection: collection_interval: 60s <--- new config parameter max_query_sample_count: 1000 top_query_count: 200 ``` <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Fixes #44607 <!--Describe what testing was performed and which tests were added.--> #### Testing Unit tests added <!--Describe the documentation added.--> #### Documentation <!--Please delete paragraphs that you did not use before submitting.-->
1 parent daab18f commit 781c48c

File tree

7 files changed

+185
-11
lines changed

7 files changed

+185
-11
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. receiver/filelog)
7+
component: receiver/oracledb
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: Added independent collection interval config for Oracle top query metrics collection
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: [44607]
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/oracledbreceiver/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,3 +94,26 @@ receivers:
9494
enabled: true
9595
```
9696

97+
## Enabling events.
98+
99+
100+
The following is a generic configuration that can be used for the default logs and metrics scraped
101+
by the oracledb receiver.
102+
103+
```yaml
104+
receivers:
105+
oracledb:
106+
collection_interval: 10s # interval for overall collection
107+
datasource: "oracle://otel:password@localhost:51521/XE"
108+
events:
109+
db.server.query_sample:
110+
enabled: true
111+
db.server.top_query:
112+
enabled: true
113+
top_query_collection: # this collection exports the most expensive queries as logs
114+
max_query_sample_count: 1000 # maximum number of samples collected from db to filter the top N
115+
top_query_count: 200 # The maximum number of queries (N) for which the metrics would be reported
116+
collection_interval: 60s # collection interval for top query collection specifically
117+
query_sample_collection: # this collection exports the currently (relate to the query time) executing queries as logs
118+
max_rows_per_query: 100 # the maximum number of samples to bre reported.
119+
```

receiver/oracledbreceiver/config.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"net"
1010
"net/url"
1111
"strconv"
12+
"time"
1213

1314
"go.opentelemetry.io/collector/scraper/scraperhelper"
1415
"go.uber.org/multierr"
@@ -29,8 +30,9 @@ var (
2930
)
3031

3132
type TopQueryCollection struct {
32-
MaxQuerySampleCount uint `mapstructure:"max_query_sample_count"`
33-
TopQueryCount uint `mapstructure:"top_query_count"`
33+
MaxQuerySampleCount uint `mapstructure:"max_query_sample_count"`
34+
TopQueryCount uint `mapstructure:"top_query_count"`
35+
CollectionInterval time.Duration `mapstructure:"collection_interval"`
3436
}
3537

3638
type QuerySample struct {

receiver/oracledbreceiver/config_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func TestValidateInvalidConfigs(t *testing.T) {
117117

118118
func TestCreateDefaultConfig(t *testing.T) {
119119
cfg := createDefaultConfig().(*Config)
120-
assert.Equal(t, 10*time.Second, cfg.CollectionInterval)
120+
assert.Equal(t, 10*time.Second, cfg.ControllerConfig.CollectionInterval)
121121
}
122122

123123
func TestParseConfig(t *testing.T) {

receiver/oracledbreceiver/factory.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ func createDefaultConfig() component.Config {
5050
TopQueryCollection: TopQueryCollection{
5151
MaxQuerySampleCount: 1000,
5252
TopQueryCount: 200,
53+
CollectionInterval: time.Minute,
5354
},
5455
}
5556
}

receiver/oracledbreceiver/scraper.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111
"encoding/json"
1212
"errors"
1313
"fmt"
14+
"math"
1415
"net"
1516
"os"
1617
"sort"
@@ -133,6 +134,7 @@ type oracleScraper struct {
133134
obfuscator *obfuscator
134135
querySampleCfg QuerySample
135136
serviceInstanceID string
137+
lastExecutionTimestamp time.Time
136138
}
137139

138140
func newScraper(metricsBuilder *metadata.MetricsBuilder, metricsBuilderConfig metadata.MetricsBuilderConfig, scrapeCfg scraperhelper.ControllerConfig, logger *zap.Logger, providerFunc dbProviderFunc, clientProviderFunc clientProviderFunc, instanceName, hostName string) (scraper.Metrics, error) {
@@ -529,9 +531,16 @@ func (s *oracleScraper) scrapeLogs(ctx context.Context) (plog.Logs, error) {
529531
var scrapeErrors []error
530532

531533
if s.logsBuilderConfig.Events.DbServerTopQuery.Enabled {
532-
topNCollectionErrors := s.collectTopNMetricData(ctx, logs)
533-
if topNCollectionErrors != nil {
534-
scrapeErrors = append(scrapeErrors, topNCollectionErrors)
534+
currentCollectionTime := time.Now()
535+
lookbackTimeCounter := s.calculateLookbackSeconds()
536+
if lookbackTimeCounter < int(s.topQueryCollectCfg.CollectionInterval.Seconds()) {
537+
s.logger.Debug("Skipping the collection of top queries because collection interval has not yet elapsed.")
538+
} else {
539+
topNCollectionErrors := s.collectTopNMetricData(ctx, logs, currentCollectionTime, lookbackTimeCounter)
540+
if topNCollectionErrors != nil {
541+
scrapeErrors = append(scrapeErrors, topNCollectionErrors)
542+
}
543+
s.lastExecutionTimestamp = currentCollectionTime
535544
}
536545
}
537546

@@ -545,13 +554,11 @@ func (s *oracleScraper) scrapeLogs(ctx context.Context) (plog.Logs, error) {
545554
return logs, errors.Join(scrapeErrors...)
546555
}
547556

548-
func (s *oracleScraper) collectTopNMetricData(ctx context.Context, logs plog.Logs) error {
557+
func (s *oracleScraper) collectTopNMetricData(ctx context.Context, logs plog.Logs, collectionTime time.Time, lookbackTimeSeconds int) error {
549558
var errs []error
550559
// get metrics and query texts from DB
551-
timestamp := pcommon.NewTimestampFromTime(time.Now())
552-
intervalSeconds := int(s.scrapeCfg.CollectionInterval.Seconds())
553560
s.oracleQueryMetricsClient = s.clientProviderFunc(s.db, oracleQueryMetricsSQL, s.logger)
554-
metricRows, metricError := s.oracleQueryMetricsClient.metricRows(ctx, intervalSeconds, s.topQueryCollectCfg.MaxQuerySampleCount)
561+
metricRows, metricError := s.oracleQueryMetricsClient.metricRows(ctx, lookbackTimeSeconds, s.topQueryCollectCfg.MaxQuerySampleCount)
555562

556563
if metricError != nil {
557564
return fmt.Errorf("error executing oracleQueryMetricsSQL: %w", metricError)
@@ -646,7 +653,7 @@ func (s *oracleScraper) collectTopNMetricData(ctx context.Context, logs plog.Log
646653
planString := string(planBytes)
647654

648655
s.lb.RecordDbServerTopQueryEvent(context.Background(),
649-
timestamp,
656+
pcommon.NewTimestampFromTime(collectionTime),
650657
dbSystemNameVal,
651658
s.hostName,
652659
hit.queryText,
@@ -872,3 +879,17 @@ func constructInstanceID(host, port, service string) string {
872879
}
873880
return fmt.Sprintf("%s:%s", host, port)
874881
}
882+
883+
func (s *oracleScraper) calculateLookbackSeconds() int {
884+
if s.lastExecutionTimestamp.IsZero() {
885+
return int(s.topQueryCollectCfg.CollectionInterval.Seconds())
886+
}
887+
888+
// vsqlRefreshLagSec is the buffer to account for v$sql maximum refresh latency (5 seconds) + 5 seconds to offset any collection delays.
889+
// PS: https://docs.oracle.com/en/database/oracle/oracle-database/21/refrn/V-SQL.html
890+
const vsqlRefreshLagSec = 10 * time.Second
891+
892+
return int(math.Ceil(time.Now().
893+
Add(vsqlRefreshLagSec).
894+
Sub(s.lastExecutionTimestamp).Seconds()))
895+
}

receiver/oracledbreceiver/scraper_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"path/filepath"
1313
"strings"
1414
"testing"
15+
"time"
1516

1617
lru "github.com/hashicorp/golang-lru/v2"
1718
"github.com/stretchr/testify/assert"
@@ -291,6 +292,7 @@ func TestScraper_ScrapeTopNLogs(t *testing.T) {
291292
}()
292293
require.NoError(t, err)
293294
expectedQueryPlanFile := filepath.Join("testdata", "expectedQueryTextAndPlanQuery.yaml")
295+
assert.True(t, scrpr.lastExecutionTimestamp.IsZero(), "No value exists on lastExecutionTimestamp before any collection.")
294296

295297
logs, err := scrpr.scrapeLogs(t.Context())
296298

@@ -304,6 +306,8 @@ func TestScraper_ScrapeTopNLogs(t *testing.T) {
304306
assert.NoError(t, errs)
305307
assert.Equal(t, "db.server.top_query", logs.ResourceLogs().At(0).ScopeLogs().At(0).LogRecords().At(0).EventName())
306308
assert.NoError(t, errs)
309+
310+
assert.False(t, scrpr.lastExecutionTimestamp.IsZero(), "lastExecutionTimestamp hasn't set after a successful collection.")
307311
}
308312
})
309313
}
@@ -426,6 +430,102 @@ func TestGetInstanceId(t *testing.T) {
426430
assert.Equal(t, "unknown:1521", localInstanceID)
427431
}
428432

433+
func TestScrapesTopNLogsOnlyWhenIntervalHasElapsed(t *testing.T) {
434+
var metricRowData []metricRow
435+
var logRowData []metricRow
436+
tests := []struct {
437+
name string
438+
dbclientFn func(db *sql.DB, s string, logger *zap.Logger) dbClient
439+
errWanted string
440+
}{
441+
{
442+
name: "valid collection",
443+
dbclientFn: func(_ *sql.DB, s string, _ *zap.Logger) dbClient {
444+
if strings.Contains(s, "V$SQL_PLAN") {
445+
metricRowFile := readFile("oracleQueryPlanData.txt")
446+
unmarshalErr := json.Unmarshal(metricRowFile, &logRowData)
447+
if unmarshalErr == nil {
448+
return &fakeDbClient{
449+
Responses: [][]metricRow{
450+
logRowData,
451+
},
452+
}
453+
}
454+
} else {
455+
metricRowFile := readFile("oracleQueryMetricsData.txt")
456+
unmarshalErr := json.Unmarshal(metricRowFile, &metricRowData)
457+
if unmarshalErr == nil {
458+
return &fakeDbClient{
459+
Responses: [][]metricRow{
460+
metricRowData,
461+
},
462+
}
463+
}
464+
}
465+
return nil
466+
},
467+
},
468+
}
469+
470+
for _, test := range tests {
471+
t.Run(test.name, func(t *testing.T) {
472+
logsCfg := metadata.DefaultLogsBuilderConfig()
473+
logsCfg.Events.DbServerTopQuery.Enabled = true
474+
metricsCfg := metadata.DefaultMetricsBuilderConfig()
475+
lruCache, _ := lru.New[string, map[string]int64](500)
476+
lruCache.Add("fxk8aq3nds8aw:0", cacheValue)
477+
478+
scrpr := oracleScraper{
479+
logger: zap.NewNop(),
480+
mb: metadata.NewMetricsBuilder(metricsCfg, receivertest.NewNopSettings(metadata.Type)),
481+
lb: metadata.NewLogsBuilder(logsCfg, receivertest.NewNopSettings(metadata.Type)),
482+
dbProviderFunc: func() (*sql.DB, error) {
483+
return nil, nil
484+
},
485+
clientProviderFunc: test.dbclientFn,
486+
metricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(),
487+
logsBuilderConfig: metadata.DefaultLogsBuilderConfig(),
488+
metricCache: lruCache,
489+
topQueryCollectCfg: TopQueryCollection{MaxQuerySampleCount: 5000, TopQueryCount: 200},
490+
obfuscator: newObfuscator(),
491+
}
492+
493+
scrpr.logsBuilderConfig.Events.DbServerTopQuery.Enabled = true
494+
scrpr.topQueryCollectCfg.CollectionInterval = 1 * time.Minute
495+
496+
err := scrpr.start(t.Context(), componenttest.NewNopHost())
497+
defer func() {
498+
assert.NoError(t, scrpr.shutdown(t.Context()))
499+
}()
500+
require.NoError(t, err)
501+
502+
assert.True(t, scrpr.lastExecutionTimestamp.IsZero(), "No value should be set for lastExecutionTimestamp before a successful collection")
503+
logsCol1, _ := scrpr.scrapeLogs(t.Context())
504+
assert.Equal(t, 1, logsCol1.ResourceLogs().At(0).ScopeLogs().Len(), "Collection should run when lastExecutionTimestamp is not available")
505+
assert.False(t, scrpr.lastExecutionTimestamp.IsZero(), "A value should be set for lastExecutionTimestamp after a successful collection")
506+
507+
scrpr.lastExecutionTimestamp = scrpr.lastExecutionTimestamp.Add(-10 * time.Second)
508+
logsCol2, err := scrpr.scrapeLogs(t.Context())
509+
assert.Equal(t, 0, logsCol2.ResourceLogs().Len(), "top_query should not be collected until %s elapsed.", scrpr.topQueryCollectCfg.CollectionInterval.String())
510+
require.NoError(t, err)
511+
})
512+
}
513+
}
514+
515+
func TestCalculateLookbackSeconds(t *testing.T) {
516+
collectionInterval := 20 * time.Second
517+
vsqlRefreshLagSec := 10 * time.Second
518+
expectedMinimumLookbackTime := int((collectionInterval + vsqlRefreshLagSec).Seconds())
519+
currentCollectionTime := time.Now()
520+
521+
scrpr := oracleScraper{
522+
lastExecutionTimestamp: currentCollectionTime.Add(-collectionInterval),
523+
}
524+
lookbackTime := scrpr.calculateLookbackSeconds()
525+
526+
assert.LessOrEqual(t, expectedMinimumLookbackTime, lookbackTime, "`lookbackTime` should be minimum %d", expectedMinimumLookbackTime)
527+
}
528+
429529
func readFile(fname string) []byte {
430530
file, err := os.ReadFile(filepath.Join("testdata", fname))
431531
if err != nil {

0 commit comments

Comments
 (0)