Skip to content

Commit

Permalink
Merge pull request eclipse-tractusx#624 from catenax-ng/feature/TRI-2…
Browse files Browse the repository at this point in the history
…05-external-dependencies-healthchecks

feat(impl):[TRI-205] external dependencies healtchecks impl
  • Loading branch information
ds-ext-kmassalski authored Nov 13, 2023
2 parents 369470b + d5ac243 commit 2c1d9e1
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 1 deletion.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added
- IRS can now check the readiness of external services. Use the new ``management.health.dependencies.enabled`` config entry to determine if external dependencies health checks should be checked (false by default).
- The map of external services healthcheck endpoints can be configured with ``management.health.dependencies.urls`` property, eg. ``service_name: http://service_name_host/health``

## [4.0.1] - 2023-11-10
### Changed
- Added state `STARTED` as acceptable state to complete the EDC transfer process to be compatible with EDC 0.5.1
Expand Down
8 changes: 8 additions & 0 deletions charts/irs-helm/templates/configmap-spring-app-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,14 @@ data:
{{- if .Values.ess.mockRecursiveEdcAsset }}
mockRecursiveEdcAsset: {{ tpl (.Values.ess.mockRecursiveEdcAsset) . | quote }}
{{- end }}
{{- if .Values.management.health.dependencies.enabled }}
management:
health:
dependencies:
enabled: {{ tpl (.Values.management.health.dependencies.enabled | default "false") . | quote }}
urls:
{{- tpl (toYaml .Values.management.health.dependencies.urls) . | nindent 10 }}
{{- end }}
{{- end }}
apiAllowedBpn: {{ tpl (.Values.bpn | default "") . | quote }}
Expand Down
6 changes: 6 additions & 0 deletions charts/irs-helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ bpn: # BPN for this IRS instance; only users with this BPN are allowed to acces
ingress:
enabled: false

management:
health:
dependencies:
enabled: false # Flag to determine if external service healthcheck endpoints should be checked
urls: {} # Map of services with corresponding healthcheck endpoint url's, example service_name: http://service_name_host.com/health

digitalTwinRegistry:
type: decentral # The type of DTR. This can be either "central" or "decentral". If "decentral", descriptorEndpoint, shellLookupEndpoint and oAuthClientId is not required.
url: # "https://<digital-twin-registry-url>"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/********************************************************************************
* Copyright (c) 2021,2022,2023
* 2022: ZF Friedrichshafen AG
* 2022: ISTOS GmbH
* 2022,2023: Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
* 2022,2023: BOSCH AG
* Copyright (c) 2021,2022,2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available 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.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
package org.eclipse.tractusx.irs.configuration;

import java.util.Map;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

/**
* Config values for external dependencies health URLs
*/
@Component
@ConfigurationProperties(prefix = "management.health.dependencies")
@Data
public class DependenciesHealthConfiguration {

private Map<String, String> urls;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/********************************************************************************
* Copyright (c) 2021,2022,2023
* 2022: ZF Friedrichshafen AG
* 2022: ISTOS GmbH
* 2022,2023: Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
* 2022,2023: BOSCH AG
* Copyright (c) 2021,2022,2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available 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.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
package org.eclipse.tractusx.irs.configuration;

import static org.eclipse.tractusx.irs.configuration.RestTemplateConfig.NO_ERROR_REST_TEMPLATE;

import java.util.Collection;
import java.util.Map;
import java.util.stream.Collectors;

import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.Status;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

/**
* External Dependencies health indicator for Spring actuator
*/
@Component
@Slf4j
@ConditionalOnEnabledHealthIndicator("dependencies")
class DependenciesHealthIndicator implements HealthIndicator {

private final DependenciesHealthConfiguration dependenciesHealthConfiguration;
private final RestTemplate restTemplate;

/* package */ DependenciesHealthIndicator(@Qualifier(NO_ERROR_REST_TEMPLATE) final RestTemplate noErrorRestTemplate,
final DependenciesHealthConfiguration dependenciesHealthConfiguration) {
this.dependenciesHealthConfiguration = dependenciesHealthConfiguration;
this.restTemplate = noErrorRestTemplate;
}

@Override
public Health health() {
final Map<String, Status> details = details();
return Health.status(globalStatus(details.values()))
.withDetails(details)
.build();
}

private Status globalStatus(final Collection<Status> statuses) {
final boolean allDependenciesAreUp = statuses.stream().allMatch(status -> status.equals(Status.UP));
return allDependenciesAreUp ? Status.UP : Status.DOWN;
}

private Map<String, Status> details() {
return dependenciesHealthConfiguration.getUrls()
.entrySet()
.stream()
.map(dependency -> {
final String dependencyName = dependency.getKey();
try {
final String dependencyHealthUrl = dependency.getValue();
final ResponseEntity<Void> health = restTemplate.getForEntity(
dependencyHealthUrl, Void.class);
log.info("Health endpoint URL for {} dependency pinged with status {}.",
dependencyName, health.getStatusCode());
return new ExternalServiceHealthStatus(dependencyName, health.getStatusCode());
} catch (final ResourceAccessException resourceAccessException) {
log.warn("Health endpoint URL for {} dependency is not reachable.", dependencyName);
return new ExternalServiceHealthStatus(dependencyName, Status.UNKNOWN);
}
}).collect(Collectors.toMap(ExternalServiceHealthStatus::getName, ExternalServiceHealthStatus::getStatus));
}

/**
* External Service Status DTO
*/
@Value
@RequiredArgsConstructor
private static final class ExternalServiceHealthStatus {
private final String name;
private final Status status;

private ExternalServiceHealthStatus(final String name, final HttpStatusCode httpStatusCode) {
this.name = name;
this.status = httpStatusCode.is2xxSuccessful() ? Status.UP : Status.DOWN;
}
}

}
3 changes: 3 additions & 0 deletions irs-api/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ management: # Spring management API config, see https://spring.io/guides/gs/cent
enabled: true
readinessstate:
enabled: true
dependencies:
enabled: false
urls: { }
metrics:
distribution:
percentiles-histogram:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/********************************************************************************
* Copyright (c) 2021,2022,2023
* 2022: ZF Friedrichshafen AG
* 2022: ISTOS GmbH
* 2022,2023: Bayerische Motoren Werke Aktiengesellschaft (BMW AG)
* 2022,2023: BOSCH AG
* Copyright (c) 2021,2022,2023 Contributors to the Eclipse Foundation
*
* See the NOTICE file(s) distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Apache License, Version 2.0 which is available 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.
*
* SPDX-License-Identifier: Apache-2.0
********************************************************************************/
package org.eclipse.tractusx.irs.configuration;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import java.util.Map;

import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.Status;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.client.RestTemplate;

class DependenciesHealthIndicatorTest {

private final RestTemplate restTemplate = mock(RestTemplate.class);
private final DependenciesHealthConfiguration dependenciesHealthConfiguration = mock(DependenciesHealthConfiguration.class);

@Test
void shouldReturnStatusUpWhenAllExternalServicesAreUp() {
// given
final DependenciesHealthIndicator dependenciesHealthIndicator = new DependenciesHealthIndicator(restTemplate, dependenciesHealthConfiguration);
final Map<String, String> externalServicesHealthUrls = externalServicesHealthUrls();
when(dependenciesHealthConfiguration.getUrls()).thenReturn(externalServicesHealthUrls);
when(restTemplate.getForEntity(anyString(), eq(Void.class))).thenReturn(ResponseEntity.ok().build());

// when
final Health health = dependenciesHealthIndicator.health();

// then
assertThat(health.getStatus()).isEqualTo(Status.UP);
assertThat(health.getDetails()).hasSameSizeAs(externalServicesHealthUrls);
verify(restTemplate, times(externalServicesHealthUrls.size())).getForEntity(anyString(), eq(Void.class));
}

@Test
void shouldReturnStatusDownWhenAnyExternalServiceIsNotReachable() {
// given
final DependenciesHealthIndicator dependenciesHealthIndicator = new DependenciesHealthIndicator(restTemplate, dependenciesHealthConfiguration);
final Map<String, String> externalServicesHealthUrls = externalServicesHealthUrls();
when(dependenciesHealthConfiguration.getUrls()).thenReturn(externalServicesHealthUrls);
when(restTemplate.getForEntity(anyString(), eq(Void.class))).thenThrow(new ResourceAccessException("Not reachable"))
.thenReturn(ResponseEntity.ok().build());

// when
final Health health = dependenciesHealthIndicator.health();

// then
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).hasSameSizeAs(externalServicesHealthUrls);
verify(restTemplate, times(externalServicesHealthUrls.size())).getForEntity(anyString(), eq(Void.class));
}

@Test
void shouldReturnStatusDownWhenAnyExternalServiceIsDown() {
// given
final DependenciesHealthIndicator dependenciesHealthIndicator = new DependenciesHealthIndicator(restTemplate, dependenciesHealthConfiguration);
final Map<String, String> externalServicesHealthUrls = externalServicesHealthUrls();
when(dependenciesHealthConfiguration.getUrls()).thenReturn(externalServicesHealthUrls);
when(restTemplate.getForEntity(anyString(), eq(Void.class))).thenReturn(ResponseEntity.notFound().build())
.thenReturn(ResponseEntity.ok().build());

// when
final Health health = dependenciesHealthIndicator.health();

// then
assertThat(health.getStatus()).isEqualTo(Status.DOWN);
assertThat(health.getDetails()).hasSameSizeAs(externalServicesHealthUrls);
verify(restTemplate, times(externalServicesHealthUrls.size())).getForEntity(anyString(), eq(Void.class));
}

@NotNull
private static Map<String, String> externalServicesHealthUrls() {
return Map.of(
"service_one", "http://service_one/health",
"service_two", "http://service_two/health",
"service_three", "http://service_three/health");
}
}

0 comments on commit 2c1d9e1

Please sign in to comment.