Initial commit

This commit is contained in:
2020-07-07 22:43:29 +02:00
commit 22af33852e
30 changed files with 1335 additions and 0 deletions

View File

@ -0,0 +1,62 @@
package de.twomartens.templateservice;
import de.twomartens.templateservice.entity.Greeting;
import de.twomartens.templateservice.interceptors.RequestTypeInterceptor;
import de.twomartens.templateservice.interceptors.TraceIdInterceptor;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.web.client.RestTemplate;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@SpringJUnitConfig
@SpringBootTest(webEnvironment = RANDOM_PORT)
class TemplateServiceRestTests {
@Autowired
private RestTemplate restTemplate;
@LocalServerPort
int randomServerPort;
@Autowired
private RequestTypeInterceptor requestTypeInterceptor;
@Autowired
private TraceIdInterceptor traceIdInterceptor;
@BeforeEach
void setup() {
traceIdInterceptor.createNewTraceId();
requestTypeInterceptor.markAsIntegrationTest();
}
@Test
void testRestService() {
ResponseEntity<Greeting> result = restTemplate.getForEntity(getEndpoint(), Greeting.class);
assertThat(result.getStatusCode().is2xxSuccessful()).isTrue();
assertThat(result.getBody()).isNotNull();
assertThat(result.getBody().getMessage()).isEqualTo("Hello RestCheck!");
}
/*
* This method is so complicated because it uses the local-grpc-server configuration for grpc to create
* a correct link for the rest endpoints in the cloud
*/
private String getEndpoint() {
String hostNameForGrpcServer = "127.0.0.1";
int port = randomServerPort;
String protocol = "http";
return String.format("%s://%s:%d/greeting?name=RestCheck",
protocol,
hostNameForGrpcServer,
port);
}
}

View File

@ -0,0 +1,29 @@
package de.twomartens.templateservice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.Optional;
@Slf4j
@SpringBootApplication
@EnableScheduling
public class TemplateServiceApplication {
public static void main(String[] args) {
log.info(">>> {}:{} <<<", getTitle(), getVersion());
SpringApplication.run(TemplateServiceApplication.class, args);
}
private static String getTitle() {
return Optional.ofNullable(TemplateServiceApplication.class.getPackage().getImplementationTitle())
.orElse("start");
}
private static String getVersion() {
return Optional.ofNullable(TemplateServiceApplication.class.getPackage().getImplementationVersion())
.orElse("snapshot");
}
}

View File

@ -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();
}

View File

@ -0,0 +1,56 @@
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 Health check which checks if the rest services are working.]
*/
@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<Greeting> 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 "template-service-rest";
}
String mkEndpoint() {
return String.format("http://%s:%d/greeting?name=RestHealthCheck", getHost(), getPort());
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,47 @@
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 org.springframework.web.servlet.view.InternalResourceViewResolver;
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 InternalResourceViewResolver defaultViewResolver() {
return new InternalResourceViewResolver();
}
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors
= restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new TraceIdInterceptor());
interceptors.add(new RequestTypeInterceptor());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,18 @@
package de.twomartens.templateservice.entity;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.*;
@Builder
@Getter
@ToString
@EqualsAndHashCode
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE)
public class Greeting {
@NonNull
@Schema(description = "This message is displayed")
private final String message;
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<Configuration status="warn" monitorInterval="30">
<Appenders>
<Console name="console" target="SYSTEM_OUT">
<PatternLayout pattern="%5p %d{HH:mm:ss.SSS} (%F:%L) [%X{REQTYPE}] %m%n%xEx{full}"
alwaysWriteExceptions="true"/>
</Console>
</Appenders>
<Loggers>
<Root level="WARN">
<AppenderRef ref="console"/>
</Root>
<Logger name="de.twomartens" level="TRACE" additivity="true"/>
</Loggers>
</Configuration>

View File

@ -0,0 +1,37 @@
package de.twomartens.templateservice.control;
import de.twomartens.templateservice.service.GreetingService;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
@WebMvcTest(GreetingController.class)
class GreetingControllerTests {
@Autowired
private MockMvc mvc;
@MockBean
private GreetingService service;
@Test
void testGreetingRest() throws Exception {
BDDMockito.given(service.createGreeting("Template"))
.willReturn("Hello Template!");
mvc.perform(MockMvcRequestBuilders.get("/greeting")
.param("name", "Template")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$", Matchers.aMapWithSize(1)))
.andExpect(MockMvcResultMatchers.jsonPath("$.message", Matchers.is("Hello Template!")))
.andReturn();
}
}

View File

@ -0,0 +1,37 @@
package de.twomartens.templateservice.service;
import de.twomartens.templateservice.configs.TemplateServiceProperties;
import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class)
class GreetingServiceTest {
private GreetingService service;
@Mock
private TemplateServiceProperties properties;
@Mock
private TemplateServiceProperties.Template template;
@BeforeEach
void beforeEach() {
given(template.getGreeting()).willReturn("Hello %s");
given(properties.getTemplate()).willReturn(template);
service = new GreetingService(new SimpleMeterRegistry(), properties);
}
@Test
void createGreeting() {
String result = service.createGreeting("Test");
Assertions.assertThat(result).isEqualTo("Hello Test");
}
}

View File

@ -0,0 +1,85 @@
plugins {
id 'com.google.cloud.tools.jib' version '1.8.0' // jib build our docker images
id 'org.springframework.boot' version '2.2.4.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
}
configurations {
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
integrationTestImplementation.extendsFrom testImplementation
}
sourceSets {
integrationTest {
java {
compileClasspath += main.output + test.output
runtimeClasspath += main.output + test.output
srcDir file('src/integration-test/java')
}
resources.srcDir file('src/integration-test/resources')
}
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springframework.boot:spring-boot-starter-websocket'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'org.springdoc:springdoc-openapi-ui:1.3.9'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
// no support for junit 4 Tests
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
// exclude hamcrest, we use asAbbringerFahrtLoeschenTypesertj in unit tests
exclude group: 'org.hamcrest', module: 'hamcrestv'
}
testImplementation 'org.testcontainers:testcontainers:1.12.5'
// add hamcrest only for mockMvcTest
// see https://github.com/spring-projects/spring-framework/issues/21178
testImplementation 'org.hamcrest:hamcrest:2.2'
}
bootJar {
manifest {
attributes 'Implementation-Title': archiveBaseName.get(),
'Implementation-Version': archiveVersion.get(),
'Implementation-Vendor': "Jim Martens"
}
}
task integrationTest(type: Test) {
useJUnitPlatform()
maxHeapSize = "4g"
group = 'verification'
workingDir = rootProject.projectDir
testClassesDirs = sourceSets.integrationTest.output.classesDirs
classpath = sourceSets.integrationTest.runtimeClasspath
}
tasks.jib.dependsOn(build)
tasks.jibDockerBuild.dependsOn(build)
jib {
from {
image = 'amazoncorretto:11'
}
to {
image = '2martens/oparl'
tags = ['latest', getProperty('version').toString().replace('+', '-')]
}
container {
jvmFlags = ['-Xms1g', '-Xmx1g', '-Dlog4j.configurationFile=log4j2-docker.xml']
mainClass = "org.springframework.boot.loader.JarLauncher"
// 8080 spring boot web, 8081 actuators (prometheus, healthchecks, ...), 9090 grpc
ports = ['8080', '8081', '9090']
}
containerizingMode = 'packaged'
}
tasks.build.dependsOn(integrationTest)
tasks.integrationTest.mustRunAfter(test)