Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[*]
indent_size=2
indent_style=space
charset=utf-8

[*.java]
continuation_indent_size=2
trim_trailing_whitespace=true
insert_final_newline=true
max_line_length=120
wildcard_import_limit=999
ij_java_blank_lines_after_imports=1
ij_java_blank_lines_before_imports=1
ij_java_class_count_to_use_import_on_demand=999
ij_java_names_count_to_use_import_on_demand=999
ij_java_imports_layout=$java.**,|,$javax.**,|,$org.**,|,$*,$com.**,|,$com.expedia.**,|,$com.hotels.**,java.**,|,javax.**,|,org.**,|,*,|,com.**,|,com.expedia.**,|,com.hotels.**
ij_java_keep_simple_methods_in_one_line=true
ij_java_keep_simple_lambdas_in_one_line=true
ij_java_keep_simple_classes_in_one_line = true
1 change: 1 addition & 0 deletions .sdkmanrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
java=21.0.8-tem
2 changes: 1 addition & 1 deletion drone-fly-app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
<dependency>
<groupId>com.expediagroup.apiary</groupId>
<artifactId>kafka-metastore-receiver</artifactId>
<version>8.1.15</version>
<version>${apiary.extensions.version}</version>
<exclusions>
<exclusion>
<groupId>jdk.tools</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2020 Expedia, Inc.
* Copyright (C) 2020-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.
Expand All @@ -17,22 +17,14 @@

import java.util.TimeZone;

import org.springframework.beans.BeansException;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;

import com.google.common.annotations.VisibleForTesting;

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableConfigurationProperties
public class DroneFly implements ApplicationContextAware {

private static ConfigurableApplicationContext context;
public class DroneFly {

public static void main(String[] args) {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
Expand All @@ -42,22 +34,4 @@ public static void main(String[] args) {
.build()
.run(args);
}

@VisibleForTesting
public static boolean isRunning() {
return context != null && context.isRunning();
}

@VisibleForTesting
public static void stop() {
if (context == null) {
throw new RuntimeException("Application context has not been started.");
}
context.close();
}

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = (ConfigurableApplicationContext) applicationContext;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2020-2025 Expedia, Inc.
* Copyright (C) 2020-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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2020 Expedia, Inc.
* Copyright (C) 2020-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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2020 Expedia, Inc.
* Copyright (C) 2020-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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2020-2025 Expedia, Inc.
* Copyright (C) 2020-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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2020 Expedia, Inc.
* Copyright (C) 2020-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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Copyright (C) 2020 Expedia, Inc.
* Copyright (C) 2020-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.
Expand Down
4 changes: 2 additions & 2 deletions drone-fly-integration-tests/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
<dependency>
<groupId>com.expediagroup.apiary</groupId>
<artifactId>kafka-metastore-listener</artifactId>
<version>8.1.18</version>
<version>${apiary.extensions.version}</version>
<scope>test</scope>
<exclusions>
<exclusion>
Expand Down Expand Up @@ -111,7 +111,7 @@
<dependency>
<groupId>com.expediagroup.apiary</groupId>
<artifactId>apiary-gluesync-listener</artifactId>
<version>8.1.18</version>
<version>${apiary.extensions.version}</version>
<classifier>all</classifier>
<scope>test</scope>
<exclusions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,39 @@
*/
package com.expediagroup.dataplatform.dronefly.core.integration;

import static java.util.Map.entry;
import static org.assertj.core.api.Assertions.assertThat;

import static com.expediagroup.dataplatform.dronefly.core.integration.support.DroneFlyIntegrationTestUtils.buildPartition;
import static com.expediagroup.dataplatform.dronefly.core.integration.support.DroneFlyIntegrationTestUtils.buildTable;
import static com.expediagroup.dataplatform.dronefly.core.integration.support.SpringMetricsUtils.metric;
import static com.expediagroup.dataplatform.dronefly.core.integration.support.SpringMetricsUtils.springMetricsIncrease;

import org.apache.hadoop.hive.metastore.HMSHandler;
import org.apache.hadoop.hive.metastore.api.MetaException;
import org.apache.hadoop.hive.metastore.events.AddPartitionEvent;
import org.apache.hadoop.hive.metastore.events.CreateTableEvent;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.mockito.Mockito;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalManagementPort;
import org.springframework.context.annotation.Import;
import org.springframework.kafka.test.context.EmbeddedKafka;

import io.micrometer.core.instrument.Metrics;

import com.expediagroup.apiary.extensions.gluesync.listener.ApiaryGlueSync;
import com.expediagroup.apiary.extensions.gluesync.listener.metrics.MetricConstants;
import com.expediagroup.dataplatform.dronefly.app.DroneFly;
import com.expediagroup.dataplatform.dronefly.app.DroneFlyRunner;
import com.expediagroup.dataplatform.dronefly.app.service.ListenerCatalog;
import com.expediagroup.dataplatform.dronefly.core.integration.support.AsyncRunnerConfig;
import com.expediagroup.dataplatform.dronefly.core.integration.support.SpringMetricsUtils;

/**
* Deployment smoke test for apiary-gluesync-listener inside a Dronefly Spring Boot context.
Expand All @@ -36,29 +57,54 @@
*/
@SpringBootTest(
classes = DroneFly.class,
// RANDOM_PORT (not NONE) mirrors production: Dronefly always runs a web server, and the
// web server's dependency chain causes PrometheusMeterRegistry to initialise before
// ListenerCatalog — preventing the JmxMeterRegistry fallback in MetricService.
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = {
"apiary.bootstrap.servers=localhost:9999",
"spring.main.allow-bean-definition-overriding=true",
"apiary.bootstrap.servers=${spring.embedded.kafka.brokers}",
"apiary.kafka.topic.name=test-topic",
"instance.name=test",
"apiary.listener.list=com.expediagroup.apiary.extensions.gluesync.listener.ApiaryGlueSync",
// Spring Boot test defaults disable metric export; re-enable so PrometheusMeterRegistry
// is added to Metrics.globalRegistry before ApiaryGlueSync is constructed.
"management.defaults.metrics.export.enabled=true",
"management.prometheus.metrics.export.enabled=true"
"management.prometheus.metrics.export.enabled=true",
"management.endpoints.web.exposure.include=metrics,prometheus"
}
)
@EmbeddedKafka(controlledShutdown = true, topics = {"test-topic"}, partitions = 1)
@Import(AsyncRunnerConfig.class)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ApiaryGlueSyncMetricsIntegrationTest {

@MockitoBean
DroneFlyRunner droneFlyRunner;
private static final Logger log = LoggerFactory.getLogger(DroneFlyIntegrationTest.class);

private static final SpringMetricsUtils.Metric CREATE_TABLE_IGNORED = metric("glue_listener_event", "COUNT",
"operation", MetricConstants.CREATE_TABLE,
"result", MetricConstants.RESULT_IGNORED,
"outcome", "ignored");
private static final SpringMetricsUtils.Metric ADD_PARTITION_IGNORED = metric("glue_listener_event", "COUNT",
"operation", MetricConstants.ADD_PARTITION,
"result", MetricConstants.RESULT_IGNORED,
"outcome", "ignored");
private static final SpringMetricsUtils.Metric TABLE_FAILURE = metric("glue_listener_table_failure", "COUNT");
private static final SpringMetricsUtils.Metric CREATE_TABLE_FAILURE = metric("glue_listener_event", "COUNT",
"operation", MetricConstants.CREATE_TABLE,
"result", MetricConstants.RESULT_FAILURE);

@LocalManagementPort
private int managementPort;

@Autowired
ListenerCatalog listenerCatalog;

@Autowired
TestRestTemplate restTemplate;

@BeforeAll
void setUp() {
log.info("Management URI: http://localhost:{}/actuator", managementPort);
}

/** Verifies the fat jar loaded cleanly and that Prometheus was registered before ApiaryGlueSync
* was constructed — wrong bean ordering would silently add a JmxMeterRegistry instead. */
@Test
Expand All @@ -70,4 +116,111 @@ void listenerLoadedWithCorrectMetricRegistry() {
assertThat(Metrics.globalRegistry.getRegistries())
.noneMatch(r -> r.getClass().getName().contains("JmxMeterRegistry"));
}

/**
* Verifies all GlueSync counters are exported via the actuator metrics endpoint,
* confirming they landed in the Prometheus registry rather than a hidden JMX one.
* {@code glue_listener_event} is on-demand, so a status=false event is fired first
* to ensure it is registered before the name list is checked.
*/
@Test
void gluesyncMetricsExported() {
ApiaryGlueSync apiaryGlueSync = listenerCatalog.getListeners().stream()
.filter(ApiaryGlueSync.class::isInstance)
.map(ApiaryGlueSync.class::cast)
.findFirst()
.orElseThrow();
try {
apiaryGlueSync.onCreateTable(new CreateTableEvent(buildTable(), false, Mockito.mock(HMSHandler.class), false));
} catch (MetaException e) {
throw new RuntimeException(e);
}

assertThat(SpringMetricsUtils.metricNames(restTemplate))
.containsAll(MetricConstants.LISTENER_METRICS)
.contains(MetricConstants.LISTENER_EVENT);
}

/**
* Verifies GlueSync counters appear in the Prometheus scrape output.
* Micrometer appends {@code _total} to counter names in Prometheus format.
* {@code glue_listener_event} is registered on-demand, so a status=false event
* is fired first to ensure it is present in the registry before scraping.
*/
@Test
void gluesyncMetricsInPrometheusFormat() {
ApiaryGlueSync apiaryGlueSync = listenerCatalog.getListeners().stream()
.filter(ApiaryGlueSync.class::isInstance)
.map(ApiaryGlueSync.class::cast)
.findFirst()
.orElseThrow();
try {
apiaryGlueSync.onCreateTable(new CreateTableEvent(buildTable(), false, Mockito.mock(HMSHandler.class), false));
} catch (MetaException e) {
throw new RuntimeException(e);
}

String body = SpringMetricsUtils.prometheusBody(restTemplate);
MetricConstants.LISTENER_METRICS.forEach(name -> assertThat(body).contains(name + "_total"));
assertThat(body).contains(MetricConstants.LISTENER_EVENT + "_total");
}

/**
* Verifies that the new tagged {@code glue_listener_event} counter is recorded and exported when
* events are processed. Uses {@code status=false} events so the HMS-failure path is taken
* inside ApiaryGlueSync — which records the "ignored" metric without making any Glue API calls.
*/
@Test
void gluesyncEventMetricRecorded() {
ApiaryGlueSync apiaryGlueSync = listenerCatalog.getListeners().stream()
.filter(ApiaryGlueSync.class::isInstance)
.map(ApiaryGlueSync.class::cast)
.findFirst()
.orElseThrow();

HMSHandler hmsHandler = Mockito.mock(HMSHandler.class);

springMetricsIncrease(
restTemplate,
() -> {
try {
apiaryGlueSync.onCreateTable(new CreateTableEvent(buildTable(), false, hmsHandler, false));
apiaryGlueSync.onAddPartition(new AddPartitionEvent(buildTable(), buildPartition(), false, hmsHandler));
} catch (MetaException e) {
throw new RuntimeException(e);
}
},
entry(CREATE_TABLE_IGNORED, 1.0),
entry(ADD_PARTITION_IGNORED, 1.0)
);
}

/**
* Verifies {@code glue_listener_table_failure} and the tagged {@code glue_listener_event}
* failure counter are recorded when the Glue API is unreachable. The surefire configuration
* sets {@code AWS_REGION=us-fake-1} with fake credentials so every real Glue call fails fast
* with an {@code UnknownHostException} — no mocking needed.
*/
@Test
void gluesyncFailureMetricRecordedOnGlueError() {
ApiaryGlueSync apiaryGlueSync = listenerCatalog.getListeners().stream()
.filter(ApiaryGlueSync.class::isInstance)
.map(ApiaryGlueSync.class::cast)
.findFirst()
.orElseThrow();

HMSHandler hmsHandler = Mockito.mock(HMSHandler.class);

springMetricsIncrease(
restTemplate,
() -> {
try {
apiaryGlueSync.onCreateTable(new CreateTableEvent(buildTable(), true, hmsHandler, false));
} catch (MetaException e) {
throw new RuntimeException(e);
}
},
entry(TABLE_FAILURE, 1.0),
entry(CREATE_TABLE_FAILURE, 1.0));
}
}
Loading
Loading