From 7941fda90d92238049502cd4db593eb8ca3794d1 Mon Sep 17 00:00:00 2001 From: WonChul Heo Date: Thu, 25 Apr 2024 18:57:45 +0900 Subject: [PATCH] Collect metrics for application instances (#4270) --- .../ROOT/pages/spring-cloud-netflix.adoc | 11 + docs/modules/ROOT/partials/_configprops.adoc | 211 +++++++++--------- .../DefaultEurekaInstanceTagsProvider.java | 37 +++ ...urekaInstanceMetricsAutoConfiguration.java | 58 +++++ .../server/metrics/EurekaInstanceMonitor.java | 76 +++++++ .../metrics/EurekaInstanceTagsProvider.java | 32 +++ ...itional-spring-configuration-metadata.json | 10 + ...ot.autoconfigure.AutoConfiguration.imports | 3 +- .../eureka/server/EurekaInstanceFixture.java | 45 ++++ .../server/EurekaInstanceMonitorTests.java | 166 ++++++++++++++ ...nceMonitorWithCustomTagsProviderTests.java | 102 +++++++++ .../eureka/server/InstanceRegistryTests.java | 26 +-- 12 files changed, 649 insertions(+), 128 deletions(-) create mode 100644 spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/DefaultEurekaInstanceTagsProvider.java create mode 100644 spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceMetricsAutoConfiguration.java create mode 100644 spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceMonitor.java create mode 100644 spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceTagsProvider.java create mode 100644 spring-cloud-netflix-eureka-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceFixture.java create mode 100644 spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceMonitorTests.java create mode 100644 spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceMonitorWithCustomTagsProviderTests.java diff --git a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc index 9317992e84..accf9901ec 100755 --- a/docs/modules/ROOT/pages/spring-cloud-netflix.adoc +++ b/docs/modules/ROOT/pages/spring-cloud-netflix.adoc @@ -604,6 +604,17 @@ when running a Eureka server you must include these dependencies in your POM or Spring Cloud Netflix Eureka Server does not support Spring AOT transformations or native images. +=== Metrics + +`EurekaInstanceMonitor` listens to events related to Eureka instance registration and creates/updates `Gauge`s for Eureka instance information in Micrometer's `MeterRegistry`. By default, this behavior is disabled. If you want to enable it, you need to set `eureka.server.metrics.enabled` to `true`. + +By default, the `Gauge`s are named `eureka.server.instances` and have the following tags: + +- `application`: application name +- `status`: instance status (`UP`, `DOWN`, `STARTING`, `OUT_OF_SERVICE`, `UNKNOWN`, see: `com.netflix.appinfo.InstanceInfo.InstanceStatus`) + +You can add additional tags by injecting your own implementation of `EurekaInstanceTagsProvider`. + == Configuration properties To see the list of all Spring Cloud Netflix related configuration properties please check link:appendix.html[the Appendix page]. diff --git a/docs/modules/ROOT/partials/_configprops.adoc b/docs/modules/ROOT/partials/_configprops.adoc index 6853e4773d..aedbe443e9 100644 --- a/docs/modules/ROOT/partials/_configprops.adoc +++ b/docs/modules/ROOT/partials/_configprops.adoc @@ -35,7 +35,7 @@ |eureka.client.on-demand-update-status-change | `+++true+++` | If set to true, local status updates via ApplicationInfoManager will trigger on-demand (but rate limited) register/updates to remote eureka servers. |eureka.client.order | `+++0+++` | Order of the discovery client used by `CompositeDiscoveryClient` for sorting available clients. |eureka.client.prefer-same-zone-eureka | `+++true+++` | Indicates whether or not this instance should try to use the eureka server in the same zone for latency and/or other reason. Ideally eureka clients are configured to talk to servers in the same zone The changes are effective at runtime at the next registry fetch cycle as specified by registryFetchIntervalSeconds -|eureka.client.property-resolver | | +|eureka.client.property-resolver | | |eureka.client.proxy-host | | Gets the proxy host to eureka server if any. |eureka.client.proxy-password | | Gets the proxy password if any. |eureka.client.proxy-port | | Gets the proxy port to eureka server if any. @@ -45,20 +45,20 @@ |eureka.client.register-with-eureka | `+++true+++` | Indicates whether or not this instance should register its information with eureka server for discovery by others. In some cases, you do not want your instances to be discovered whereas you just want do discover other instances. |eureka.client.registry-fetch-interval-seconds | `+++30+++` | Indicates how often(in seconds) to fetch the registry information from the eureka server. |eureka.client.registry-refresh-single-vip-address | | Indicates whether the client is only interested in the registry information for a single VIP. -|eureka.client.rest-template-timeout.connect-request-timeout | `+++0+++` | +|eureka.client.rest-template-timeout.connect-request-timeout | `+++0+++` | |eureka.client.rest-template-timeout.connect-timeout | `+++0+++` | Default values are set to 180000, in keeping with {@link RequestConfig} and {@link SocketConfig} defaults. -|eureka.client.rest-template-timeout.socket-timeout | `+++0+++` | +|eureka.client.rest-template-timeout.socket-timeout | `+++0+++` | |eureka.client.service-url | | Map of availability zone to list of fully qualified URLs to communicate with eureka server. Each value can be a single URL or a comma separated list of alternative locations. Typically the eureka server URLs carry protocol,host,port,context and version information if any. Example: https://ec2-256-156-243-129.compute-1.amazonaws.com:7001/eureka/ The changes are effective at runtime at the next service url refresh cycle as specified by eurekaServiceUrlPollIntervalSeconds. |eureka.client.should-enforce-registration-at-init | `+++false+++` | Indicates whether the client should enforce registration during initialization. Defaults to false. |eureka.client.should-unregister-on-shutdown | `+++true+++` | Indicates whether the client should explicitly unregister itself from the remote server on client shutdown. -|eureka.client.tls.enabled | | -|eureka.client.tls.key-password | | -|eureka.client.tls.key-store | | -|eureka.client.tls.key-store-password | | -|eureka.client.tls.key-store-type | | -|eureka.client.tls.trust-store | | -|eureka.client.tls.trust-store-password | | -|eureka.client.tls.trust-store-type | | +|eureka.client.tls.enabled | | +|eureka.client.tls.key-password | | +|eureka.client.tls.key-store | | +|eureka.client.tls.key-store-password | | +|eureka.client.tls.key-store-type | | +|eureka.client.tls.trust-store | | +|eureka.client.tls.trust-store-password | | +|eureka.client.tls.trust-store-type | | |eureka.client.use-dns-for-fetching-service-urls | `+++false+++` | Indicates whether the eureka client should use the DNS mechanism to fetch a list of eureka servers to talk to. When the DNS name is updated to have additional servers, that information is used immediately after the eureka client polls for that information as specified in eurekaServiceUrlPollIntervalSeconds. Alternatively, the service urls can be returned serviceUrls, but the users should implement their own mechanism to return the updated list in case of changes. The changes are effective at runtime. |eureka.client.webclient.enabled | `+++false+++` | Enables the use of WebClient for Eureka HTTP Client. |eureka.dashboard.enabled | `+++true+++` | Flag to enable the Eureka dashboard. Default true. @@ -70,8 +70,8 @@ |eureka.instance.appname | `+++unknown+++` | Get the name of the application to be registered with eureka. |eureka.instance.async-client-initialization | `+++false+++` | If true the EurekaClient will be initialized asynchronously when the InstanceRegistry bean is created. |eureka.instance.data-center-info | | Returns the data center this instance is deployed. This information is used to get some AWS specific instance information if the instance is deployed in AWS. -|eureka.instance.default-address-resolution-order | `+++[]+++` | -|eureka.instance.environment | | +|eureka.instance.default-address-resolution-order | `+++[]+++` | +|eureka.instance.environment | | |eureka.instance.health-check-url | | Gets the absolute health check page URL for this instance. The users can provide the healthCheckUrlPath if the health check page resides in the same instance talking to eureka, else in the cases where the instance is a proxy for some other server, users can provide the full URL. If the full URL is provided it takes precedence.

It is normally used for making educated decisions based on the health of the instance - for example, it can be used to determine whether to proceed deployments to an entire farm or stop the deployments without causing further damage. The full URL should follow the format http://${eureka.hostname}:7001/ where the value ${eureka.hostname} is replaced at runtime. |eureka.instance.health-check-url-path | | Gets the relative health check URL path for this instance. The health check page URL is then constructed out of the hostname and the type of communication - secure or unsecure as specified in securePort and nonSecurePort. It is normally used for making educated decisions based on the health of the instance - for example, it can be used to determine whether to proceed deployments to an entire farm or stop the deployments without causing further damage. |eureka.instance.home-page-url | | Gets the absolute home page URL for this instance. The users can provide the homePageUrlPath if the home page resides in the same instance talking to eureka, else in the cases where the instance is a proxy for some other server, users can provide the full URL. If the full URL is provided it takes precedence. It is normally used for informational purposes for other services to use it as a landing page. The full URL should follow the format http://${eureka.hostname}:7001/ where the value ${eureka.hostname} is replaced at runtime. @@ -85,12 +85,13 @@ |eureka.instance.lease-renewal-interval-in-seconds | `+++30+++` | Indicates how often (in seconds) the eureka client needs to send heartbeats to eureka server to indicate that it is still alive. If the heartbeats are not received for the period specified in leaseExpirationDurationInSeconds, eureka server will remove the instance from its view, there by disallowing traffic to this instance. Note that the instance could still not take traffic if it implements HealthCheckCallback and then decides to make itself unavailable. |eureka.instance.metadata-map | | Gets the metadata name/value pairs associated with this instance. This information is sent to eureka server and can be used by other instances. |eureka.instance.metadata-map.weight | `+++1+++` | The weight of service instance for weighted load balancing. +|eureka.instance.metrics.enabled | `+++false+++` | Indicates whether the metrics should be enabled for eureka instances. |eureka.instance.namespace | `+++eureka+++` | Get the namespace used to find properties. Ignored in Spring Cloud. |eureka.instance.non-secure-port | `+++80+++` | Get the non-secure port on which the instance should receive traffic. |eureka.instance.non-secure-port-enabled | `+++true+++` | Indicates whether the non-secure port should be enabled for traffic or not. |eureka.instance.prefer-ip-address | `+++false+++` | Flag to say that, when guessing a hostname, the IP address of the server should be used in preference to the hostname reported by the OS. |eureka.instance.registry.default-open-for-traffic-count | `+++1+++` | Value used in determining when leases are cancelled, default to 1 for standalone. Should be set to 0 for peer replicated eurekas -|eureka.instance.registry.expected-number-of-clients-sending-renews | `+++1+++` | +|eureka.instance.registry.expected-number-of-clients-sending-renews | `+++1+++` | |eureka.instance.secure-health-check-url | | Gets the absolute secure health check page URL for this instance. The users can provide the secureHealthCheckUrl if the health check page resides in the same instance talking to eureka, else in the cases where the instance is a proxy for some other server, users can provide the full URL. If the full URL is provided it takes precedence.

It is normally used for making educated decisions based on the health of the instance - for example, it can be used to determine whether to proceed deployments to an entire farm or stop the deployments without causing further damage. The full URL should follow the format http://${eureka.hostname}:7001/ where the value ${eureka.hostname} is replaced at runtime. |eureka.instance.secure-port | `+++443+++` | Get the Secure port on which the instance should receive traffic. |eureka.instance.secure-port-enabled | `+++false+++` | Indicates whether the secure port should be enabled for traffic or not. @@ -98,82 +99,82 @@ |eureka.instance.status-page-url | | Gets the absolute status page URL path for this instance. The users can provide the statusPageUrlPath if the status page resides in the same instance talking to eureka, else in the cases where the instance is a proxy for some other server, users can provide the full URL. If the full URL is provided it takes precedence. It is normally used for informational purposes for other services to find about the status of this instance. Users can provide a simple HTML indicating what is the current status of the instance. |eureka.instance.status-page-url-path | | Gets the relative status page URL path for this instance. The status page URL is then constructed out of the hostName and the type of communication - secure or unsecure as specified in securePort and nonSecurePort. It is normally used for informational purposes for other services to find about the status of this instance. Users can provide a simple HTML indicating what is the current status of the instance. |eureka.instance.virtual-host-name | `+++unknown+++` | Gets the virtual host name defined for this instance. This is typically the way other instance would find this instance by using the virtual host name.Think of this as similar to the fully qualified domain name, that the users of your services will need to find this instance. -|eureka.server.a-s-g-cache-expiry-timeout-ms | `+++0+++` | -|eureka.server.a-s-g-query-timeout-ms | `+++300+++` | -|eureka.server.a-s-g-update-interval-ms | `+++0+++` | -|eureka.server.a-w-s-access-id | | -|eureka.server.a-w-s-secret-key | | -|eureka.server.batch-replication | `+++false+++` | -|eureka.server.binding-strategy | | -|eureka.server.delta-retention-timer-interval-in-ms | `+++0+++` | -|eureka.server.disable-delta | `+++false+++` | -|eureka.server.disable-delta-for-remote-regions | `+++false+++` | -|eureka.server.disable-transparent-fallback-to-other-region | `+++false+++` | -|eureka.server.e-i-p-bind-rebind-retries | `+++3+++` | -|eureka.server.e-i-p-binding-retry-interval-ms | `+++0+++` | -|eureka.server.e-i-p-binding-retry-interval-ms-when-unbound | `+++0+++` | -|eureka.server.enable-replicated-request-compression | `+++false+++` | -|eureka.server.enable-self-preservation | `+++true+++` | -|eureka.server.eviction-interval-timer-in-ms | `+++0+++` | -|eureka.server.expected-client-renewal-interval-seconds | `+++30+++` | -|eureka.server.g-zip-content-from-remote-region | `+++true+++` | -|eureka.server.initial-capacity-of-response-cache | `+++1000+++` | -|eureka.server.json-codec-name | | -|eureka.server.list-auto-scaling-groups-role-name | `+++ListAutoScalingGroups+++` | -|eureka.server.log-identity-headers | `+++true+++` | -|eureka.server.max-elements-in-peer-replication-pool | `+++10000+++` | -|eureka.server.max-elements-in-status-replication-pool | `+++10000+++` | -|eureka.server.max-idle-thread-age-in-minutes-for-peer-replication | `+++15+++` | -|eureka.server.max-idle-thread-in-minutes-age-for-status-replication | `+++10+++` | -|eureka.server.max-threads-for-peer-replication | `+++20+++` | -|eureka.server.max-threads-for-status-replication | `+++1+++` | -|eureka.server.max-time-for-replication | `+++30000+++` | -|eureka.server.min-available-instances-for-peer-replication | `+++-1+++` | -|eureka.server.min-threads-for-peer-replication | `+++5+++` | -|eureka.server.min-threads-for-status-replication | `+++1+++` | -|eureka.server.my-url | | -|eureka.server.number-of-replication-retries | `+++5+++` | -|eureka.server.peer-eureka-nodes-update-interval-ms | `+++0+++` | -|eureka.server.peer-eureka-status-refresh-time-interval-ms | `+++0+++` | -|eureka.server.peer-node-connect-timeout-ms | `+++200+++` | -|eureka.server.peer-node-connection-idle-timeout-seconds | `+++30+++` | -|eureka.server.peer-node-read-timeout-ms | `+++200+++` | -|eureka.server.peer-node-total-connections | `+++1000+++` | -|eureka.server.peer-node-total-connections-per-host | `+++500+++` | -|eureka.server.prime-aws-replica-connections | `+++true+++` | -|eureka.server.property-resolver | | -|eureka.server.rate-limiter-burst-size | `+++10+++` | -|eureka.server.rate-limiter-enabled | `+++false+++` | -|eureka.server.rate-limiter-full-fetch-average-rate | `+++100+++` | -|eureka.server.rate-limiter-privileged-clients | | -|eureka.server.rate-limiter-registry-fetch-average-rate | `+++500+++` | -|eureka.server.rate-limiter-throttle-standard-clients | `+++false+++` | -|eureka.server.registry-sync-retries | `+++0+++` | -|eureka.server.registry-sync-retry-wait-ms | `+++0+++` | -|eureka.server.remote-region-app-whitelist | | -|eureka.server.remote-region-connect-timeout-ms | `+++1000+++` | -|eureka.server.remote-region-connection-idle-timeout-seconds | `+++30+++` | -|eureka.server.remote-region-fetch-thread-pool-size | `+++20+++` | -|eureka.server.remote-region-read-timeout-ms | `+++1000+++` | -|eureka.server.remote-region-registry-fetch-interval | `+++30+++` | -|eureka.server.remote-region-total-connections | `+++1000+++` | -|eureka.server.remote-region-total-connections-per-host | `+++500+++` | -|eureka.server.remote-region-trust-store | | -|eureka.server.remote-region-trust-store-password | `+++changeit+++` | -|eureka.server.remote-region-urls | | -|eureka.server.remote-region-urls-with-name | | -|eureka.server.renewal-percent-threshold | `+++0.85+++` | -|eureka.server.renewal-threshold-update-interval-ms | `+++0+++` | -|eureka.server.response-cache-auto-expiration-in-seconds | `+++180+++` | -|eureka.server.response-cache-update-interval-ms | `+++0+++` | -|eureka.server.retention-time-in-m-s-in-delta-queue | `+++0+++` | -|eureka.server.route53-bind-rebind-retries | `+++3+++` | -|eureka.server.route53-binding-retry-interval-ms | `+++0+++` | -|eureka.server.route53-domain-t-t-l | `+++30+++` | -|eureka.server.sync-when-timestamp-differs | `+++true+++` | -|eureka.server.use-read-only-response-cache | `+++true+++` | -|eureka.server.wait-time-in-ms-when-sync-empty | `+++0+++` | -|eureka.server.xml-codec-name | | +|eureka.server.a-s-g-cache-expiry-timeout-ms | `+++0+++` | +|eureka.server.a-s-g-query-timeout-ms | `+++300+++` | +|eureka.server.a-s-g-update-interval-ms | `+++0+++` | +|eureka.server.a-w-s-access-id | | +|eureka.server.a-w-s-secret-key | | +|eureka.server.batch-replication | `+++false+++` | +|eureka.server.binding-strategy | | +|eureka.server.delta-retention-timer-interval-in-ms | `+++0+++` | +|eureka.server.disable-delta | `+++false+++` | +|eureka.server.disable-delta-for-remote-regions | `+++false+++` | +|eureka.server.disable-transparent-fallback-to-other-region | `+++false+++` | +|eureka.server.e-i-p-bind-rebind-retries | `+++3+++` | +|eureka.server.e-i-p-binding-retry-interval-ms | `+++0+++` | +|eureka.server.e-i-p-binding-retry-interval-ms-when-unbound | `+++0+++` | +|eureka.server.enable-replicated-request-compression | `+++false+++` | +|eureka.server.enable-self-preservation | `+++true+++` | +|eureka.server.eviction-interval-timer-in-ms | `+++0+++` | +|eureka.server.expected-client-renewal-interval-seconds | `+++30+++` | +|eureka.server.g-zip-content-from-remote-region | `+++true+++` | +|eureka.server.initial-capacity-of-response-cache | `+++1000+++` | +|eureka.server.json-codec-name | | +|eureka.server.list-auto-scaling-groups-role-name | `+++ListAutoScalingGroups+++` | +|eureka.server.log-identity-headers | `+++true+++` | +|eureka.server.max-elements-in-peer-replication-pool | `+++10000+++` | +|eureka.server.max-elements-in-status-replication-pool | `+++10000+++` | +|eureka.server.max-idle-thread-age-in-minutes-for-peer-replication | `+++15+++` | +|eureka.server.max-idle-thread-in-minutes-age-for-status-replication | `+++10+++` | +|eureka.server.max-threads-for-peer-replication | `+++20+++` | +|eureka.server.max-threads-for-status-replication | `+++1+++` | +|eureka.server.max-time-for-replication | `+++30000+++` | +|eureka.server.min-available-instances-for-peer-replication | `+++-1+++` | +|eureka.server.min-threads-for-peer-replication | `+++5+++` | +|eureka.server.min-threads-for-status-replication | `+++1+++` | +|eureka.server.my-url | | +|eureka.server.number-of-replication-retries | `+++5+++` | +|eureka.server.peer-eureka-nodes-update-interval-ms | `+++0+++` | +|eureka.server.peer-eureka-status-refresh-time-interval-ms | `+++0+++` | +|eureka.server.peer-node-connect-timeout-ms | `+++200+++` | +|eureka.server.peer-node-connection-idle-timeout-seconds | `+++30+++` | +|eureka.server.peer-node-read-timeout-ms | `+++200+++` | +|eureka.server.peer-node-total-connections | `+++1000+++` | +|eureka.server.peer-node-total-connections-per-host | `+++500+++` | +|eureka.server.prime-aws-replica-connections | `+++true+++` | +|eureka.server.property-resolver | | +|eureka.server.rate-limiter-burst-size | `+++10+++` | +|eureka.server.rate-limiter-enabled | `+++false+++` | +|eureka.server.rate-limiter-full-fetch-average-rate | `+++100+++` | +|eureka.server.rate-limiter-privileged-clients | | +|eureka.server.rate-limiter-registry-fetch-average-rate | `+++500+++` | +|eureka.server.rate-limiter-throttle-standard-clients | `+++false+++` | +|eureka.server.registry-sync-retries | `+++0+++` | +|eureka.server.registry-sync-retry-wait-ms | `+++0+++` | +|eureka.server.remote-region-app-whitelist | | +|eureka.server.remote-region-connect-timeout-ms | `+++1000+++` | +|eureka.server.remote-region-connection-idle-timeout-seconds | `+++30+++` | +|eureka.server.remote-region-fetch-thread-pool-size | `+++20+++` | +|eureka.server.remote-region-read-timeout-ms | `+++1000+++` | +|eureka.server.remote-region-registry-fetch-interval | `+++30+++` | +|eureka.server.remote-region-total-connections | `+++1000+++` | +|eureka.server.remote-region-total-connections-per-host | `+++500+++` | +|eureka.server.remote-region-trust-store | | +|eureka.server.remote-region-trust-store-password | `+++changeit+++` | +|eureka.server.remote-region-urls | | +|eureka.server.remote-region-urls-with-name | | +|eureka.server.renewal-percent-threshold | `+++0.85+++` | +|eureka.server.renewal-threshold-update-interval-ms | `+++0+++` | +|eureka.server.response-cache-auto-expiration-in-seconds | `+++180+++` | +|eureka.server.response-cache-update-interval-ms | `+++0+++` | +|eureka.server.retention-time-in-m-s-in-delta-queue | `+++0+++` | +|eureka.server.route53-bind-rebind-retries | `+++3+++` | +|eureka.server.route53-binding-retry-interval-ms | `+++0+++` | +|eureka.server.route53-domain-t-t-l | `+++30+++` | +|eureka.server.sync-when-timestamp-differs | `+++true+++` | +|eureka.server.use-read-only-response-cache | `+++true+++` | +|eureka.server.wait-time-in-ms-when-sync-empty | `+++0+++` | +|eureka.server.xml-codec-name | | |spring.cloud.compatibility-verifier.compatible-boot-versions | | Default accepted versions for the Spring Boot dependency. You can set {@code x} for the patch version if you don't want to specify a concrete value. Example: {@code 3.4.x} |spring.cloud.compatibility-verifier.enabled | `+++false+++` | Enables creation of Spring Cloud compatibility verification. |spring.cloud.config.allow-override | `+++true+++` | Flag to indicate that {@link #isOverrideSystemProperties() systemPropertiesOverride} can be used. Set to false to prevent users from changing the default accidentally. Default true. @@ -182,24 +183,24 @@ |spring.cloud.config.override-system-properties | `+++true+++` | Flag to indicate that the external properties should override system properties. Default true. |spring.cloud.decrypt-environment-post-processor.enabled | `+++true+++` | Enable the DecryptEnvironmentPostProcessor. |spring.cloud.discovery.client.composite-indicator.enabled | `+++true+++` | Enables discovery client composite health indicator. -|spring.cloud.discovery.client.health-indicator.enabled | `+++true+++` | -|spring.cloud.discovery.client.health-indicator.include-description | `+++false+++` | +|spring.cloud.discovery.client.health-indicator.enabled | `+++true+++` | +|spring.cloud.discovery.client.health-indicator.include-description | `+++false+++` | |spring.cloud.discovery.client.health-indicator.use-services-query | `+++true+++` | Whether or not the indicator should use {@link DiscoveryClient#getServices} to check its health. When set to {@code false} the indicator instead uses the lighter {@link DiscoveryClient#probe()}. This can be helpful in large deployments where the number of services returned makes the operation unnecessarily heavy. -|spring.cloud.discovery.client.simple.instances | | -|spring.cloud.discovery.client.simple.local.host | | -|spring.cloud.discovery.client.simple.local.instance-id | | -|spring.cloud.discovery.client.simple.local.metadata | | -|spring.cloud.discovery.client.simple.local.port | `+++0+++` | -|spring.cloud.discovery.client.simple.local.secure | `+++false+++` | -|spring.cloud.discovery.client.simple.local.service-id | | -|spring.cloud.discovery.client.simple.local.uri | | -|spring.cloud.discovery.client.simple.order | | +|spring.cloud.discovery.client.simple.instances | | +|spring.cloud.discovery.client.simple.local.host | | +|spring.cloud.discovery.client.simple.local.instance-id | | +|spring.cloud.discovery.client.simple.local.metadata | | +|spring.cloud.discovery.client.simple.local.port | `+++0+++` | +|spring.cloud.discovery.client.simple.local.secure | `+++false+++` | +|spring.cloud.discovery.client.simple.local.service-id | | +|spring.cloud.discovery.client.simple.local.uri | | +|spring.cloud.discovery.client.simple.order | | |spring.cloud.discovery.enabled | `+++true+++` | Enables discovery client health indicators. |spring.cloud.features.enabled | `+++true+++` | Enables the features endpoint. |spring.cloud.httpclientfactories.apache.enabled | `+++true+++` | Enables creation of Apache Http Client factory beans. |spring.cloud.httpclientfactories.ok.enabled | `+++true+++` | Enables creation of OK Http Client factory beans. -|spring.cloud.hypermedia.refresh.fixed-delay | `+++5000+++` | -|spring.cloud.hypermedia.refresh.initial-delay | `+++10000+++` | +|spring.cloud.hypermedia.refresh.fixed-delay | `+++5000+++` | +|spring.cloud.hypermedia.refresh.initial-delay | `+++10000+++` | |spring.cloud.inetutils.default-hostname | `+++localhost+++` | The default hostname. Used in case of errors. |spring.cloud.inetutils.default-ip-address | `+++127.0.0.1+++` | The default IP address. Used in case of errors. |spring.cloud.inetutils.ignored-interfaces | | List of Java regular expressions for network interfaces that will be ignored. @@ -211,7 +212,7 @@ |spring.cloud.loadbalancer.cache.enabled | `+++true+++` | Enables Spring Cloud LoadBalancer caching mechanism. |spring.cloud.loadbalancer.cache.ttl | `+++35s+++` | Time To Live - time counted from writing of the record, after which cache entries are expired, expressed as a {@link Duration}. The property {@link String} has to be in keeping with the appropriate syntax as specified in Spring Boot StringToDurationConverter. @see StringToDurationConverter.java |spring.cloud.loadbalancer.call-get-with-request-on-delegates | `+++true+++` | If this flag is set to {@code true}, {@code ServiceInstanceListSupplier#get(Request request)} method will be implemented to call {@code delegate.get(request)} in classes assignable from {@code DelegatingServiceInstanceListSupplier} that don't already implement that method, with the exclusion of {@code CachingServiceInstanceListSupplier} and {@code HealthCheckServiceInstanceListSupplier}, which should be placed in the instance supplier hierarchy directly after the supplier performing instance retrieval over the network, before any request-based filtering is done, {@code true} by default. -|spring.cloud.loadbalancer.clients | | +|spring.cloud.loadbalancer.clients | | |spring.cloud.loadbalancer.configurations | `+++default+++` | Enables a predefined LoadBalancer configuration. |spring.cloud.loadbalancer.eager-load.clients | | Names of the clients. |spring.cloud.loadbalancer.enabled | `+++true+++` | Enables Spring Cloud LoadBalancer. @@ -257,4 +258,4 @@ |spring.cloud.service-registry.auto-registration.register-management | `+++true+++` | Whether to register the management as a service. Defaults to true. |spring.cloud.util.enabled | `+++true+++` | Enables creation of Spring Cloud utility beans. -|=== \ No newline at end of file +|=== diff --git a/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/DefaultEurekaInstanceTagsProvider.java b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/DefaultEurekaInstanceTagsProvider.java new file mode 100644 index 0000000000..3dbf097f90 --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/DefaultEurekaInstanceTagsProvider.java @@ -0,0 +1,37 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.netflix.eureka.server.metrics; + +import com.netflix.appinfo.InstanceInfo; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; + +/** + * Default implementation for {@link EurekaInstanceTagsProvider}. + * + * @author Wonchul Heo + * @since 4.1.2 + */ +class DefaultEurekaInstanceTagsProvider implements EurekaInstanceTagsProvider { + + @Override + public Tags eurekaInstanceTags(InstanceInfo instanceInfo) { + return Tags.of(Tag.of("application", instanceInfo.getAppName()), + Tag.of("status", instanceInfo.getStatus().name())); + } + +} diff --git a/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceMetricsAutoConfiguration.java b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceMetricsAutoConfiguration.java new file mode 100644 index 0000000000..62bce04e12 --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceMetricsAutoConfiguration.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.netflix.eureka.server.metrics; + +import com.netflix.eureka.registry.PeerAwareInstanceRegistry; +import io.micrometer.core.instrument.MeterRegistry; + +import org.springframework.boot.actuate.autoconfigure.metrics.CompositeMeterRegistryAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.metrics.MetricsAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration; +import org.springframework.context.annotation.Bean; + +/** + * Auto-configuration for Eureka Instance metrics. + * + * @author Wonchul Heo + * @since 4.1.2 + */ +@ConditionalOnClass(MeterRegistry.class) +@ConditionalOnBean(MeterRegistry.class) +@AutoConfigureAfter({ MetricsAutoConfiguration.class, CompositeMeterRegistryAutoConfiguration.class, + EurekaServerAutoConfiguration.class }) +@ConditionalOnProperty(name = "eureka.server.metrics.enabled", havingValue = "true") +class EurekaInstanceMetricsAutoConfiguration { + + @ConditionalOnMissingBean + @Bean + public EurekaInstanceTagsProvider eurekaInstanceTagProvider() { + return new DefaultEurekaInstanceTagsProvider(); + } + + @ConditionalOnMissingBean + @Bean + public EurekaInstanceMonitor eurekaInstanceMeterBinder(MeterRegistry meterRegistry, + PeerAwareInstanceRegistry instanceRegistry, EurekaInstanceTagsProvider tagProvider) { + return new EurekaInstanceMonitor(meterRegistry, instanceRegistry, tagProvider); + } + +} diff --git a/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceMonitor.java b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceMonitor.java new file mode 100644 index 0000000000..5c029fed59 --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceMonitor.java @@ -0,0 +1,76 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.netflix.eureka.server.metrics; + +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import com.netflix.eureka.registry.PeerAwareInstanceRegistry; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.MultiGauge; +import io.micrometer.core.instrument.Tags; + +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceCanceledEvent; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; +import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRenewedEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.SmartApplicationListener; + +/** + * {@link SmartApplicationListener} for collecting event metrics from + * {@link PeerAwareInstanceRegistry}. + * + * @author Wonchul Heo + * @since 4.1.2 + */ +public class EurekaInstanceMonitor implements SmartApplicationListener { + + private final MultiGauge eurekaInstances; + + private final PeerAwareInstanceRegistry instanceRegistry; + + private final EurekaInstanceTagsProvider tagProvider; + + EurekaInstanceMonitor(MeterRegistry meterRegistry, PeerAwareInstanceRegistry instanceRegistry, + EurekaInstanceTagsProvider tagProvider) { + Objects.requireNonNull(meterRegistry); + this.instanceRegistry = Objects.requireNonNull(instanceRegistry); + this.tagProvider = Objects.requireNonNull(tagProvider); + this.eurekaInstances = MultiGauge.builder("eureka.server.instances") + .description("Number of application instances registered with the Eureka server.") + .register(meterRegistry); + } + + @Override + public boolean supportsEventType(Class eventType) { + // If events that change state are added, an event class must be added. + return EurekaInstanceCanceledEvent.class.isAssignableFrom(eventType) + || EurekaInstanceRegisteredEvent.class.isAssignableFrom(eventType) + || EurekaInstanceRenewedEvent.class.isAssignableFrom(eventType); + } + + @Override + public void onApplicationEvent(ApplicationEvent event) { + final Map aggregatedCounts = instanceRegistry.getApplications().getRegisteredApplications().stream() + .flatMap(application -> application.getInstances().stream()) + .collect(Collectors.groupingBy(tagProvider::eurekaInstanceTags, Collectors.counting())); + eurekaInstances.register(aggregatedCounts.entrySet().stream() + .map(entry -> MultiGauge.Row.of(entry.getKey(), entry.getValue())).collect(Collectors.toList()), true); + } + +} diff --git a/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceTagsProvider.java b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceTagsProvider.java new file mode 100644 index 0000000000..d162d35d42 --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/main/java/org/springframework/cloud/netflix/eureka/server/metrics/EurekaInstanceTagsProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.netflix.eureka.server.metrics; + +import com.netflix.appinfo.InstanceInfo; +import io.micrometer.core.instrument.Tags; + +/** + * Provides {@link Tags} for Eureka instance metrics. + * + * @author Wonchul Heo + * @since 4.1.2 + */ +public interface EurekaInstanceTagsProvider { + + Tags eurekaInstanceTags(InstanceInfo instanceInfo); + +} diff --git a/spring-cloud-netflix-eureka-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-netflix-eureka-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000000..14d30bdd0f --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,10 @@ +{ + "properties": [ + { + "name": "eureka.server.metrics.enabled", + "type": "java.lang.Boolean", + "defaultValue": "false", + "description": "Indicates whether the metrics should be enabled for eureka instances." + } + ] +} diff --git a/spring-cloud-netflix-eureka-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/spring-cloud-netflix-eureka-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index e6f8989cd0..ac59ffdca3 100644 --- a/spring-cloud-netflix-eureka-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/spring-cloud-netflix-eureka-server/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1 +1,2 @@ -org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration \ No newline at end of file +org.springframework.cloud.netflix.eureka.server.EurekaServerAutoConfiguration +org.springframework.cloud.netflix.eureka.server.metrics.EurekaInstanceMetricsAutoConfiguration diff --git a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceFixture.java b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceFixture.java new file mode 100644 index 0000000000..738deea18b --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceFixture.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.netflix.eureka.server; + +import com.netflix.appinfo.InstanceInfo; +import com.netflix.appinfo.LeaseInfo; + +public final class EurekaInstanceFixture { + + private EurekaInstanceFixture() { + } + + public static LeaseInfo getLeaseInfo() { + LeaseInfo.Builder leaseBuilder = LeaseInfo.Builder.newBuilder(); + leaseBuilder.setRenewalIntervalInSecs(10); + leaseBuilder.setDurationInSecs(15); + return leaseBuilder.build(); + } + + public static InstanceInfo getInstanceInfo(String appName, String hostName, String instanceId, int port, + LeaseInfo leaseInfo) { + InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(); + builder.setAppName(appName); + builder.setHostName(hostName); + builder.setInstanceId(instanceId); + builder.setPort(port); + builder.setLeaseInfo(leaseInfo); + return builder.build(); + } + +} diff --git a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceMonitorTests.java b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceMonitorTests.java new file mode 100644 index 0000000000..b548c4f6d7 --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceMonitorTests.java @@ -0,0 +1,166 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.netflix.eureka.server; + +import java.util.Map; + +import com.netflix.appinfo.InstanceInfo; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Tags; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.netflix.eureka.server.EurekaInstanceFixture.getInstanceInfo; +import static org.springframework.cloud.netflix.eureka.server.EurekaInstanceFixture.getLeaseInfo; + +/** + * @author Wonchul Heo + */ +@SpringBootTest(classes = EurekaInstanceMonitorTests.Application.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + value = { "spring.application.name=eureka", "eureka.server.metrics.enabled=true" }) +class EurekaInstanceMonitorTests { + + private static final String FOO_APP_NAME = "FOO-APP-NAME"; + + private static final String BAR_APP_NAME = "BAR-APP-NAME"; + + @Autowired + private InstanceRegistry instanceRegistry; + + @Autowired + private MeterRegistry meterRegistry; + + private InstanceInfo fooInstanceInfo; + + private InstanceInfo fooInstanceInfo2; + + private InstanceInfo barInstanceInfo; + + private InstanceInfo barInstanceInfo2; + + @BeforeEach + void setup() { + instanceRegistry.clearRegistry(); + meterRegistry.clear(); + fooInstanceInfo = getInstanceInfo(FOO_APP_NAME, "my-host-name", "my-host-name:8008", 8008, getLeaseInfo()); + fooInstanceInfo2 = getInstanceInfo(FOO_APP_NAME, "my-host-name", "my-host-name:8009", 8009, getLeaseInfo()); + barInstanceInfo = getInstanceInfo(BAR_APP_NAME, "my-host-name", "my-host-name:8010", 8010, getLeaseInfo()); + barInstanceInfo2 = getInstanceInfo(BAR_APP_NAME, "my-host-name", "my-host-name:8011", 8011, getLeaseInfo()); + } + + @Test + void testNoRegistration() { + assertThat(meterRegistry.find("eureka.server.instances").gauge()).isNull(); + } + + @Test + void testMultipleRegistrations() { + instanceRegistry.register(fooInstanceInfo, false); + instanceRegistry.register(fooInstanceInfo2, false); + instanceRegistry.register(barInstanceInfo, false); + + final Map counts = Map.of(tags(fooInstanceInfo), 2L, tags(barInstanceInfo), 1L); + assertEurekaInstance(counts); + } + + @Test + void testPartialDeregistrationAfterMultipleRegistrations() { + instanceRegistry.register(fooInstanceInfo, false); + instanceRegistry.register(fooInstanceInfo2, false); + instanceRegistry.register(barInstanceInfo, false); + instanceRegistry.register(barInstanceInfo2, false); + + instanceRegistry.internalCancel(fooInstanceInfo.getAppName(), fooInstanceInfo.getInstanceId(), false); + + final Map counts = Map.of(tags(fooInstanceInfo), 1L, tags(barInstanceInfo), 2L); + assertEurekaInstance(counts); + } + + @Test + void testPartialDeregistrationAndThenRegistrationAfterMultipleRegistrations() { + instanceRegistry.register(fooInstanceInfo, false); + instanceRegistry.register(fooInstanceInfo2, false); + instanceRegistry.register(barInstanceInfo, false); + instanceRegistry.register(barInstanceInfo2, false); + + instanceRegistry.internalCancel(fooInstanceInfo.getAppName(), fooInstanceInfo.getInstanceId(), false); + instanceRegistry.register(fooInstanceInfo, false); + + final Map counts = Map.of(tags(fooInstanceInfo), 2L, tags(barInstanceInfo), 2L); + assertEurekaInstance(counts); + } + + @Test + void testPartialNonRenewalAfterMultipleRegistrations() { + instanceRegistry.register(fooInstanceInfo, false); + instanceRegistry.register(fooInstanceInfo2, false); + instanceRegistry.register(barInstanceInfo, false); + instanceRegistry.register(barInstanceInfo2, false); + + instanceRegistry.statusUpdate(fooInstanceInfo.getAppName(), fooInstanceInfo.getInstanceId(), + InstanceInfo.InstanceStatus.DOWN, null, false); + + final Map meterRegistryCounts = Map.of(tags(fooInstanceInfo2), 2L); + assertEurekaInstance(meterRegistryCounts); + } + + @Test + void testPartialRenewalAfterMultipleRegistrations() { + instanceRegistry.register(fooInstanceInfo, false); + instanceRegistry.register(fooInstanceInfo2, false); + instanceRegistry.register(barInstanceInfo, false); + instanceRegistry.register(barInstanceInfo2, false); + + instanceRegistry.statusUpdate(fooInstanceInfo.getAppName(), fooInstanceInfo.getInstanceId(), + InstanceInfo.InstanceStatus.DOWN, null, false); + instanceRegistry.statusUpdate(barInstanceInfo2.getAppName(), barInstanceInfo2.getInstanceId(), + InstanceInfo.InstanceStatus.STARTING, null, false); + instanceRegistry.renew(fooInstanceInfo.getAppName(), fooInstanceInfo.getInstanceId(), false); + instanceRegistry.renew(barInstanceInfo2.getAppName(), barInstanceInfo2.getInstanceId(), false); + + final Map counts = Map.of(tags(fooInstanceInfo), 1L, tags(fooInstanceInfo2), 1L, + tags(barInstanceInfo), 1L, tags(barInstanceInfo2), 1L); + assertEurekaInstance(counts); + } + + private static Tags tags(InstanceInfo instanceInfo) { + return Tags.of(Tag.of("application", instanceInfo.getAppName()), + Tag.of("status", instanceInfo.getStatus().name())); + } + + private void assertEurekaInstance(Map meterRegistryCounts) { + meterRegistryCounts.forEach((tags, + count) -> assertThat((long) meterRegistry.get("eureka.server.instances").tags(tags).gauge().value()) + .isEqualTo(count)); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @EnableEurekaServer + protected static class Application { + + } + +} diff --git a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceMonitorWithCustomTagsProviderTests.java b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceMonitorWithCustomTagsProviderTests.java new file mode 100644 index 0000000000..bd86521503 --- /dev/null +++ b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/EurekaInstanceMonitorWithCustomTagsProviderTests.java @@ -0,0 +1,102 @@ +/* + * Copyright 2013-2024 the original author or authors. + * + * 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 + * + * https://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 org.springframework.cloud.netflix.eureka.server; + +import java.util.Map; + +import com.netflix.appinfo.InstanceInfo; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Tags; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.netflix.eureka.server.metrics.EurekaInstanceTagsProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.netflix.eureka.server.EurekaInstanceFixture.getInstanceInfo; +import static org.springframework.cloud.netflix.eureka.server.EurekaInstanceFixture.getLeaseInfo; + +/** + * @author Wonchul Heo + */ +@SpringBootTest(classes = EurekaInstanceMonitorWithCustomTagsProviderTests.Application.class, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + value = { "spring.application.name=eureka", "eureka.server.metrics.enabled=true" }) +class EurekaInstanceMonitorWithCustomTagsProviderTests { + + private static final String APP_NAME = "FOO-APP-NAME"; + + @Autowired + private InstanceRegistry instanceRegistry; + + @Autowired + private MeterRegistry meterRegistry; + + private InstanceInfo firstInstanceInfo; + + private InstanceInfo secondInstanceInfo; + + @BeforeEach + void setup() { + instanceRegistry.clearRegistry(); + meterRegistry.clear(); + firstInstanceInfo = getInstanceInfo(APP_NAME, "my-host-name", "my-host-name:8008", 8008, getLeaseInfo()); + secondInstanceInfo = getInstanceInfo(APP_NAME, "my-host-name", "my-host-name:8009", 8009, getLeaseInfo()); + } + + @Test + void testNoRegistration() { + assertThat(meterRegistry.find("eureka.server.instances").gauge()).isNull(); + } + + @Test + void testMultipleRegistrations() { + instanceRegistry.register(firstInstanceInfo, false); + instanceRegistry.register(secondInstanceInfo, false); + + final Map counts = Map.of(tags(firstInstanceInfo), 1L, tags(secondInstanceInfo), 1L); + assertEurekaInstance(counts); + } + + private static Tags tags(InstanceInfo instanceInfo) { + return Tags.of("port", String.valueOf(instanceInfo.getPort())); + } + + private void assertEurekaInstance(Map meterRegistryCounts) { + meterRegistryCounts.forEach((tags, + count) -> assertThat((long) meterRegistry.get("eureka.server.instances").tags(tags).gauge().value()) + .isEqualTo(count)); + } + + @Configuration(proxyBeanMethods = false) + @EnableAutoConfiguration + @EnableEurekaServer + protected static class Application { + + @Bean + EurekaInstanceTagsProvider customEurekaInstanceTagsProvider() { + return instanceInfo -> Tags.of("port", String.valueOf(instanceInfo.getPort())); + } + + } + +} diff --git a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/InstanceRegistryTests.java b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/InstanceRegistryTests.java index e9a12fcfa4..a2310a3bf7 100644 --- a/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/InstanceRegistryTests.java +++ b/spring-cloud-netflix-eureka-server/src/test/java/org/springframework/cloud/netflix/eureka/server/InstanceRegistryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 the original author or authors. + * Copyright 2016-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,14 +21,12 @@ import com.netflix.appinfo.InstanceInfo; import com.netflix.appinfo.LeaseInfo; -import com.netflix.eureka.registry.PeerAwareInstanceRegistry; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.SpyBean; import org.springframework.cloud.netflix.eureka.server.InstanceRegistryTests.TestApplication; import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceCanceledEvent; import org.springframework.cloud.netflix.eureka.server.event.EurekaInstanceRegisteredEvent; @@ -39,6 +37,8 @@ import org.springframework.context.event.SmartApplicationListener; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.cloud.netflix.eureka.server.EurekaInstanceFixture.getInstanceInfo; +import static org.springframework.cloud.netflix.eureka.server.EurekaInstanceFixture.getLeaseInfo; /** * @author Bartlomiej Slota @@ -56,7 +56,7 @@ class InstanceRegistryTests { private static final int PORT = 8008; - @SpyBean(PeerAwareInstanceRegistry.class) + @Autowired private InstanceRegistry instanceRegistry; @BeforeEach @@ -147,24 +147,6 @@ void testRenew() { assertThat(event2.getInstanceInfo()).isEqualTo(instanceInfo2); } - private LeaseInfo getLeaseInfo() { - LeaseInfo.Builder leaseBuilder = LeaseInfo.Builder.newBuilder(); - leaseBuilder.setRenewalIntervalInSecs(10); - leaseBuilder.setDurationInSecs(15); - return leaseBuilder.build(); - } - - private InstanceInfo getInstanceInfo(String appName, String hostName, String instanceId, int port, - LeaseInfo leaseInfo) { - InstanceInfo.Builder builder = InstanceInfo.Builder.newBuilder(); - builder.setAppName(appName); - builder.setHostName(hostName); - builder.setInstanceId(instanceId); - builder.setPort(port); - builder.setLeaseInfo(leaseInfo); - return builder.build(); - } - @Configuration(proxyBeanMethods = false) @EnableAutoConfiguration @EnableEurekaServer