Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
33 changes: 33 additions & 0 deletions .chloggen/oracledbreceiver-query-sample-two-pass.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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.

# 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]'
62 changes: 62 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,62 @@ 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
var placeholders []string
for _, row := range rows {
id := row[lookupSQLID]
if id == "" {
continue
}
if _, dup := seen[id]; dup {
continue
}
seen[id] = struct{}{}
placeholders = append(placeholders, fmt.Sprintf(":%d", len(ids)+1))
ids = append(ids, id)
}
if len(ids) == 0 {
return nil
}

// Oracle IN list is capped at 1000 expressions; batch if needed.
const oracleInLimit = 1000
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]
batchPlaceholders := placeholders[i:end]

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.

This reuses a global :1..:N placeholder list across batches, but each iteration is a separate statement whose args are bound at positions 1..len(batchIDs). Batch 1 is fine (:1..:1000); batch 2 slices out :1001..:2000 yet still passes only 1000 args, so those bind positions are never supplied. Depending on how go-ora maps numbered placeholders that's either a silent wrong-bind or ORA-01008: not all variables are bound.

Simplest robust fix is to regenerate :1..:n inside the batch loop so the numbers always match the args for that call (same convention as getChildAddressToPlanMap). Replace the id-collection + batch loop with something like:

    // Collect distinct, non-empty sql_ids in stable order.
    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 caps an IN list at 1000 expressions; batch to stay under it.
    const oracleInLimit = 1000
    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]
        // Regenerate :1..:n for THIS batch so bind positions match the args
        // passed below, regardless of driver placeholder semantics.
        placeholders := make([]string, len(batchIDs))
        for j := range batchIDs {
            placeholders[j] = fmt.Sprintf(":%d", j+1)
        }
        sqlQuery := fmt.Sprintf(samplesStatsQuery, strings.Join(placeholders, ", "))
        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
        }
    }

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.

Thanks for catching this. Fixed in the latest commit

sqlQuery := fmt.Sprintf(samplesStatsQuery, strings.Join(batchPlaceholders, ", "))
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
126 changes: 88 additions & 38 deletions receiver/oracledbreceiver/scraper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"log"
"maps"
"os"
"path/filepath"
"sort"
Expand Down Expand Up @@ -834,41 +835,67 @@ 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)}}
}
}

var sessionEventQueryResponses = map[string][]metricRow{
sessionEventQuery: {
{
Expand Down Expand Up @@ -903,42 +930,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 +1015,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
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ S.USERNAME,
S.SCHEMANAME,
S.SQL_ID,
S.SQL_CHILD_NUMBER,
RAWTOHEX(Q.CHILD_ADDRESS) AS CHILD_ADDRESS,
COALESCE(S.SQL_ID, S.PREV_SQL_ID) AS LOOKUP_SQL_ID,
COALESCE(S.SQL_CHILD_NUMBER, S.PREV_CHILD_NUMBER) AS LOOKUP_CHILD_NUMBER,
S.SID,
S.SERIAL#,
Q.SQL_FULLTEXT,
S.OSUSER,
S.PROCESS,
TO_CHAR(SYS_EXTRACT_UTC(FROM_TZ(CAST(S.LOGON_TIME AS TIMESTAMP), DBTIMEZONE)), 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS LOGON_TIME,
Expand All @@ -20,7 +20,6 @@ S.STATUS,
S.STATE,
S.SERVICE_NAME,
NVL(C.NAME, '') AS DB_NAMESPACE,
Q.PLAN_HASH_VALUE,
P.OBJECT_ID AS PROCEDURE_ID,
GREATEST(NVL((SYSDATE - S.SQL_EXEC_START) * 86400, 0), 0) AS DURATION_SEC,
TO_CHAR(SYS_EXTRACT_UTC(FROM_TZ(CAST(S.SQL_EXEC_START AS TIMESTAMP), DBTIMEZONE)), 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS SQL_EXEC_START,
Expand Down Expand Up @@ -71,9 +70,6 @@ FROM V$SESSION S
GROUP BY SID
) LK
ON LK.SID = S.SID
JOIN V$SQL Q
ON Q.SQL_ID = COALESCE(S.SQL_ID, S.PREV_SQL_ID)
AND Q.CHILD_NUMBER = COALESCE(S.SQL_CHILD_NUMBER, S.PREV_CHILD_NUMBER)
WHERE S.TYPE = 'USER'
AND S.SID != SYS_CONTEXT('USERENV', 'SID')
AND (S.STATUS = 'ACTIVE'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* otel-collector */ SELECT
SQL_ID,
CHILD_NUMBER,
RAWTOHEX(CHILD_ADDRESS) AS CHILD_ADDRESS,
PLAN_HASH_VALUE,
SQL_FULLTEXT
FROM V$SQL
WHERE SQL_ID IN (%s)
Loading