All Downloads are FREE. Search and download functionalities are using the official Maven repository.

de.codecentric.boot.admin.server.services.endpoints.QueryIndexEndpointStrategy Maven / Gradle / Ivy

/*
 * Copyright 2014-2019 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
 *
 *     http://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 de.codecentric.boot.admin.server.services.endpoints;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.ClientResponse;
import reactor.core.publisher.Mono;

import de.codecentric.boot.admin.server.domain.entities.Instance;
import de.codecentric.boot.admin.server.domain.values.Endpoint;
import de.codecentric.boot.admin.server.domain.values.Endpoints;
import de.codecentric.boot.admin.server.domain.values.InstanceId;
import de.codecentric.boot.admin.server.domain.values.Registration;
import de.codecentric.boot.admin.server.web.client.InstanceWebClient;

public class QueryIndexEndpointStrategy implements EndpointDetectionStrategy {

	private static final Logger log = LoggerFactory.getLogger(QueryIndexEndpointStrategy.class);

	private final InstanceWebClient instanceWebClient;

	private static final MediaType actuatorMediaType = MediaType.parseMediaType(ActuatorMediaType.V2_JSON);

	public QueryIndexEndpointStrategy(InstanceWebClient instanceWebClient) {
		this.instanceWebClient = instanceWebClient;
	}

	@Override
	public Mono detectEndpoints(Instance instance) {
		Registration registration = instance.getRegistration();
		String managementUrl = registration.getManagementUrl();
		if (managementUrl == null || Objects.equals(registration.getServiceUrl(), managementUrl)) {
			log.debug("Querying actuator-index for instance {} omitted.", instance.getId());
			return Mono.empty();
		}

		return this.instanceWebClient.instance(instance).get().uri(managementUrl).exchange()
				.flatMap(this.convert(instance, managementUrl)).onErrorResume((e) -> {
					log.warn("Querying actuator-index for instance {} on '{}' failed: {}", instance.getId(),
							managementUrl, e.getMessage());
					log.debug("Querying actuator-index for instance {} on '{}' failed.", instance.getId(),
							managementUrl, e);
					return Mono.empty();
				});
	}

	protected Function> convert(Instance instance, String managementUrl) {
		return (response) -> {
			if (!response.statusCode().is2xxSuccessful()) {
				log.debug("Querying actuator-index for instance {} on '{}' failed with status {}.", instance.getId(),
						managementUrl, response.rawStatusCode());
				return response.releaseBody().then(Mono.empty());
			}

			if (!response.headers().contentType().map(actuatorMediaType::isCompatibleWith).orElse(false)) {
				log.debug("Querying actuator-index for instance {} on '{}' failed with incompatible Content-Type '{}'.",
						instance.getId(), managementUrl,
						response.headers().contentType().map(Objects::toString).orElse("(missing)"));
				return response.releaseBody().then(Mono.empty());
			}

			log.debug("Querying actuator-index for instance {} on '{}' successful.", instance.getId(), managementUrl);
			return response.bodyToMono(Response.class).flatMap(this::convertResponse)
					.map(this.alignWithManagementUrl(instance.getId(), managementUrl));
		};
	}

	protected Function alignWithManagementUrl(InstanceId instanceId, String managementUrl) {
		return (endpoints) -> {
			if (!managementUrl.startsWith("https:")) {
				return endpoints;
			}
			if (endpoints.stream().noneMatch((e) -> e.getUrl().startsWith("http:"))) {
				return endpoints;
			}
			log.warn(
					"Endpoints for instance {} queried from {} are falsely using http. Rewritten to https. Consider configuring this instance to use 'server.forward-headers-strategy=native'.",
					instanceId, managementUrl);

			return Endpoints.of(
					endpoints.stream().map((e) -> Endpoint.of(e.getId(), e.getUrl().replaceFirst("http:", "https:")))
							.collect(Collectors.toList()));
		};
	}

	protected Mono convertResponse(Response response) {
		List endpoints = response.getLinks().entrySet().stream()
				.filter((e) -> !e.getKey().equals("self") && !e.getValue().isTemplated())
				.map((e) -> Endpoint.of(e.getKey(), e.getValue().getHref())).collect(Collectors.toList());
		return endpoints.isEmpty() ? Mono.empty() : Mono.just(Endpoints.of(endpoints));
	}

	@Data
	protected static class Response {

		@JsonProperty("_links")
		private Map links = new HashMap<>();

		@Data
		protected static class EndpointRef {

			private final String href;

			private final boolean templated;

			@JsonCreator
			EndpointRef(@JsonProperty("href") String href, @JsonProperty("templated") boolean templated) {
				this.href = href;
				this.templated = templated;
			}

		}

	}

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy