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
10 changes: 10 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
root = true

[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.java]
indent_style = space
indent_size = 4
10 changes: 6 additions & 4 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ concurrency:
cancel-in-progress: true

jobs:
check-pre-commit:
check-spotless:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-python@v6
- uses: actions/setup-java@v5
with:
python-version: "3.14"
- uses: pre-commit/action@v3.0.1
java-version: 21
distribution: temurin
- name: Check formatting
run: mvn -B spotless:check

test-coverage:
runs-on: ubuntu-latest
Expand Down
7 changes: 0 additions & 7 deletions .pre-commit-config.yaml

This file was deleted.

15 changes: 15 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@
<skipResolution>true</skipResolution>
</configuration>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<java>
<googleJavaFormat>
<version>1.33.0</version>
<style>AOSP</style>
</googleJavaFormat>
<removeUnusedImports />
<formatAnnotations />
</java>
</configuration>
</plugin>
Comment on lines +115 to +129
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

com.diffplug.spotless spotless-maven-plugin latest version 2024 2025

💡 Result:

  • Latest released in 2024: spotless-maven-plugin 2.43.0 (released Jan 23, 2024). [1]

  • Latest released in 2025: spotless-maven-plugin 2.46.0 (released Jul 20, 2025). [2]

Sources: Maven Central listings for com.diffplug.spotless:spotless-maven-plugin. [1] [2]

References:
[1] https://repo.maven.apache.org/maven2/com/diffplug/spotless/spotless-maven-plugin/
[2] https://repo.maven.apache.org/maven2/com/diffplug/spotless/spotless-maven-plugin/2.46.0/


🌐 Web query:

google-java-format 1.33.0 Java 17 compatibility requirements

💡 Result:

Short answer: google-java-format 1.33.0 can format Java 17 source, but the formatter itself requires a newer JDK to run — the project raised the minimum JDK to run google-java-format to JDK 21 (change introduced in v1.29.0). See the README note that the minimum runtime is declared in core/pom.xml and the v1.29.0 release note. [1][2]

Sources:

  • google-java-format README (notes about minimum Java in core/pom.xml / usage). [1]
  • google-java-format releases (v1.29.0: "The minimum support JDK version to run google-java-format is now JDK 21"). [2]

🌐 Web query:

spotless-maven-plugin 3.1.0 release date Java compatibility

💡 Result:

  • Release date: spotless-maven-plugin 3.1.0 was published to Maven Central on 2025-11-18. [1]

  • Java compatibility: Spotless bumped its required Java to Java 17 in the 3.0.0 series, so 3.1.0 requires Java 17 (or newer). [2]

Sources:
[1] Maven Central (com.diffplug.spotless:spotless-maven-plugin — 3.1.0 listing).
[2] diffplug/spotless GitHub releases — note for Maven/3.0.0: "Bump the required Java to 17."


Incompatible JDK requirement: google-java-format 1.33.0 requires JDK 21 to run.

The plugin version 3.1.0 requires Java 17, but google-java-format 1.33.0 (used within it) requires JDK 21 runtime as of v1.29.0. This means your build environment must have JDK 21 available for the formatter to execute, even if compiling with Java 17. Either upgrade to a higher Java version for the build or use an older google-java-format version compatible with Java 17.

🤖 Prompt for AI Agents
In `@pom.xml` around lines 115 - 129, The Spotless config uses google-java-format
version 1.33.0 (inside the <googleJavaFormat> block) which requires JDK 21 at
runtime while the plugin (<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId> version 3.1.0) targets Java 17;
fix by either updating your build/runtime JDK to 21 so the formatter can run, or
change the <googleJavaFormat><version> value to a pre-1.29.x release compatible
with Java 17 (e.g., 1.28.x) and keep the rest of the Spotless configuration
intact; update whichever option you choose consistently in the pom where
<googleJavaFormat> version is declared.

</plugins>
</build>

Expand Down
9 changes: 3 additions & 6 deletions src/main/java/io/apitally/common/ApitallyAppender.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
package io.apitally.common;

import java.util.ArrayList;
import java.util.List;

import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.AppenderBase;

import io.apitally.common.dto.LogRecord;
import java.util.ArrayList;
import java.util.List;
import org.slf4j.LoggerFactory;

public class ApitallyAppender extends AppenderBase<ILoggingEvent> {
private static final String NAME = "ApitallyAppender";
Expand Down
207 changes: 115 additions & 92 deletions src/main/java/io/apitally/common/ApitallyClient.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.apitally.common;

import io.apitally.common.dto.Path;
import io.apitally.common.dto.StartupData;
import io.apitally.common.dto.SyncData;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
Expand All @@ -17,15 +20,10 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.retry.support.RetryTemplate;

import io.apitally.common.dto.Path;
import io.apitally.common.dto.StartupData;
import io.apitally.common.dto.SyncData;

public class ApitallyClient {
public static class RetryableHubRequestException extends Exception {
public RetryableHubRequestException(String message) {
Expand All @@ -46,16 +44,18 @@ public enum HubRequestStatus {
private static final int INITIAL_PERIOD_SECONDS = 3600;
private static final int MAX_QUEUE_TIME_SECONDS = 3600;
private static final int REQUEST_TIMEOUT_SECONDS = 10;
private static final String HUB_BASE_URL = Optional.ofNullable(System.getenv("APITALLY_HUB_BASE_URL"))
.filter(s -> !s.trim().isEmpty())
.orElse("https://hub.apitally.io");
private static final String HUB_BASE_URL =
Optional.ofNullable(System.getenv("APITALLY_HUB_BASE_URL"))
.filter(s -> !s.trim().isEmpty())
.orElse("https://hub.apitally.io");

private static final Logger logger = LoggerFactory.getLogger(ApitallyClient.class);
private static final RetryTemplate retryTemplate = RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(Duration.ofSeconds(1), 2, Duration.ofSeconds(4), true)
.retryOn(RetryableHubRequestException.class)
.build();
private static final RetryTemplate retryTemplate =
RetryTemplate.builder()
.maxAttempts(3)
.exponentialBackoff(Duration.ofSeconds(1), 2, Duration.ofSeconds(4), true)
.retryOn(RetryableHubRequestException.class)
.build();

private final String clientId;
private final String env;
Expand Down Expand Up @@ -121,31 +121,35 @@ private void sendStartupData() {
if (startupData == null) {
return;
}
HttpRequest request = HttpRequest.newBuilder()
.uri(getHubUrl("startup"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(startupData.toJSON()))
.build();
sendHubRequest(request).thenAccept(status -> {
if (status == HubRequestStatus.OK) {
startupDataSent = true;
startupData = null;
} else if (status == HubRequestStatus.VALIDATION_ERROR) {
startupDataSent = false;
startupData = null;
} else {
startupDataSent = false;
}
});
HttpRequest request =
HttpRequest.newBuilder()
.uri(getHubUrl("startup"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(startupData.toJSON()))
.build();
sendHubRequest(request)
.thenAccept(
status -> {
if (status == HubRequestStatus.OK) {
startupDataSent = true;
startupData = null;
} else if (status == HubRequestStatus.VALIDATION_ERROR) {
startupDataSent = false;
startupData = null;
} else {
startupDataSent = false;
}
});
}

private void sendSyncData() {
SyncData data = new SyncData(
instanceLock.getInstanceUuid(),
requestCounter.getAndResetRequests(),
validationErrorCounter.getAndResetValidationErrors(),
serverErrorCounter.getAndResetServerErrors(),
consumerRegistry.getAndResetConsumers());
SyncData data =
new SyncData(
instanceLock.getInstanceUuid(),
requestCounter.getAndResetRequests(),
validationErrorCounter.getAndResetValidationErrors(),
serverErrorCounter.getAndResetServerErrors(),
consumerRegistry.getAndResetConsumers());
syncDataQueue.offer(data);

int i = 0;
Expand All @@ -158,11 +162,12 @@ private void sendSyncData() {
// Add random delay between retries
Thread.sleep(100 + random.nextInt(400));
}
HttpRequest request = HttpRequest.newBuilder()
.uri(getHubUrl("sync"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toJSON()))
.build();
HttpRequest request =
HttpRequest.newBuilder()
.uri(getHubUrl("sync"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload.toJSON()))
.build();
HubRequestStatus status = sendHubRequest(request).join();
if (status == HubRequestStatus.RETRYABLE_ERROR) {
syncDataQueue.offer(payload);
Expand Down Expand Up @@ -191,11 +196,12 @@ private void sendLogData() {
}
}
try (InputStream inputStream = logFile.getInputStream()) {
HttpRequest request = HttpRequest.newBuilder()
.uri(getHubUrl("log", "uuid=" + logFile.getUuid().toString()))
.header("Content-Type", "application/octet-stream")
.POST(HttpRequest.BodyPublishers.ofInputStream(() -> inputStream))
.build();
HttpRequest request =
HttpRequest.newBuilder()
.uri(getHubUrl("log", "uuid=" + logFile.getUuid().toString()))
.header("Content-Type", "application/octet-stream")
.POST(HttpRequest.BodyPublishers.ofInputStream(() -> inputStream))
.build();
HubRequestStatus status = sendHubRequest(request).join();
if (status == HubRequestStatus.PAYMENT_REQUIRED) {
requestLogger.clear();
Expand All @@ -217,70 +223,87 @@ private void sendLogData() {
}

public CompletableFuture<HubRequestStatus> sendHubRequest(HttpRequest request) {
return CompletableFuture.supplyAsync(() -> {
try {
return retryTemplate.execute(context -> {
return CompletableFuture.supplyAsync(
() -> {
try {
logger.debug("Sending request to Apitally hub: {}", request.uri());
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 200 && response.statusCode() < 300) {
return HubRequestStatus.OK;
} else if (response.statusCode() == 402) {
return HubRequestStatus.PAYMENT_REQUIRED;
} else if (response.statusCode() == 404) {
enabled = false;
stopSync();
requestLogger.close();
logger.error("Invalid Apitally client ID: {}", clientId);
return HubRequestStatus.INVALID_CLIENT_ID;
} else if (response.statusCode() == 422) {
logger.error("Received validation error from Apitally hub: {}", response.body());
return HubRequestStatus.VALIDATION_ERROR;
} else {
throw new RetryableHubRequestException(
"Hub request failed with status code " + response.statusCode());
}
return retryTemplate.execute(
context -> {
try {
logger.debug(
"Sending request to Apitally hub: {}",
request.uri());
HttpResponse<String> response =
httpClient.send(
request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 200
&& response.statusCode() < 300) {
return HubRequestStatus.OK;
} else if (response.statusCode() == 402) {
return HubRequestStatus.PAYMENT_REQUIRED;
} else if (response.statusCode() == 404) {
enabled = false;
stopSync();
requestLogger.close();
logger.error(
"Invalid Apitally client ID: {}", clientId);
return HubRequestStatus.INVALID_CLIENT_ID;
} else if (response.statusCode() == 422) {
logger.error(
"Received validation error from Apitally hub: {}",
response.body());
return HubRequestStatus.VALIDATION_ERROR;
} else {
throw new RetryableHubRequestException(
"Hub request failed with status code "
+ response.statusCode());
}
} catch (Exception e) {
throw new RetryableHubRequestException(
"Hub request failed with exception: "
+ e.getMessage());
}
});
} catch (Exception e) {
throw new RetryableHubRequestException(
"Hub request failed with exception: " + e.getMessage());
logger.error("Error sending request to Apitally hub", e);
return HubRequestStatus.RETRYABLE_ERROR;
}
});
} catch (Exception e) {
logger.error("Error sending request to Apitally hub", e);
return HubRequestStatus.RETRYABLE_ERROR;
}
});
}

public void startSync() {
if (scheduler == null) {
scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread thread = new Thread(r, "apitally-sync");
thread.setDaemon(true);
return thread;
});
scheduler =
Executors.newSingleThreadScheduledExecutor(
r -> {
Thread thread = new Thread(r, "apitally-sync");
thread.setDaemon(true);
return thread;
});
}

if (syncTask != null) {
syncTask.cancel(false);
}

// Start with shorter initial sync interval
syncTask = scheduler.scheduleAtFixedRate(
this::sync,
0,
INITIAL_SYNC_INTERVAL_SECONDS,
TimeUnit.SECONDS);
syncTask =
scheduler.scheduleAtFixedRate(
this::sync, 0, INITIAL_SYNC_INTERVAL_SECONDS, TimeUnit.SECONDS);

// Schedule a one-time task to switch to regular sync interval
scheduler.schedule(() -> {
syncTask.cancel(false);
syncTask = scheduler.scheduleAtFixedRate(
this::sync,
SYNC_INTERVAL_SECONDS,
SYNC_INTERVAL_SECONDS,
TimeUnit.SECONDS);
}, INITIAL_PERIOD_SECONDS, TimeUnit.SECONDS);
scheduler.schedule(
() -> {
syncTask.cancel(false);
syncTask =
scheduler.scheduleAtFixedRate(
this::sync,
SYNC_INTERVAL_SECONDS,
SYNC_INTERVAL_SECONDS,
TimeUnit.SECONDS);
},
INITIAL_PERIOD_SECONDS,
TimeUnit.SECONDS);
}

public void stopSync() {
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/io/apitally/common/ConsumerRegistry.java
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
package io.apitally.common;

import io.apitally.common.dto.Consumer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import io.apitally.common.dto.Consumer;

public class ConsumerRegistry {
private final Map<String, Consumer> consumers;
private final Set<String> updated;
Expand Down Expand Up @@ -57,7 +56,8 @@ public void addOrUpdateConsumer(Consumer consumer) {
}

if (hasChanges) {
consumers.put(consumer.getIdentifier(),
consumers.put(
consumer.getIdentifier(),
new Consumer(consumer.getIdentifier(), newName, newGroup));
updated.add(consumer.getIdentifier());
}
Expand Down
Loading