Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .chloggen/oracledbreceiver-query-sample-two-pass.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Use this changelog template to create an entry for release notes.

# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
component: receiver/oracledb

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: Speed up query-sample collection by splitting it into two queries so the SQL text/plan lookup no longer joins V$SESSION against the entire cursor cache.

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [49874]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext: |
The session query no longer joins V$SQL (which forced Oracle to scan the whole
shared-pool cursor cache and materialize SQL_FULLTEXT for every cursor). Instead,
the receiver collects the active sessions first, then fetches SQL_FULLTEXT /
CHILD_ADDRESS / PLAN_HASH_VALUE from V$SQL for only those sql_ids via
WHERE SQL_ID IN (...), and joins the results in the collector. Sessions whose
cursor has aged out of the shared pool are skipped, preserving the previous
inner-join semantics. Note: the two queries are issued at slightly different SCNs;
for actively-executing cursors this is immaterial (they are pinned in the shared
pool), but cursors that age out between Pass-1 and Pass-2 will not appear in the
output, which matches the previous inner-join semantics.

# If your change doesn't affect end users or the exported elements of any package,
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
# Optional: The change log or logs in which this entry should be included.
# e.g. '[user]' or '[user, api]'
# Include 'user' if the change is relevant to end users.
# Include 'api' if there is a change to a library API.
# Default: '[user]'
64 changes: 64 additions & 0 deletions receiver/oracledbreceiver/scraper.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,8 @@ const (
var (
//go:embed templates/oracleQuerySampleSql.tmpl
samplesQuery string
//go:embed templates/oracleQuerySampleStatsSql.tmpl
samplesStatsQuery string
//go:embed templates/oracleQueryMetricsAndTextSql.tmpl
oracleQueryMetricsSQL string
//go:embed templates/oracleQueryPlanSql.tmpl
Expand Down Expand Up @@ -1432,6 +1434,10 @@ func (s *oracleScraper) collectQuerySamples(ctx context.Context, logs plog.Logs)
scrapeErrors = append(scrapeErrors, fmt.Errorf("error executing %s: %w", samplesQuery, err))
}

if err := s.enrichSamplesWithSQLStats(ctx, rows); err != nil {
scrapeErrors = append(scrapeErrors, err)
}

rb := s.setupResourceBuilder(s.lb.NewResourceBuilder())

for _, row := range rows {
Expand Down Expand Up @@ -1502,6 +1508,64 @@ func (s *oracleScraper) collectQuerySamples(ctx context.Context, logs plog.Logs)
return errors.Join(scrapeErrors...)
}

func (s *oracleScraper) enrichSamplesWithSQLStats(ctx context.Context, rows []metricRow) error {
const lookupSQLID = "LOOKUP_SQL_ID"
const lookupChildNumber = "LOOKUP_CHILD_NUMBER"

if len(rows) == 0 {
return nil
}

seen := make(map[string]struct{})
var ids []any
for _, row := range rows {
id := row[lookupSQLID]
if id == "" {
continue
}
if _, dup := seen[id]; dup {
continue
}
seen[id] = struct{}{}
ids = append(ids, id)
}
if len(ids) == 0 {
return nil
}

// Oracle IN list is capped at 1000 expressions; batch to stay under it.
// Build :1..:N once up to the max batch size and slice per batch.
const oracleInLimit = 1000
maxPlaceholders := make([]string, min(len(ids), oracleInLimit))
for j := range maxPlaceholders {
maxPlaceholders[j] = fmt.Sprintf(":%d", j+1)
}
stats := make(map[string]metricRow, len(ids))
for i := 0; i < len(ids); i += oracleInLimit {
end := min(i+oracleInLimit, len(ids))
batchIDs := ids[i:end]
sqlQuery := fmt.Sprintf(samplesStatsQuery, strings.Join(maxPlaceholders[:len(batchIDs)], ", "))
statsRows, err := s.clientProviderFunc(s.db, sqlQuery, s.logger).metricRows(ctx, batchIDs...)
if err != nil {
return fmt.Errorf("failed to fetch V$SQL stats for query samples: %w", err)
}
for _, sr := range statsRows {
stats[sr[sqlIDAttr]+":"+sr[childNumberAttr]] = sr
}
}

for _, row := range rows {
st, ok := stats[row[lookupSQLID]+":"+row[lookupChildNumber]]
if !ok {
continue
}
row[sqlTextAttr] = st[sqlTextAttr]
row[childAddressAttr] = st[childAddressAttr]
row[planHashValueAttr] = st[planHashValueAttr]
}
return nil
}

func (s *oracleScraper) collectSessionWaitEvents(ctx context.Context, logs plog.Logs) error {
const sid = "SID"
const serial = "SERIAL#"
Expand Down
193 changes: 155 additions & 38 deletions receiver/oracledbreceiver/scraper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
package oracledbreceiver

import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"log"
"maps"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -834,41 +837,132 @@ func TestScraper_ScrapeTopNLogs(t *testing.T) {

var samplesQueryResponses = map[string][]metricRow{
samplesQuery: {{
"ACTION": "00-0af7651916cd43dd8448eb211c80319c-a7ad6b7169203331-01", "MACHINE": "TEST-MACHINE", "USERNAME": "ADMIN", "SCHEMANAME": "ADMIN", "SQL_ID": "48bc50b6fuz4y", "WAIT_CLASS": "ONE", "WAIT_TIME_SEC": "0.5", "PROCEDURE_NAME": "BLAH", "CHILD_ADDRESS": "SDF3SDF1234D",
"SQL_CHILD_NUMBER": "0", "SID": "675", "SERIAL#": "51295", "SQL_FULLTEXT": "test_query", "OSUSER": "test-user", "PROCESS": "1115", "PROCEDURE_TYPE": "PROCEDURE_TYPE-A", "PROCEDURE_ID": "12345",
"PORT": "54440", "PROGRAM": "Oracle SQL Developer for VS Code", "MODULE": "Oracle SQL Developer for VS Code", "STATUS": "ACTIVE", "STATE": "WAITED KNOWN TIME", "PLAN_HASH_VALUE": "4199919568", "DURATION_SEC": "1", "SERVICE_NAME": "", "DB_NAMESPACE": "",
"ACTION": "00-0af7651916cd43dd8448eb211c80319c-a7ad6b7169203331-01", "MACHINE": "TEST-MACHINE", "USERNAME": "ADMIN", "SCHEMANAME": "ADMIN", "SQL_ID": "48bc50b6fuz4y", "WAIT_CLASS": "ONE", "WAIT_TIME_SEC": "0.5", "PROCEDURE_NAME": "BLAH",
"SQL_CHILD_NUMBER": "0", "LOOKUP_SQL_ID": "48bc50b6fuz4y", "LOOKUP_CHILD_NUMBER": "0", "SID": "675", "SERIAL#": "51295", "OSUSER": "test-user", "PROCESS": "1115", "PROCEDURE_TYPE": "PROCEDURE_TYPE-A", "PROCEDURE_ID": "12345",
"PORT": "54440", "PROGRAM": "Oracle SQL Developer for VS Code", "MODULE": "Oracle SQL Developer for VS Code", "STATUS": "ACTIVE", "STATE": "WAITED KNOWN TIME", "DURATION_SEC": "1", "SERVICE_NAME": "", "DB_NAMESPACE": "",
"SQL_EXEC_START": "2026-01-01T12:00:00Z", "LOGON_TIME": "2026-01-01T12:00:00Z", "SESSION_DURATION_SEC": "0",
"BLOCKING_SESSION": "", "FINAL_BLOCKING_SESSION": "", "BLOCKING_SESSION_STATUS": "", "SECONDS_IN_WAIT": "0",
"BLOCKING_START_TIME": "", "LOCK_TYPE": "", "LOCK_MODE": "", "BLOCKED_OBJECT_OWNER": "", "BLOCKED_OBJECT_NAME": "",
}},
"invalidQuery": {{
"MACHINE": "TEST-MACHINE", "USERNAME": "ADMIN", "SCHEMANAME": "ADMIN", "SQL_ID": "48bc50b6fuz4y",
"SQL_CHILD_NUMBER": "0", "S.SID": "675", "SERIAL#": "51295", "SQL_FULLTEXT": "test_query", "OSUSER": "test-user", "PROCESS": "1115",
"PORT": "54440", "PROGRAM": "Oracle SQL Developer for VS Code", "MODULE": "Oracle SQL Developer for VS Code", "STATUS": "ACTIVE", "STATE": "WAITED KNOWN TIME", "PLAN_HASH_VALUE": "4199919568", "DURATION_SEC": "",
"SQL_CHILD_NUMBER": "0", "LOOKUP_SQL_ID": "48bc50b6fuz4y", "LOOKUP_CHILD_NUMBER": "0", "S.SID": "675", "SERIAL#": "51295", "OSUSER": "test-user", "PROCESS": "1115",
"PORT": "54440", "PROGRAM": "Oracle SQL Developer for VS Code", "MODULE": "Oracle SQL Developer for VS Code", "STATUS": "ACTIVE", "STATE": "WAITED KNOWN TIME", "DURATION_SEC": "",
"SQL_EXEC_START": "", "LOGON_TIME": "", "SESSION_DURATION_SEC": "0",
"BLOCKING_SESSION": "", "FINAL_BLOCKING_SESSION": "", "BLOCKING_SESSION_STATUS": "", "SECONDS_IN_WAIT": "0",
"BLOCKING_START_TIME": "", "LOCK_TYPE": "", "LOCK_MODE": "", "BLOCKED_OBJECT_OWNER": "", "BLOCKED_OBJECT_NAME": "",
}},
"blockedQuery": {{
// Blocked session waiting on SID 100
"ACTION": "", "MACHINE": "DB-CLIENT-HOST", "USERNAME": "APP_USER", "SCHEMANAME": "APP_USER", "SQL_ID": "9fkq2mxyzabc1", "WAIT_CLASS": "Application", "PROCEDURE_NAME": "", "CHILD_ADDRESS": "ABCD1234",
"SQL_CHILD_NUMBER": "0", "SID": "200", "SERIAL#": "12345", "SQL_FULLTEXT": "UPDATE orders SET status = 1 WHERE id = 42", "OSUSER": "oracle", "PROCESS": "9876", "PROCEDURE_TYPE": "", "PROCEDURE_ID": "",
"PORT": "54441", "PROGRAM": "JDBC Thin Client", "MODULE": "app", "STATUS": "ACTIVE", "STATE": "WAITING", "PLAN_HASH_VALUE": "1234567890", "DURATION_SEC": "15", "SERVICE_NAME": "ORCL", "DB_NAMESPACE": "ORCLPDB1",
"ACTION": "", "MACHINE": "DB-CLIENT-HOST", "USERNAME": "APP_USER", "SCHEMANAME": "APP_USER", "SQL_ID": "9fkq2mxyzabc1", "WAIT_CLASS": "Application", "PROCEDURE_NAME": "",
"SQL_CHILD_NUMBER": "0", "LOOKUP_SQL_ID": "9fkq2mxyzabc1", "LOOKUP_CHILD_NUMBER": "0", "SID": "200", "SERIAL#": "12345", "OSUSER": "oracle", "PROCESS": "9876", "PROCEDURE_TYPE": "", "PROCEDURE_ID": "",
"PORT": "54441", "PROGRAM": "JDBC Thin Client", "MODULE": "app", "STATUS": "ACTIVE", "STATE": "WAITING", "DURATION_SEC": "15", "SERVICE_NAME": "ORCL", "DB_NAMESPACE": "ORCLPDB1",
"SQL_EXEC_START": "2026-05-06T09:59:45Z", "LOGON_TIME": "2026-05-06T09:00:00Z", "SESSION_DURATION_SEC": "3600",
"BLOCKING_SESSION": "100", "FINAL_BLOCKING_SESSION": "100", "BLOCKING_SESSION_STATUS": "VALID", "SECONDS_IN_WAIT": "15",
"BLOCKING_START_TIME": "2026-05-06T10:00:00Z", "LOCK_TYPE": "TX", "LOCK_MODE": "EXCLUSIVE", "BLOCKED_OBJECT_OWNER": "APP_USER", "BLOCKED_OBJECT_NAME": "ORDERS",
}},
"idleBlockerQuery": {{
// Idle session (blocker) that is holding a lock — no longer ACTIVE but appearing in BLOCKING_SESSION subquery
"ACTION": "", "MACHINE": "DBA-WORKSTATION", "USERNAME": "DBA_USER", "SCHEMANAME": "DBA_USER", "SQL_ID": "7abc123def456", "WAIT_CLASS": "", "PROCEDURE_NAME": "", "CHILD_ADDRESS": "DEADBEEF",
"SQL_CHILD_NUMBER": "0", "SID": "100", "SERIAL#": "5678", "SQL_FULLTEXT": "UPDATE orders SET status = 2 WHERE id = 42", "OSUSER": "dba", "PROCESS": "1234", "PROCEDURE_TYPE": "", "PROCEDURE_ID": "",
"PORT": "54442", "PROGRAM": "SQL*Plus", "MODULE": "", "STATUS": "INACTIVE", "STATE": "WAITED KNOWN TIME", "PLAN_HASH_VALUE": "9876543210", "DURATION_SEC": "120", "SERVICE_NAME": "ORCL", "DB_NAMESPACE": "ORCLPDB1",
"ACTION": "", "MACHINE": "DBA-WORKSTATION", "USERNAME": "DBA_USER", "SCHEMANAME": "DBA_USER", "SQL_ID": "7abc123def456", "WAIT_CLASS": "", "PROCEDURE_NAME": "",
"SQL_CHILD_NUMBER": "0", "LOOKUP_SQL_ID": "7abc123def456", "LOOKUP_CHILD_NUMBER": "0", "SID": "100", "SERIAL#": "5678", "OSUSER": "dba", "PROCESS": "1234", "PROCEDURE_TYPE": "", "PROCEDURE_ID": "",
"PORT": "54442", "PROGRAM": "SQL*Plus", "MODULE": "", "STATUS": "INACTIVE", "STATE": "WAITED KNOWN TIME", "DURATION_SEC": "120", "SERVICE_NAME": "ORCL", "DB_NAMESPACE": "ORCLPDB1",
"SQL_EXEC_START": "", "LOGON_TIME": "", "SESSION_DURATION_SEC": "0",
"BLOCKING_SESSION": "", "FINAL_BLOCKING_SESSION": "", "BLOCKING_SESSION_STATUS": "", "SECONDS_IN_WAIT": "0",
"BLOCKING_START_TIME": "", "LOCK_TYPE": "", "LOCK_MODE": "", "BLOCKED_OBJECT_OWNER": "", "BLOCKED_OBJECT_NAME": "",
}},
}

var sampleStatsRows = []metricRow{
{"SQL_ID": "48bc50b6fuz4y", "CHILD_NUMBER": "0", "CHILD_ADDRESS": "SDF3SDF1234D", "PLAN_HASH_VALUE": "4199919568", "SQL_FULLTEXT": "test_query"},
{"SQL_ID": "9fkq2mxyzabc1", "CHILD_NUMBER": "0", "CHILD_ADDRESS": "ABCD1234", "PLAN_HASH_VALUE": "1234567890", "SQL_FULLTEXT": "UPDATE orders SET status = 1 WHERE id = 42"},
{"SQL_ID": "7abc123def456", "CHILD_NUMBER": "0", "CHILD_ADDRESS": "DEADBEEF", "PLAN_HASH_VALUE": "9876543210", "SQL_FULLTEXT": "UPDATE orders SET status = 2 WHERE id = 42"},
}

// copy so subtests don't share mutated fixture rows
func cloneRows(rows []metricRow) []metricRow {
out := make([]metricRow, len(rows))
for i, r := range rows {
nr := make(metricRow, len(r))
maps.Copy(nr, r)
out[i] = nr
}
return out
}

func twoPassSampleClient(sessionRows []metricRow) func(*sql.DB, string, *zap.Logger) dbClient {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These tests route through fakeDbClient, whose metricRows(context.Context, ...any) discards both the SQL text and the bind args and returns canned rows. So none of the sample tests validate the batching in enrichSamplesWithSQLStats: every case uses ≤3 sql_ids (one batch), and even at >1000 the fake would return the same rows and hide a placeholder/arg mismatch.

Could you add a test that drives >1000 distinct sql_ids and asserts on the generated SQL + args? Something like:

    // capturingClient records each metricRows call so we can assert on the
    // generated SQL and bind args (fakeDbClient discards both).
    type capturingClient struct {
        sql    string
        calls  *[]capturedCall
        respFn func(args []any) []metricRow
    }
    type capturedCall struct {
        sql  string
        args []any
    }
    func (c *capturingClient) metricRows(_ context.Context, args ...any) ([]metricRow, error) {
        *c.calls = append(*c.calls, capturedCall{sql: c.sql, args: args})
        return c.respFn(args), nil
    }

    func TestEnrichSamplesBatchesInLists(t *testing.T) {
        const total = 2500 // -> 3 batches: 1000, 1000, 500
        sessionRows := make([]metricRow, total)
        statsBySQLID := make(map[string]metricRow, total)
        for i := 0; i < total; i++ {
            id := fmt.Sprintf("sqlid_%04d", i)
            sessionRows[i] = metricRow{"LOOKUP_SQL_ID": id, "LOOKUP_CHILD_NUMBER": "0"}
            statsBySQLID[id] = metricRow{
                "SQL_ID": id, "CHILD_NUMBER": "0", "CHILD_ADDRESS": "ADDR_" + id,
                "PLAN_HASH_VALUE": "1", "SQL_FULLTEXT": "text " + id,
            }
        }

        var calls []capturedCall
        scrpr := oracleScraper{
            logger:         zap.NewNop(),
            dbProviderFunc: func() (*sql.DB, error) { return nil, nil },
            clientProviderFunc: func(_ *sql.DB, q string, _ *zap.Logger) dbClient {
                return &capturingClient{sql: q, calls: &calls, respFn: func(args []any) []metricRow {
                    out := make([]metricRow, 0, len(args))
                    for _, a := range args {
                        if r, ok := statsBySQLID[a.(string)]; ok {
                            out = append(out, r)
                        }
                    }
                    return out
                }}
            },
        }

        require.NoError(t, scrpr.enrichSamplesWithSQLStats(t.Context(), sessionRows))

        require.Len(t, calls, 3)
        wantSizes := []int{1000, 1000, 500}
        for i, call := range calls {
            assert.Len(t, call.args, wantSizes[i])
            assert.Equal(t, wantSizes[i], strings.Count(call.sql, ":"),
                "placeholder count must equal arg count for batch %d", i)
            assert.Contains(t, call.sql, ":1,", "batch %d must start at :1", i)
            assert.Contains(t, call.sql, fmt.Sprintf(":%d)", wantSizes[i]),
                "batch %d must end at :%d", i, wantSizes[i])
        }
        for _, row := range sessionRows {
            assert.Equal(t, "text "+row["LOOKUP_SQL_ID"], row["SQL_FULLTEXT"])
        }
    }

On the current code this fails at batch 2 (:1, and :1000) are absent because it emits :1001..:2000); with the placeholder fix it passes and also proves the cross-batch join reassembles text/plan correctly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test case to cover this scenerio

return func(_ *sql.DB, s string, _ *zap.Logger) dbClient {
if strings.Contains(s, "FROM V$SQL") && strings.Contains(s, "SQL_ID IN") {
return &fakeDbClient{Responses: [][]metricRow{cloneRows(sampleStatsRows)}}
}
return &fakeDbClient{Responses: [][]metricRow{cloneRows(sessionRows)}}
}
}

// capturingClient records each metricRows call so we can assert on the
// generated SQL and bind args (fakeDbClient discards both).
type capturingClient struct {
sql string
calls *[]capturedCall
respFn func(args []any) []metricRow
}

type capturedCall struct {
sql string
args []any
}

func (c *capturingClient) metricRows(_ context.Context, args ...any) ([]metricRow, error) {
*c.calls = append(*c.calls, capturedCall{sql: c.sql, args: args})
return c.respFn(args), nil
}

func TestEnrichSamplesBatchesInLists(t *testing.T) {
const total = 2500 // -> 3 batches: 1000, 1000, 500
sessionRows := make([]metricRow, total)
statsBySQLID := make(map[string]metricRow, total)
for i := range total {
id := fmt.Sprintf("sqlid_%04d", i)
sessionRows[i] = metricRow{"LOOKUP_SQL_ID": id, "LOOKUP_CHILD_NUMBER": "0"}
statsBySQLID[id] = metricRow{
"SQL_ID": id, "CHILD_NUMBER": "0", "CHILD_ADDRESS": "ADDR_" + id,
"PLAN_HASH_VALUE": "1", "SQL_FULLTEXT": "text " + id,
}
}

var calls []capturedCall
scrpr := oracleScraper{
logger: zap.NewNop(),
dbProviderFunc: func() (*sql.DB, error) { return nil, nil },
clientProviderFunc: func(_ *sql.DB, q string, _ *zap.Logger) dbClient {
return &capturingClient{sql: q, calls: &calls, respFn: func(args []any) []metricRow {
out := make([]metricRow, 0, len(args))
for _, a := range args {
if r, ok := statsBySQLID[a.(string)]; ok {
out = append(out, r)
}
}
return out
}}
},
}

require.NoError(t, scrpr.enrichSamplesWithSQLStats(t.Context(), sessionRows))

require.Len(t, calls, 3)
wantSizes := []int{1000, 1000, 500}
for i, call := range calls {
assert.Len(t, call.args, wantSizes[i])
assert.Equal(t, wantSizes[i], strings.Count(call.sql, ":"),
"placeholder count must equal arg count for batch %d", i)
assert.Contains(t, call.sql, ":1,", "batch %d must start at :1", i)
assert.Contains(t, call.sql, fmt.Sprintf(":%d)", wantSizes[i]),
"batch %d must end at :%d", i, wantSizes[i])
}
for _, row := range sessionRows {
assert.Equal(t, "text "+row["LOOKUP_SQL_ID"], row["SQL_FULLTEXT"])
}
}

var sessionEventQueryResponses = map[string][]metricRow{
sessionEventQuery: {
{
Expand Down Expand Up @@ -903,42 +997,24 @@ func TestSamplesQuery(t *testing.T) {
checkBlockingAttr bool
}{
{
name: "valid",
dbclientFn: func(_ *sql.DB, _ string, _ *zap.Logger) dbClient {
return &fakeDbClient{
Responses: [][]metricRow{
samplesQueryResponses[samplesQuery],
},
}
},
name: "valid",
dbclientFn: twoPassSampleClient(samplesQueryResponses[samplesQuery]),
goldenFile: filepath.Join("testdata", "expectedSamplesFile.yaml"),
},
{
name: "bad samples data",
dbclientFn: func(_ *sql.DB, _ string, _ *zap.Logger) dbClient {
return &fakeDbClient{Responses: [][]metricRow{
samplesQueryResponses["invalidQuery"],
}}
},
errWanted: `failed to parse int64 for Duration, value was : strconv.ParseFloat: parsing "": invalid syntax`,
name: "bad samples data",
dbclientFn: twoPassSampleClient(samplesQueryResponses["invalidQuery"]),
errWanted: `failed to parse int64 for Duration, value was : strconv.ParseFloat: parsing "": invalid syntax`,
},
{
name: "blocked session emits blocking attributes",
dbclientFn: func(_ *sql.DB, _ string, _ *zap.Logger) dbClient {
return &fakeDbClient{Responses: [][]metricRow{
samplesQueryResponses["blockedQuery"],
}}
},
name: "blocked session emits blocking attributes",
dbclientFn: twoPassSampleClient(samplesQueryResponses["blockedQuery"]),
goldenFile: filepath.Join("testdata", "expectedBlockedSessionFile.yaml"),
checkBlockingAttr: true,
},
{
name: "idle blocker session emits empty blocking attributes",
dbclientFn: func(_ *sql.DB, _ string, _ *zap.Logger) dbClient {
return &fakeDbClient{Responses: [][]metricRow{
samplesQueryResponses["idleBlockerQuery"],
}}
},
name: "idle blocker session emits empty blocking attributes",
dbclientFn: twoPassSampleClient(samplesQueryResponses["idleBlockerQuery"]),
goldenFile: filepath.Join("testdata", "expectedIdleBlockerFile.yaml"),
},
}
Expand Down Expand Up @@ -1006,6 +1082,47 @@ func TestSamplesQuery(t *testing.T) {
}
}

func TestSamplesQueryCursorAgedOut(t *testing.T) {
logsCfg := metadata.DefaultLogsBuilderConfig()
logsCfg.ResourceAttributes.OracledbInstanceName.Enabled = true
logsCfg.ResourceAttributes.HostName.Enabled = true
logsCfg.Events.DbServerTopQuery.Enabled = false
logsCfg.Events.DbServerQuerySample.Enabled = true

scrpr := oracleScraper{
logger: zap.NewNop(),
dbProviderFunc: func() (*sql.DB, error) { return nil, nil },
clientProviderFunc: func(_ *sql.DB, s string, _ *zap.Logger) dbClient {
if strings.Contains(s, "FROM V$SQL") && strings.Contains(s, "SQL_ID IN") {
// Cursor aged out of the shared pool: no V$SQL row for the session's sql_id.
return &fakeDbClient{Responses: [][]metricRow{{}}}
}
return &fakeDbClient{Responses: [][]metricRow{cloneRows(samplesQueryResponses[samplesQuery])}}
},
id: component.ID{},
lb: metadata.NewLogsBuilder(logsCfg, receivertest.NewNopSettings(metadata.Type)),
logsBuilderConfig: logsCfg,
obfuscator: newObfuscator(),
instanceName: "oraclehost:1521/ORCL",
serviceInstanceID: getInstanceID("oraclehost:1521/ORCL", zap.NewNop()),
}
require.NoError(t, scrpr.start(t.Context(), componenttest.NewNopHost()))
defer func() { assert.NoError(t, scrpr.shutdown(t.Context())) }()

logs, err := scrpr.scrapeLogs(t.Context())
require.NoError(t, err)

// No query_sample records should be emitted when the cursor is gone.
recordCount := 0
for i := 0; i < logs.ResourceLogs().Len(); i++ {
sls := logs.ResourceLogs().At(i).ScopeLogs()
for j := 0; j < sls.Len(); j++ {
recordCount += sls.At(j).LogRecords().Len()
}
}
assert.Zero(t, recordCount, "session whose cursor aged out of V$SQL must not emit a query_sample")
}

func TestScraperWithQueryComments(t *testing.T) {
sql := "/* application=test-123 */ SELECT * FROM test_table"

Expand Down
Loading
Loading