diff --git a/.gitignore b/.gitignore index f3939a82..9c30c83b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,6 @@ dependency-reduced-pom.xml # Jenv file .java-version + +# Maven shade plugin artifact +dependency-reduced-pom.xml diff --git a/.sdkmanrc b/.sdkmanrc new file mode 100644 index 00000000..a49f7da8 --- /dev/null +++ b/.sdkmanrc @@ -0,0 +1 @@ +java=8.0.472-amzn diff --git a/CHANGELOG.md b/CHANGELOG.md index d8f01c3b..d75f04cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 8.2.0 - TBD +### Added +- `apiary-gluesync-listener`: per-event observability via a new `glue_listener_event` Micrometer counter, tagged with `operation` (e.g. `create_table`), `result` (`success`, `failure`, `ignored`), and `outcome` (e.g. `created`, `updated`, `deleted`, `not_found`, `renamed`, exception class name). Covers all 8 HMS event handlers. A `glue_listener_table_rename_duration` timer is also recorded on every table rename. +- `kafka-metastore-receiver`: Kafka consumer metrics via Micrometer `KafkaClientMetrics`. Bind a registry at build time with `KafkaMessageReaderBuilder.withMeterRegistry(registry)`. +### Fixed +- `apiary-gluesync-listener`: fallback Glue operations (update-after-`AlreadyExistsException`, create-after-`EntityNotFoundException`) that previously escaped their catch blocks unhandled are now consistently caught, logged, and metered by the outer exception handler. +### Changed +- Bump Micrometer from `1.9.9` to `1.14.14`. + ## 8.1.18 - 2026-06-09 ### Fixed - `apiary-gluesync-listener`: glue sync metrics were silently dropped in HMS deployments; counters are now exported via JMX. In framework deployments (e.g. Dronefly/Spring Boot), metrics continue to flow to the existing registry (e.g. Prometheus) unchanged. diff --git a/apiary-metastore-events/apiary-hive-events/pom.xml b/apiary-metastore-events/apiary-hive-events/pom.xml index 59146dee..dfa22fd2 100644 --- a/apiary-metastore-events/apiary-hive-events/pom.xml +++ b/apiary-metastore-events/apiary-hive-events/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-metastore-events-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-hive-events diff --git a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-integration-tests/pom.xml b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-integration-tests/pom.xml index 59115fa1..eb442755 100644 --- a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-integration-tests/pom.xml +++ b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-integration-tests/pom.xml @@ -5,7 +5,7 @@ kafka-metastore-events-parent com.expediagroup.apiary - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT kafka-metastore-integration-tests diff --git a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-listener/pom.xml b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-listener/pom.xml index 715e86ae..96e7cf78 100644 --- a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-listener/pom.xml +++ b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-listener/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary kafka-metastore-events-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT kafka-metastore-listener diff --git a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/pom.xml b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/pom.xml index e50a44c4..4dc1d3ed 100644 --- a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/pom.xml +++ b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/pom.xml @@ -5,7 +5,7 @@ kafka-metastore-events-parent com.expediagroup.apiary - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT kafka-metastore-receiver @@ -22,6 +22,14 @@ kafka-clients ${kafka.version} + + io.micrometer + micrometer-core + + + io.micrometer + micrometer-registry-jmx + diff --git a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/src/main/java/com/expediagroup/apiary/extensions/events/metastore/kafka/messaging/KafkaMessageReader.java b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/src/main/java/com/expediagroup/apiary/extensions/events/metastore/kafka/messaging/KafkaMessageReader.java index 81ec7bd9..a2367171 100644 --- a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/src/main/java/com/expediagroup/apiary/extensions/events/metastore/kafka/messaging/KafkaMessageReader.java +++ b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/src/main/java/com/expediagroup/apiary/extensions/events/metastore/kafka/messaging/KafkaMessageReader.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2020 Expedia, Inc. + * Copyright (C) 2018-2026 Expedia, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,6 +28,15 @@ import org.apache.kafka.clients.consumer.ConsumerRecord; import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.micrometer.core.instrument.Clock; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.binder.kafka.KafkaClientMetrics; +import io.micrometer.jmx.JmxConfig; +import io.micrometer.jmx.JmxMeterRegistry; import com.google.common.annotations.VisibleForTesting; @@ -37,21 +46,27 @@ public class KafkaMessageReader implements Iterator, Closeable { + private static final Logger log = LoggerFactory.getLogger(KafkaMessageReader.class); private static final Duration POLL_TIMEOUT = Duration.ofMinutes(5); private KafkaConsumer consumer; private MetaStoreEventSerDe eventSerDe; private Iterator> records; + private KafkaClientMetrics kafkaClientMetrics; - private KafkaMessageReader(String topicName, MetaStoreEventSerDe eventSerDe, Properties consumerProperties) { - this(topicName, eventSerDe, new KafkaConsumer(consumerProperties)); + private KafkaMessageReader(String topicName, MetaStoreEventSerDe eventSerDe, Properties consumerProperties, MeterRegistry meterRegistry) { + this(topicName, eventSerDe, new KafkaConsumer(consumerProperties), meterRegistry); } @VisibleForTesting - KafkaMessageReader(String topicName, MetaStoreEventSerDe eventSerDe, KafkaConsumer consumer) { + KafkaMessageReader(String topicName, MetaStoreEventSerDe eventSerDe, KafkaConsumer consumer, MeterRegistry meterRegistry) { this.eventSerDe = eventSerDe; this.consumer = consumer; this.consumer.subscribe(Collections.singletonList(topicName)); + if (meterRegistry != null) { + this.kafkaClientMetrics = new KafkaClientMetrics(consumer); + this.kafkaClientMetrics.bindTo(meterRegistry); + } } @Override @@ -73,7 +88,13 @@ public void remove() { @Override public void close() { - consumer.close(); + try { + if (kafkaClientMetrics != null) { + kafkaClientMetrics.close(); + } + } finally { + consumer.close(); + } } private void readRecordsIfNeeded() { @@ -89,6 +110,7 @@ public static final class KafkaMessageReaderBuilder { private String groupId = "apiary-kafka-metastore-receiver-"; private MetaStoreEventSerDe metaStoreEventSerDe = new JsonMetaStoreEventSerDe(); private Properties consumerProperties = new Properties(); + private MeterRegistry meterRegistry; private KafkaMessageReaderBuilder(String bootstrapServers, String topicName, String applicationName) { this.bootstrapServers = bootstrapServers; @@ -114,6 +136,11 @@ public KafkaMessageReaderBuilder withConsumerProperties(Properties consumerPrope return this; } + public KafkaMessageReaderBuilder withMeterRegistry(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + return this; + } + public KafkaMessageReader build() { Properties props = new Properties(); props.put(BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); @@ -121,7 +148,24 @@ public KafkaMessageReader build() { props.put("key.deserializer", "org.apache.kafka.common.serialization.LongDeserializer"); props.put("value.deserializer", "org.apache.kafka.common.serialization.ByteArrayDeserializer"); consumerProperties.forEach((key, value) -> props.merge(key, value, (v1, v2) -> v1)); - return new KafkaMessageReader(topicName, metaStoreEventSerDe, props); + MeterRegistry registry = meterRegistry != null ? meterRegistry : configuredRegistry(); + return new KafkaMessageReader(topicName, metaStoreEventSerDe, props, registry); + } + + // DO NOT extract to a shared utility. MetricService in apiary-gluesync-listener has a + // similar method, but the two implementations intentionally differ: that module shades and + // relocates micrometer-jmx for HMS classpath isolation, and uses a Dropwizard-backed + // JmxMeterRegistry with TaggedObjectNameFactory for tagged JMX ObjectName properties. This + // module runs outside HMS and uses a plain JmxMeterRegistry without tag promotion. + // Each module must own its own copy. + private static synchronized MeterRegistry configuredRegistry() { + if (Metrics.globalRegistry.getRegistries().isEmpty()) { + log.info("No MeterRegistry found; registering JmxMeterRegistry for Kafka consumer metrics"); + Metrics.addRegistry(new JmxMeterRegistry(JmxConfig.DEFAULT, Clock.SYSTEM)); + } else { + log.info("Using existing MeterRegistry for Kafka consumer metrics: {}", Metrics.globalRegistry); + } + return Metrics.globalRegistry; } } } diff --git a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/src/test/java/com/expediagroup/apiary/extensions/events/metastore/kafka/messaging/KafkaMessageReaderTest.java b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/src/test/java/com/expediagroup/apiary/extensions/events/metastore/kafka/messaging/KafkaMessageReaderTest.java index 87373ab8..beb493bf 100644 --- a/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/src/test/java/com/expediagroup/apiary/extensions/events/metastore/kafka/messaging/KafkaMessageReaderTest.java +++ b/apiary-metastore-events/kafka-metastore-events/kafka-metastore-receiver/src/test/java/com/expediagroup/apiary/extensions/events/metastore/kafka/messaging/KafkaMessageReaderTest.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2020 Expedia, Inc. + * Copyright (C) 2018-2026 Expedia, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; + import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -60,9 +62,11 @@ public class KafkaMessageReaderTest { private ConsumerRecords messages; private KafkaMessageReader reader; + private SimpleMeterRegistry meterRegistry; @Before public void init() { + meterRegistry = new SimpleMeterRegistry(); List> messageList = ImmutableList.of(message); Map>> messageMap = ImmutableMap .of(new TopicPartition(TOPIC_NAME, PARTITION), messageList); @@ -70,7 +74,7 @@ public void init() { when(consumer.poll(any(Duration.class))).thenReturn(messages); when(message.value()).thenReturn(MESSAGE_CONTENT); when(serDe.unmarshal(MESSAGE_CONTENT)).thenReturn(event); - reader = new KafkaMessageReader(TOPIC_NAME, serDe, consumer); + reader = new KafkaMessageReader(TOPIC_NAME, serDe, consumer, meterRegistry); } @Test diff --git a/apiary-metastore-events/kafka-metastore-events/pom.xml b/apiary-metastore-events/kafka-metastore-events/pom.xml index 266ee69d..66e16569 100644 --- a/apiary-metastore-events/kafka-metastore-events/pom.xml +++ b/apiary-metastore-events/kafka-metastore-events/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-metastore-events-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT kafka-metastore-events-parent diff --git a/apiary-metastore-events/pom.xml b/apiary-metastore-events/pom.xml index e94857d6..af761b37 100644 --- a/apiary-metastore-events/pom.xml +++ b/apiary-metastore-events/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-extensions-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-metastore-events-parent diff --git a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/metastore-consumer-common/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/metastore-consumer-common/pom.xml index 31c787e5..d2b504cd 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/metastore-consumer-common/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/metastore-consumer-common/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-metastore-consumers-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT metastore-consumer-common diff --git a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/pom.xml index 7d5964ea..f51bcb86 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary sns-metastore-events-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-metastore-consumers-parent diff --git a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/pom.xml index c12881a6..1901662b 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-metastore-consumers-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-privileges-grantor-parent diff --git a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/privileges-grantor-core/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/privileges-grantor-core/pom.xml index 5c6dafb4..8b3d18d7 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/privileges-grantor-core/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/privileges-grantor-core/pom.xml @@ -5,7 +5,7 @@ com.expediagroup.apiary apiary-privileges-grantor-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-privileges-grantor-core diff --git a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/privileges-grantor-lambda/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/privileges-grantor-lambda/pom.xml index 1aa7fdb8..26babf9c 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/privileges-grantor-lambda/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-metastore-consumers/privileges-grantor/privileges-grantor-lambda/pom.xml @@ -5,7 +5,7 @@ com.expediagroup.apiary apiary-privileges-grantor-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-privileges-grantor-lambda diff --git a/apiary-metastore-events/sns-metastore-events/apiary-metastore-listener/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-metastore-listener/pom.xml index 993a4171..3ebe8379 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-metastore-listener/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-metastore-listener/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary sns-metastore-events-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-metastore-listener diff --git a/apiary-metastore-events/sns-metastore-events/apiary-receivers/apiary-receiver-common/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-receivers/apiary-receiver-common/pom.xml index 18f9f9de..5447611a 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-receivers/apiary-receiver-common/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-receivers/apiary-receiver-common/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-receivers-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-receiver-common diff --git a/apiary-metastore-events/sns-metastore-events/apiary-receivers/apiary-receiver-sqs/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-receivers/apiary-receiver-sqs/pom.xml index a60877a5..bf13ddfe 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-receivers/apiary-receiver-sqs/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-receivers/apiary-receiver-sqs/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-receivers-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-receiver-sqs diff --git a/apiary-metastore-events/sns-metastore-events/apiary-receivers/pom.xml b/apiary-metastore-events/sns-metastore-events/apiary-receivers/pom.xml index d2ed2eba..e5d8a643 100644 --- a/apiary-metastore-events/sns-metastore-events/apiary-receivers/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/apiary-receivers/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary sns-metastore-events-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-receivers-parent diff --git a/apiary-metastore-events/sns-metastore-events/pom.xml b/apiary-metastore-events/sns-metastore-events/pom.xml index 37b5fbd4..36b09fd8 100644 --- a/apiary-metastore-events/sns-metastore-events/pom.xml +++ b/apiary-metastore-events/sns-metastore-events/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-metastore-events-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT sns-metastore-events-parent diff --git a/apiary-metastore-metrics/pom.xml b/apiary-metastore-metrics/pom.xml index 361a628e..d55bff51 100644 --- a/apiary-metastore-metrics/pom.xml +++ b/apiary-metastore-metrics/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary apiary-extensions-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-metastore-metrics diff --git a/hive-event-listeners/apiary-gluesync-listener/pom.xml b/hive-event-listeners/apiary-gluesync-listener/pom.xml index 3b468594..62a0355d 100644 --- a/hive-event-listeners/apiary-gluesync-listener/pom.xml +++ b/hive-event-listeners/apiary-gluesync-listener/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary hive-event-listeners-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-gluesync-listener Apiary GlueSync Listener diff --git a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/ApiaryGlueSync.java b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/ApiaryGlueSync.java index 0031518d..23d99a55 100644 --- a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/ApiaryGlueSync.java +++ b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/ApiaryGlueSync.java @@ -17,7 +17,9 @@ import java.io.PrintWriter; import java.io.StringWriter; +import java.util.Arrays; import java.util.Iterator; +import java.util.List; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.hive.metastore.MetaStoreEventListener; @@ -52,6 +54,16 @@ public class ApiaryGlueSync extends MetaStoreEventListener { private static final Logger log = LoggerFactory.getLogger(ApiaryGlueSync.class); + // Simple names of AWS exception types to surface as outcome tags. Walking the class hierarchy + // from specific to general means subclasses (e.g. OperationTimeoutException) are matched before + // their superclass (AmazonServiceException) without relying on class literals, which break when + // the shade plugin relocates com.amazonaws.* to a shaded package at runtime. + private static final List KNOWN_EXCEPTION_NAMES = Arrays.asList( + "OperationTimeoutException", + "InvalidInputException", + "AmazonServiceException" + ); + private final AWSGlue glueClient; private final GlueDatabaseService glueDatabaseService; private final GlueTableService glueTableService; @@ -106,19 +118,18 @@ public ApiaryGlueSync(Configuration config, AWSGlue glueClient, String gluePrefi @Override public void onCreateDatabase(CreateDatabaseEvent event) throws MetaException { if (!event.getStatus()) { + metricService.recordEvent(MetricConstants.CREATE_DATABASE, MetricConstants.RESULT_IGNORED, "ignored"); return; } Database database = event.getDatabase(); try { - glueDatabaseService.create(database); - metricService.incrementCounter(MetricConstants.LISTENER_DATABASE_SUCCESS); - } catch (AlreadyExistsException e) { - log.info(database + " database already exists in glue, updating...."); - glueDatabaseService.update(database); + String outcome = createOrUpdateDatabase(database); metricService.incrementCounter(MetricConstants.LISTENER_DATABASE_SUCCESS); + metricService.recordEvent(MetricConstants.CREATE_DATABASE, MetricConstants.RESULT_SUCCESS, outcome); } catch (Exception e) { log.error("Failed create database {} in glue", database.getName(), e); metricService.incrementCounter(MetricConstants.LISTENER_DATABASE_FAILURE); + metricService.recordEvent(MetricConstants.CREATE_DATABASE, MetricConstants.RESULT_FAILURE, toOutcome(e)); if (throwExceptions) { throw wrap(e); } @@ -128,15 +139,18 @@ public void onCreateDatabase(CreateDatabaseEvent event) throws MetaException { @Override public void onDropDatabase(DropDatabaseEvent event) throws MetaException { if (!event.getStatus()) { + metricService.recordEvent(MetricConstants.DROP_DATABASE, MetricConstants.RESULT_IGNORED, "ignored"); return; } Database database = event.getDatabase(); try { glueDatabaseService.delete(database); metricService.incrementCounter(MetricConstants.LISTENER_DATABASE_SUCCESS); + metricService.recordEvent(MetricConstants.DROP_DATABASE, MetricConstants.RESULT_SUCCESS, "deleted"); } catch (Exception e) { log.error("Failed drop database {} in glue", database.getName(), e); metricService.incrementCounter(MetricConstants.LISTENER_DATABASE_FAILURE); + metricService.recordEvent(MetricConstants.DROP_DATABASE, MetricConstants.RESULT_FAILURE, toOutcome(e)); if (throwExceptions) { throw wrap(e); } @@ -146,19 +160,18 @@ public void onDropDatabase(DropDatabaseEvent event) throws MetaException { @Override public void onCreateTable(CreateTableEvent event) throws MetaException { if (!event.getStatus()) { + metricService.recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_IGNORED, "ignored"); return; } Table table = event.getTable(); try { - glueTableService.create(table); - metricService.incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); - } catch (AlreadyExistsException e) { - log.info(table + " table already exists in glue, updating...."); - glueTableService.update(table); + String outcome = createOrUpdateTable(table); metricService.incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + metricService.recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_SUCCESS, outcome); } catch (Exception e) { log.error("Failed create table {}.{} in glue", table.getDbName(), table.getTableName(), e); metricService.incrementCounter(MetricConstants.LISTENER_TABLE_FAILURE); + metricService.recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_FAILURE, toOutcome(e)); if (throwExceptions) { throw wrap(e); } @@ -168,20 +181,24 @@ public void onCreateTable(CreateTableEvent event) throws MetaException { @Override public void onDropTable(DropTableEvent event) throws MetaException { if (!event.getStatus()) { + metricService.recordEvent(MetricConstants.DROP_TABLE, MetricConstants.RESULT_IGNORED, "ignored"); return; } Table table = event.getTable(); try { glueTableService.delete(table); metricService.incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + metricService.recordEvent(MetricConstants.DROP_TABLE, MetricConstants.RESULT_SUCCESS, "deleted"); } catch (EntityNotFoundException e) { log.info(table + " table doesn't exist in glue catalog"); + metricService.recordEvent(MetricConstants.DROP_TABLE, MetricConstants.RESULT_SUCCESS, "not_found"); if (throwExceptions) { throw wrap(e); } } catch (Exception e) { log.error("Failed drop table {}.{} in glue", table.getDbName(), table.getTableName(), e); metricService.incrementCounter(MetricConstants.LISTENER_TABLE_FAILURE); + metricService.recordEvent(MetricConstants.DROP_TABLE, MetricConstants.RESULT_FAILURE, toOutcome(e)); if (throwExceptions) { throw wrap(e); } @@ -191,6 +208,7 @@ public void onDropTable(DropTableEvent event) throws MetaException { @Override public void onAlterTable(AlterTableEvent event) throws MetaException { if (!event.getStatus()) { + metricService.recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_IGNORED, "ignored"); return; } Table oldTable = event.getOldTable(); @@ -202,24 +220,74 @@ public void onAlterTable(AlterTableEvent event) throws MetaException { doRenameOperation(oldTable, newTable); return; } - glueTableService.update(newTable); - metricService.incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); - } catch (EntityNotFoundException e) { - log.info(newTable + " table doesn't exist in glue, creating...."); - glueTableService.create(newTable); + String outcome = updateOrCreateTable(newTable); metricService.incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); - if (throwExceptions) { - throw wrap(e); - } + metricService.recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_SUCCESS, outcome); } catch (Exception e) { log.error("Failed alter table {}.{} in glue", oldTable.getDbName(), oldTable.getTableName(), e); metricService.incrementCounter(MetricConstants.LISTENER_TABLE_FAILURE); + metricService.recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_FAILURE, toOutcome(e)); if (throwExceptions) { throw wrap(e); } } } + private String createOrUpdateDatabase(Database database) { + try { + glueDatabaseService.create(database); + return MetricConstants.OUTCOME_CREATED; + } catch (AlreadyExistsException e) { + log.info("{} database already exists in glue, updating....", database.getName()); + glueDatabaseService.update(database); + return MetricConstants.OUTCOME_UPDATED; + } + } + + private String createOrUpdateTable(Table table) { + try { + glueTableService.create(table); + return MetricConstants.OUTCOME_CREATED; + } catch (AlreadyExistsException e) { + log.info("{} table already exists in glue, updating....", table.getTableName()); + glueTableService.update(table); + return MetricConstants.OUTCOME_UPDATED; + } + } + + private String updateOrCreateTable(Table table) { + try { + glueTableService.update(table); + return MetricConstants.OUTCOME_UPDATED; + } catch (EntityNotFoundException e) { + log.info("{} table doesn't exist in glue, creating....", table.getTableName()); + glueTableService.create(table); + return MetricConstants.OUTCOME_CREATED; + } + } + + private String createOrUpdatePartition(Table table, Partition partition) { + try { + gluePartitionService.create(table, partition); + return MetricConstants.OUTCOME_CREATED; + } catch (AlreadyExistsException e) { + log.info("{} partition already exists in glue, updating....", partition); + gluePartitionService.update(table, partition); + return MetricConstants.OUTCOME_UPDATED; + } + } + + private String updateOrCreatePartition(Table table, Partition partition) { + try { + gluePartitionService.update(table, partition); + return MetricConstants.OUTCOME_UPDATED; + } catch (EntityNotFoundException e) { + log.info("{} partition doesn't exist in glue, creating....", partition); + gluePartitionService.create(table, partition); + return MetricConstants.OUTCOME_CREATED; + } + } + private void doRenameOperation(Table oldTable, Table newTable) { log.info("{} glue table rename detected to {}", oldTable.getTableName(), newTable.getTableName()); long startTime = System.currentTimeMillis(); @@ -227,8 +295,10 @@ private void doRenameOperation(Table oldTable, Table newTable) { gluePartitionService.copyPartitions(newTable, gluePartitionService.getPartitions(oldTable)); glueTableService.delete(oldTable); metricService.incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + metricService.recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_SUCCESS, "renamed"); long duration = System.currentTimeMillis() - startTime; - log.info("{} glue table rename to {} finised in {}ms", oldTable.getTableName(), newTable.getTableName(), duration); + metricService.recordDuration(MetricConstants.LISTENER_TABLE_RENAME_DURATION, duration); + log.info("{} glue table rename to {} finished in {}ms", oldTable.getTableName(), newTable.getTableName(), duration); } private boolean isTableRename(Table oldTable, Table newTable) { @@ -238,6 +308,7 @@ private boolean isTableRename(Table oldTable, Table newTable) { @Override public void onAddPartition(AddPartitionEvent event) throws MetaException { if (!event.getStatus()) { + metricService.recordEvent(MetricConstants.ADD_PARTITION, MetricConstants.RESULT_IGNORED, "ignored"); return; } Table table = event.getTable(); @@ -245,14 +316,13 @@ public void onAddPartition(AddPartitionEvent event) throws MetaException { while (partitions.hasNext()) { Partition partition = partitions.next(); try { - gluePartitionService.create(table, partition); - metricService.incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); - } catch (AlreadyExistsException e) { - gluePartitionService.update(table, partition); + String outcome = createOrUpdatePartition(table, partition); metricService.incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); + metricService.recordEvent(MetricConstants.ADD_PARTITION, MetricConstants.RESULT_SUCCESS, outcome); } catch (Exception e) { log.error("Failed add partition on table {}.{} in glue", table.getDbName(), table.getTableName(), e); metricService.incrementCounter(MetricConstants.LISTENER_PARTITION_FAILURE); + metricService.recordEvent(MetricConstants.ADD_PARTITION, MetricConstants.RESULT_FAILURE, toOutcome(e)); if (throwExceptions) { throw wrap(e); } @@ -263,6 +333,7 @@ public void onAddPartition(AddPartitionEvent event) throws MetaException { @Override public void onDropPartition(DropPartitionEvent event) throws MetaException { if (!event.getStatus()) { + metricService.recordEvent(MetricConstants.DROP_PARTITION, MetricConstants.RESULT_IGNORED, "ignored"); return; } Table table = event.getTable(); @@ -272,9 +343,17 @@ public void onDropPartition(DropPartitionEvent event) throws MetaException { try { gluePartitionService.delete(table, partition); metricService.incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); + metricService.recordEvent(MetricConstants.DROP_PARTITION, MetricConstants.RESULT_SUCCESS, "deleted"); + } catch (EntityNotFoundException e) { + log.info("{} partition doesn't exist in glue catalog", partition); + metricService.recordEvent(MetricConstants.DROP_PARTITION, MetricConstants.RESULT_SUCCESS, "not_found"); + if (throwExceptions) { + throw wrap(e); + } } catch (Exception e) { log.error("Failed drop partition on table {}.{} in glue", table.getDbName(), table.getTableName(), e); metricService.incrementCounter(MetricConstants.LISTENER_PARTITION_FAILURE); + metricService.recordEvent(MetricConstants.DROP_PARTITION, MetricConstants.RESULT_FAILURE, toOutcome(e)); if (throwExceptions) { throw wrap(e); } @@ -285,28 +364,36 @@ public void onDropPartition(DropPartitionEvent event) throws MetaException { @Override public void onAlterPartition(AlterPartitionEvent event) throws MetaException { if (!event.getStatus()) { + metricService.recordEvent(MetricConstants.ALTER_PARTITION, MetricConstants.RESULT_IGNORED, "ignored"); return; } Table table = event.getTable(); Partition partition = event.getNewPartition(); try { - gluePartitionService.update(table, partition); - metricService.incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); - } catch (EntityNotFoundException e) { - gluePartitionService.create(table, partition); + String outcome = updateOrCreatePartition(table, partition); metricService.incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); - if (throwExceptions) { - throw wrap(e); - } + metricService.recordEvent(MetricConstants.ALTER_PARTITION, MetricConstants.RESULT_SUCCESS, outcome); } catch (Exception e) { log.error("Failed alter partition on table {}.{} in glue", table.getDbName(), table.getTableName(), e); metricService.incrementCounter(MetricConstants.LISTENER_PARTITION_FAILURE); + metricService.recordEvent(MetricConstants.ALTER_PARTITION, MetricConstants.RESULT_FAILURE, toOutcome(e)); if (throwExceptions) { throw wrap(e); } } } + private static String toOutcome(Exception e) { + Class type = e.getClass(); + while (type != null) { + if (KNOWN_EXCEPTION_NAMES.contains(type.getSimpleName())) { + return type.getSimpleName(); + } + type = type.getSuperclass(); + } + return "other"; + } + /* * Helper method to wrap random exceptions into a MetaException along with their * stack traces so we don't lose info. This is really only needed in the CLI diff --git a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricConstants.java b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricConstants.java index ed6e0575..618181a7 100644 --- a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricConstants.java +++ b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricConstants.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2025 Expedia, Inc. + * Copyright (C) 2018-2026 Expedia, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,29 @@ public class MetricConstants { public static final String LISTENER_PARTITION_FAILURE = "glue_listener_partition_failure"; public static final String LISTENER_PARTITION_SUCCESS = "glue_listener_partition_success"; + public static final String LISTENER_EVENT = "glue_listener_event"; + public static final String LISTENER_TABLE_RENAME_DURATION = "glue_listener_table_rename_duration"; + + public static final String TAG_OPERATION = "operation"; + public static final String TAG_RESULT = "result"; + public static final String TAG_OUTCOME = "outcome"; + + public static final String RESULT_SUCCESS = "success"; + public static final String RESULT_FAILURE = "failure"; + public static final String RESULT_IGNORED = "ignored"; + + public static final String OUTCOME_CREATED = "created"; + public static final String OUTCOME_UPDATED = "updated"; + + public static final String CREATE_DATABASE = "create_database"; + public static final String DROP_DATABASE = "drop_database"; + public static final String CREATE_TABLE = "create_table"; + public static final String DROP_TABLE = "drop_table"; + public static final String ALTER_TABLE = "alter_table"; + public static final String ADD_PARTITION = "add_partition"; + public static final String DROP_PARTITION = "drop_partition"; + public static final String ALTER_PARTITION = "alter_partition"; + public static final List LISTENER_METRICS = Arrays.asList( LISTENER_DATABASE_FAILURE, LISTENER_DATABASE_SUCCESS, diff --git a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricService.java b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricService.java index b332194d..4caa0080 100644 --- a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricService.java +++ b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricService.java @@ -15,7 +15,15 @@ */ package com.expediagroup.apiary.extensions.gluesync.listener.metrics; +import static com.expediagroup.apiary.extensions.gluesync.listener.metrics.MetricConstants.LISTENER_EVENT; +import static com.expediagroup.apiary.extensions.gluesync.listener.metrics.MetricConstants.TAG_OPERATION; +import static com.expediagroup.apiary.extensions.gluesync.listener.metrics.MetricConstants.TAG_OUTCOME; +import static com.expediagroup.apiary.extensions.gluesync.listener.metrics.MetricConstants.TAG_RESULT; + +import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -25,15 +33,24 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; +import io.micrometer.core.instrument.util.HierarchicalNameMapper; import io.micrometer.jmx.JmxConfig; import io.micrometer.jmx.JmxMeterRegistry; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jmx.JmxReporter; + public class MetricService { private static final Logger log = LoggerFactory.getLogger(MetricService.class); + private final MeterRegistry registry; private final Map metrics; + private final Map events = new ConcurrentHashMap<>(); public MetricService(MeterRegistry registry) { + this.registry = registry; this.metrics = MetricConstants.LISTENER_METRICS.stream() .collect(Collectors.toMap( metricName -> metricName, @@ -45,13 +62,47 @@ public MetricService() { this(configuredRegistry()); } - private static MeterRegistry configuredRegistry() { + // DO NOT extract to a shared utility. KafkaMessageReaderBuilder in kafka-metastore-receiver + // has a similar method, but the two implementations intentionally differ: this module shades + // and relocates micrometer-jmx for HMS classpath isolation, and uses a Dropwizard-backed + // JmxMeterRegistry with TaggedObjectNameFactory to promote Micrometer tags to proper JMX + // ObjectName key properties. KafkaMessageReaderBuilder runs outside HMS and uses a plain + // JmxMeterRegistry without tag promotion. Each module must own its own copy. + private static synchronized MeterRegistry configuredRegistry() { if (Metrics.globalRegistry.getRegistries().isEmpty()) { - Metrics.addRegistry(new JmxMeterRegistry(JmxConfig.DEFAULT, Clock.SYSTEM)); + MetricRegistry dropwizardRegistry = new MetricRegistry(); + JmxReporter reporter = JmxReporter.forRegistry(dropwizardRegistry) + .inDomain("metrics") + .createsObjectNamesWith(new TaggedObjectNameFactory()) + .build(); + Metrics.addRegistry(new JmxMeterRegistry(JmxConfig.DEFAULT, Clock.SYSTEM, taggedNameMapper(), dropwizardRegistry, reporter)); } return Metrics.globalRegistry; } + // Encodes Micrometer tags as "name[key=value,key=value]" in the Dropwizard metric name so that + // TaggedObjectNameFactory can promote them to proper JMX ObjectName key properties. + static HierarchicalNameMapper taggedNameMapper() { + return (id, convention) -> { + String baseName = convention.name(id.getName(), id.getType(), id.getBaseUnit()); + List tags = id.getTags(); + if (tags.isEmpty()) { + return baseName; + } + StringBuilder sb = new StringBuilder(baseName).append('['); + for (int i = 0; i < tags.size(); i++) { + Tag tag = tags.get(i); + if (i > 0) { + sb.append(','); + } + sb.append(convention.tagKey(tag.getKey())) + .append('=') + .append(convention.tagValue(tag.getValue())); + } + return sb.append(']').toString(); + }; + } + public void incrementCounter(String name) { try { Counter counter = metrics.get(name); @@ -64,4 +115,26 @@ public void incrementCounter(String name) { log.warn("Unable to increment counter {}", name, e); } } + + public void recordDuration(String name, long durationMs) { + try { + Timer.builder(name) + .register(registry) + .record(durationMs, TimeUnit.MILLISECONDS); + } catch (Exception e) { + log.warn("Unable to record duration {} {}ms", name, durationMs, e); + } + } + + public void recordEvent(String operation, String result, String outcome) { + try { + events.computeIfAbsent(operation + "|" + result + "|" + outcome, k -> + Counter.builder(LISTENER_EVENT) + .tags(TAG_OPERATION, operation, TAG_RESULT, result, TAG_OUTCOME, outcome) + .register(registry)) + .increment(); + } catch (Exception e) { + log.warn("Unable to record event {} {} {}", operation, result, outcome, e); + } + } } diff --git a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/TaggedObjectNameFactory.java b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/TaggedObjectNameFactory.java new file mode 100644 index 00000000..2ab7b609 --- /dev/null +++ b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/TaggedObjectNameFactory.java @@ -0,0 +1,62 @@ +/** + * Copyright (C) 2018-2026 Expedia, Inc. + * + * Licensed 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 com.expediagroup.apiary.extensions.gluesync.listener.metrics; + +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.codahale.metrics.jmx.ObjectNameFactory; + +/** + * Decodes tag key-value pairs encoded as {@code name[k=v,k=v]} by + * {@link MetricService#taggedNameMapper()} into proper JMX ObjectName key properties, + * so jmx-exporter can reference them as Prometheus labels without regex string-splitting. + * + *

Un-tagged metrics (no {@code [}) fall back to the standard + * {@code domain:name=,type=} ObjectName. + */ +class TaggedObjectNameFactory implements ObjectNameFactory { + + private static final Logger log = LoggerFactory.getLogger(TaggedObjectNameFactory.class); + + @Override + public ObjectName createName(String type, String domain, String name) { + int bracketIdx = name.indexOf('['); + try { + if (bracketIdx < 0) { + return new ObjectName(domain + ":name=" + name + ",type=" + type); + } + String baseName = name.substring(0, bracketIdx); + String tagStr = name.substring(bracketIdx + 1, name.length() - 1); + StringBuilder sb = new StringBuilder(domain).append(":name=").append(baseName); + for (String kv : tagStr.split(",")) { + sb.append(',').append(kv); + } + sb.append(",type=").append(type); + return new ObjectName(sb.toString()); + } catch (MalformedObjectNameException e) { + log.warn("Could not create JMX ObjectName for metric '{}', falling back to quoted name", name, e); + try { + return new ObjectName(domain + ":name=" + ObjectName.quote(name) + ",type=" + type); + } catch (MalformedObjectNameException ex) { + throw new RuntimeException(ex); + } + } + } +} diff --git a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/service/GlueTableService.java b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/service/GlueTableService.java index 32f08e71..2e82fe40 100644 --- a/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/service/GlueTableService.java +++ b/hive-event-listeners/apiary-gluesync-listener/src/main/java/com/expediagroup/apiary/extensions/gluesync/listener/service/GlueTableService.java @@ -1,5 +1,5 @@ /** - * Copyright (C) 2018-2025 Expedia, Inc. + * Copyright (C) 2018-2026 Expedia, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/ApiaryGlueSyncTest.java b/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/ApiaryGlueSyncTest.java index aca9cc66..347ac736 100644 --- a/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/ApiaryGlueSyncTest.java +++ b/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/ApiaryGlueSyncTest.java @@ -21,7 +21,9 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -70,6 +72,7 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import com.amazonaws.AmazonServiceException; import com.amazonaws.services.glue.AWSGlue; import com.amazonaws.services.glue.model.AlreadyExistsException; import com.amazonaws.services.glue.model.BatchCreatePartitionRequest; @@ -155,6 +158,7 @@ public void onCreateDatabase() throws MetaException { verify(glueClient).createDatabase(createDatabaseRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_DATABASE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.CREATE_DATABASE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); CreateDatabaseRequest createDatabaseRequest = createDatabaseRequestCaptor.getValue(); assertThat(createDatabaseRequest.getDatabaseInput().getName(), is(gluePrefix + dbName)); @@ -175,6 +179,7 @@ public void onCreateDatabaseThatAlreadyExists() throws MetaException { verify(glueClient).createDatabase(createDatabaseRequestCaptor.capture()); verify(glueClient).updateDatabase(updateDatabaseRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_DATABASE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.CREATE_DATABASE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_UPDATED); UpdateDatabaseRequest updateDatabaseRequest = updateDatabaseRequestCaptor.getValue(); assertThat(updateDatabaseRequest.getName(), is(gluePrefix + dbName)); @@ -195,6 +200,7 @@ public void onDropDatabase() throws MetaException { verify(glueClient).deleteDatabase(deleteDatabaseRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_DATABASE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.DROP_DATABASE, MetricConstants.RESULT_SUCCESS, "deleted"); DeleteDatabaseRequest deleteDatabaseRequest = deleteDatabaseRequestCaptor.getValue(); assertThat(deleteDatabaseRequest.getName(), is(gluePrefix + dbName)); } @@ -212,6 +218,7 @@ public void onDropDatabaseThatDoesntExist() throws MetaException { verify(glueClient).getDatabase(any()); verify(metricService).incrementCounter(MetricConstants.LISTENER_DATABASE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.DROP_DATABASE, MetricConstants.RESULT_SUCCESS, "deleted"); verify(glueClient).deleteDatabase(deleteDatabaseRequestCaptor.capture()); } @@ -227,6 +234,7 @@ public void onDropDatabaseNotCreatedByGlueSync() throws MetaException { verify(glueClient).getDatabase(any()); verify(metricService).incrementCounter(MetricConstants.LISTENER_DATABASE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.DROP_DATABASE, MetricConstants.RESULT_SUCCESS, "deleted"); verifyNoMoreInteractions(glueClient); } @@ -242,6 +250,7 @@ public void onCreateHiveTable() throws MetaException { verify(glueClient).createTable(createTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); CreateTableRequest createTableRequest = createTableRequestCaptor.getValue(); assertThat(createTableRequest.getDatabaseName(), is(gluePrefix + dbName)); @@ -269,6 +278,7 @@ public void onCreateHiveTable_withIncorrectFormat() throws MetaException { verify(glueClient, times(2)).createTable(createTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); CreateTableRequest createTableRequest = createTableRequestCaptor.getValue(); assertThat(createTableRequest.getDatabaseName(), is(gluePrefix + dbName)); @@ -288,6 +298,7 @@ public void onCreateIcebergTable() throws MetaException { verify(glueClient).createTable(createTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); CreateTableRequest createTableRequest = createTableRequestCaptor.getValue(); assertThat(createTableRequest.getDatabaseName(), is(gluePrefix + dbName)); @@ -314,6 +325,7 @@ public void onCreateHiveView() throws MetaException { verify(glueClient).createTable(createTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); CreateTableRequest createTableRequest = createTableRequestCaptor.getValue(); assertThat(createTableRequest.getDatabaseName(), is(gluePrefix + dbName)); @@ -339,6 +351,7 @@ public void onAlterHiveTable() throws MetaException { verify(glueClient).updateTable(updateTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_UPDATED); UpdateTableRequest updateTableRequest = updateTableRequestCaptor.getValue(); assertThat(updateTableRequest.getDatabaseName(), is(gluePrefix + dbName)); @@ -366,6 +379,7 @@ public void onAlterHiveTableSkipArchiveOverride() throws MetaException { verify(glueClient).updateTable(updateTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_UPDATED); UpdateTableRequest updateTableRequest = updateTableRequestCaptor.getValue(); assertThat(updateTableRequest.getDatabaseName(), is(gluePrefix + dbName)); @@ -465,6 +479,8 @@ public void onAlterHiveTable_RenameTable() throws MetaException { BatchCreatePartitionRequest batchCreatePartitionRequest = batchCreatePartitionRequestCaptor.getValue(); verify(glueClient).deleteTable(deleteTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_SUCCESS, "renamed"); + verify(metricService).recordDuration(eq(MetricConstants.LISTENER_TABLE_RENAME_DURATION), anyLong()); DeleteTableRequest deleteTableRequest = deleteTableRequestCaptor.getValue(); // test create new table @@ -495,6 +511,7 @@ public void onCreateUnpartitionedHiveTable() throws MetaException { verify(glueClient).createTable(createTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); CreateTableRequest createTableRequest = createTableRequestCaptor.getValue(); assertThat(createTableRequest.getDatabaseName(), is(gluePrefix + dbName)); @@ -529,6 +546,7 @@ public void onAddPartition_withIncorrectFormat() throws MetaException { verify(glueClient, times(2)).createPartition(createPartitionRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ADD_PARTITION, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); CreatePartitionRequest createTableRequest = createPartitionRequestCaptor.getValue(); assertThat(createTableRequest.getDatabaseName(), is(gluePrefix + dbName)); @@ -548,6 +566,7 @@ public void onCreateHiveTableThatAlreadyExists() throws MetaException { verify(glueClient).createTable(any()); verify(glueClient).updateTable(updateTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_UPDATED); assertThat(updateTableRequestCaptor.getValue().getDatabaseName(), is(gluePrefix + dbName)); assertThat(updateTableRequestCaptor.getValue().getTableInput().getName(), is(tableName)); } @@ -566,6 +585,7 @@ public void onAlterHiveTableThatDoesntExistInGlue() throws MetaException { verify(glueClient).updateTable(any()); verify(glueClient).createTable(createTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); assertThat(createTableRequestCaptor.getValue().getDatabaseName(), is(gluePrefix + dbName)); assertThat(createTableRequestCaptor.getValue().getTableInput().getName(), is(tableName)); } @@ -585,6 +605,7 @@ public void onAlterPartition() throws MetaException { verify(glueClient).updatePartition(updatePartitionRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ALTER_PARTITION, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_UPDATED); assertThat(updatePartitionRequestCaptor.getValue().getDatabaseName(), is(gluePrefix + dbName)); assertThat(updatePartitionRequestCaptor.getValue().getTableName(), is(tableName)); } @@ -606,6 +627,7 @@ public void onAlterPartitionThatDoesntExistInGlue() throws MetaException { verify(glueClient).updatePartition(any()); verify(glueClient).createPartition(createPartitionRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ALTER_PARTITION, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); assertThat(createPartitionRequestCaptor.getValue().getDatabaseName(), is(gluePrefix + dbName)); assertThat(createPartitionRequestCaptor.getValue().getTableName(), is(tableName)); } @@ -625,6 +647,7 @@ public void onAddPartition() throws MetaException { verify(glueClient).createPartition(createPartitionRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ADD_PARTITION, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_CREATED); assertThat(createPartitionRequestCaptor.getValue().getDatabaseName(), is(gluePrefix + dbName)); assertThat(createPartitionRequestCaptor.getValue().getTableName(), is(tableName)); } @@ -646,6 +669,7 @@ public void onAddPartitionThatAlreadyExists() throws MetaException { verify(glueClient).createPartition(any()); verify(glueClient).updatePartition(updatePartitionRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ADD_PARTITION, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_UPDATED); assertThat(updatePartitionRequestCaptor.getValue().getDatabaseName(), is(gluePrefix + dbName)); assertThat(updatePartitionRequestCaptor.getValue().getTableName(), is(tableName)); } @@ -660,6 +684,7 @@ public void onDropTable() throws MetaException { verify(glueClient).deleteTable(deleteTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.DROP_TABLE, MetricConstants.RESULT_SUCCESS, "deleted"); assertThat(deleteTableRequestCaptor.getValue().getDatabaseName(), is(gluePrefix + dbName)); assertThat(deleteTableRequestCaptor.getValue().getName(), is(tableName)); } @@ -674,9 +699,27 @@ public void onDropTableThatDoesntExistInGlue() throws MetaException { glueSync.onDropTable(event); verify(glueClient).deleteTable(any()); + verify(metricService).recordEvent(MetricConstants.DROP_TABLE, MetricConstants.RESULT_SUCCESS, "not_found"); verifyNoMoreInteractions(metricService); } + @Test + public void onAlterHiveTable_RenameOperationFailureIsMetered() throws MetaException { + AlterTableEvent event = mock(AlterTableEvent.class); + when(event.getStatus()).thenReturn(true); + Table oldTable = simpleHiveTable(simpleSchema(), simplePartitioning()); + Table newTable = simpleHiveTable(simpleSchema(), simplePartitioning()); + newTable.setTableName("table_renamed"); + when(event.getOldTable()).thenReturn(oldTable); + when(event.getNewTable()).thenReturn(newTable); + when(glueClient.createTable(any())).thenThrow(new OperationTimeoutException("timeout")); + + glueSync.onAlterTable(event); + + verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_FAILURE); + verify(metricService).recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_FAILURE, "OperationTimeoutException"); + } + @Test public void onDropPartition() throws MetaException { DropPartitionEvent event = mock(DropPartitionEvent.class); @@ -692,6 +735,7 @@ public void onDropPartition() throws MetaException { verify(glueClient).deletePartition(any()); verify(metricService).incrementCounter(MetricConstants.LISTENER_PARTITION_SUCCESS); + verify(metricService).recordEvent(MetricConstants.DROP_PARTITION, MetricConstants.RESULT_SUCCESS, "deleted"); } @Test @@ -723,6 +767,14 @@ public void allHandlers_ignoredWhenEventStatusFalse() throws MetaException { glueSync.onAlterPartition(alterPartition); verifyZeroInteractions(glueClient); + verify(metricService).recordEvent(MetricConstants.CREATE_DATABASE, MetricConstants.RESULT_IGNORED, "ignored"); + verify(metricService).recordEvent(MetricConstants.DROP_DATABASE, MetricConstants.RESULT_IGNORED, "ignored"); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_IGNORED, "ignored"); + verify(metricService).recordEvent(MetricConstants.DROP_TABLE, MetricConstants.RESULT_IGNORED, "ignored"); + verify(metricService).recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_IGNORED, "ignored"); + verify(metricService).recordEvent(MetricConstants.ADD_PARTITION, MetricConstants.RESULT_IGNORED, "ignored"); + verify(metricService).recordEvent(MetricConstants.DROP_PARTITION, MetricConstants.RESULT_IGNORED, "ignored"); + verify(metricService).recordEvent(MetricConstants.ALTER_PARTITION, MetricConstants.RESULT_IGNORED, "ignored"); verifyNoMoreInteractions(metricService); } @@ -736,9 +788,40 @@ public void onCreateTable_failureMetricsRecordedOnException() throws MetaExcepti glueSync.onCreateTable(event); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_FAILURE); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_FAILURE, "OperationTimeoutException"); verifyNoMoreInteractions(metricService); } + // KNOWN_EXCEPTION_NAMES holds simple class names (strings) rather than Class literals because the + // Maven shade plugin relocates com.amazonaws.* to com.expediagroup.apiary.shaded.com.amazonaws.* + // at runtime. Class.isInstance() against the original class literal would never match the shaded + // runtime type; getSimpleName() is package-agnostic and survives relocation. + @Test + public void toOutcome_awsSubclassNotInKnownList_matchesSuperclassName() throws MetaException { + // UnknownGlueException extends AmazonServiceException but is not in KNOWN_EXCEPTION_NAMES. + // The hierarchy walk should surface "AmazonServiceException" rather than "other". + CreateTableEvent event = mock(CreateTableEvent.class); + when(event.getStatus()).thenReturn(true); + when(event.getTable()).thenReturn(simpleHiveTable(simpleSchema(), simplePartitioning())); + when(glueClient.createTable(any())).thenThrow(new UnknownGlueException("unlisted AWS error")); + + glueSync.onCreateTable(event); + + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_FAILURE, "AmazonServiceException"); + } + + @Test + public void toOutcome_unknownException_returnsOther() throws MetaException { + CreateTableEvent event = mock(CreateTableEvent.class); + when(event.getStatus()).thenReturn(true); + when(event.getTable()).thenReturn(simpleHiveTable(simpleSchema(), simplePartitioning())); + when(glueClient.createTable(any())).thenThrow(new RuntimeException("unexpected")); + + glueSync.onCreateTable(event); + + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_FAILURE, "other"); + } + @Test public void onCreateTable_exceptionRethrownWhenThrowExceptionsEnabled() throws MetaException { ApiaryGlueSync throwingSync = new ApiaryGlueSync(configuration, glueClient, gluePrefix, metricService, true); @@ -755,6 +838,7 @@ public void onCreateTable_exceptionRethrownWhenThrowExceptionsEnabled() throws M } verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_FAILURE); + verify(metricService).recordEvent(MetricConstants.CREATE_TABLE, MetricConstants.RESULT_FAILURE, "OperationTimeoutException"); } @Test @@ -770,11 +854,32 @@ public void onAlterIcebergTable_RenameTableSkipsRenameOperation() throws MetaExc verify(glueClient).updateTable(updateTableRequestCaptor.capture()); verify(metricService).incrementCounter(MetricConstants.LISTENER_TABLE_SUCCESS); + verify(metricService).recordEvent(MetricConstants.ALTER_TABLE, MetricConstants.RESULT_SUCCESS, MetricConstants.OUTCOME_UPDATED); assertThat(updateTableRequestCaptor.getValue().getTableInput().getName(), is("table_renamed")); + // rename operation (copy+delete) must not be triggered for Iceberg tables verify(glueClient, times(0)).deleteTable(any()); verify(glueClient, times(0)).batchCreatePartition(any()); } + @Test + public void onDropPartition_partitionNotFoundInGlue() throws MetaException { + DropPartitionEvent event = mock(DropPartitionEvent.class); + when(event.getStatus()).thenReturn(true); + + Table table = simpleHiveTable(simpleSchema(), simplePartitioning()); + Partition partition = new Partition(); + partition.setValues(Arrays.asList("part1Value", "part2Value")); + partition.setSd(table.getSd()); + when(event.getTable()).thenReturn(table); + when(event.getPartitionIterator()).thenReturn(Arrays.asList(partition).iterator()); + when(glueClient.deletePartition(any())).thenThrow(new EntityNotFoundException("")); + + glueSync.onDropPartition(event); + + verify(metricService).recordEvent(MetricConstants.DROP_PARTITION, MetricConstants.RESULT_SUCCESS, "not_found"); + verifyNoMoreInteractions(metricService); + } + private Table simpleHiveTable(List schema, List partitions) { Table table = new Table(); table.setTableName(tableName); @@ -827,4 +932,10 @@ private GetDatabaseResult getGlueDatabaseResult(Map params) { return new GetDatabaseResult().withDatabase(new com.amazonaws.services.glue.model.Database().withName( dbName).withParameters(params)); } + + private static class UnknownGlueException extends AmazonServiceException { + UnknownGlueException(String msg) { + super(msg); + } + } } diff --git a/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricServiceTest.java b/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricServiceTest.java index ab8c275a..2464887c 100644 --- a/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricServiceTest.java +++ b/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/MetricServiceTest.java @@ -20,6 +20,7 @@ import java.lang.management.ManagementFactory; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.management.MBeanServer; import javax.management.ObjectName; @@ -32,6 +33,9 @@ import io.micrometer.jmx.JmxConfig; import io.micrometer.jmx.JmxMeterRegistry; +import com.codahale.metrics.MetricRegistry; +import com.codahale.metrics.jmx.JmxReporter; + public class MetricServiceTest { @Test @@ -60,6 +64,17 @@ public void allMetricsRegisteredOnConstruction() { } } + @Test + public void recordDurationRegistersTimer() { + MeterRegistry registry = new SimpleMeterRegistry(); + MetricService metricService = new MetricService(registry); + + metricService.recordDuration(MetricConstants.LISTENER_TABLE_RENAME_DURATION, 500L); + + assertThat(registry.get(MetricConstants.LISTENER_TABLE_RENAME_DURATION).timer().count(), is(1L)); + assertThat(registry.get(MetricConstants.LISTENER_TABLE_RENAME_DURATION).timer().totalTime(TimeUnit.MILLISECONDS), is(500.0)); + } + @Test public void jmxRegistryExposesCountersAsMBeans() throws Exception { JmxMeterRegistry jmxRegistry = new JmxMeterRegistry(JmxConfig.DEFAULT, Clock.SYSTEM); @@ -75,4 +90,30 @@ public void jmxRegistryExposesCountersAsMBeans() throws Exception { jmxRegistry.close(); } + + @Test + public void taggedEventCounterExposesTagsAsJmxKeyProperties() throws Exception { + MetricRegistry dropwizardRegistry = new MetricRegistry(); + JmxReporter reporter = JmxReporter.forRegistry(dropwizardRegistry) + .inDomain("metrics") + .createsObjectNamesWith(new TaggedObjectNameFactory()) + .build(); + JmxMeterRegistry jmxRegistry = new JmxMeterRegistry( + JmxConfig.DEFAULT, Clock.SYSTEM, + MetricService.taggedNameMapper(), dropwizardRegistry, reporter); + MetricService metricService = new MetricService(jmxRegistry); + + metricService.recordEvent("alter_table", "failure", "other"); + + MBeanServer mbs = ManagementFactory.getPlatformMBeanServer(); + Set beans = mbs.queryNames( + new ObjectName("metrics:name=" + MetricConstants.LISTENER_EVENT + ",*"), null); + assertThat("expected exactly one event MBean", beans.size(), is(1)); + ObjectName bean = beans.iterator().next(); + assertThat(bean.getKeyProperty("operation"), is("alter_table")); + assertThat(bean.getKeyProperty("result"), is("failure")); + assertThat(bean.getKeyProperty("outcome"), is("other")); + + jmxRegistry.close(); + } } diff --git a/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/TaggedObjectNameFactoryTest.java b/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/TaggedObjectNameFactoryTest.java new file mode 100644 index 00000000..29f8aa8b --- /dev/null +++ b/hive-event-listeners/apiary-gluesync-listener/src/test/java/com/expediagroup/apiary/extensions/gluesync/listener/metrics/TaggedObjectNameFactoryTest.java @@ -0,0 +1,58 @@ +/** + * Copyright (C) 2018-2026 Expedia, Inc. + * + * Licensed 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 com.expediagroup.apiary.extensions.gluesync.listener.metrics; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; + +import javax.management.ObjectName; + +import org.junit.Test; + +public class TaggedObjectNameFactoryTest { + + private final TaggedObjectNameFactory factory = new TaggedObjectNameFactory(); + + @Test + public void untaggedMetricProducesStandardObjectName() throws Exception { + ObjectName name = factory.createName("meters", "metrics", "glue_listener_table_failure"); + + assertThat(name.getDomain(), is("metrics")); + assertThat(name.getKeyProperty("name"), is("glue_listener_table_failure")); + assertThat(name.getKeyProperty("type"), is("meters")); + } + + @Test + public void taggedMetricPromotesTagsToKeyProperties() throws Exception { + ObjectName name = factory.createName("meters", "metrics", + "glue_listener_event[operation=alter_table,outcome=other,result=failure]"); + + assertThat(name.getDomain(), is("metrics")); + assertThat(name.getKeyProperty("name"), is("glue_listener_event")); + assertThat(name.getKeyProperty("type"), is("meters")); + assertThat(name.getKeyProperty("operation"), is("alter_table")); + assertThat(name.getKeyProperty("outcome"), is("other")); + assertThat(name.getKeyProperty("result"), is("failure")); + } + + @Test + public void untaggedMetricHasNoExtraKeyProperties() throws Exception { + ObjectName name = factory.createName("meters", "metrics", "glue_listener_table_success"); + + assertThat(name.getKeyProperty("operation"), is(nullValue())); + } +} diff --git a/hive-event-listeners/apiary-metastore-auth/pom.xml b/hive-event-listeners/apiary-metastore-auth/pom.xml index d8a75e83..44ba5956 100644 --- a/hive-event-listeners/apiary-metastore-auth/pom.xml +++ b/hive-event-listeners/apiary-metastore-auth/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary hive-event-listeners-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-metastore-auth diff --git a/hive-event-listeners/apiary-ranger-metastore-plugin/pom.xml b/hive-event-listeners/apiary-ranger-metastore-plugin/pom.xml index 34a77696..8dc93400 100644 --- a/hive-event-listeners/apiary-ranger-metastore-plugin/pom.xml +++ b/hive-event-listeners/apiary-ranger-metastore-plugin/pom.xml @@ -4,7 +4,7 @@ com.expediagroup.apiary hive-event-listeners-parent - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT apiary-ranger-metastore-plugin diff --git a/hive-event-listeners/pom.xml b/hive-event-listeners/pom.xml index 69772d3b..977404a0 100644 --- a/hive-event-listeners/pom.xml +++ b/hive-event-listeners/pom.xml @@ -4,7 +4,7 @@ apiary-extensions-parent com.expediagroup.apiary - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT hive-event-listeners-parent diff --git a/hive-hooks/pom.xml b/hive-hooks/pom.xml index 769a70fb..d7bac07b 100644 --- a/hive-hooks/pom.xml +++ b/hive-hooks/pom.xml @@ -4,7 +4,7 @@ apiary-extensions-parent com.expediagroup.apiary - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT hive-hooks diff --git a/pom.xml b/pom.xml index 8fb9cca5..12c031ff 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.expediagroup.apiary apiary-extensions-parent Various extensions to Apiary that provide additional, optional functionality - 8.1.19-SNAPSHOT + 8.2.0-SNAPSHOT pom Apiary Extensions 2018 @@ -28,7 +28,7 @@ 2.7.1 2.3.7 1.0.0 - 1.9.9 + 1.14.14