Skip to content

Commit bbf73e9

Browse files
authored
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
1 parent 1a80cc5 commit bbf73e9

7 files changed

Lines changed: 179 additions & 9 deletions

File tree

standalone-metastore/metastore-common/src/main/java/org/apache/hadoop/hive/metastore/conf/MetastoreConf.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import java.util.regex.Matcher;
3434
import java.util.regex.Pattern;
3535

36+
import com.google.common.base.Preconditions;
3637
import org.apache.hadoop.conf.Configuration;
3738
import org.apache.hadoop.hive.common.ZooKeeperHiveHelper;
3839
import org.apache.hadoop.hive.metastore.utils.StringUtils;
@@ -1935,6 +1936,10 @@ public enum ConfVars {
19351936
"hive.metastore.iceberg.catalog.cache.expiry", -1,
19361937
"HMS Iceberg Catalog cache expiry."
19371938
),
1939+
ICEBERG_CATALOG_METRICS_REPORTERS("metastore.iceberg.catalog.metrics.reporters",
1940+
"hive.metastore.iceberg.catalog.metrics.reporters", "org.apache.iceberg.rest.metrics.LoggingMetricsReporter",
1941+
"A comma separated list of custom Iceberg Metrics Reporting plugins."
1942+
),
19381943
HTTPSERVER_THREADPOOL_MIN("hive.metastore.httpserver.threadpool.min",
19391944
"hive.metastore.httpserver.threadpool.min", 8,
19401945
"HMS embedded HTTP server minimum number of threads."
@@ -2516,6 +2521,30 @@ public static <I> Class<? extends I> getClass(Configuration conf, ConfVars var,
25162521
conf.getClass(var.varname, defaultValue, xface);
25172522
}
25182523

2524+
/**
2525+
* Get class instances based on a configuration value.
2526+
*
2527+
* @param conf configuration file to retrieve it from
2528+
* @param confVar variable to retrieve
2529+
* @return instances of the classes
2530+
*/
2531+
public static Class<?>[] getClasses(Configuration conf, ConfVars confVar) {
2532+
Preconditions.checkArgument(confVar.defaultVal.getClass() == String.class);
2533+
Class<?> defaultClass;
2534+
try {
2535+
defaultClass = Class.forName((String) confVar.defaultVal);
2536+
} catch (ClassNotFoundException e) {
2537+
throw new IllegalArgumentException(
2538+
String.format("Failed to load the the default value of %s: %s", confVar.defaultVal, confVar.varname),
2539+
e
2540+
);
2541+
}
2542+
String val = conf.get(confVar.varname);
2543+
return val == null
2544+
? conf.getClasses(confVar.hiveName, defaultClass)
2545+
: conf.getClasses(confVar.varname, defaultClass);
2546+
}
2547+
25192548
/**
25202549
* Set the class name in the configuration file
25212550
* @param conf configuration file to set it in

standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
package org.apache.iceberg.rest;
2121

2222
import com.google.common.base.Preconditions;
23+
24+
import java.io.IOException;
25+
import java.time.Clock;
2326
import java.util.Arrays;
2427
import java.util.List;
2528
import java.util.Map;
@@ -52,6 +55,7 @@
5255
import org.apache.iceberg.relocated.com.google.common.collect.ImmutableMap;
5356
import org.apache.iceberg.relocated.com.google.common.collect.Lists;
5457
import org.apache.iceberg.rest.HTTPRequest.HTTPMethod;
58+
import org.apache.iceberg.rest.metrics.IcebergMetricsReporter;
5559
import org.apache.iceberg.rest.requests.CommitTransactionRequest;
5660
import org.apache.iceberg.rest.requests.CreateNamespaceRequest;
5761
import org.apache.iceberg.rest.requests.CreateTableRequest;
@@ -72,12 +76,15 @@
7276
import org.apache.iceberg.rest.responses.UpdateNamespacePropertiesResponse;
7377
import org.apache.iceberg.util.Pair;
7478
import org.apache.iceberg.util.PropertyUtil;
79+
import org.slf4j.Logger;
80+
import org.slf4j.LoggerFactory;
7581

7682
/**
7783
* 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>
7884
* Adaptor class to translate REST requests into {@link Catalog} API calls.
7985
*/
8086
public class HMSCatalogAdapter implements RESTClient {
87+
private static final Logger LOG = LoggerFactory.getLogger(HMSCatalogAdapter.class);
8188
private static final Splitter SLASH = Splitter.on('/');
8289

8390
private static final Map<Class<? extends Exception>, Integer> EXCEPTION_ERROR_CODES =
@@ -99,17 +106,21 @@ public class HMSCatalogAdapter implements RESTClient {
99106
.put(CommitStateUnknownException.class, 500)
100107
.buildOrThrow();
101108

109+
private final String catalogName;
102110
private final Catalog catalog;
103111
private final SupportsNamespaces asNamespaceCatalog;
104112
private final ViewCatalog asViewCatalog;
113+
private final List<IcebergMetricsReporter> metricsReporters;
114+
private final Clock clock = Clock.systemUTC();
105115

106-
107-
public HMSCatalogAdapter(Catalog catalog) {
116+
public HMSCatalogAdapter(String catalogName, Catalog catalog, List<IcebergMetricsReporter> metricsReporters) {
108117
Preconditions.checkArgument(catalog instanceof SupportsNamespaces);
109118
Preconditions.checkArgument(catalog instanceof ViewCatalog);
119+
this.catalogName = catalogName;
110120
this.catalog = catalog;
111121
this.asNamespaceCatalog = (SupportsNamespaces) catalog;
112122
this.asViewCatalog = (ViewCatalog) catalog;
123+
this.metricsReporters = metricsReporters;
113124
}
114125

115126
enum Route {
@@ -315,9 +326,11 @@ private RESTResponse renameTable(Object body) {
315326
return null;
316327
}
317328

318-
private RESTResponse reportMetrics(Object body) {
319-
// nothing to do here other than checking that we're getting the correct request
320-
castRequest(ReportMetricsRequest.class, body);
329+
private RESTResponse reportMetrics(Map<String, String> vars, Object body) {
330+
final TableIdentifier ident = identFromPathVars(vars);
331+
final var report = castRequest(ReportMetricsRequest.class, body).report();
332+
final var receivedAt = clock.instant();
333+
metricsReporters.forEach(reporter -> reporter.report(catalogName, ident, report, receivedAt));
321334
return null;
322335
}
323336

@@ -456,7 +469,7 @@ private <T extends RESTResponse> T handleRequest(
456469
return (T) renameTable(body);
457470

458471
case REPORT_METRICS:
459-
return (T) reportMetrics(body);
472+
return (T) reportMetrics(vars, body);
460473

461474
case COMMIT_TRANSACTION:
462475
return (T) commitTransaction(body);
@@ -576,6 +589,13 @@ public <T extends RESTResponse> T postForm(
576589
@Override
577590
public void close() {
578591
// The caller is responsible for closing the underlying catalog backing this REST catalog.
592+
for (IcebergMetricsReporter reporter : metricsReporters) {
593+
try {
594+
reporter.close();
595+
} catch (IOException e) {
596+
LOG.error("Failed to close metrics reporter: {}", reporter, e);
597+
}
598+
}
579599
}
580600

581601
private static class BadResponseType extends RuntimeException {

standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
*/
1919
package org.apache.iceberg.rest;
2020

21+
import java.lang.reflect.InvocationTargetException;
22+
import java.util.Arrays;
2123
import java.util.Collections;
2224
import java.util.List;
2325
import java.util.Map;
@@ -33,6 +35,7 @@
3335
import org.apache.iceberg.CatalogProperties;
3436
import org.apache.iceberg.catalog.Catalog;
3537
import org.apache.iceberg.hive.HiveCatalog;
38+
import org.apache.iceberg.rest.metrics.IcebergMetricsReporter;
3639

3740
/**
3841
* Catalog &amp; servlet factory.
@@ -109,7 +112,21 @@ private HttpServlet createServlet(Catalog catalog) {
109112
// Iceberg REST client uses "catalog" by default
110113
List<String> scopes = Collections.singletonList("catalog");
111114
ServletSecurity security = new ServletSecurity(AuthType.fromString(authType), configuration, req -> scopes);
112-
return security.proxy(new HMSCatalogServlet(new HMSCatalogAdapter(catalog)));
115+
String catalogName = MetastoreConf.getVar(configuration, ConfVars.CATALOG_DEFAULT);
116+
List<IcebergMetricsReporter> reporters = createReporters();
117+
return security.proxy(new HMSCatalogServlet(new HMSCatalogAdapter(catalogName, catalog, reporters)));
118+
}
119+
120+
private List<IcebergMetricsReporter> createReporters() {
121+
final var classes = MetastoreConf.getClasses(configuration, ConfVars.ICEBERG_CATALOG_METRICS_REPORTERS);
122+
return Arrays.stream(classes).map(clazz -> {
123+
try {
124+
final var constructor = clazz.getDeclaredConstructor(Configuration.class);
125+
return (IcebergMetricsReporter) constructor.newInstance(configuration);
126+
} catch (NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) {
127+
throw new IllegalArgumentException("Failed to instantiate IcebergMetricsReporter: " + clazz.getName(), e);
128+
}
129+
}).toList();
113130
}
114131

115132
/**

standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogServlet.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,12 @@ private Consumer<ErrorResponse> handle(HttpServletResponse response) {
100100
};
101101
}
102102

103+
@Override
104+
public void destroy() {
105+
super.destroy();
106+
restCatalogAdapter.close();
107+
}
108+
103109
public static class ServletRequestContext {
104110
private HTTPMethod method;
105111
private String path;
@@ -147,8 +153,15 @@ static ServletRequestContext from(HttpServletRequest request) throws IOException
147153
Route route = routeContext.first();
148154
Object requestBody = null;
149155
if (route.requestClass() != null) {
150-
requestBody =
151-
RESTObjectMapper.mapper().readValue(request.getReader(), route.requestClass());
156+
try {
157+
requestBody = RESTObjectMapper.mapper().readValue(request.getReader(), route.requestClass());
158+
} catch (Exception e) {
159+
return new ServletRequestContext(ErrorResponse.builder()
160+
.responseCode(400)
161+
.withType(e.getClass().getSimpleName())
162+
.withMessage(e.getMessage())
163+
.build());
164+
}
152165
}
153166

154167
Map<String, String> queryParams =
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.iceberg.rest.metrics;
20+
21+
import org.apache.iceberg.catalog.TableIdentifier;
22+
import org.apache.iceberg.metrics.MetricsReport;
23+
24+
import java.io.Closeable;
25+
import java.time.Instant;
26+
27+
/**
28+
* An event reporter for Iceberg metrics.
29+
*/
30+
public interface IcebergMetricsReporter extends Closeable {
31+
/**
32+
* Report an event.
33+
*
34+
* @param catalog the catalog name
35+
* @param identifier the table identifier
36+
* @param report the event
37+
* @param receivedAt the timestamp when the Iceberg REST Catalog received the event
38+
*/
39+
void report(String catalog, TableIdentifier identifier, MetricsReport report, Instant receivedAt);
40+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.iceberg.rest.metrics;
20+
21+
import org.apache.hadoop.conf.Configuration;
22+
import org.apache.iceberg.catalog.TableIdentifier;
23+
import org.apache.iceberg.metrics.MetricsReport;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
27+
import java.time.Instant;
28+
29+
/**
30+
* A metrics reporter that logs events.
31+
*/
32+
public class LoggingMetricsReporter implements IcebergMetricsReporter {
33+
private static final Logger LOG = LoggerFactory.getLogger(LoggingMetricsReporter.class);
34+
35+
public LoggingMetricsReporter(Configuration conf) {
36+
}
37+
38+
@Override
39+
public void report(String catalog, TableIdentifier identifier, MetricsReport report, Instant receivedAt) {
40+
LOG.info("Event reported at {}: catalog={}, table={}, report={}", receivedAt, catalog, identifier, report);
41+
}
42+
43+
@Override
44+
public void close() {
45+
LOG.info("Closing {}", getClass().getName());
46+
}
47+
}

standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/RESTCatalogServer.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
package org.apache.iceberg.rest.extension;
2121

22+
import java.nio.file.Files;
2223
import java.nio.file.Path;
2324
import java.util.UUID;
2425
import org.apache.hadoop.conf.Configuration;
@@ -60,6 +61,9 @@ public void start(Configuration conf) throws Exception {
6061

6162
String uniqueTestKey = String.format("RESTCatalogServer_%s", UUID.randomUUID());
6263
warehouseDir = Path.of(MetaStoreTestUtils.getTestWarehouseDir(uniqueTestKey));
64+
Files.createDirectories(warehouseDir);
65+
System.setProperty("derby.stream.error.file",
66+
warehouseDir.resolve("derby.log").toAbsolutePath().toString());
6367
String jdbcUrl = String.format("jdbc:derby:memory:%s;create=true",
6468
warehouseDir.resolve("metastore_db").toAbsolutePath());
6569
MetastoreConf.setVar(conf, ConfVars.CONNECT_URL_KEY, jdbcUrl);

0 commit comments

Comments
 (0)