Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ab99206
feat(metrics): implement Table Metrics REST API
Apr 2, 2026
f9345c2
docs: add changelog and documentation for Table Metrics REST API
Apr 2, 2026
2106b0f
feat(metrics): adopt stable envelope design for metrics reports API
Apr 2, 2026
d21c8e5
fix(metrics): harden error handling and test coverage in MetricsRepor…
Apr 2, 2026
699405f
fix(metrics): remove internal catalogId/tableId from metrics report r…
Apr 2, 2026
2a7039a
refactor(metrics): remove AtomicReference and columns param per revie…
Apr 10, 2026
83f2292
Spotless fixes
Apr 10, 2026
0a2d009
refactor(metrics): address PR review comments
Apr 15, 2026
e6b75c2
docs(metrics): mark Metrics Reports API as beta in changelog and spec
Apr 16, 2026
94bece7
fix(metrics): add resteasy-reactive testRuntimeOnly to fix ClassNotFo…
Apr 16, 2026
ea89998
fix(metrics): add @Beta to PolarisMetricsManager and PolarisMetricsRe…
Apr 20, 2026
a0ffa8b
Review comments.
Apr 28, 2026
756bf49
Replace Map<String,Object> response with typed Jackson record classes
May 16, 2026
66bfedc
fix(jdbc): replace unresolved @Nonnull with @NonNull in JdbcBasePersi…
May 26, 2026
0800631
fix(metrics-api): use JSON arrays for projectedFieldIds/Names, add #4…
Jun 1, 2026
a0a4f62
fix(bom): add polaris-api-metrics-reports-service to BOM
Jun 1, 2026
f370e93
ci: retrigger CI (transient RH registry 500 on minio job)
Jun 1, 2026
8f5ae68
refactor(metrics): fix SPI layering — move IcebergMetricsReporter to …
Jun 14, 2026
4119788
fix(metrics): restore MetricsPersistence SPI types to polaris-core
Jun 14, 2026
86ee037
fix(metrics): remove MetricsPersistence from JdbcBasePersistenceImpl,…
Jun 14, 2026
e904d25
fix(metrics): rename lambda params to avoid shadowing createHandler's…
Jun 14, 2026
474c039
fix(metrics): fix three runtime failures from Scope 1 refactor
Jun 14, 2026
9d336c8
fix(metrics): use 'default' identifier for LoggingMetricsReporter to …
Jun 14, 2026
4702adf
docs(config): regenerate config reference for metrics reporting default
Jun 14, 2026
286adc6
refactor(metrics): move IcebergMetricsReporter SPI to extensions/metr…
Jun 15, 2026
48816e4
fix(metrics): use runtimeOnly for metrics-reports impl dep in runtime…
Jun 15, 2026
c8a050c
fix(metrics): remove metrics-reports impl dep from runtime/service
Jun 15, 2026
8a779cc
refactor(metrics): move NoOpMetricsReporter to SPI; address review co…
Jun 16, 2026
f611188
refactor(metrics): address PR #4115 review comments
Jun 16, 2026
cc33a7f
refactor(metrics): address PR review comments r3424416195, r342442294…
Jun 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,14 @@ request adding CHANGELOG notes for breaking (!) changes and possibly other secti

- The (Before/After)CommitViewEvent has been removed.
- The (Before/After)CommitTableEvent has been removed.
- The `PolarisMetricsReporter.reportMetric()` method signature has been extended to include a `receivedTimestamp` parameter of type `java.time.Instant`.
- The `PolarisMetricsReporter` SPI (previously in `runtime/service`) has been replaced by `IcebergMetricsReporter` in `extensions/metrics-reports/spi`. Custom reporters must implement `org.apache.polaris.extension.metrics.IcebergMetricsReporter` and update the `@Identifier` annotation for CDI selection via `polaris.iceberg-metrics.reporting.type`. The method signature now includes a `receivedTimestamp` parameter of type `java.time.Instant`.
- The `ExternalCatalogFactory.createCatalog()` and `createGenericCatalog()` method signatures have been extended to include a `catalogProperties` parameter of type `Map<String, String>` for passing through proxy and timeout settings to federated catalog HTTP clients.
- Metrics reporting now requires the `TABLE_READ_DATA` privilege on the target table for read (scan) metrics and `TABLE_WRITE_DATA` for write (commit) metrics.
- The `REVOKE_CATALOG_ROLE_FROM_PRINCIPAL_ROLE` operation no longer requires the `PRINCIPAL_ROLE_MANAGE_GRANTS_FOR_GRANTEE` privilege. Only `CATALOG_ROLE_MANAGE_GRANTS_ON_SECURABLE` on the catalog role is now required, making revoke symmetric with assign. This allows catalog administrators to fully manage catalog role assignments without requiring elevated privileges on principal roles.

### New Features

- Added a **beta** Table Metrics REST API (`/api/metrics-reports/v1/`) for querying Iceberg scan and commit metrics. In this release the read path returns an empty result set; durable storage backing is provided by the `polaris-extensions-metrics-reports-jdbc` extension in a follow-up release. Querying requires the new `TABLE_READ_METRICS` privilege on the target table.
- Added `deploymentAnnotations` support in Helm chart.
- Added KMS properties (optional) to catalog storage config to enable S3 data encryption.
- Added `topologySpreadConstraints` support in Helm chart.
Expand Down
93 changes: 93 additions & 0 deletions api/metrics-reports-service/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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.
*/

import org.openapitools.generator.gradle.plugin.tasks.GenerateTask

plugins {
alias(libs.plugins.openapi.generator)
id("polaris-client")
id("org.kordamp.gradle.jandex")
}

dependencies {
implementation(project(":polaris-core"))

compileOnly(platform(libs.jackson.bom))
compileOnly("com.fasterxml.jackson.core:jackson-annotations")

compileOnly(libs.jakarta.annotation.api)
compileOnly(libs.jakarta.inject.api)
compileOnly(libs.jakarta.validation.api)
compileOnly(libs.microprofile.fault.tolerance.api)
compileOnly(libs.swagger.annotations)

implementation(libs.jakarta.servlet.api)
implementation(libs.jakarta.ws.rs.api)

compileOnly(platform(libs.micrometer.bom))
compileOnly("io.micrometer:micrometer-core")

implementation(libs.slf4j.api)
}

val rootDir = rootProject.layout.projectDirectory
val specsDir = rootDir.dir("spec")
val templatesDir = rootDir.dir("server-templates")
val generatedDir = project.layout.buildDirectory.dir("generated-openapi")
val generatedOpenApiSrcDir = project.layout.buildDirectory.dir("generated-openapi/src/main/java")

openApiGenerate {
inputSpec = provider { specsDir.file("metrics-reports-service.yml").asFile.absolutePath }
generatorName = "jaxrs-resteasy"
outputDir = provider { generatedDir.get().asFile.absolutePath }
apiPackage = "org.apache.polaris.service.metrics.api"
modelPackage = "org.apache.polaris.core.metrics.api.model"
ignoreFileOverride.set(provider { rootDir.file(".openapi-generator-ignore").asFile.absolutePath })
removeOperationIdPrefix.set(true)
templateDir.set(provider { templatesDir.asFile.absolutePath })
globalProperties.put("apis", "")
globalProperties.put("models", "")
globalProperties.put("apiDocs", "false")
globalProperties.put("modelTests", "false")
configOptions.put("openApiNullable", "false")
configOptions.put("useBeanValidation", "true")
configOptions.put("sourceFolder", "src/main/java")
configOptions.put("useJakartaEe", "true")
configOptions.put("generateBuilders", "true")
configOptions.put("generateConstructorWithAllArgs", "true")
configOptions.put("hideGenerationTimestamp", "true")
additionalProperties.put("apiNamePrefix", "Polaris")
additionalProperties.put("apiNameSuffix", "Api")
additionalProperties.put("metricsPrefix", "polaris")
serverVariables.put("basePath", "api/metrics-reports/v1")
}

listOf("sourcesJar", "compileJava", "processResources").forEach { task ->
tasks.named(task) { dependsOn("openApiGenerate") }
}

sourceSets { main { java { srcDir(generatedOpenApiSrcDir) } } }

tasks.named<GenerateTask>("openApiGenerate") {
inputs.dir(templatesDir)
inputs.dir(specsDir)
actions.addFirst { delete { delete(generatedDir) } }
}

tasks.named("javadoc") { dependsOn("jandex") }
3 changes: 3 additions & 0 deletions bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies {
api(project(":polaris-api-iceberg-service"))
api(project(":polaris-api-management-model"))
api(project(":polaris-api-management-service"))
api(project(":polaris-api-metrics-reports-service"))

api(project(":polaris-container-spec-helper"))
api(project(":polaris-azurite-testcontainer"))
Expand Down Expand Up @@ -101,6 +102,7 @@ dependencies {
api(project(":polaris-extensions-federation-bigquery"))
api(project(":polaris-extensions-federation-hadoop"))
api(project(":polaris-extensions-federation-hive"))
api(project(":polaris-extensions-metrics-reports-spi"))
api(project(":polaris-hms-testcontainer"))

api(project(":polaris-spark-3.5_2.12"))
Expand All @@ -112,6 +114,7 @@ dependencies {
api(project(":polaris-spark-4.0_2.13"))
api(project(":polaris-spark-integration-4.0_2.13"))
}
api(project(":polaris-extensions-metrics-reports"))

api(project(":polaris-admin"))
api(project(":polaris-runtime-common"))
Expand Down
39 changes: 39 additions & 0 deletions extensions/metrics-reports/impl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* 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.
*/

plugins {
id("polaris-server")
id("org.kordamp.gradle.jandex")
}

dependencies {
implementation(project(":polaris-extensions-metrics-reports-spi"))

implementation(platform(libs.iceberg.bom))
implementation("org.apache.iceberg:iceberg-api")

implementation(libs.jakarta.enterprise.cdi.api)
implementation(libs.jakarta.inject.api)
implementation(libs.smallrye.common.annotation)
implementation(libs.slf4j.api)

testImplementation(platform(libs.junit.bom))
testImplementation("org.junit.jupiter:junit-jupiter")
testImplementation(libs.mockito.core)
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,37 +16,30 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.polaris.service.reporting;
package org.apache.polaris.extension.metrics.reports;

import com.google.common.annotations.VisibleForTesting;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.Instant;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.metrics.MetricsReport;
import org.apache.polaris.extension.metrics.IcebergMetricsReporter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* Default implementation of {@link PolarisMetricsReporter} that logs metrics to the configured
* logger.
* Log-only implementation of {@link IcebergMetricsReporter}.
*
* <p>This implementation is selected when {@code polaris.iceberg-metrics.reporting.type} is set to
* {@code "default"} (the default value).
* <p>Selected when {@code polaris.iceberg-metrics.reporting.type} is set to {@code "log"}.
*
* <p>By default, logging is disabled. To enable metrics logging, set the logger level for {@code
* org.apache.polaris.service.reporting} to {@code INFO} in your logging configuration.
*
* @see PolarisMetricsReporter
* <p>Logging is at INFO level. Enable it by setting the logger level for {@code
* org.apache.polaris.extension.metrics.reports} to {@code INFO}.
*/
@ApplicationScoped
@Identifier("default")
public class DefaultMetricsReporter implements PolarisMetricsReporter {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultMetricsReporter.class);

private final ReportConsumer reportConsumer;
@Identifier("log")
public class LoggingMetricsReporter implements IcebergMetricsReporter {
private static final Logger LOGGER = LoggerFactory.getLogger(LoggingMetricsReporter.class);

/** Functional interface for consuming metrics reports with timestamp. */
@FunctionalInterface
interface ReportConsumer {
void accept(
Expand All @@ -58,15 +51,15 @@ void accept(
Instant receivedTimestamp);
}

/** Creates a new DefaultMetricsReporter that logs metrics to the class logger. */
public DefaultMetricsReporter() {
private final ReportConsumer reportConsumer;

public LoggingMetricsReporter() {
this(
(catalogName, catalogId, table, tableId, metricsReport, receivedTimestamp) ->
LOGGER.info("{}.{} (ts={}): {}", catalogName, table, receivedTimestamp, metricsReport));
}

@VisibleForTesting
DefaultMetricsReporter(ReportConsumer reportConsumer) {
LoggingMetricsReporter(ReportConsumer reportConsumer) {
this.reportConsumer = reportConsumer;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.extension.metrics.reports;

import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import java.time.Instant;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.metrics.MetricsReport;
import org.apache.polaris.extension.metrics.IcebergMetricsReporter;

/**
* No-op implementation of {@link IcebergMetricsReporter} that silently discards all metrics.
*
* <p>Selected when {@code polaris.iceberg-metrics.reporting.type} is set to {@code "no-op"}.
*/
@ApplicationScoped
@Identifier("no-op")
public class NoOpMetricsReporter implements IcebergMetricsReporter {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor layering question: this is an implementation, but it lives in the spi module and pulls CDI annotations into the SPI artifact. It should go hand in hand along with LoggingMetricsReporter.

Also, I'm a bit confused here, why we are having both no op and logging impls? which one is the default? thought this pr is meant to include default impl only? tho I think the other one can sneak in given it is straightforward.

IIUC, you are placing no op as the default? if so, lets update the javadoc accordingly as I pointed out above


@Override
public void reportMetric(
String catalogName,
long catalogId,
TableIdentifier table,
long tableId,
MetricsReport metricsReport,
Instant receivedTimestamp) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.polaris.service.reporting;
package org.apache.polaris.extension.metrics.reports;

import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
Expand All @@ -26,13 +26,13 @@
import org.apache.iceberg.metrics.MetricsReport;
import org.junit.jupiter.api.Test;

public class DefaultMetricsReporterTest {
public class LoggingMetricsReporterTest {

@Test
void testLogging() {
DefaultMetricsReporter.ReportConsumer mockConsumer =
mock(DefaultMetricsReporter.ReportConsumer.class);
DefaultMetricsReporter reporter = new DefaultMetricsReporter(mockConsumer);
LoggingMetricsReporter.ReportConsumer mockConsumer =
mock(LoggingMetricsReporter.ReportConsumer.class);
LoggingMetricsReporter reporter = new LoggingMetricsReporter(mockConsumer);
String warehouse = "testWarehouse";
long catalogId = 12345L;
TableIdentifier table = TableIdentifier.of("testNamespace", "testTable");
Expand Down
27 changes: 27 additions & 0 deletions extensions/metrics-reports/spi/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* 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.
*/

plugins {
id("polaris-server")
}

dependencies {
implementation(platform(libs.iceberg.bom))
implementation("org.apache.iceberg:iceberg-api")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,38 +16,33 @@
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.polaris.service.reporting;
package org.apache.polaris.extension.metrics;

import java.time.Instant;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.metrics.MetricsReport;

/**
* SPI interface for reporting Iceberg metrics received by Polaris.
* SPI for reporting Iceberg metrics received by Polaris.
*
* <p>Implementations can be used to send metrics to external systems for analysis and monitoring.
* Custom implementations can be annotated with appropriate {@code Quarkus} scope and {@link
* io.smallrye.common.annotation.Identifier @Identifier("my-reporter-type")} for CDI discovery.
* <p>Implementations receive a resolved table context (catalog name/id and table name/id) along
* with the raw Iceberg {@link MetricsReport}. Custom implementations should be annotated with the
* appropriate CDI scope and {@code @Identifier("my-type")} for selection via the {@code
* polaris.iceberg-metrics.reporting.type} configuration property.
*
* <p>The implementation to use is selected via the {@code polaris.iceberg-metrics.reporting.type}
* configuration property, which defaults to {@code "default"}.
*
* <p>Implementations can inject other CDI beans for context.
*
* @see DefaultMetricsReporter
* @see MetricsReportingConfiguration
* <p>This interface is intentionally runtime/framework-agnostic. CDI and configuration concerns
* belong in the implementing class, not here.
*/
public interface PolarisMetricsReporter {
public interface IcebergMetricsReporter {

/**
* Reports an Iceberg metrics report for a specific table.
* Reports an Iceberg metrics report for a resolved table.
*
* @param catalogName the name of the catalog containing the table
* @param catalogId the internal Polaris ID of the catalog
* @param table the identifier of the table the metrics are for
* @param tableId the internal Polaris ID of the table entity
* @param metricsReport the Iceberg metrics report (e.g., {@link
* org.apache.iceberg.metrics.ScanReport} or {@link org.apache.iceberg.metrics.CommitReport})
* @param metricsReport the Iceberg metrics report
* @param receivedTimestamp the timestamp when the metrics were received by Polaris
*/
void reportMetric(
Expand Down
Loading