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
5 changes: 5 additions & 0 deletions judoscale-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8
}

// Use --release to enforce API compatibility at compile time
tasks.withType<JavaCompile> {
options.release.set(8)
}

dependencies {
// JSON processing
implementation(libs.jackson.databind)
Expand Down
75 changes: 75 additions & 0 deletions judoscale-core/src/main/java/com/judoscale/core/ConfigBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
*/
public class ConfigBase {

/**
* The current runtime container identifier.
* Detected from environment variables at initialization.
*/
private final String runtimeContainer;

/**
* The base URL for the Judoscale API.
* Typically set via JUDOSCALE_URL environment variable.
Expand Down Expand Up @@ -46,6 +52,75 @@ public class ConfigBase {
*/
private boolean enabled = true;

/**
* Creates a new ConfigBase, detecting the runtime container from environment variables.
*/
public ConfigBase() {
this.runtimeContainer = detectRuntimeContainer();
}

/**
* Detects the runtime container from various platform-specific environment variables.
* Checks in order: JUDOSCALE_CONTAINER, DYNO (Heroku), RENDER_INSTANCE_ID (Render),
* ECS_CONTAINER_METADATA_URI (AWS ECS), FLY_MACHINE_ID (Fly.io), RAILWAY_REPLICA_ID (Railway).
*
* @return the detected container identifier, or empty string if not detected
*/
private String detectRuntimeContainer() {
String container = System.getenv("JUDOSCALE_CONTAINER");
if (container != null && !container.isEmpty()) {
return container;
}

// Heroku
String dyno = System.getenv("DYNO");
if (dyno != null && !dyno.isEmpty()) {
return dyno;
}

// Render
String renderInstanceId = System.getenv("RENDER_INSTANCE_ID");
if (renderInstanceId != null && !renderInstanceId.isEmpty()) {
String renderServiceId = System.getenv("RENDER_SERVICE_ID");
if (renderServiceId != null && renderInstanceId.startsWith(renderServiceId + "-")) {
return renderInstanceId.substring(renderServiceId.length() + 1);
}
return renderInstanceId;
}

// AWS ECS
String ecsMetadataUri = System.getenv("ECS_CONTAINER_METADATA_URI");
if (ecsMetadataUri != null && !ecsMetadataUri.isEmpty()) {
int lastSlash = ecsMetadataUri.lastIndexOf('/');
if (lastSlash >= 0 && lastSlash < ecsMetadataUri.length() - 1) {
return ecsMetadataUri.substring(lastSlash + 1);
}
}

// Fly.io
String flyMachineId = System.getenv("FLY_MACHINE_ID");
if (flyMachineId != null && !flyMachineId.isEmpty()) {
return flyMachineId;
}

// Railway
String railwayReplicaId = System.getenv("RAILWAY_REPLICA_ID");
if (railwayReplicaId != null && !railwayReplicaId.isEmpty()) {
return railwayReplicaId;
}

return "";
}

/**
* Returns the current runtime container identifier.
*
* @return the runtime container, or empty string if not detected
*/
public String getRuntimeContainer() {
return runtimeContainer;
}

/**
* Returns the API base URL, preferring explicit apiBaseUrl over url.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import java.io.IOException;
import java.io.InputStream;
import java.lang.management.ManagementFactory;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
Expand All @@ -27,11 +28,16 @@ private ReportBuilder() {
*
* @param metrics the metrics to include in the report
* @param adapters the adapters to include in the report (supports multiple adapters)
* @param runtimeContainer the runtime container identifier
* @return the JSON string
*/
public static String buildReportJson(List<Metric> metrics, Collection<Adapter> adapters) {
public static String buildReportJson(List<Metric> metrics, Collection<Adapter> adapters, String runtimeContainer) {
ObjectNode root = objectMapper.createObjectNode();

// Include runtime container identifier and process ID
root.put("container", runtimeContainer != null ? runtimeContainer : "");
root.put("pid", getPid());

// Build metrics array: each metric is [timestamp, value, identifier, queueName?]
ArrayNode metricsArray = objectMapper.createArrayNode();
for (Metric m : metrics) {
Expand Down Expand Up @@ -62,6 +68,21 @@ public static String buildReportJson(List<Metric> metrics, Collection<Adapter> a
}
}

/**
* Gets the current process ID in a Java 8-compatible way.
*
* @return the process ID, or -1 if it cannot be determined
*/
private static long getPid() {
// RuntimeMXBean.getName() returns "pid@hostname" on most JVMs
String name = ManagementFactory.getRuntimeMXBean().getName();
try {
return Long.parseLong(name.split("@")[0]);
} catch (NumberFormatException e) {
return -1;
}
}

/**
* Loads the adapter version from the META-INF/judoscale.properties file.
* Falls back to "unknown" if the file cannot be read.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,11 @@ void settersUpdateValues() {
assertThat(config.getLogLevel()).isEqualTo("DEBUG");
assertThat(config.isEnabled()).isFalse();
}

@Test
void runtimeContainerIsNeverNull() {
// Runtime container is detected from environment variables.
// In test environment without those vars set, it should return empty string, not null.
assertThat(config.getRuntimeContainer()).isNotNull();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ void buildReportJsonFormatsMetricsCorrectly() {
new Metric("at", 50, time)
);

String json = ReportBuilder.buildReportJson(metrics, Collections.singletonList(TEST_ADAPTER));
String json = ReportBuilder.buildReportJson(metrics, Collections.singletonList(TEST_ADAPTER), "web.1");

assertThat(json).contains("\"container\":\"web.1\"");
assertThat(json).containsPattern("\"pid\":\\d+");
assertThat(json).contains("\"metrics\":");
assertThat(json).contains("[1705314600,100,\"qt\"]");
assertThat(json).contains("[1705314600,50,\"at\"]");
Expand All @@ -38,16 +40,17 @@ void buildReportJsonIncludesQueueNameWhenPresent() {
new Metric("qd", 5, time, "default")
);

String json = ReportBuilder.buildReportJson(metrics, Collections.singletonList(TEST_ADAPTER));
String json = ReportBuilder.buildReportJson(metrics, Collections.singletonList(TEST_ADAPTER), "web.1");

assertThat(json).contains("[1705314600,5,\"qd\",\"default\"]");
}

@Test
void buildReportJsonHandlesEmptyMetricsList() {
String json = ReportBuilder.buildReportJson(Collections.emptyList(), Collections.singletonList(TEST_ADAPTER));
String json = ReportBuilder.buildReportJson(Collections.emptyList(), Collections.singletonList(TEST_ADAPTER), "web.1");

assertThat(json).contains("\"metrics\":[]");
assertThat(json).contains("\"container\":\"web.1\"");
}

@Test
Expand All @@ -57,18 +60,32 @@ void buildReportJsonEscapesSpecialCharactersInQueueName() {
new Metric("qd", 5, time, "queue\"with\\special")
);

String json = ReportBuilder.buildReportJson(metrics, Collections.singletonList(TEST_ADAPTER));
String json = ReportBuilder.buildReportJson(metrics, Collections.singletonList(TEST_ADAPTER), "web.1");

assertThat(json).contains("\"queue\\\"with\\\\special\"");
}

@Test
void buildReportJsonHandlesNullContainer() {
String json = ReportBuilder.buildReportJson(Collections.emptyList(), Collections.singletonList(TEST_ADAPTER), null);

assertThat(json).contains("\"container\":\"\"");
}

@Test
void buildReportJsonHandlesEmptyContainer() {
String json = ReportBuilder.buildReportJson(Collections.emptyList(), Collections.singletonList(TEST_ADAPTER), "");

assertThat(json).contains("\"container\":\"\"");
}

@Test
void buildReportJsonSupportsMultipleAdapters() {
Adapter springBootAdapter = new Adapter("judoscale-spring-boot", "1.0.0");
Adapter sidekiqAdapter = new Adapter("judoscale-sidekiq", "2.0.0");
List<Adapter> adapters = Arrays.asList(springBootAdapter, sidekiqAdapter);

String json = ReportBuilder.buildReportJson(Collections.emptyList(), adapters);
String json = ReportBuilder.buildReportJson(Collections.emptyList(), adapters, "web.1");

assertThat(json).contains("\"judoscale-spring-boot\"");
assertThat(json).contains("\"judoscale-sidekiq\"");
Expand All @@ -78,7 +95,7 @@ void buildReportJsonSupportsMultipleAdapters() {

@Test
void buildReportJsonHandlesEmptyAdaptersList() {
String json = ReportBuilder.buildReportJson(Collections.emptyList(), Collections.emptyList());
String json = ReportBuilder.buildReportJson(Collections.emptyList(), Collections.emptyList(), "web.1");

assertThat(json).contains("\"adapters\":{}");
}
Expand Down
5 changes: 5 additions & 0 deletions judoscale-spring-boot-2-starter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ java {
targetCompatibility = JavaVersion.VERSION_1_8
}

// Use --release to enforce API compatibility at compile time
tasks.withType<JavaCompile> {
options.release.set(8)
}

// Generate version properties file during build
tasks.register("generateVersionProperties") {
val outputDir = layout.buildDirectory.dir("generated/resources")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public boolean reportMetrics(List<Metric> metrics) {
return false;
}

String json = ReportBuilder.buildReportJson(metrics, Collections.singletonList(ADAPTER));
String json = ReportBuilder.buildReportJson(metrics, Collections.singletonList(ADAPTER), config.getRuntimeContainer());
String url = config.getApiBaseUrl() + "/v3/reports";

for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public boolean reportMetrics(List<Metric> metrics) {
return false;
}

String json = ReportBuilder.buildReportJson(metrics, List.of(ADAPTER));
String json = ReportBuilder.buildReportJson(metrics, List.of(ADAPTER), config.getRuntimeContainer());
String url = config.getApiBaseUrl() + "/v3/reports";

for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
Expand Down
Loading