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

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


The following commit(s) were added to refs/heads/master by this push:
     new bbf73e95c32 HIVE-29593: Server-side Metrics Reporting for Iceberg 
operations (#6461)
bbf73e95c32 is described below

commit bbf73e95c326465399b33e3d8b5adcf81fde786f
Author: Shohei Okumiya <[email protected]>
AuthorDate: Wed May 6 12:38:34 2026 +0900

    HIVE-29593: Server-side Metrics Reporting for Iceberg operations (#6461)
    
    * HIVE-29593: Server-side Metrics Reporting for Iceberg operations
    
    * Remove unused import
    
    * Fix an error message
    
    * Add a type name for readability
    
    * Invoke all IcebergMetricsReporter#close
    
    * Use MetastoreConf.getClasses
    
    * Address checkstyle issues
---
 .../hadoop/hive/metastore/conf/MetastoreConf.java  | 29 +++++++++++++
 .../org/apache/iceberg/rest/HMSCatalogAdapter.java | 32 ++++++++++++---
 .../org/apache/iceberg/rest/HMSCatalogFactory.java | 19 ++++++++-
 .../org/apache/iceberg/rest/HMSCatalogServlet.java | 17 +++++++-
 .../rest/metrics/IcebergMetricsReporter.java       | 40 ++++++++++++++++++
 .../rest/metrics/LoggingMetricsReporter.java       | 47 ++++++++++++++++++++++
 .../iceberg/rest/extension/RESTCatalogServer.java  |  4 ++
 7 files changed, 179 insertions(+), 9 deletions(-)

diff --git 
a/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
 
b/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
index 10015f74837..8f8befd8348 100644
--- 
a/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
+++ 
b/standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java
@@ -33,6 +33,7 @@
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import com.google.common.base.Preconditions;
 import org.apache.hadoop.conf.Configuration;
 import org.apache.hadoop.hive.common.ZooKeeperHiveHelper;
 import org.apache.hadoop.hive.metastore.utils.StringUtils;
@@ -1935,6 +1936,10 @@ public enum ConfVars {
         "hive.metastore.iceberg.catalog.cache.expiry", -1,
         "HMS Iceberg Catalog cache expiry."
     ),
+    
ICEBERG_CATALOG_METRICS_REPORTERS("metastore.iceberg.catalog.metrics.reporters",
+        "hive.metastore.iceberg.catalog.metrics.reporters", 
"org.apache.iceberg.rest.metrics.LoggingMetricsReporter",
+        "A comma separated list of custom Iceberg Metrics Reporting plugins."
+    ),
     HTTPSERVER_THREADPOOL_MIN("hive.metastore.httpserver.threadpool.min",
             "hive.metastore.httpserver.threadpool.min", 8,
             "HMS embedded HTTP server minimum number of threads."
@@ -2516,6 +2521,30 @@ public static <I> Class<? extends I> 
getClass(Configuration conf, ConfVars var,
         conf.getClass(var.varname, defaultValue, xface);
   }
 
+  /**
+   * Get class instances based on a configuration value.
+   *
+   * @param conf configuration file to retrieve it from
+   * @param confVar variable to retrieve
+   * @return instances of the classes
+   */
+  public static Class<?>[] getClasses(Configuration conf, ConfVars confVar) {
+    Preconditions.checkArgument(confVar.defaultVal.getClass() == String.class);
+    Class<?> defaultClass;
+    try {
+      defaultClass = Class.forName((String) confVar.defaultVal);
+    } catch (ClassNotFoundException e) {
+      throw new IllegalArgumentException(
+          String.format("Failed to load the the default value of %s: %s", 
confVar.defaultVal, confVar.varname),
+          e
+      );
+    }
+    String val = conf.get(confVar.varname);
+    return val == null
+        ? conf.getClasses(confVar.hiveName, defaultClass)
+        : conf.getClasses(confVar.varname, defaultClass);
+  }
+
   /**
    * Set the class name in the configuration file
    * @param conf configuration file to set it in
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
index 73d23ae5daf..252bde85a5e 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
@@ -20,6 +20,9 @@
 package org.apache.iceberg.rest;
 
 import com.google.common.base.Preconditions;
+
+import java.io.IOException;
+import java.time.Clock;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -52,6 +55,7 @@
 import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
 import org.apache.iceberg.relocated.com.google.common.collect.Lists;
 import org.apache.iceberg.rest.HTTPRequest.HTTPMethod;
+import org.apache.iceberg.rest.metrics.IcebergMetricsReporter;
 import org.apache.iceberg.rest.requests.CommitTransactionRequest;
 import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
@@ -72,12 +76,15 @@
 import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse;
 import org.apache.iceberg.util.Pair;
 import org.apache.iceberg.util.PropertyUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Original @ <a 
href="https://github.com/apache/iceberg/blob/apache-iceberg-1.9.1/core/src/test/java/org/apache/iceberg/rest/RESTCatalogAdapter.java";>RESTCatalogAdapter.java</a>
  * Adaptor class to translate REST requests into {@link Catalog} API calls.
  */
 public class HMSCatalogAdapter implements RESTClient {
+  private static final Logger LOG = 
LoggerFactory.getLogger(HMSCatalogAdapter.class);
   private static final Splitter SLASH = Splitter.on('/');
 
   private static final Map<Class<? extends Exception>, Integer> 
EXCEPTION_ERROR_CODES =
@@ -99,17 +106,21 @@ public class HMSCatalogAdapter implements RESTClient {
           .put(CommitStateUnknownException.class, 500)
           .buildOrThrow();
 
+  private final String catalogName;
   private final Catalog catalog;
   private final SupportsNamespaces asNamespaceCatalog;
   private final ViewCatalog asViewCatalog;
+  private final List<IcebergMetricsReporter> metricsReporters;
+  private final Clock clock = Clock.systemUTC();
 
-
-  public HMSCatalogAdapter(Catalog catalog) {
+  public HMSCatalogAdapter(String catalogName, Catalog catalog, 
List<IcebergMetricsReporter> metricsReporters) {
     Preconditions.checkArgument(catalog instanceof SupportsNamespaces);
     Preconditions.checkArgument(catalog instanceof ViewCatalog);
+    this.catalogName = catalogName;
     this.catalog = catalog;
     this.asNamespaceCatalog = (SupportsNamespaces) catalog;
     this.asViewCatalog = (ViewCatalog) catalog;
+    this.metricsReporters = metricsReporters;
   }
 
   enum Route {
@@ -315,9 +326,11 @@ private RESTResponse renameTable(Object body) {
     return null;
   }
 
-  private RESTResponse reportMetrics(Object body) {
-    // nothing to do here other than checking that we're getting the correct 
request
-    castRequest(ReportMetricsRequest.class, body);
+  private RESTResponse reportMetrics(Map<String, String> vars, Object body) {
+    final TableIdentifier ident = identFromPathVars(vars);
+    final var report = castRequest(ReportMetricsRequest.class, body).report();
+    final var receivedAt = clock.instant();
+    metricsReporters.forEach(reporter -> reporter.report(catalogName, ident, 
report, receivedAt));
     return null;
   }
 
@@ -456,7 +469,7 @@ private <T extends RESTResponse> T handleRequest(
         return (T) renameTable(body);
 
       case REPORT_METRICS:
-        return (T) reportMetrics(body);
+        return (T) reportMetrics(vars, body);
 
       case COMMIT_TRANSACTION:
         return (T) commitTransaction(body);
@@ -576,6 +589,13 @@ public <T extends RESTResponse> T postForm(
   @Override
   public void close() {
     // The caller is responsible for closing the underlying catalog backing 
this REST catalog.
+    for (IcebergMetricsReporter reporter : metricsReporters) {
+      try {
+        reporter.close();
+      } catch (IOException e) {
+        LOG.error("Failed to close metrics reporter: {}", reporter, e);
+      }
+    }
   }
 
   private static class BadResponseType extends RuntimeException {
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java
index 31bba91e77f..d7bd3251a0c 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java
@@ -18,6 +18,8 @@
  */
 package org.apache.iceberg.rest;
 
+import java.lang.reflect.InvocationTargetException;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
@@ -33,6 +35,7 @@
 import org.apache.iceberg.CatalogProperties;
 import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.hive.HiveCatalog;
+import org.apache.iceberg.rest.metrics.IcebergMetricsReporter;
 
 /**
  * Catalog &amp; servlet factory.
@@ -109,7 +112,21 @@ private HttpServlet createServlet(Catalog catalog) {
     // Iceberg REST client uses "catalog" by default
     List<String> scopes = Collections.singletonList("catalog");
     ServletSecurity security = new 
ServletSecurity(AuthType.fromString(authType), configuration, req -> scopes);
-    return security.proxy(new HMSCatalogServlet(new 
HMSCatalogAdapter(catalog)));
+    String catalogName = MetastoreConf.getVar(configuration, 
ConfVars.CATALOG_DEFAULT);
+    List<IcebergMetricsReporter> reporters = createReporters();
+    return security.proxy(new HMSCatalogServlet(new 
HMSCatalogAdapter(catalogName, catalog, reporters)));
+  }
+
+  private List<IcebergMetricsReporter> createReporters() {
+    final var classes = MetastoreConf.getClasses(configuration, 
ConfVars.ICEBERG_CATALOG_METRICS_REPORTERS);
+    return Arrays.stream(classes).map(clazz -> {
+      try {
+        final var constructor = 
clazz.getDeclaredConstructor(Configuration.class);
+        return (IcebergMetricsReporter) constructor.newInstance(configuration);
+      } catch (NoSuchMethodException | InstantiationException | 
IllegalAccessException | InvocationTargetException e) {
+        throw new IllegalArgumentException("Failed to instantiate 
IcebergMetricsReporter: " + clazz.getName(), e);
+      }
+    }).toList();
   }
 
   /**
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogServlet.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogServlet.java
index 6140f40b2de..e608ba5229a 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogServlet.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogServlet.java
@@ -100,6 +100,12 @@ private Consumer<ErrorResponse> handle(HttpServletResponse 
response) {
     };
   }
 
+  @Override
+  public void destroy() {
+    super.destroy();
+    restCatalogAdapter.close();
+  }
+
   public static class ServletRequestContext {
     private HTTPMethod method;
     private String path;
@@ -147,8 +153,15 @@ static ServletRequestContext from(HttpServletRequest 
request) throws IOException
       Route route = routeContext.first();
       Object requestBody = null;
       if (route.requestClass() != null) {
-        requestBody =
-            RESTObjectMapper.mapper().readValue(request.getReader(), 
route.requestClass());
+        try {
+          requestBody = 
RESTObjectMapper.mapper().readValue(request.getReader(), route.requestClass());
+        } catch (Exception e) {
+          return new ServletRequestContext(ErrorResponse.builder()
+              .responseCode(400)
+              .withType(e.getClass().getSimpleName())
+              .withMessage(e.getMessage())
+              .build());
+        }
       }
 
       Map<String, String> queryParams =
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/metrics/IcebergMetricsReporter.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/metrics/IcebergMetricsReporter.java
new file mode 100644
index 00000000000..d7f781e3623
--- /dev/null
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/metrics/IcebergMetricsReporter.java
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iceberg.rest.metrics;
+
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.metrics.MetricsReport;
+
+import java.io.Closeable;
+import java.time.Instant;
+
+/**
+ * An event reporter for Iceberg metrics.
+ */
+public interface IcebergMetricsReporter extends Closeable {
+  /**
+   * Report an event.
+   *
+   * @param catalog the catalog name
+   * @param identifier the table identifier
+   * @param report the event
+   * @param receivedAt the timestamp when the Iceberg REST Catalog received 
the event
+   */
+  void report(String catalog, TableIdentifier identifier, MetricsReport 
report, Instant receivedAt);
+}
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/metrics/LoggingMetricsReporter.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/metrics/LoggingMetricsReporter.java
new file mode 100644
index 00000000000..afc37cd4066
--- /dev/null
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/metrics/LoggingMetricsReporter.java
@@ -0,0 +1,47 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.iceberg.rest.metrics;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.metrics.MetricsReport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.time.Instant;
+
+/**
+ * A metrics reporter that logs events.
+ */
+public class LoggingMetricsReporter implements IcebergMetricsReporter {
+  private static final Logger LOG = 
LoggerFactory.getLogger(LoggingMetricsReporter.class);
+
+  public LoggingMetricsReporter(Configuration conf) {
+  }
+
+  @Override
+  public void report(String catalog, TableIdentifier identifier, MetricsReport 
report, Instant receivedAt) {
+    LOG.info("Event reported at {}: catalog={}, table={}, report={}", 
receivedAt, catalog, identifier, report);
+  }
+
+  @Override
+  public void close() {
+    LOG.info("Closing {}", getClass().getName());
+  }
+}
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/RESTCatalogServer.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/RESTCatalogServer.java
index 836e18cb8e7..13e322bcd4b 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/RESTCatalogServer.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/RESTCatalogServer.java
@@ -19,6 +19,7 @@
 
 package org.apache.iceberg.rest.extension;
 
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.UUID;
 import org.apache.hadoop.conf.Configuration;
@@ -60,6 +61,9 @@ public void start(Configuration conf) throws Exception {
 
     String uniqueTestKey = String.format("RESTCatalogServer_%s", 
UUID.randomUUID());
     warehouseDir = 
Path.of(MetaStoreTestUtils.getTestWarehouseDir(uniqueTestKey));
+    Files.createDirectories(warehouseDir);
+    System.setProperty("derby.stream.error.file",
+        warehouseDir.resolve("derby.log").toAbsolutePath().toString());
     String jdbcUrl = String.format("jdbc:derby:memory:%s;create=true",
         warehouseDir.resolve("metastore_db").toAbsolutePath());
     MetastoreConf.setVar(conf, ConfVars.CONNECT_URL_KEY, jdbcUrl);

Reply via email to