voonhous commented on code in PR #19476:
URL: https://github.com/apache/hudi/pull/19476#discussion_r3703934066
##########
hudi-aws/src/test/java/org/apache/hudi/aws/metrics/cloudwatch/TestCloudWatchReporter.java:
##########
@@ -168,18 +167,33 @@ public void testReporter() {
Mockito.verify(cloudWatchAsync).close();
}
+ /**
+ * A metric name with no dot has no table name to report under. Such names
do reach the reporter -
+ * {@code HoodieMetadataMetrics#setMetric} registers gauges without a
prefix, so
+ * {@code HoodieMetadataMetrics.getStats} contributes a bare {@code
partitionCount} - and this used to
+ * throw. {@link com.codahale.metrics.ScheduledReporter} suppresses whatever
{@code report()} throws, so
+ * the result was that no metrics reached CloudWatch at all, which is what
#12182 and #13051 report.
+ * The unmappable metric is now skipped and the rest of the batch is still
published.
+ */
@Test
- public void testReportOnMetricsWithoutTableName() {
+ public void testReportSkipsMetricsWithoutTableNameAndPublishesTheRest() {
SortedMap<String, Gauge> gauges = new TreeMap<>();
- Gauge<Long> gauge1 = () -> 100L;
- Gauge<Double> gauge2 = () -> 100.1;
- gauges.put("gauge1", gauge1);
- gauges.put(TABLE_NAME + ".gauge2", gauge2);
+ Gauge<Long> unmappable = () -> 7L;
+ Gauge<Double> wellFormed = () -> 100.1;
+ gauges.put("partitionCount", unmappable);
Review Comment:
The PR's headline example is not reachable: `partitionCount` can never reach
the reporter, so this fixture does not represent the reported bug.
`STAT_COUNT_PARTITION` is only put into the stats map when `detailed ==
true` (`HoodieMetadataMetrics.java:143-144`), but the gauge-registering path
calls `getStats(false, ...)` (`HoodieMetadataMetrics.java:163`). The only
`detailed=true` caller is `HoodieBackedTableMetadata.stats()`, whose sole
consumer in the tree is `hudi-cli/.../MetadataCommand.java:197`, which prints
the map and never registers a gauge:
```
grep -rn '\.stats()' --include='*.java' . | grep -v /target/
```
The bug is real, just not via this name. Two dotless names that genuinely
are registered on master:
- `lookup_meta_index_bloom_filters_file_count` --
`BaseTableMetadata.java:212`, on the normal bloom-index read path
- `<partition>_bootstrap_error` -- `HoodieBackedTableMetadataWriter.java:482`
Please use a real one here:
```suggestion
gauges.put("lookup_meta_index_bloom_filters_file_count", unmappable);
```
Then update the javadoc above (lines 171-176) to cite
`BaseTableMetadata#getBloomFilters` instead of
`HoodieMetadataMetrics.getStats`, and fix the same claim in the PR description
and in its Verification transcript. As written, a committer who checks the
cited example concludes the bug does not exist.
##########
hudi-aws/src/main/java/org/apache/hudi/aws/metrics/cloudwatch/CloudWatchReporter.java:
##########
@@ -276,8 +279,18 @@ private void stageMetricDatum(String metricName,
long timestampMilliSec,
List<MetricDatum> metricData) {
String[] metricNameParts = metricName.split("\\.", 2);
- ValidationUtils.checkArgument(metricNameParts.length >= 2,
- "metricName doesn't follow the naming convention and doesn't
contain a dot as splitter! metricName:" + metricName);
+ if (metricNameParts.length < 2) {
+ // The table dimension comes from the part before the first dot, so a
name without one cannot be
+ // mapped. Skip just this metric rather than throwing: ScheduledReporter
suppresses whatever
+ // report() throws, so failing here dropped every metric staged in the
same interval and left no
+ // metrics in CloudWatch at all.
+ if (unmappableMetricNames.add(metricName)) {
+ log.warn("Not reporting metric \"{}\" to CloudWatch: it contains no
dot, so there is no table name "
+ + "to report it under. Metrics are expected to be named
<table>.<metric>. Other metrics in this "
+ + "batch are unaffected, and this is logged once per metric
name.", metricName);
+ }
+ return;
+ }
String tableName = metricNameParts[0];
Review Comment:
The Impact section overstates the result. Users do not go from "no metrics"
to "every well-formed metric" -- they also start receiving metrics under a
wrong `Table` dimension.
Names that contain a dot but no table pass the guard above and land here
with the *action* as `tableName`:
- `<action>.count` / `<action>.totalDuration` --
`HoodieMetadataMetrics.java:156-157` -> `Table="lookup_partitions"`,
`Table="initialize"`, ...
-
`<mdt_partition>.{baseFileCount,logFileCount,totalBaseFileSizeInBytes,totalLogFileSizeInBytes}`
-- `HoodieMetadataMetrics.java:137-140` -> `Table="files"`,
`Table="column_stats"`, ...
These really are registered: `TestJavaHoodieBackedMetadata.java:2612-2618`
asserts the registry contains `initialize.count`, `files.baseFileCount` and
friends.
This was already broken before your change, but for exactly the affected
users it was invisible, because the batch died before anything could be sent.
CloudWatch bills per unique metric name + dimension set, so roughly 18+ bogus
custom metrics start being charged the moment this merges.
Please state this plainly in the Impact section rather than only in "Not in
this PR", so a committer can weigh the cost implication.
##########
hudi-aws/src/main/java/org/apache/hudi/aws/metrics/cloudwatch/CloudWatchReporter.java:
##########
@@ -276,8 +279,18 @@ private void stageMetricDatum(String metricName,
long timestampMilliSec,
List<MetricDatum> metricData) {
String[] metricNameParts = metricName.split("\\.", 2);
- ValidationUtils.checkArgument(metricNameParts.length >= 2,
- "metricName doesn't follow the naming convention and doesn't
contain a dot as splitter! metricName:" + metricName);
+ if (metricNameParts.length < 2) {
Review Comment:
The guard misses the empty-table-name case, which reproduces the same bug
class this PR exists to fix.
`hoodie.metrics.reporter.metricsname.prefix` defaults to `""`
(`HoodieMetricsConfig.java:78-81`; the infer-from-`hoodie.table.name` at :82-87
only fires when that key is present in the props). With an empty prefix,
`Metrics.java:157` builds `"" + "." + k`, i.e. a leading dot. `".foo"` then
splits to `["", "foo"]`, passes `length >= 2`, and emits `Dimension(Table,
"")`. CloudWatch requires a dimension value of at least one non-whitespace
character and rejects the whole `PutMetricData` request with
`InvalidParameterValue`, so one such metric again takes down up to
`maxDatumsPerRequest` datums with it.
One line closes it:
```suggestion
if (metricNameParts.length < 2 ||
StringUtils.isNullOrEmpty(metricNameParts[0])) {
```
Needs `import org.apache.hudi.common.util.StringUtils;`. Please also add a
`".foo"` case to the new test.
##########
hudi-aws/src/main/java/org/apache/hudi/aws/metrics/cloudwatch/CloudWatchReporter.java:
##########
@@ -276,8 +279,18 @@ private void stageMetricDatum(String metricName,
long timestampMilliSec,
List<MetricDatum> metricData) {
String[] metricNameParts = metricName.split("\\.", 2);
- ValidationUtils.checkArgument(metricNameParts.length >= 2,
- "metricName doesn't follow the naming convention and doesn't
contain a dot as splitter! metricName:" + metricName);
+ if (metricNameParts.length < 2) {
+ // The table dimension comes from the part before the first dot, so a
name without one cannot be
+ // mapped. Skip just this metric rather than throwing: ScheduledReporter
suppresses whatever
+ // report() throws, so failing here dropped every metric staged in the
same interval and left no
+ // metrics in CloudWatch at all.
+ if (unmappableMetricNames.add(metricName)) {
+ log.warn("Not reporting metric \"{}\" to CloudWatch: it contains no
dot, so there is no table name "
+ + "to report it under. Metrics are expected to be named
<table>.<metric>. Other metrics in this "
+ + "batch are unaffected, and this is logged once per metric
name.", metricName);
+ }
+ return;
Review Comment:
Worth flagging the direction explicitly here, because both prior fixes for
this exact symptom went producer-side and a committer will likely ask why this
one does not:
- `1a5a9f7f03ec` [HUDI-4439] "Fix Amazon CloudWatch reporter for metadata
enabled tables" (#6164) fixed it by pushing `HoodieWriteConfig.TBL_NAME` into
the metadata table's `HoodieMetricsConfig`. Zero reporter changes.
- `100e9ac47590` [HUDI-9068] (#12873) fixed two producer names *and* added
the `checkArgument` you are removing -- i.e. that guard was a detector for
producer bugs, not the fix itself.
The producer fix is also small and central: `HoodieMetadataMetrics` already
receives `HoodieMetricsConfig` at `HoodieMetadataMetrics.java:91` and discards
it. Retaining `getMetricReporterMetricsNamePrefix()` and applying it in
`setMetric`/`incrementMetric` would fix both the dotless case and the
wrong-`Table` case, for every reporter at once.
Your counter-argument (renaming breaks existing
Graphite/Prometheus/JMX/Datadog dashboards) is legitimate and I am not asking
you to do it in this PR. But please file the follow-up JIRA now and reference
its id from this code comment and from the PR description, so this lands as
explicitly the "stop the bleeding" half rather than reading as the whole fix.
##########
hudi-aws/src/main/java/org/apache/hudi/aws/metrics/cloudwatch/CloudWatchReporter.java:
##########
@@ -276,8 +279,18 @@ private void stageMetricDatum(String metricName,
long timestampMilliSec,
List<MetricDatum> metricData) {
String[] metricNameParts = metricName.split("\\.", 2);
Review Comment:
Supportive note rather than a request, since the obvious objection to this
PR is "you reverted HUDI-9068": there is house precedent on your side, and
citing it in the description would help this land.
This is the only place in the codebase where a reporter derives a dimension
by splitting the metric name:
```
grep -rn "split(" hudi-common/src/main/java/org/apache/hudi/metrics
hudi-aws/src/main/java
```
The closest analogue is `MetricUtils`, used by `DatadogReporter.java:96` and
`PushGatewayReporter.java:133`. Its convention is exactly the one you are
adopting: missing optional structure degrades gracefully (`return
Pair.of(tokens[0], "")`, `MetricUtils.java:61`), while genuinely malformed
input throws (`:55`). "No dot" is missing structure, not malformed input.
Also worth stating explicitly in the description: #12873's substantive half
is untouched here. The `TABLE_SERVICE_EXECUTION_*` prefixing survives at
`HoodieBackedTableMetadataWriter.java:1565-1579` and its assertions at
`TestJavaHoodieBackedMetadata.java:2622-2623`. Only the reporter-side throw
changes.
(Unrelated, for a future PR: `MetricUtils.getMetricAndLabels` throws from
inside `DatadogReporter.report()` and `PushGatewayReporter.report()` on a name
containing two `;` -- identical whole-batch-loss bug class, still live on
master.)
##########
hudi-aws/src/main/java/org/apache/hudi/aws/metrics/cloudwatch/CloudWatchReporter.java:
##########
@@ -66,6 +67,8 @@ public class CloudWatchReporter extends ScheduledReporter {
private final String prefix;
private final String namespace;
private final int maxDatumsPerRequest;
+ /** Metric names already reported as unmappable, so the warning is logged
once rather than every interval. */
+ private final Set<String> unmappableMetricNames =
ConcurrentHashMap.newKeySet();
Review Comment:
This field is the only new mutable state in the class, and it has no test
coverage: its "already seen" branch is never taken, because the new test calls
`report()` exactly once. This is precisely the 1 partial line codecov reports
(80% patch coverage).
Concretely, two regressions pass the suite unchanged today: deleting this
field entirely and logging unconditionally, and a change where the set
suppressed the *datum* rather than the *log*.
The repo already has the idiom -- `TestLogAppender extends AbstractAppender`
at `TestPriorityBasedFileSystemView.java:877-892`, used at `:139-160`.
`log4j-core` reaches hudi-aws at test scope through `hudi-tests-common`, and
`org.apache.hudi` logs at DEBUG/WARN under both surefire log4j2 configs, so a
WARN is captured either way.
Please add a test that calls `reporter.report()` twice against the same
registry and asserts exactly one WARN event from `CloudWatchReporter`.
##########
hudi-aws/src/main/java/org/apache/hudi/aws/metrics/cloudwatch/CloudWatchReporter.java:
##########
@@ -276,8 +279,18 @@ private void stageMetricDatum(String metricName,
long timestampMilliSec,
List<MetricDatum> metricData) {
String[] metricNameParts = metricName.split("\\.", 2);
- ValidationUtils.checkArgument(metricNameParts.length >= 2,
- "metricName doesn't follow the naming convention and doesn't
contain a dot as splitter! metricName:" + metricName);
+ if (metricNameParts.length < 2) {
+ // The table dimension comes from the part before the first dot, so a
name without one cannot be
+ // mapped. Skip just this metric rather than throwing: ScheduledReporter
suppresses whatever
+ // report() throws, so failing here dropped every metric staged in the
same interval and left no
+ // metrics in CloudWatch at all.
+ if (unmappableMetricNames.add(metricName)) {
+ log.warn("Not reporting metric \"{}\" to CloudWatch: it contains no
dot, so there is no table name "
+ + "to report it under. Metrics are expected to be named
<table>.<metric>. Other metrics in this "
+ + "batch are unaffected, and this is logged once per metric
name.", metricName);
Review Comment:
nit, feel free to ignore: the message states a convention Hudi does not
actually follow, and is not actionable for an operator.
"Metrics are expected to be named `<table>.<metric>`" is false for every
metadata metric -- `HoodieMetadataMetrics.java:137-140` and `:156-157` produce
`<mdt_partition>.<stat>` and `<action>.count`, none of which carry a table
name. And the operator has no knob that fixes the names being skipped, so the
warning reports a problem without pointing anywhere.
```suggestion
log.warn("Not reporting metric \"{}\" to CloudWatch: it has no dot,
so no table name can be "
+ "derived for the Table dimension. Metric names normally carry
the "
+ "hoodie.metrics.reporter.metricsname.prefix, but some
Hudi-internal metadata metrics do "
+ "not (see HUDI-XXXX). Other metrics in this batch are
unaffected, and this is logged "
+ "once per metric name.", metricName);
```
Fill in the follow-up JIRA id from the other comment.
##########
hudi-aws/src/test/java/org/apache/hudi/aws/metrics/cloudwatch/TestCloudWatchReporter.java:
##########
@@ -168,18 +167,33 @@ public void testReporter() {
Mockito.verify(cloudWatchAsync).close();
}
+ /**
+ * A metric name with no dot has no table name to report under. Such names
do reach the reporter -
+ * {@code HoodieMetadataMetrics#setMetric} registers gauges without a
prefix, so
+ * {@code HoodieMetadataMetrics.getStats} contributes a bare {@code
partitionCount} - and this used to
+ * throw. {@link com.codahale.metrics.ScheduledReporter} suppresses whatever
{@code report()} throws, so
+ * the result was that no metrics reached CloudWatch at all, which is what
#12182 and #13051 report.
+ * The unmappable metric is now skipped and the rest of the batch is still
published.
+ */
@Test
- public void testReportOnMetricsWithoutTableName() {
+ public void testReportSkipsMetricsWithoutTableNameAndPublishesTheRest() {
SortedMap<String, Gauge> gauges = new TreeMap<>();
- Gauge<Long> gauge1 = () -> 100L;
- Gauge<Double> gauge2 = () -> 100.1;
- gauges.put("gauge1", gauge1);
- gauges.put(TABLE_NAME + ".gauge2", gauge2);
+ Gauge<Long> unmappable = () -> 7L;
+ Gauge<Double> wellFormed = () -> 100.1;
+ gauges.put("partitionCount", unmappable);
+ gauges.put(TABLE_NAME + ".gauge2", wellFormed);
Mockito.when(metricRegistry.getGauges(MetricFilter.ALL)).thenReturn(gauges);
- // should fail if metric name doesn't have at least two parts
- assertThrows(IllegalArgumentException.class, () -> reporter.report());
+ reporter.report();
+
+ Mockito.verify(cloudWatchAsync,
Mockito.times(1)).putMetricData(putMetricDataRequestCaptor.capture());
+ List<MetricDatum> metricData =
putMetricDataRequestCaptor.getValue().metricData();
+ assertEquals(1, metricData.size(),
+ "The unmappable metric should be skipped and the well-formed one still
published");
+ assertEquals(PREFIX + ".gauge2", metricData.get(0).metricName());
+ assertEquals(wellFormed.getValue(), metricData.get(0).value());
+ assertDimensions(metricData.get(0).dimensions(),
DIMENSION_GAUGE_TYPE_VALUE);
reporter.stop();
Mockito.verify(cloudWatchAsync).close();
Review Comment:
nit, feel free to ignore: `reporter.stop();` +
`Mockito.verify(cloudWatchAsync).close();` is copied verbatim from
`testReporter` (lines 166-167). It asserts stop/close plumbing unrelated to
this fix, and a failure there would mask the real assertion above it.
Consider moving it into an `@AfterEach` so the tail does not get re-pasted
into every future test in this class.
##########
hudi-aws/src/test/java/org/apache/hudi/aws/metrics/cloudwatch/TestCloudWatchReporter.java:
##########
@@ -168,18 +167,33 @@ public void testReporter() {
Mockito.verify(cloudWatchAsync).close();
}
+ /**
+ * A metric name with no dot has no table name to report under. Such names
do reach the reporter -
+ * {@code HoodieMetadataMetrics#setMetric} registers gauges without a
prefix, so
+ * {@code HoodieMetadataMetrics.getStats} contributes a bare {@code
partitionCount} - and this used to
+ * throw. {@link com.codahale.metrics.ScheduledReporter} suppresses whatever
{@code report()} throws, so
+ * the result was that no metrics reached CloudWatch at all, which is what
#12182 and #13051 report.
+ * The unmappable metric is now skipped and the rest of the batch is still
published.
+ */
@Test
- public void testReportOnMetricsWithoutTableName() {
+ public void testReportSkipsMetricsWithoutTableNameAndPublishesTheRest() {
SortedMap<String, Gauge> gauges = new TreeMap<>();
- Gauge<Long> gauge1 = () -> 100L;
- Gauge<Double> gauge2 = () -> 100.1;
- gauges.put("gauge1", gauge1);
- gauges.put(TABLE_NAME + ".gauge2", gauge2);
+ Gauge<Long> unmappable = () -> 7L;
+ Gauge<Double> wellFormed = () -> 100.1;
+ gauges.put("partitionCount", unmappable);
+ gauges.put(TABLE_NAME + ".gauge2", wellFormed);
Mockito.when(metricRegistry.getGauges(MetricFilter.ALL)).thenReturn(gauges);
- // should fail if metric name doesn't have at least two parts
- assertThrows(IllegalArgumentException.class, () -> reporter.report());
+ reporter.report();
+
+ Mockito.verify(cloudWatchAsync,
Mockito.times(1)).putMetricData(putMetricDataRequestCaptor.capture());
+ List<MetricDatum> metricData =
putMetricDataRequestCaptor.getValue().metricData();
+ assertEquals(1, metricData.size(),
Review Comment:
This misses the one genuinely new runtime state the change introduces: an
interval in which *every* metric is unmappable, leaving `metricsData` empty.
That path is currently correct -- the partition loop at
`CloudWatchReporter.java:220-223` builds zero partitions from an empty list, so
`putMetricData` is never called -- but nothing asserts it. A later refactor
that sent an empty `PutMetricDataRequest` (AWS rejects it with a validation
error) would pass the suite unchanged.
Please add a case whose registry holds only dotless gauges, and assert:
```java
Mockito.verify(cloudWatchAsync,
Mockito.never()).putMetricData(ArgumentMatchers.any(PutMetricDataRequest.class));
```
That is safe under `MockitoExtension` STRICT_STUBS because `cloudWatchAsync`
is `@Mock(lenient = true)`, so the unused `putMetricData` stub in `setup()`
will not trip `UnnecessaryStubbing`.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]