diff --git a/template-server/src/main/java/de/twomartens/templateservice/actuator/AbstractHealthCheck.java b/template-server/src/main/java/de/twomartens/templateservice/actuator/AbstractHealthCheck.java new file mode 100644 index 0000000..8b5ffb5 --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/actuator/AbstractHealthCheck.java @@ -0,0 +1,68 @@ +package de.twomartens.templateservice.actuator; + +import de.twomartens.templateservice.interceptors.RequestTypeInterceptor; +import de.twomartens.templateservice.interceptors.TraceIdInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.Health; +import org.springframework.boot.actuate.health.HealthIndicator; + +@Slf4j +public abstract class AbstractHealthCheck implements HealthIndicator { + + private static final String DEFAULT_HOST = "localhost"; + private static final String HEALTH_KEY_ENDPOINT = "endpoint"; + + private final String endpoint; + + private final TraceIdInterceptor traceIdInterceptor; + + private final RequestTypeInterceptor requestTypeInterceptor; + + private final int port; + + public AbstractHealthCheck(int port, + TraceIdInterceptor traceIdInterceptor, + RequestTypeInterceptor requestTypeInterceptor) { + this.traceIdInterceptor = traceIdInterceptor; + this.requestTypeInterceptor = requestTypeInterceptor; + this.port = port; + this.endpoint = mkEndpoint(); + } + + @Override + public Health health() { + requestTypeInterceptor.markAsHealthCheck(); + traceIdInterceptor.createNewTraceId(); + Health result; + try { + if (isEndpointAvailable()) { + result = Health.up().withDetail(HEALTH_KEY_ENDPOINT, getEndpoint()).build(); + } else { + result = Health.down().withDetail(HEALTH_KEY_ENDPOINT, getEndpoint()).build(); + } + } catch (Exception e) { + result = Health.down().withDetail(HEALTH_KEY_ENDPOINT, getEndpoint()).withException(e).build(); + } + log.info("health check invoked for '{}' with status '{}'", getMethodName(), result.getStatus()); + return result; + } + + String getHost() { + return DEFAULT_HOST; + } + + int getPort() { + return this.port; + } + + String getEndpoint() { + return this.endpoint; + } + + abstract boolean isEndpointAvailable(); + + abstract String getMethodName(); + + abstract String mkEndpoint(); + +} diff --git a/template-server/src/main/java/de/twomartens/templateservice/actuator/RestHealthCheck.java b/template-server/src/main/java/de/twomartens/templateservice/actuator/RestHealthCheck.java new file mode 100644 index 0000000..608bdca --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/actuator/RestHealthCheck.java @@ -0,0 +1,64 @@ +package de.twomartens.templateservice.actuator; + +import de.twomartens.templateservice.entity.Greeting; +import de.twomartens.templateservice.interceptors.RequestTypeInterceptor; +import de.twomartens.templateservice.interceptors.TraceIdInterceptor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.actuate.health.HealthIndicator; +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; + +import java.util.Optional; + +/** + * A Healtcheck which checks if the rest services are working. + * + * If you have a complex service behind grpc, you should think about an easy greeting or echo service, which only tests + * the network/service stack and not the full application. + * + * The healthcheck will be called by kubernetes to check if the container/pod shuold be in loadbalancing. It is possible + * to have as much healthchecks as you loke. + * + * There should be a healtcheck which is ok not before all data is loaded. + */ +@Slf4j +@Component +public class RestHealthCheck extends AbstractHealthCheck implements HealthIndicator { + + private final RestTemplate restTemplate; + + public RestHealthCheck(ServerProperties serverProperties, + TraceIdInterceptor traceIdInterceptor, + RequestTypeInterceptor requestTypeInterceptor, + RestTemplate restTemplate) { + super(Optional.ofNullable(serverProperties.getPort()).orElse(8080), + traceIdInterceptor, requestTypeInterceptor); + this.restTemplate = restTemplate; + } + + @Override + boolean isEndpointAvailable() { + try { + ResponseEntity result = restTemplate.getForEntity(getEndpoint(), Greeting.class); + Greeting body = result.getBody(); + if (body == null) { + return false; + } + return !body.getMessage().isEmpty(); + } catch (RestClientException e) { + return false; + } + } + + @Override + String getMethodName() { + return "infodb-rest"; + } + + String mkEndpoint() { + return String.format("http://%s:%d/greeting?name=RestHealthCheck", getHost(), getPort()); + } +} diff --git a/template-server/src/main/java/de/twomartens/templateservice/configs/TemplateServiceProperties.java b/template-server/src/main/java/de/twomartens/templateservice/configs/TemplateServiceProperties.java new file mode 100644 index 0000000..9b527a4 --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/configs/TemplateServiceProperties.java @@ -0,0 +1,19 @@ +package de.twomartens.templateservice.configs; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "de.twomartens.templateservice") +public class TemplateServiceProperties { + + private final Template template = new Template(); + + @Data + public static class Template { + + private String greeting; + } +} diff --git a/template-server/src/main/java/de/twomartens/templateservice/configs/WebConfig.java b/template-server/src/main/java/de/twomartens/templateservice/configs/WebConfig.java new file mode 100644 index 0000000..137d0a3 --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/configs/WebConfig.java @@ -0,0 +1,41 @@ +package de.twomartens.templateservice.configs; + +import de.twomartens.templateservice.interceptors.RequestTypeInterceptor; +import de.twomartens.templateservice.interceptors.TraceIdInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.util.CollectionUtils; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.util.ArrayList; +import java.util.List; + +@EnableWebMvc +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(new TraceIdInterceptor()); + registry.addInterceptor(new RequestTypeInterceptor()); + } + + @Bean + public RestTemplate restTemplate() { + RestTemplate restTemplate = new RestTemplate(); + + List interceptors + = restTemplate.getInterceptors(); + if (CollectionUtils.isEmpty(interceptors)) { + interceptors = new ArrayList<>(); + } + interceptors.add(new TraceIdInterceptor()); + interceptors.add(new RequestTypeInterceptor()); + restTemplate.setInterceptors(interceptors); + return restTemplate; + } +} diff --git a/template-server/src/main/java/de/twomartens/templateservice/control/GreetingController.java b/template-server/src/main/java/de/twomartens/templateservice/control/GreetingController.java new file mode 100644 index 0000000..06f530a --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/control/GreetingController.java @@ -0,0 +1,24 @@ +package de.twomartens.templateservice.control; + +import de.twomartens.templateservice.entity.Greeting; +import de.twomartens.templateservice.service.GreetingService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class GreetingController { + + private final GreetingService service; + + GreetingController(GreetingService service) { + this.service = service; + } + + @GetMapping("/greeting") + @ResponseBody + public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) { + return Greeting.builder().message(service.createGreeting(name)).build(); + } +} diff --git a/template-server/src/main/java/de/twomartens/templateservice/entity/Greeting.java b/template-server/src/main/java/de/twomartens/templateservice/entity/Greeting.java new file mode 100644 index 0000000..5c067fa --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/entity/Greeting.java @@ -0,0 +1,16 @@ +package de.twomartens.templateservice.entity; + +import lombok.*; + +@Builder +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) +public class Greeting { + + @NonNull + private final String message; + +} diff --git a/template-server/src/main/java/de/twomartens/templateservice/interceptors/RequestTypeInterceptor.java b/template-server/src/main/java/de/twomartens/templateservice/interceptors/RequestTypeInterceptor.java new file mode 100644 index 0000000..27cebfd --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/interceptors/RequestTypeInterceptor.java @@ -0,0 +1,52 @@ +package de.twomartens.templateservice.interceptors; + +import lombok.NoArgsConstructor; +import org.slf4j.MDC; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * marks requests of certain types like health check or integration test for better logging + */ +@Component +@NoArgsConstructor +public class RequestTypeInterceptor extends HandlerInterceptorAdapter + implements ClientHttpRequestInterceptor { + + private static final String LOGGER_ID = "REQTYPE"; + private static final String HEADER_FIELD_ID = "x-type"; + + public void markAsHealthCheck() { + MDC.put(LOGGER_ID, "HEALTH_CHECK"); + } + + public void markAsIntegrationTest() { + MDC.put(LOGGER_ID, "INTEGRATION_TEST"); + } + + // web client interceptor + @Override + public ClientHttpResponse intercept( + HttpRequest request, + byte[] body, + ClientHttpRequestExecution execution) throws IOException { + request.getHeaders().add(HEADER_FIELD_ID, MDC.get(LOGGER_ID)); + return execution.execute(request, body); + } + + // web server interceptor + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + MDC.put(LOGGER_ID, request.getHeader(HEADER_FIELD_ID)); + return true; + } + +} diff --git a/template-server/src/main/java/de/twomartens/templateservice/interceptors/TraceIdInterceptor.java b/template-server/src/main/java/de/twomartens/templateservice/interceptors/TraceIdInterceptor.java new file mode 100644 index 0000000..b262f35 --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/interceptors/TraceIdInterceptor.java @@ -0,0 +1,87 @@ +package de.twomartens.templateservice.interceptors; + +import lombok.NoArgsConstructor; +import org.slf4j.MDC; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.UUID; + +@Component +@NoArgsConstructor +public class TraceIdInterceptor extends HandlerInterceptorAdapter + implements ClientHttpRequestInterceptor { + + private static final String LOGGER_TRACE_ID = "TraceID"; + private static final String HEADER_FIELD_TRACE_ID = "X-TraceId"; + + /** + * Gets the TraceId from the MDC or create a new one and put it to the MDC + */ + public String getTraceId() { + String traceId = MDC.get(LOGGER_TRACE_ID); + if (traceId == null || traceId.trim().isEmpty()) { + traceId = UUID.randomUUID().toString(); + MDC.put(LOGGER_TRACE_ID, traceId); + } + return traceId; + } + + + /** + * A failsafe method to set a TraceId to the MDC. + * + * @param traceId should be a traceId, if it is null or empty a new one will be created. + * @return the traceId param, or a new one, if it was empty + */ + public String setOrCreateTraceId(String traceId) { + if (traceId == null || traceId.trim().isEmpty()) { + traceId = UUID.randomUUID().toString(); + } + MDC.put(LOGGER_TRACE_ID, traceId); + return traceId; + } + + /** + * Removes the traceId from the MDC. + */ + public void resetTraceId() { + MDC.remove(LOGGER_TRACE_ID); + } + + /** + * Creates a new traceId for this thread. + * + * An old one will be overwritten! + */ + public void createNewTraceId() { + resetTraceId(); + getTraceId(); + } + + // client interceptor web + @Override + public ClientHttpResponse intercept( + HttpRequest request, + byte[] body, + ClientHttpRequestExecution execution) throws IOException { + request.getHeaders().add(HEADER_FIELD_TRACE_ID, getTraceId()); + return execution.execute(request, body); + } + + // server interceptor web + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String traceId = request.getHeader(HEADER_FIELD_TRACE_ID); + setOrCreateTraceId(traceId); + response.addHeader(HEADER_FIELD_TRACE_ID, getTraceId()); + return true; + } +} diff --git a/template-server/src/main/java/de/twomartens/templateservice/service/GreetingService.java b/template-server/src/main/java/de/twomartens/templateservice/service/GreetingService.java new file mode 100644 index 0000000..3c8dd04 --- /dev/null +++ b/template-server/src/main/java/de/twomartens/templateservice/service/GreetingService.java @@ -0,0 +1,30 @@ +package de.twomartens.templateservice.service; + +import de.twomartens.templateservice.configs.TemplateServiceProperties; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class GreetingService { + + private final MeterRegistry meterRegistry; + private final TemplateServiceProperties properties; + private final Counter counter; + + public GreetingService(MeterRegistry meterRegistry, TemplateServiceProperties properties) { + this.meterRegistry = meterRegistry; + this.properties = properties; + counter = meterRegistry.counter("infodb.callCounter"); + } + + public String createGreeting(String name) { + log.info("Create greeting for '{}'", name); + counter.increment(); + meterRegistry.gauge("infodb.nameLength", name.length()); + String greeting = properties.getTemplate().getGreeting(); + return String.format(greeting, name); + } +} diff --git a/template-server/src/main/resources/application.yaml b/template-server/src/main/resources/application.yaml index fe5b003..e24bd43 100644 Binary files a/template-server/src/main/resources/application.yaml and b/template-server/src/main/resources/application.yaml differ