Skip to content

Core: Add table-name filter for MetricsReporter#16574

Open
moomindani wants to merge 3 commits into
apache:mainfrom
moomindani:moomindani/metrics-reporter-table-filter
Open

Core: Add table-name filter for MetricsReporter#16574
moomindani wants to merge 3 commits into
apache:mainfrom
moomindani:moomindani/metrics-reporter-table-filter

Conversation

@moomindani
Copy link
Copy Markdown
Contributor

@moomindani moomindani commented May 27, 2026

Closes #16573.

Adds an optional filtering layer above any MetricsReporter implementation that drops ScanReport and CommitReport instances whose tableName() does not pass the configured include / exclude regex. The filter applies uniformly to LoggingMetricsReporter, RESTMetricsReporter, and custom user-supplied reporters. The proposal surfaced in the dev@ DISCUSS thread for #16250 (per-table cardinality of the OTel reporter) and is intentionally scoped as cross-reporter, not OTel-specific.

Design

CatalogUtil.loadMetricsReporter wraps the resolved reporter in a FilteringMetricsReporter when either of the new properties is set. When neither is set, the resolved reporter is returned unchanged — no wrapper instantiated, no runtime overhead on the default path. MetricsReport subtypes that do not expose a table name (anything other than ScanReport / CommitReport) are forwarded without filtering.

Configuration

Two new catalog properties:

metrics-reporter.table-name.include=prod_db\..*
metrics-reporter.table-name.exclude=.*\.tmp_.*

Values are Java regex patterns matched against the table name. When both are set, exclude wins over include (an explicit deny overrides an include). Empty values are treated as not set to avoid accidentally silencing all metrics on misconfiguration. Invalid regex values fail fast at catalog initialization with a clear error pointing at the offending property.

Behavior:

  • include only: forward reports whose table name matches; drop others.
  • exclude only: drop reports whose table name matches; forward others.
  • Both set: drop if exclude matches; otherwise forward only if include matches.
  • Neither set: forward everything (current behavior).

This mirrors the existing route-regex pattern used in iceberg-kafka-connect (IcebergSinkConfig), where a user-supplied regex from configuration is compiled via Pattern.compile() and matched against incoming data. Same trust model: catalog property = admin-controlled.

Design choice — where the filter wrap lives in REST catalog flow

The REST catalog adds an additional RESTMetricsReporter per-table inside RESTSessionCatalog.metricsReporter(...), separate from the user's metrics-reporter-impl. To make the table-name filter apply uniformly to both reporters, three shapes were considered:

A. Wrap inside RESTSessionCatalog.metricsReporter(...) (chosen). The RESTMetricsReporter is wrapped with FilteringMetricsReporter using the catalog properties stored at init, then combined with the user reporter. Smallest local change. Keeps MetricsReporters and FilteringMetricsReporter mutually unaware. The additional wrap lives next to the existing combine(reporter, restMetricsReporter) line, which is itself REST-specific wiring.

B. Make MetricsReporters.combine() aware of FilteringMetricsReporter. Detect when one input is a filtering wrapper and "lift" it to wrap the composite. Avoids touching REST catalog code; would apply uniformly to any future combine site. But couples a generic utility to a specific reporter implementation and changes combine() semantics for all callers.

C. Wrap at the scan/commit framework layer (TableScanContext / BaseTable.combineMetricsReporter). Apply the filter wrap on the final composed reporter at the point it's used. Catalog-agnostic. But touches scan/commit framework code for what is logically a catalog-level concern, and requires plumbing filter properties down to that layer.

Option A was preferred because the combine(reporter, restMetricsReporter) line in RESTSessionCatalog is already REST-specific wiring (no other catalog does this combine), so the additional wrap lives in the same conceptual location rather than introducing knowledge of filtering elsewhere. Happy to revisit if reviewers prefer one of the other shapes.

Disclosure

Per the project's AI-assisted contribution guidelines, I used Claude Code to help draft this work. I reviewed every change by hand and ran the full test/lint loop locally before opening this PR. The design and motivation discussion is in #16573.

cc @ebyhr @jbonofre — happy to address any feedback.

Add an optional filtering layer above any MetricsReporter implementation
that drops ScanReports and CommitReports whose tableName() does not pass
the configured include / exclude regex. Two new catalog properties
control the filter: metrics-reporter.table-name.include and
metrics-reporter.table-name.exclude. Both are Java regex patterns
matched against the table name; when both are set, exclude wins over
include.

When neither property is set, CatalogUtil.loadMetricsReporter returns
the underlying reporter unchanged, so the default code path incurs no
runtime overhead. Empty values are treated as not set to avoid
accidentally silencing all metrics on misconfiguration. Invalid regex
values fail fast at catalog initialization with a clear error pointing
at the offending property.

The filter applies uniformly across all reporter implementations
(LoggingMetricsReporter, RESTMetricsReporter, and custom user-supplied
ones). Reports whose subtype does not expose a table name are forwarded
without filtering.

Closes apache#16573
Copy link
Copy Markdown
Contributor

@gaborkaszab gaborkaszab left a comment

Choose a reason for hiding this comment

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

Hey @moomindani ,
I'm not sure this works with REST catalog. It creates a reporter using CatalogUtil.loadMetricsReporter where you wrap it with FilteringMetricsReporter, but then for each table it combines this with a RESTMetricsReporter that is not filtering. Is this the intended design? Would be nice to see some REST catalog tests too. Maybe some general catalog test where we set up the MetricsReporter via configs, and then it gets wrapped with the filtering one?
Before going deeper into the code, I'd wait to see if there is community buy in for this change, TBH.

The table-name filter introduced earlier in this PR is applied via
CatalogUtil.loadMetricsReporter to the user-configured reporter, but
RESTSessionCatalog injects an additional RESTMetricsReporter per table
inside metricsReporter(...), which previously bypassed the filter. Wrap
that RESTMetricsReporter with the same FilteringMetricsReporter (using
the catalog properties stored at init) before combining with the user
reporter, so both sides of the combined reporter honor the configured
table-name filter.

Add two tests:

- TestRESTCatalog.metricsFilterAppliesToRestMetricsReporter exercises
  RESTSessionCatalog.metricsReporter(...) with filter properties and a
  mock RESTClient, verifying that one filtered + one unfiltered scan
  report produce exactly one post() and that the filtered report
  short-circuits before reaching the client.

- TestFilteringMetricsReporter.loadMetricsReporterFiltersThroughUserConfiguredReporter
  goes through CatalogUtil.loadMetricsReporter with a static-singleton
  capturing reporter, demonstrating that the wrap applies at the
  catalog wiring level when metrics-reporter-impl plus the filter
  properties are configured together.
@moomindani
Copy link
Copy Markdown
Contributor Author

Hi @gaborkaszab,

Thanks for the catch — that was unintended. The filter is meant to apply uniformly, and the asymmetry between the CatalogUtil-wrapped user reporter and the catalog-injected RESTMetricsReporter was a real bug.

Pushed a follow-up commit:

  • Fix: RESTSessionCatalog.metricsReporter(...) now wraps the RESTMetricsReporter with the same FilteringMetricsReporter before combining with the user reporter, so both sides of the combined reporter honor the filter. Went with the local wrap in RESTSessionCatalog over a couple of alternative shapes (combine-aware filter, scan/commit-framework wrap) — added a short rationale section to the PR description.

  • REST catalog test (TestRESTCatalog.metricsFilterAppliesToRestMetricsReporter): exercises RESTSessionCatalog.metricsReporter(...) with filter properties and a mock RESTClient; verifies that one filtered + one unfiltered report produce exactly one post() (the filtered one short-circuits before reaching the client).

  • Configs-driven catalog test (TestFilteringMetricsReporter.loadMetricsReporterFiltersThroughUserConfiguredReporter): sets up metrics-reporter-impl + filter properties through CatalogUtil.loadMetricsReporter with a static-singleton capturing reporter, demonstrating the wrap actually applies at the catalog wiring level — that's the "general catalog test" you asked about.

I also locally combined this PR with the proposed OTel MetricsReporter from #16250 and ran an integration test against InMemoryMetricReader to verify the composition. Excluded tables don't reach the OTel pipeline; included tables emit normally. The two layers (table-name filter + OTel attribute allowlist) compose cleanly via the MetricsReporter interface without special wiring. Not committed here since #16250 hasn't landed.

Fully agree on the community buy-in point — the cardinality concern that motivated this proposal came up in the dev@ DISCUSS for #16250 (the OTel reporter, which is where Grant flagged it), not on a thread specific to this PR. Happy to wait for broader signal. Just wanted the design question and the tests to be in a reviewable state for when reviewers come back.

Thanks again for the careful read.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Core: Add table-level filtering for MetricsReporter implementations

2 participants