This is an automated email from the ASF dual-hosted git repository.

voonhous pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hudi.git


The following commit(s) were added to refs/heads/master by this push:
     new 637996c5ab20 fix(metrics): explain how to enable the CloudWatch 
reporter when hudi-aws is absent (#19418)
637996c5ab20 is described below

commit 637996c5ab2078dfd858a2a425da8f28af1ce789
Author: Ranga Reddy <[email protected]>
AuthorDate: Mon Aug 3 14:06:50 2026 +0530

    fix(metrics): explain how to enable the CloudWatch reporter when hudi-aws 
is absent (#19418)
    
    * fix(metrics): explain how to enable the CloudWatch reporter when hudi-aws 
is absent
    
    The CloudWatch reporter lives in the optional hudi-aws module and is loaded
    reflectively, but hudi-spark-bundle does not shade that module. Selecting
    hoodie.metrics.reporter.type=CLOUDWATCH with only hudi-spark-bundle on the
    classpath therefore failed with "Unable to load class", which names neither
    the missing class nor a remedy.
    
    Report the missing class, point at hudi-aws-bundle, and name the config to
    change instead. Other reflection failures are rethrown unchanged.
    
    Closes #15293
    
    * fix(common): name the class in the generic reflective-load failure
    
    Review feedback on the CloudWatch-specific message.
    
    ReflectionUtils.getClass threw "Unable to load class" with no class name, 
which
    is the message every reflective load in Hudi produces on a missing class:
    payload classes, key generators, index types, sources, sinks, write clients.
    Naming the class there fixes half of the reported defect for all of them, 
and
    the CloudWatch remedy now layers on an already-useful base message.
    
    Also from review:
    - Dropped the hudi-spark-bundle references from the user-facing message and 
the
      javadoc. Naming one bundle's contents in hudi-common couples the string to
      packaging with nothing keeping them in sync, and it is noise for a Flink 
or
      Kafka Connect user who reaches the line another way.
    - Made CLOUDWATCH_REPORTER_CLASS package-private with @VisibleForTesting so 
the
      tests reference it instead of repeating the FQCN in three places.
    - Added two tests that pin the branch deterministically rather than by
      classpath: a ClassNotFoundException cause is rewritten, and any other 
failure
      passes through unchanged so an unrelated instantiation error never 
becomes a
      misleading "add hudi-aws-bundle".
    
    * test(metrics): stop naming a specific bundle in the CloudWatch test 
javadoc
    
    The javadoc pinned the test's rationale to hudi-spark-bundle not shading
    hudi-aws. That is a packaging fact with nothing keeping it in sync: if that
    bundle ever shades hudi-aws, which is what #15293 asks for, the comment goes
    stale silently. The reason the test works is that the module's test 
classpath
    lacks hudi-aws, which is what any non-shading bundle looks like, so say that
    instead. Same reasoning already applied to the user-facing message and the
    production javadoc in bf4ef23.
    
    ---------
    
    Co-authored-by: voonhous <[email protected]>
---
 .../hudi/metrics/MetricsReporterFactory.java       | 30 +++++++++-
 .../hudi/metrics/TestMetricsReporterFactory.java   | 64 +++++++++++++++++++++-
 .../apache/hudi/common/util/ReflectionUtils.java   |  2 +-
 3 files changed, 92 insertions(+), 4 deletions(-)

diff --git 
a/hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java 
b/hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java
index 93ad8ddc0583..403d25b9ead6 100644
--- 
a/hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java
+++ 
b/hudi-common/src/main/java/org/apache/hudi/metrics/MetricsReporterFactory.java
@@ -22,6 +22,7 @@ import 
org.apache.hudi.common.config.metrics.HoodieMetricsConfig;
 import org.apache.hudi.common.util.Option;
 import org.apache.hudi.common.util.ReflectionUtils;
 import org.apache.hudi.common.util.StringUtils;
+import org.apache.hudi.common.util.VisibleForTesting;
 import org.apache.hudi.exception.HoodieException;
 import org.apache.hudi.metrics.custom.CustomizableMetricsReporter;
 import org.apache.hudi.metrics.datadog.DatadogMetricsReporter;
@@ -40,6 +41,10 @@ import java.util.Properties;
 @Slf4j
 public class MetricsReporterFactory {
 
+  @VisibleForTesting
+  static final String CLOUDWATCH_REPORTER_CLASS =
+      "org.apache.hudi.aws.metrics.cloudwatch.CloudWatchMetricsReporter";
+
   public static Option<MetricsReporter> createReporter(HoodieMetricsConfig 
metricsConfig, MetricRegistry registry) {
     String reporterClassName = metricsConfig.getMetricReporterClassName();
 
@@ -84,8 +89,7 @@ public class MetricsReporterFactory {
         reporter = new ConsoleMetricsReporter(registry);
         break;
       case CLOUDWATCH:
-        reporter = (MetricsReporter) 
ReflectionUtils.loadClass("org.apache.hudi.aws.metrics.cloudwatch.CloudWatchMetricsReporter",
-            new Class[]{HoodieMetricsConfig.class, MetricRegistry.class}, 
metricsConfig, registry);
+        reporter = createCloudWatchReporter(metricsConfig, registry);
         break;
       case M3:
         reporter = new M3MetricsReporter(metricsConfig, registry);
@@ -99,4 +103,26 @@ public class MetricsReporterFactory {
     }
     return Option.ofNullable(reporter);
   }
+
+  /**
+   * The CloudWatch reporter ships in the optional {@code hudi-aws} module and 
so is loaded reflectively.
+   * Not every engine bundle shades that module, in which case class loading 
fails without pointing at a
+   * remedy. Translate that into an actionable error.
+   */
+  private static MetricsReporter createCloudWatchReporter(HoodieMetricsConfig 
metricsConfig, MetricRegistry registry) {
+    try {
+      return (MetricsReporter) 
ReflectionUtils.loadClass(CLOUDWATCH_REPORTER_CLASS,
+          new Class[] {HoodieMetricsConfig.class, MetricRegistry.class}, 
metricsConfig, registry);
+    } catch (HoodieException e) {
+      if (e.getCause() instanceof ClassNotFoundException) {
+        throw new HoodieException(String.format(
+            "Cannot report metrics to CloudWatch: %s was not found on the 
classpath. It ships in the "
+                + "optional hudi-aws module, which not every engine bundle 
includes. Add the "
+                + "hudi-aws-bundle jar matching your Hudi version to the 
classpath, or set %s to a "
+                + "different reporter type.",
+            CLOUDWATCH_REPORTER_CLASS, 
HoodieMetricsConfig.METRICS_REPORTER_TYPE_VALUE.key()), e);
+      }
+      throw e;
+    }
+  }
 }
diff --git 
a/hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java
 
b/hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java
index 68a85572086d..d1bcee365873 100644
--- 
a/hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java
+++ 
b/hudi-common/src/test/java/org/apache/hudi/metrics/TestMetricsReporterFactory.java
@@ -82,7 +82,7 @@ class TestMetricsReporterFactory {
     try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {
       mockedStatic.when(() ->
           ReflectionUtils.loadClass(
-              
eq("org.apache.hudi.aws.metrics.cloudwatch.CloudWatchMetricsReporter"),
+              eq(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS),
               any(Class[].class),
               eq(metricsConfig),
               eq(registry)
@@ -94,6 +94,68 @@ class TestMetricsReporterFactory {
     }
   }
 
+  /**
+   * {@code hudi-aws} is deliberately absent from this module's test 
classpath, which is exactly the
+   * situation a user hits on an engine bundle that does not shade that 
module: the reflectively loaded
+   * CloudWatch reporter cannot be found. The failure must name the missing 
class and how to fix it,
+   * not just report that some class could not be loaded.
+   */
+  @Test
+  void 
metricsReporterFactoryShouldExplainHowToEnableCloudWatchWhenHudiAwsIsMissing() {
+    
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
+
+    HoodieException exception = assertThrows(HoodieException.class,
+        () -> MetricsReporterFactory.createReporter(metricsConfig, registry));
+
+    String message = exception.getMessage();
+    
assertTrue(message.contains(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS),
+        () -> "The failure should name the missing reporter class, but was: " 
+ message);
+    assertTrue(message.contains("hudi-aws-bundle"),
+        () -> "The failure should name the bundle that provides the reporter, 
but was: " + message);
+    
assertTrue(message.contains(HoodieMetricsConfig.METRICS_REPORTER_TYPE_VALUE.key()),
+        () -> "The failure should name the config to change, but was: " + 
message);
+  }
+
+  /**
+   * The classpath-based test above exercises the real failure but passes for 
any resolution failure.
+   * This pins the mapping itself: a ClassNotFoundException cause is what 
triggers the rewrite.
+   */
+  @Test
+  void metricsReporterFactoryRewritesClassNotFoundIntoAnActionableMessage() {
+    
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
+    try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {
+      mockedStatic.when(() -> ReflectionUtils.loadClass(
+          eq(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS), 
any(Class[].class), eq(metricsConfig), eq(registry)))
+          .thenThrow(new HoodieException("Unable to load class " + 
MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS,
+              new 
ClassNotFoundException(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS)));
+
+      HoodieException exception = assertThrows(HoodieException.class,
+          () -> MetricsReporterFactory.createReporter(metricsConfig, 
registry));
+      assertTrue(exception.getMessage().contains("hudi-aws-bundle"),
+          () -> "Expected the remedy to be named, but was: " + 
exception.getMessage());
+    }
+  }
+
+  /**
+   * The other direction, and the branch most likely to regress: a failure 
that is not a missing class must
+   * pass through untouched, so an unrelated instantiation error is never 
rewritten into "add hudi-aws-bundle".
+   */
+  @Test
+  void metricsReporterFactoryLeavesNonClassNotFoundFailuresUntouched() {
+    
when(metricsConfig.getMetricsReporterType()).thenReturn(MetricsReporterType.CLOUDWATCH);
+    try (MockedStatic<ReflectionUtils> mockedStatic = 
Mockito.mockStatic(ReflectionUtils.class)) {
+      mockedStatic.when(() -> ReflectionUtils.loadClass(
+          eq(MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS), 
any(Class[].class), eq(metricsConfig), eq(registry)))
+          .thenThrow(new HoodieException("Unable to instantiate class " + 
MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS,
+              new NoSuchMethodException("<init>")));
+
+      HoodieException exception = assertThrows(HoodieException.class,
+          () -> MetricsReporterFactory.createReporter(metricsConfig, 
registry));
+      assertEquals("Unable to instantiate class " + 
MetricsReporterFactory.CLOUDWATCH_REPORTER_CLASS,
+          exception.getMessage(), "A non-ClassNotFound failure must not be 
rewritten");
+    }
+  }
+
   @Test
   void metricsReporterFactoryShouldReturnUserDefinedReporter() {
     
when(metricsConfig.getMetricReporterClassName()).thenReturn(DummyMetricsReporter.class.getName());
diff --git 
a/hudi-io/src/main/java/org/apache/hudi/common/util/ReflectionUtils.java 
b/hudi-io/src/main/java/org/apache/hudi/common/util/ReflectionUtils.java
index cb4cbc4290f8..6a6bd436ab41 100644
--- a/hudi-io/src/main/java/org/apache/hudi/common/util/ReflectionUtils.java
+++ b/hudi-io/src/main/java/org/apache/hudi/common/util/ReflectionUtils.java
@@ -50,7 +50,7 @@ public class ReflectionUtils {
       try {
         return Class.forName(c);
       } catch (ClassNotFoundException e) {
-        throw new HoodieException("Unable to load class", e);
+        throw new HoodieException("Unable to load class " + c, e);
       }
     });
   }

Reply via email to