The Eureka server must be based upon the Eureka codebase at version 2.0.4 or later or undefined behavior may result.
+ * + *Any failure of registration or deregistration or any other significant operation will be logged and will not + * prevent the current microservice from otherwise operating normally.
+ * + *Instances of this class are safe for concurrent use by multiple threads.
+ * + *Ensure this class is on your classpath.
+ * + *Eureka defines its own configuration mechanism and naming scheme for clients that use its native tools. To make + * migration easier, Helidon's Eureka integration reuses these names, their hierarchy, and their default values where + * possible.
+ * + *Name | + * + *Type | + * + *Description | + * + *Default Value | + * + *Notes | + * + *
---|---|---|---|---|
{@code eureka.client.registration} | + * + *{@link Config} | + * + *A configuration node that describes an {@link + * Http1ClientConfig HttpClientConfig} | + * + *none | + * + *At a minimum, the {@code base-uri} leaf node is required. For testing, a value of {@code + * http://localhost:8761/eureka} is often suitable. | + * + *
{@code eureka.instance.instanceId} | + * + *{@link String} | + * + *The identifier identifying the service instance to be registered. | + * + *The value of the {@code eureka.instance.hostName} key, concatenated with a "{@code :}", concatenated with + * the value of the port on which the webserver is currently running | + * + *+ * + * |
{@code eureka.instance.name} | + * + *{@link String} | + * + *The name of the service instance to be registered with Eureka. | + * + *{@code unknown} | + * + *+ * + * |
{@code eureka.instance.appGroup} | + * + *{@link String} | + * + *The name of the application group to which the service belongs. | + * + *{@code unknown} | + * + *+ * + * |
{@code eureka.instance.dataCenterInfo.name} | + * + *{@link String} | + * + *The Eureka-defined name of the datacenter type of the datacenter within which the service + * instance is deployed. | + * + *{@code MyOwn} | + * + *Eureka permits two values here: either {@code Amazon} or {@code MyOwn}. | + * + *
{@code eureka.instance.ipAddr} | + * + *{@link String} | + * + *The IP (4) address to be registered for this service instance. | + * + *The return value of an invocation of {@link java.net.InetAddress#getHostAddress()} on the return value of + * an invocation of {@link java.net.InetAddress#getLocalHost()} | + * + *+ * + * |
{@code eureka.instance.hostName} | + * + *{@link String} | + * + *The hostname to be registered for this service instance. | + * + *The return value of an invocation of {@link java.net.InetAddress#getHostName()} on the return value of an + * invocation of {@link java.net.InetAddress#getLocalHost()} | + * + *+ * + * |
{@code eureka.instance.port} | + * + *{@code int} | + * + *The port to be registered for this service instance. | + * + *If the {@linkplain WebServer#hasTls() webserver has TLS enabled}, the default value is {@code 80}. If the + * webserver does not have TLS enabled, the default value is the port on which the webserver is currently running | + * + *Eureka makes a distinction between a port and a secure port. Both values + * are registered, even if one of the two is not applicable. | + * + *
{@code eureka.instance.securePort} | + * + *{@code int} | + * + *The secure port to be registered for this service instance. | + * + *If the {@linkplain WebServer#hasTls() webserver has TLS enabled}, the default value is the port on which the webserver is currently running. If the + * webserver does not have TLS enabled, the default value is {@code 443}. | + * + *Eureka makes a distinction between a port and a secure port. Both values + * are registered, even if one of the two is not applicable. | + * + *
{@code eureka.instance.traffic.enabled} | + * + *{@code boolean} | + * + *Whether the service instance is able to respond to requests upon registration. | + * + *{@code true} | + * + *If this value is set to {@code false}, then a call must be made out of band, normally by a health check + * mechanism, to the {@link #markUp()} method to report to Eureka that the service instance is able to respond to + * requests. | + * + *
{@code eureka.instance.lease.renewalInterval} | + * + *{@code int} | + * + *The duration, in seconds, between registration (lease) renewal attempts. | + * + *{@code 30} | + * + *+ * + * |
{@code eureka.instance.lease.duration} | + * + *{@code int} | + * + *The duration, in seconds, of a successful registration (lease). | + * + *{@code 90} | + * + *+ * + * |
{@code eureka.instance.metadata} | + * + *{@link Map Map<String, String>} | + * + *{@link String}-typed key-value pairs describing metadata to accompany a service instance registration. | + * + *none | + * + *+ * + * |
The {@link Logger} used by instances of this class is named {@code io.helidon.integtrations.eureka.EurekaRegistrationFeature}.
+ * + * @see #afterStart(WebServer) + */ +public final class EurekaRegistrationFeature implements HttpFeature { + + + /* + * Static fields. + */ + + + private static final JsonBuilderFactory JBF = createBuilderFactory(Map.of()); + + private static final Logger LOGGER = getLogger(EurekaRegistrationFeature.class.getName()); + + private static final JsonString UP = createValue("UP"); + private static final JsonString DOWN = createValue("DOWN"); + private static final JsonString STARTING = createValue("STARTING"); + private static final JsonString OUT_OF_SERVICE = createValue("OUT_OF_SERVICE"); + private static final JsonString UNKNOWN = createValue("UNKNOWN"); + + + /* + * Instance fields. + */ + + + private volatile JsonObject instanceInfo; + + private volatile boolean stop; + + private volatile Thread renewer; + + private volatile HttpClient extends Http1ClientRequest> client; + + + /* + * Constructors. + */ + + + /** + * Creates a new {@link EurekaRegistrationFeature}. + * + * @deprecated For service loader use only. + */ + @Deprecated // For service loader use only + public EurekaRegistrationFeature() { + super(); + } + + + /* + * Instance methods. + */ + + + /** + * Begins the process of registering the current microservice as a Eureka service instance in an + * (external) Eureka server. + * + * @param webServer the {@link WebServer} that has successfully started; must not be {@code null} + * + * @exception NullPointerException if {@code webServer} is {@code null} + * + * @deprecated End users should not call this method. + */ + @Deprecated // End users should not call this method + @Override // HttpFeature (ServerLifecycle) + public void afterStart(WebServer webServer) { + if (webServer.isRunning()) { + this.afterStart(Services.get(Config.class), webServer.port(), webServer.hasTls()); + } + } + + // for testing + void afterStart(Config rootConfig, int actualPort, boolean tls) { + if (this.stop) { // volatile read + // Some other thread called this.afterStop() for some reason. This is technically a programming error, but + // afterStop() is public, so there are many codepaths that might result in it being called for a variety of + // reasons, some of which may? perhaps? be valid. + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "Unexpected stop explicitly requested;" + + " no attempt at registration will occur"); + } + return; + } + Config eurekaConfig = rootConfig.get("eureka"); + if (!eurekaConfig.isObject()) { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "No top-level object node named \"eureka\" in global configuration;" + + " no attempt at registration will occur"); + } + return; + } + final Http1ClientConfig.Builder builder; + try { + builder = Http1ClientConfig.builder() + .sendExpectContinue(false) // Spring's version of Eureka server has trouble otherwise + .config(eurekaConfig.get("client.registration")); + } catch (RuntimeException e) { + if (LOGGER.isLoggable(ERROR)) { + LOGGER.log(ERROR, + "Error configuring Eureka registration client from top-level object node named " + + " \"eureka.client.registration\"" + + " in global configuration;" + + " no attempt at registration will occur", + e); + } + return; + } + builder.baseUri() + .ifPresentOrElse(x -> { + Config eurekaInstanceConfig = eurekaConfig.get("instance"); + if (!eurekaConfig.isObject()) { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "No top-level object node named \"eureka.instance\" in global configuration;" + + " no attempt at registration will occur"); + } + return; + } + JsonObject instanceInfo = json(eurekaInstanceConfig, actualPort, tls); + this.instanceInfo = instanceInfo; // volatile write + this.client = builder.build(); // volatile write + + // Register, then kick off a renewal loop. + if (this.register(instanceInfo)) { + long sleepTimeInMilliSeconds = instanceInfo.getJsonObject("instance") // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L55 + .getJsonObject("leaseInfo") + .getInt("renewalIntervalInSecs") * 1000L; + this.renewer = Thread.ofVirtual() // volatile write + .name("Eureka lease renewer") + .uncaughtExceptionHandler((t, e) -> { + if (LOGGER.isLoggable(ERROR)) { + LOGGER.log(ERROR, e); + } + this.stop = true; // volatile write + }) + .start(() -> { + // Simplest possible heartbeat loop; nothing more complicated is needed. + // Sleep first; we just finished registration so there needs to be a time gap before + // renewal. + try { + sleep(sleepTimeInMilliSeconds); + } catch (InterruptedException e) { + } + while (!this.stop) { // volatile read + JsonObject newInstanceInfo = this.renew(); + if (newInstanceInfo != this.instanceInfo) { // volatile read + // The server gave us something new for some reason; use it. + this.instanceInfo = newInstanceInfo; // volatile write + } + try { + sleep(sleepTimeInMilliSeconds); + } catch (InterruptedException e) { + } + } + }); + // Mark our status as up if it wasn't already + this.up(instanceInfo, true); + } else if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "Registration failed;" + + " no further attempt at registration will occur"); + } + }, + () -> { + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "No Eureka Server URL found in configuration node named" + + " \"eureka.client.registration.base-uri\"" + + " in global configuration; no attempt at registration will occur"); + } + }); + } + + /** + * Unregisters the current microservice as an available Eureka service instance in an (external) Eureka + * server. + * + *This method deliberately has no effect after the first time it is invoked.
+ * + * @deprecated End users should not call this method. + */ + @Deprecated // End users should not call this method + @Override // HttpFeature (ServerLifecycle) + public void afterStop() { + // Although users *should* not call this method, they might, from any thread. Proceed with caution. + if (this.stop) { // volatile read + return; + } + this.stop = true; // volatile write + var client = this.client; // volatile read + if (client == null) { + // Registration never happened + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, + "No cancellation necessary; registration never occurred"); + } + return; + } + Thread renewer = this.renewer; // volatile read + if (renewer != null) { + renewer.interrupt(); + } + boolean canceled = false; + RuntimeException e = null; + try { + canceled = this.cancel(client); + } catch (RuntimeException e0) { + e = e0; + } finally { + try { + client.closeResource(); + } catch (RuntimeException e1) { + if (e == null) { + e = e1; + } else { + e.addSuppressed(e1); + } + } + if (!canceled && LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "Cancellation operation failed"); + } + if (e != null && LOGGER.isLoggable(ERROR)) { + LOGGER.log(ERROR, e); + } + } + } + + // (Called only by the afterStop method.) + private boolean cancel(HttpClient extends Http1ClientRequest> client) { + if (!this.stop) { // volatile read + // Programming error internal to this class. Truly an illegal state. + throw new IllegalStateException(); + } + JsonObject instanceInfo = this.instanceInfo; // volatile read; never null here + // Native Eureka sets the status to DOWN, but then does not publish this status change, and instead simply + // forcibly unregisters the instance. I'm not sure what the status setting accomplishes, although buried in the + // sediment seems to be some kind of status change event system that pertains to Eureka's peer-to-peer + // replication machinery that we may simply not care about here. We'll follow suit in case this sequencing turns + // out to be important. + this.up(instanceInfo, false); + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L55 + JsonObject instance = instanceInfo.getJsonObject("instance"); + return this.cancel(client, instance.getString("app"), instance.getString("instanceId")); + } + + // DELETE {baseUri}/v2/apps/{appName}/{id} + private boolean cancel(HttpClient extends Http1ClientRequest> client, String appName, String id) { + try (var response = client + .delete("/v2/apps/" + appName.toString() + "/" + id.toString()) // trigger NPEs if needed + .request()) { + if (response.status().family() == SUCCESSFUL) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, + "DELETE /v2/apps/" + appName.toString() + "/" + id.toString() + ": " + response.status()); + } + return true; + } else if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "DELETE /v2/apps/" + appName.toString() + "/" + id.toString() + ": " + response.status()); + } + return false; + } + } + + // PUT {baseUri}/v2/apps/{appName}/{id}?status={status}&lastDirtyTimestamp={lastDirtyTimestamp} + // + // (...&overriddenstatus={someOverriddenStatus} is recognized by the Eureka server, but never sent by Eureka's + // registration client.) + private HttpClientResponse heartbeat(String appName, + String id, + String status, + Long lastDirtyTimestamp) { + var request = this.client // volatile read + .put("/v2/apps/" + appName.toString() + "/" + id.toString()) // trigger NPEs if necessary + .accept(APPLICATION_JSON); + if (status != null) { + request.queryParam("status", status); + } + if (lastDirtyTimestamp != null) { + request.queryParam("lastDirtyTimestamp", lastDirtyTimestamp.toString()); + } + return request.request(); + } + + /** + * Marks this microservice as being {@code UP} for purposes of Eureka registration and for no other purpose. + * + * @return {@code true} if the status change was successfully recorded for eventual registration or renewal; {@code + * false} if no action was taken + */ + public boolean markUp() { + return this.up(this.instanceInfo, true); // volatile read + } + + /** + * Marks this microservice as being {@code DOWN} for purposes of Eureka registration and for no other purpose. + * + * @return {@code true} if the status change was successfully recorded for eventual registration or renewal; {@code + * false} if no action was taken + */ + public boolean markDown() { + return this.up(this.instanceInfo, false); // volatile read + } + + /** + * Calls the {@link #heartbeat(String, String, String, Long)} method and handles its response appropriately. + * + *This method is normally invoked in a loop every 30 seconds or so.
+ * + * @return the {@link JsonObject} representing the service registration details, possibly amended to contain a + * different status and/or other attributes depending on what the server supplied; never {@code null} + * + * @see #heartbeat(String, String, String, Long) + */ + private JsonObject renew() { + JsonObject instanceInfo = this.instanceInfo; + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L55 + JsonObject instance = instanceInfo.getJsonObject("instance"); + try (var response = + this.heartbeat(instance.getString("app"), + instance.getString("instanceId"), + instance.getString("status"), + Long.valueOf(instance.getJsonNumber("lastDirtyTimestamp").longValueExact()))) { + switch (response.status()) { + case Status s when s.family() == SUCCESSFUL: + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, + "Successfully renewed lease"); + } + // See + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-core/src/main/java/com/netflix/eureka/resources/InstanceResource.java; + // there is often no entity returned, presumably to indicate no changes. + if (response.entity().hasEntity()) { + instanceInfo = response.entity().as(JsonObject.class); + assert instanceInfo != null : "Eureka Server contract violation; instanceInfo == null"; + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, + "New registration details received: " + instanceInfo); + } + } + break; + case Status s when s == NOT_FOUND_404: + // Eureka's native machinery re-registers here. + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, + "Lease not found; reregistering"); + } + instanceInfo = json(instanceInfo, System.currentTimeMillis()); + boolean registrationResult = this.register(instanceInfo); + if (!registrationResult && LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, + "Reregistration failed"); + } + break; + default: + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, + "Heartbeat HTTP status: " + response.status()); + if (response.entity().hasEntity()) { + LOGGER.log(WARNING, + response.entity().as(JsonObject.class).getString("error")); + } + } + break; + } + } + return instanceInfo; + } + + /** + * Implements the {@link HttpFeature#setup(HttpRouting.Builder)} method by deliberately doing nothing. + * + * @param routingBuilder an {@link HttpRouting.Builder}; ignored + * + * @deprecated End users should not call this method. + */ + @Deprecated // End users should not call this method + @Override // HttpFeature + public void setup(HttpRouting.Builder routingBuilder) { + // Nothing to do. + } + + private void statusChange() { + // For later, perhaps; Eureka's DiscoveryClient can be configured to notify the server "on demand"; not sure + // whether we should as well + } + + // POST {baseUri}/v2/apps/{payload.getJsonObject("instance").getString("app")} + private boolean register(JsonObject payload) { + if (payload == null) { + return false; + } + try (var response = this.client // volatile read + .post("/v2/apps/" + payload.getJsonObject("instance").getString("app")) + .accept(APPLICATION_JSON) // needed? native client has it, but throws any entity away + .contentType(APPLICATION_JSON) + .header(ACCEPT_ENCODING, "gzip") + .submit(payload)) { + switch (response.status().code()) { + case 200: + if (LOGGER.isLoggable(DEBUG)) { + if (response.entity().hasEntity()) { + LOGGER.log(DEBUG, + "Registration succeeded: 200; " + response.entity().as(JsonObject.class)); + } + } + return true; + case 204: + return true; + default: + if (response.status().family() == SUCCESSFUL) { + return true; + } + if (LOGGER.isLoggable(WARNING)) { + if (response.entity().hasEntity()) { + LOGGER.log(WARNING, + "Registration failed: " + response.status() + + "; " + response.entity().as(JsonObject.class).getString("error")); + } else { + LOGGER.log(WARNING, + "Registration failed: " + response.status()); + } + } + return false; + } + } + } + + private boolean up(JsonObject oldInstanceInfo, boolean up) { + if (oldInstanceInfo == null) { + return false; + } + JsonObject instanceInfo = json(oldInstanceInfo, up ? UP : DOWN); + if (instanceInfo == oldInstanceInfo) { + return false; + } + this.instanceInfo = instanceInfo; // volatile write + this.statusChange(); + return true; + } + + + /* + * Static methods. + */ + + + private static String hostName(Config c) { + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/AbstractInstanceConfig.java#L216-L226 + return c.get("hostName").asString() + .orElseGet(() -> localhost().map(InetAddress::getHostName).orElse("")); + } + + private static String instanceId(Config c, int actualPort) { + return c.get("instanceId").asString() + .orElseGet(() -> { + // "Native" Eureka and Spring Cloud Eureka have different defaults. + // + // See + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/providers/EurekaConfigBasedInstanceInfoProvider.java#L60-L69; + // default is simply hostName + // + // See + // https://cloud.spring.io/spring-cloud-netflix/multi/multi__service_discovery_eureka_clients.html#_changing_the_eureka_instance_id + // + // Our default will split the difference and use host and port. + return c.get("dataCenterInfo.metadata.instance-id").asString() + .orElseGet(() -> hostName(c) + ":" + actualPort); + }); + } + + private static String ipAddress(Config c) { + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/AbstractInstanceConfig.java#L216-L226 + return c.get("ipAddr").asString() + .orElseGet(() -> localhost().map(InetAddress::getHostAddress).orElse("")); + } + + /** + * Returns a {@link JsonObject} representing service instance registration details suitable for sending to a Eureka + * server. + * + * @param config a {@link Config} representing Eureka-related data; often acquired via {@code + * Services.get(Config.class).get("eureka.instance")}; must not be {@code null} + * + * @param actualPort an {@code int} representing the port the currently running microservice is exposed on; not + * validated in any way + * + * @param tls whether TLS is in effect for the currently running microservice; Eureka makes distinctions throughout + * the registration details concerning "secure" and "non-secure" items based on the value of this parameter + * + * @return a {@link JsonObject} representing service instance registration details; never {@code null} + * + * @exception NullPointerException if {@code config} is {@code null} + */ + static JsonObject json(Config config, int actualPort, boolean tls) { + // JSON will validate successfully against ../../../../../resources/META-INF/instance-info.schema.json. + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L55 + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L164-L189 + // https://github.com/Netflix/eureka/issues/1563#issuecomment-2625648853 + var instance = JBF.createObjectBuilder(); + + instance.add("instanceId", instanceId(config, actualPort)); + + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L892 + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/PropertiesInstanceConfig.java#L233-L236 + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/PropertyBasedInstanceConfigConstants.java#L12 + instance.add("app", config.get("name").asString().orElse("unknown")); + + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/PropertyBasedInstanceConfigConstants.java#L13 + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/PropertiesInstanceConfig.java#L238-L241 + instance.add("appGroupName", config.get("appGroup").asString().orElse("unknown")); + + var dataCenterInfo = JBF.createObjectBuilder(); + Config dataCenterInfoConfig = config.get("dataCenterInfo"); + String n = dataCenterInfoConfig.isObject() ? dataCenterInfoConfig.get("name").asString().orElse("MyOwn") : "MyOwn"; + switch (n) { + case "Amazon": + dataCenterInfo.add("name", "Amazon"); + dataCenterInfo.add("@class", "com.netflix.appinfo.AmazonInfo"); + Config dataCenterInfoMetadataConfig = dataCenterInfoConfig.get("metadata"); + if (dataCenterInfoMetadataConfig.isObject()) { + dataCenterInfoMetadataConfig.asMap().ifPresent(m -> m.forEach(dataCenterInfo::add)); + } + break; + default: + dataCenterInfo.add("name", "MyOwn"); + dataCenterInfo.add("@class", "com.netflix.appinfo.MyDataCenterInfo"); + break; + } + instance.add("dataCenterInfo", dataCenterInfo); + + instance.add("ipAddr", ipAddress(config)); + + instance.add("hostName", hostName(config)); + + // Add the extremely bizarre port structure. + instance.add("port", JBF.createObjectBuilder() + .add("$", port(config, tls, actualPort)) + .add("@enabled", portEnabled(config, tls))); + + // Add the extremely bizarre secure port structure. + instance.add("securePort", JBF.createObjectBuilder() + .add("$", securePort(config, tls, actualPort)) + .add("@enabled", securePortEnabled(config, tls))); + + if (portEnabled(config, tls)) { + instance.add("vipAddress", + config.get("vipAddress").asString() + .orElseGet(() -> hostName(config) + ":" + port(config, tls, actualPort))); + } + + if (securePortEnabled(config, tls)) { + instance.add("secureVipAddress", + config.get("secureVipAddress").asString() + .orElseGet(() -> hostName(config) + ":" + securePort(config, tls, actualPort))); + } + + instance.add("homePageUrl", + config.get("homePageUrl").asString() + .orElseGet(() -> "http://" + hostName(config) + ":" + + port(config, tls, actualPort) + + config.get("homePageUrlPath").asString().orElse("/"))); + + instance.add("statusPageUrl", + config.get("statusPageUrl").asString() + .orElseGet(() -> "http://" + hostName(config) + ":" + + port(config, tls, actualPort) + + config.get("statusPageUrlPath").asString().orElse("/Status"))); + + // The acronym ASG means "auto scaling group". + config.get("asgName").asString().ifPresent(s -> instance.add("asgName", s)); + + instance.add("healthCheckUrl", config.get("healthCheckUrl") + .asString() + .orElseGet(() -> "http://" + hostName(config) + ":" + + port(config, tls, actualPort) + + config.get("healthCheckUrlPath").asString() + .orElseGet(() -> config.root().get("server.features.observe.observers.health.endpoint").asString() + .orElse("/observe/health")))); // Helidon convention; Eureka's is /healthcheck + + instance.add("secureHealthCheckUrl", config.get("healthCheckUrl") + .asString() + .orElseGet(() -> "https://" + hostName(config) + ":" + + port(config, tls, actualPort) + + config.get("healthCheckUrlPath").asString() + .orElseGet(() -> config.root().get("server.features.observe.observers.health.endpoint").asString() + .orElse("/observe/health")))); // Helidon convention; Eureka's is /healthcheck + + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L98-L100 + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L919-L929 + // But also: + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L228-L230 + instance.add("sid", config.get("sid").asString().orElse("na")); + + // countryId; cannot be anything other than 1; must be shipped in the payload, however, to ensure data integrity + // on the server side + instance.add("countryId", 1); + + // (The default value must be shipped with the payload to ensure data integrity on the server. Or so it seems? + // There are places in the Eureka codebase where the field is dereferenced assuming it is not null; the JSON + // marshalling can and absolutely will set it to null. Which is correct? it is hard to say.) + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L209 + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L138 + instance.add("overriddenStatus", "UNKNOWN"); + + Long ts = Long.valueOf(System.currentTimeMillis()); + instance.add("lastUpdatedTimestamp", ts); + instance.add("lastDirtyTimestamp", ts); + + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L137 + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/InstanceInfo.java#L316-L321 + // https://github.com/Netflix/eureka/blob/v2.0.4/eureka-client/src/main/java/com/netflix/appinfo/providers/EurekaConfigBasedInstanceInfoProvider.java#L104-L113 + // Eureka uses false as a default value, but I think true is better given that we are running in afterStart() + instance.add("status", config.get("traffic.enabled").asBoolean().orElse(true) ? "UP" : "STARTING"); + + Config metadataConfig = config.get("metadata"); + if (metadataConfig.isObject()) { + Map