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