[ENG-44755] Emit discovery_path label on lakeView_table_discovery_failure - #195
Conversation
…ilure
metadataExtractorDiscoveryFailure fires per-org on
lakeView_table_discovery_failure_total with no table-level dimension, so
on-call can only silence the whole org's alert — which blinds us to any
NEW table failure for that org during the silence window (hit live on
ENG-44423 / ENG-44424, Cardlytics: a customer-side S3 bucket-policy Deny
hit 3 of hundreds of paths and the workaround was an org-wide 2-week
silence).
Add a table_base_path tag to the discovery-failure counter, wired at
TableDiscoveryService (the per-path .exceptionally, the only call site
with the failing path in scope). Run-level failure sites route through
the reason-only overload, which now supplies an "unknown" sentinel so
the counter's tag set stays consistent across all call sites (required
for a clean Prometheus label family). Once emitted, an Alertmanager
silence can target {orgId, table_base_path} instead of the whole org.
Note: tableId is not available at discovery-failure time (discovery is
what resolves a path to a tableId), so the failing base path is the
table-level dimension, per the ticket's "and/or table base path".
Test plan:
- LakeViewExtractorMetricsTest: existing reason-only cases now assert the
"unknown" sentinel tag; new case asserts the real path tag.
- TableDiscoveryServiceTest.testTableDiscoveryEncountersRateLimitException
updated to the path-aware signature.
Local checks:
- ./gradlew :lakeview:spotlessApply — clean, no rewrites
- ./gradlew :lakeview:test --tests LakeViewExtractorMetricsTest --tests TableDiscoveryServiceTest — all passed
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| getMetadataExtractorFailureReason( | ||
| e, MetricsConstants.MetadataUploadFailureReasons.UNKNOWN)); | ||
| e, MetricsConstants.MetadataUploadFailureReasons.UNKNOWN), | ||
| path); |
There was a problem hiding this comment.
nit: path here is whatever directory the failed listAllFilesInDir was on, which is the table root only sometimes. Discovery stops recursing once it sees .hoodie/, so if the Deny lands on a prefix above the tables (e.g. s3://.../Domain=CustomerOfferEvent/) that list fails before we ever descend, and the emitted value is that domain prefix — a container of many tables, not a table base path. The table_base_path label name over-promises for exactly the broad-Deny case this feature targets.
Since the metric is new (no back-compat cost), maybe rename the key to something like discovery_path / scan_path, or add a one-line note that the value is the scan path at failure depth and can be a prefix above the table. Non-blocking.
…ry_path Review feedback (kroushan-nit): the emitted value is the prefix whose listing failed in the recursive discovery walk. When a Deny lands on a parent prefix (e.g. Domain=CustomerOfferEvent/) the list fails before descending, so the value is a container of many tables, not a table base path — table_base_path over-promised for exactly the broad-Deny case this targets. Rename the label + constants to discovery_path before it becomes a deployed contract (silences / dashboards / runbook reference it). Behaviour unchanged; label key and identifiers only. Local checks: - ./gradlew :lakeview:spotlessApply — clean - ./gradlew :lakeview:test --tests LakeViewExtractorMetricsTest --tests TableDiscoveryServiceTest — passed Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|



Summary
metadataExtractorDiscoveryFailurefires per-org onlakeView_table_discovery_failure_totalwith no table-level dimension — so on-call can only silence the whole org's alert, which then blinds us to any NEW table failure for that org during the silence window. Hit live on ENG-44423 / ENG-44424 (Cardlytics): a customer-side S3 bucket-policy explicit-Deny hit 3 of hundreds of paths underDomain=CustomerOfferEvent/, and the only lever was an org-wide 2-week Alertmanager silence.Fix
table_base_pathtag tolakeView_table_discovery_failure, wired atTableDiscoveryService— the per-path.exceptionallyis the only failure call site with the failing path in scope.Main,TableDiscoveryAndUploadJob) route through the reason-only overload, which now supplies an"unknown"sentinel so the counter's tag set stays consistent across all call sites (needed for a clean Prometheus label family).{orgId, table_base_path}instead of the whole org.tableIdis deliberately not used: discovery is the step that resolves a path into a tableId, so at discovery-failure time no tableId exists yet — the failing base path is the available table-level dimension (matches the ticket's "and/or table base path").Follow-ups (not in this PR)
{{ $labels.table_base_path }}on themetadataExtractorDiscoveryFailurealert + update the runbook silence step to{orgId, table_base_path}.table_base_pathis emitted only on failure (0 series in steady state) and is naturally bounded by the customer's Deny scope. A defensive per-round cap (collapse to__many__above K distinct failing paths) is a candidate hardening if a wholesale-denial pathological case shows up — open for reviewer input.Test plan
LakeViewExtractorMetricsTest: reason-only cases now assert the"unknown"sentinel tag; newtestIncrementTableDiscoveryFailureCounterWithTableBasePathasserts the real path tag.TableDiscoveryServiceTest.testTableDiscoveryEncountersRateLimitExceptionupdated to the path-aware signature../gradlew :lakeview:spotlessApply— clean, no rewrites./gradlew :lakeview:test --tests LakeViewExtractorMetricsTest --tests TableDiscoveryServiceTest— all passed locallyReferences
🤖 Generated with Claude Code