Skip to content

feat(database_observability.mysql): Add primary and unique index labels and a new row_count metric - #7095

Merged
gaantunes merged 2 commits into
mainfrom
feat/dbo11y-mysql-index-primary-unique-labels
Sep 11, 2026
Merged

gaantunes merged 2 commits into
mainfrom
feat/dbo11y-mysql-index-primary-unique-labels

Conversation

@gaantunes

@gaantunes gaantunes commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Brief description of Pull Request

Adds two signals to the index_stats/table_stats collectors, mirroring the same additions made to the postgres collectors in #7068:

  • is_primary/is_unique labels on mysql_index_stats_size_bytes. is_primary is derived from INDEX_NAME = 'PRIMARY' (no extra query needed); is_unique is joined in from information_schema.statistics.NON_UNIQUE.
  • mysql_table_stats_row_count, a new gauge sourced from mysql.innodb_table_stats.n_rows (the sibling table to mysql.innodb_index_stats, already used for index size).

Pull Request Details

Both are needed for the missing/unused-index Knowledge Graph insight:

  • Without is_primary/is_unique, the unused-index insight has no signal to avoid recommending the removal of an index that's actually backing a primary key or unique constraint just because its usage counters look low. These are folded onto the existing size_bytes gauge as labels rather than a separate metric, since both attributes are 1:1 with an index the same way schema/table/index already are — this adds no additional series.
  • Without row_count, the missing-index insight can't tell a table that's genuinely being hammered with full scans from one that's just small enough that a full scan doesn't matter — the same gap closed on the postgres side by pg_table_stats_row_count.

Issue(s) fixed by this Pull Request

Notes to the Reviewer

PR Checklist

  • Documentation added
  • Tests updated
  • Config converters updated
  • This pull request was substantially generated with AI assistance (see the GenAI policy)

🤖 Generated with Claude Code

Mirrors the same addition on the postgres index_stats PR: fold these
onto the existing size_bytes gauge as labels rather than a separate
metric, since both are 1:1 with an index the same way schema/table/index
already are. is_primary comes free from INDEX_NAME (MySQL always names
the primary key index literally "PRIMARY"); is_unique is joined in from
information_schema.statistics.NON_UNIQUE. Needed so the unused-index
insight doesn't recommend dropping an index that's actually backing a
primary key or unique constraint.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@gaantunes
gaantunes requested a review from a team as a code owner September 11, 2026 17:25
Sourced from mysql.innodb_table_stats.n_rows, the sibling table to
mysql.innodb_index_stats already used by index_stats.go for index size
-- same refresh mechanism, same grant requirement already relied on
there. Without a row-count signal, the missing-index insight can't tell
a table that's genuinely being hammered with full scans from one that's
just small enough that a full scan doesn't matter, the same gap this
closed on the postgres side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
}

func (c *TableStats) collectRowCount(ctx context.Context, ch chan<- prometheus.Metric) {
query := fmt.Sprintf(selectTableRowCount, buildExcludedSchemasClause(c.excludeSchemas))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Semgrep identified a blocking 🔴 issue in your code:

collectRowCount interpolates c.excludeSchemas into SQL before QueryContext executes it. Malicious schema configuration could alter the filter and expose data through the metrics queries.

More details about this

collectRowCount builds the SQL text with fmt.Sprintf(selectTableRowCount, buildExcludedSchemasClause(c.excludeSchemas)) and then executes that text through c.dbConnection.QueryContext. The value returned by buildExcludedSchemasClause is inserted into the SQL before the database receives it, rather than being passed as a query parameter.

If c.excludeSchemas can be influenced by deployment configuration or another attacker-controlled input, an attacker could supply a schema value such as tenant_a') OR 1=1 -- . If buildExcludedSchemasClause places that value inside a quoted NOT IN clause, the generated query could change from an exclusion filter into a condition that matches every row. When the metrics collection path calls collectRowCount, QueryContext would then return row counts for schemas that were intended to be excluded; a payload crafted to add a UNION or additional statement could expose other database data if the MySQL driver and account permit it. The same construction pattern is also present in collectNoIdxFetch with selectTableIOWaitsNoIndex and c.excludeSchemas, so the impact may include both table row-count and index-usage metrics.

To resolve this comment:

🔧 No guidance has been designated for this issue. Fix according to your organization's approved methods.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by string-formatted-query.

We're currently testing semgrep's diff-aware PR comment feature on a subset of our repos-- if you run into issues or find this spammy, please reach out to @danny.cooper in slack and give feedback.

For backwards compatability with gosec, its best to use polyglot suppression comments of the following format for false positives:
// #nosec <gosec rule ID> nosemgrep: <semgrep rule ID>

You can view more details about this finding in the Semgrep AppSec Platform.

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.

/fp this is running inside an alloy instance and the param is fully under the user control via config file, so the risk is rather low.

@gaantunes gaantunes changed the title feat(database_observability.mysql): Add is_primary/is_unique labels to index_stats size metric feat(database_observability.mysql): Add primary/unique index labels and row_count metric Sep 11, 2026
@gaantunes gaantunes changed the title feat(database_observability.mysql): Add primary/unique index labels and row_count metric feat(database_observability.mysql): Add primary and unique index labels and a new row_count metric Sep 11, 2026
@gaantunes
gaantunes enabled auto-merge (squash) September 11, 2026 18:47
@gaantunes
gaantunes merged commit 9199e72 into main Sep 11, 2026
52 of 54 checks passed
@gaantunes
gaantunes deleted the feat/dbo11y-mysql-index-primary-unique-labels branch September 11, 2026 18:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants