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

adutra pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git


The following commit(s) were added to refs/heads/main by this push:
     new 361b7e924 Added interface for reporting metrics (#2887)
361b7e924 is described below

commit 361b7e9241da4fdbb0af9f10cf57788fbb8f2dcb
Author: cccs-cat001 <[email protected]>
AuthorDate: Wed Nov 5 05:44:02 2025 -0500

    Added interface for reporting metrics (#2887)
    
    Co-authored-by: Alexandre Dutra <[email protected]>
---
 CHANGELOG.md                                       |  7 +++
 .../it/test/PolarisRestCatalogIntegrationBase.java | 26 +++++++++++
 .../src/main/resources/application.properties      |  5 +++
 .../catalog/iceberg/IcebergCatalogAdapter.java     | 11 ++++-
 .../polaris/service/config/ServiceProducers.java   |  9 ++++
 .../service/reporting/DefaultMetricsReporter.java  | 52 ++++++++++++++++++++++
 .../reporting/MetricsReportingConfiguration.java   | 28 ++++++++++++
 .../service/reporting/PolarisMetricsReporter.java  | 26 +++++++++++
 .../reporting/DefaultMetricsReporterTest.java      | 43 ++++++++++++++++++
 .../org/apache/polaris/service/TestServices.java   |  4 +-
 10 files changed, 209 insertions(+), 2 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3e7f93964..53145b590 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,6 +29,13 @@ request adding CHANGELOG notes for breaking (!) changes and 
possibly other secti
 
 ### Highlights
 
+- Support for [Iceberg Metrics Reporting] has been introduced in Polaris. Out 
of the box, metrics can
+  be printed to the logs by setting the `org.apache.polaris.service.reporting` 
logger level to `INFO` (it's
+  set to `OFF` by default). Custom reporters can be implemented and configured 
to send metrics to
+  external systems for further analysis and monitoring.
+
+[Iceberg Metrics Reporting]: 
https://iceberg.apache.org/docs/latest/metrics-reporting/
+
 ### Upgrade notes
 
 - The legacy management endpoints at `/metrics` and `/healthcheck` have been 
removed. Please use the
diff --git 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
index 222022969..0b38f864a 100644
--- 
a/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
+++ 
b/integration-tests/src/main/java/org/apache/polaris/service/it/test/PolarisRestCatalogIntegrationBase.java
@@ -65,9 +65,14 @@ import org.apache.iceberg.exceptions.RESTException;
 import org.apache.iceberg.expressions.Expressions;
 import org.apache.iceberg.io.FileIO;
 import org.apache.iceberg.io.ResolvingFileIO;
+import org.apache.iceberg.metrics.ImmutableScanReport;
+import org.apache.iceberg.metrics.ScanMetrics;
+import org.apache.iceberg.metrics.ScanMetricsResult;
+import org.apache.iceberg.metrics.ScanReport;
 import org.apache.iceberg.rest.RESTCatalog;
 import org.apache.iceberg.rest.RESTUtil;
 import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.apache.iceberg.rest.requests.ReportMetricsRequest;
 import org.apache.iceberg.rest.responses.ErrorResponse;
 import org.apache.iceberg.rest.responses.ListNamespacesResponse;
 import org.apache.iceberg.rest.responses.ListTablesResponse;
@@ -882,6 +887,27 @@ public abstract class PolarisRestCatalogIntegrationBase 
extends CatalogTests<RES
     }
   }
 
+  @Test
+  public void testSendMetricsReport() {
+    ScanReport scanReport =
+        ImmutableScanReport.builder()
+            .tableName("tbl1")
+            .schemaId(4)
+            .addProjectedFieldIds(1, 2, 3)
+            .addProjectedFieldNames("c1", "c2", "c3")
+            .snapshotId(23L)
+            .filter(Expressions.alwaysTrue())
+            .scanMetrics(ScanMetricsResult.fromScanMetrics(ScanMetrics.noop()))
+            .build();
+    Invocation.Builder metricEndpoint =
+        catalogApi.request(
+            "v1/{cat}/namespaces/ns1/tables/tbl1/metrics", Map.of("cat", 
currentCatalogName));
+    try (Response response =
+        metricEndpoint.post(Entity.json(ReportMetricsRequest.of(scanReport)))) 
{
+      assertThat(response).returns(Response.Status.NO_CONTENT.getStatusCode(), 
Response::getStatus);
+    }
+  }
+
   @Test
   public void testSendNotificationInternalCatalog() {
     Map<String, String> payload =
diff --git a/runtime/defaults/src/main/resources/application.properties 
b/runtime/defaults/src/main/resources/application.properties
index 91d41f925..1a1331cfd 100644
--- a/runtime/defaults/src/main/resources/application.properties
+++ b/runtime/defaults/src/main/resources/application.properties
@@ -242,6 +242,11 @@ polaris.oidc.principal-roles-mapper.type=default
 # Polaris Credential Manager Config
 polaris.credential-manager.type=default
 
+# Configuration for the behaviour of the metrics endpoint
+polaris.iceberg-metrics.reporting.type=default
+# Set to INFO if you want to see iceberg metric reports logged
+quarkus.log.category."org.apache.polaris.service.reporting".level=OFF
+
 quarkus.arc.ignored-split-packages=\
   org.apache.polaris.service.catalog.api,\
   org.apache.polaris.service.catalog.api.impl,\
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
index e7ac69141..c636fb075 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/catalog/iceberg/IcebergCatalogAdapter.java
@@ -87,6 +87,7 @@ import 
org.apache.polaris.service.context.catalog.CallContextCatalogFactory;
 import org.apache.polaris.service.events.listeners.PolarisEventListener;
 import org.apache.polaris.service.http.IcebergHttpUtil;
 import org.apache.polaris.service.http.IfNoneMatch;
+import org.apache.polaris.service.reporting.PolarisMetricsReporter;
 import org.apache.polaris.service.types.CommitTableRequest;
 import org.apache.polaris.service.types.CommitViewRequest;
 import org.apache.polaris.service.types.NotificationRequest;
@@ -150,6 +151,7 @@ public class IcebergCatalogAdapter
   private final Instance<ExternalCatalogFactory> externalCatalogFactories;
   private final PolarisEventListener polarisEventListener;
   private final AccessConfigProvider accessConfigProvider;
+  private final PolarisMetricsReporter metricsReporter;
 
   @Inject
   public IcebergCatalogAdapter(
@@ -167,7 +169,8 @@ public class IcebergCatalogAdapter
       CatalogHandlerUtils catalogHandlerUtils,
       @Any Instance<ExternalCatalogFactory> externalCatalogFactories,
       PolarisEventListener polarisEventListener,
-      AccessConfigProvider accessConfigProvider) {
+      AccessConfigProvider accessConfigProvider,
+      PolarisMetricsReporter metricsReporter) {
     this.diagnostics = diagnostics;
     this.realmContext = realmContext;
     this.callContext = callContext;
@@ -184,6 +187,7 @@ public class IcebergCatalogAdapter
     this.externalCatalogFactories = externalCatalogFactories;
     this.polarisEventListener = polarisEventListener;
     this.accessConfigProvider = accessConfigProvider;
+    this.metricsReporter = metricsReporter;
   }
 
   /**
@@ -755,6 +759,11 @@ public class IcebergCatalogAdapter
       ReportMetricsRequest reportMetricsRequest,
       RealmContext realmContext,
       SecurityContext securityContext) {
+    String catalogName = prefixParser.prefixToCatalogName(realmContext, 
prefix);
+    Namespace ns = decodeNamespace(namespace);
+    TableIdentifier tableIdentifier = TableIdentifier.of(ns, 
RESTUtil.decodeString(table));
+
+    metricsReporter.reportMetric(catalogName, tableIdentifier, 
reportMetricsRequest.report());
     return Response.status(Response.Status.NO_CONTENT).build();
   }
 
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
index 01e9f6e89..bd03c022b 100644
--- 
a/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/config/ServiceProducers.java
@@ -83,6 +83,8 @@ import org.apache.polaris.service.ratelimiter.RateLimiter;
 import org.apache.polaris.service.ratelimiter.RateLimiterFilterConfiguration;
 import org.apache.polaris.service.ratelimiter.TokenBucketConfiguration;
 import org.apache.polaris.service.ratelimiter.TokenBucketFactory;
+import org.apache.polaris.service.reporting.MetricsReportingConfiguration;
+import org.apache.polaris.service.reporting.PolarisMetricsReporter;
 import org.apache.polaris.service.secrets.SecretsManagerConfiguration;
 import org.apache.polaris.service.storage.StorageConfiguration;
 import org.apache.polaris.service.storage.aws.S3AccessConfig;
@@ -441,4 +443,11 @@ public class ServiceProducers {
   public void closeTaskExecutor(@Disposes @Identifier("task-executor") 
ManagedExecutor executor) {
     executor.close();
   }
+
+  @Produces
+  @ApplicationScoped
+  public PolarisMetricsReporter metricsReporter(
+      MetricsReportingConfiguration config, @Any 
Instance<PolarisMetricsReporter> reporters) {
+    return reporters.select(Identifier.Literal.of(config.type())).get();
+  }
 }
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/reporting/DefaultMetricsReporter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/reporting/DefaultMetricsReporter.java
new file mode 100644
index 000000000..5c7b4934a
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/reporting/DefaultMetricsReporter.java
@@ -0,0 +1,52 @@
+/*
+ * 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.polaris.service.reporting;
+
+import com.google.common.annotations.VisibleForTesting;
+import io.smallrye.common.annotation.Identifier;
+import jakarta.enterprise.context.ApplicationScoped;
+import org.apache.commons.lang3.function.TriConsumer;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.metrics.MetricsReport;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@ApplicationScoped
+@Identifier("default")
+public class DefaultMetricsReporter implements PolarisMetricsReporter {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(DefaultMetricsReporter.class);
+
+  private final TriConsumer<String, TableIdentifier, MetricsReport> 
reportConsumer;
+
+  public DefaultMetricsReporter() {
+    this(
+        (catalogName, table, metricsReport) ->
+            LOGGER.info("{}.{}: {}", catalogName, table, metricsReport));
+  }
+
+  @VisibleForTesting
+  DefaultMetricsReporter(TriConsumer<String, TableIdentifier, MetricsReport> 
reportConsumer) {
+    this.reportConsumer = reportConsumer;
+  }
+
+  @Override
+  public void reportMetric(String catalogName, TableIdentifier table, 
MetricsReport metricsReport) {
+    reportConsumer.accept(catalogName, table, metricsReport);
+  }
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/reporting/MetricsReportingConfiguration.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/reporting/MetricsReportingConfiguration.java
new file mode 100644
index 000000000..3d60302ab
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/reporting/MetricsReportingConfiguration.java
@@ -0,0 +1,28 @@
+/*
+ * 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.polaris.service.reporting;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.WithDefault;
+
+@ConfigMapping(prefix = "polaris.iceberg-metrics.reporting")
+public interface MetricsReportingConfiguration {
+  @WithDefault("default")
+  String type();
+}
diff --git 
a/runtime/service/src/main/java/org/apache/polaris/service/reporting/PolarisMetricsReporter.java
 
b/runtime/service/src/main/java/org/apache/polaris/service/reporting/PolarisMetricsReporter.java
new file mode 100644
index 000000000..7ffd84f4d
--- /dev/null
+++ 
b/runtime/service/src/main/java/org/apache/polaris/service/reporting/PolarisMetricsReporter.java
@@ -0,0 +1,26 @@
+/*
+ * 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.polaris.service.reporting;
+
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.metrics.MetricsReport;
+
+public interface PolarisMetricsReporter {
+  public void reportMetric(String catalogName, TableIdentifier table, 
MetricsReport metricsReport);
+}
diff --git 
a/runtime/service/src/test/java/org/apache/polaris/service/reporting/DefaultMetricsReporterTest.java
 
b/runtime/service/src/test/java/org/apache/polaris/service/reporting/DefaultMetricsReporterTest.java
new file mode 100644
index 000000000..a8b214683
--- /dev/null
+++ 
b/runtime/service/src/test/java/org/apache/polaris/service/reporting/DefaultMetricsReporterTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.polaris.service.reporting;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import org.apache.commons.lang3.function.TriConsumer;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.metrics.MetricsReport;
+import org.junit.jupiter.api.Test;
+
+public class DefaultMetricsReporterTest {
+
+  @Test
+  void testLogging() {
+    TriConsumer<String, TableIdentifier, MetricsReport> mockConsumer = 
mock(TriConsumer.class);
+    DefaultMetricsReporter reporter = new DefaultMetricsReporter(mockConsumer);
+    String warehouse = "testWarehouse";
+    TableIdentifier table = TableIdentifier.of("testNamespace", "testTable");
+    MetricsReport metricsReport = mock(MetricsReport.class);
+
+    reporter.reportMetric(warehouse, table, metricsReport);
+
+    verify(mockConsumer).accept(warehouse, table, metricsReport);
+  }
+}
diff --git 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
index b39ff5a08..06d01ca06 100644
--- 
a/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
+++ 
b/runtime/service/src/testFixtures/java/org/apache/polaris/service/TestServices.java
@@ -82,6 +82,7 @@ import 
org.apache.polaris.service.events.listeners.PolarisEventListener;
 import org.apache.polaris.service.events.listeners.TestPolarisEventListener;
 import 
org.apache.polaris.service.identity.provider.DefaultServiceIdentityProvider;
 import 
org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory;
+import org.apache.polaris.service.reporting.DefaultMetricsReporter;
 import org.apache.polaris.service.secrets.UnsafeInMemorySecretsManagerFactory;
 import 
org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl;
 import org.apache.polaris.service.task.TaskExecutor;
@@ -281,7 +282,8 @@ public record TestServices(
               catalogHandlerUtils,
               externalCatalogFactory,
               polarisEventListener,
-              accessConfigProvider);
+              accessConfigProvider,
+              new DefaultMetricsReporter());
 
       IcebergRestCatalogApi restApi = new 
IcebergRestCatalogApi(catalogService);
       IcebergRestConfigurationApi restConfigurationApi =

Reply via email to