[NO-ISSUE] Initialize project

This commit is contained in:
woozu-shin
2023-10-10 18:08:44 +09:00
commit 8d078ebb10
25 changed files with 1005 additions and 0 deletions

View File

@@ -0,0 +1,13 @@
package com.myoa.engineering.sample;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class SpringBootRequestResponseLoggingExampleApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootRequestResponseLoggingExampleApplication.class, args);
}
}

View File

@@ -0,0 +1,9 @@
package com.myoa.engineering.sample.configuration;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@Configuration
public class DataSourceConfiguration {
}

View File

@@ -0,0 +1,21 @@
package com.myoa.engineering.sample.configuration;
import com.myoa.engineering.sample.configuration.logging.LoggingInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
private final LoggingInterceptor loggingInterceptor;
public WebConfiguration(LoggingInterceptor loggingInterceptor) {
this.loggingInterceptor = loggingInterceptor;
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loggingInterceptor).addPathPatterns("/api/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
}

View File

@@ -0,0 +1,32 @@
package com.myoa.engineering.sample.configuration.logging;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
import java.util.UUID;
@Slf4j
@Component
public class CustomServletWrappingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
RereadableRequestWrapper wrappedRequest = new RereadableRequestWrapper(request);
ContentCachingResponseWrapper wrappedResponse = new ContentCachingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
wrappedResponse.copyBodyToResponse();
MDC.clear();
}
}

View File

@@ -0,0 +1,115 @@
package com.myoa.engineering.sample.configuration.logging;
import com.myoa.engineering.sample.entity.RequestLog;
import com.myoa.engineering.sample.entity.ResponseLog;
import com.myoa.engineering.sample.service.LogPersistenceService;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingResponseWrapper;
import java.io.IOException;
import java.time.Instant;
@Slf4j
@Component
public class LoggingInterceptor implements HandlerInterceptor {
private final LogPersistenceService logPersistenceService;
public LoggingInterceptor(LogPersistenceService logPersistenceService) {
this.logPersistenceService = logPersistenceService;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (isLoggable(request)) {
RequestLog requestLog = RequestLog.builder()
.httpMethod(request.getMethod())
.uri(request.getRequestURI())
.queryString(getQueryString(request))
.contentType(request.getContentType())
.body(getBodyIfAvailable(request))
.requestedAt(Instant.now())
.traceId(MDC.get("traceId"))
.build();
loggingRequest(requestLog);
logPersistenceService.save(requestLog);
}
return HandlerInterceptor.super.preHandle(request, response, handler);
}
private boolean isLoggable(HttpServletRequest request) {
return request.getDispatcherType() != DispatcherType.ERROR;
}
private String getBodyIfAvailable(HttpServletRequest request) throws IOException {
if (request instanceof RereadableRequestWrapper wrappedRequest) {
if (wrappedRequest.getContentType() != null && wrappedRequest.getContentType().contains(MimeTypeUtils.APPLICATION_JSON_VALUE)) {
return new String(wrappedRequest.getContents());
}
}
return null;
}
private void loggingRequest(RequestLog requestLog) {
log.info("[REQUEST {}] {}{} ({}) [{}]\nbody: {}",
requestLog.getHttpMethod(),
requestLog.getUri(),
requestLog.getQueryString(),
requestLog.getContentType(),
requestLog.getTraceId(),
requestLog.getBody());
}
private String getQueryString(HttpServletRequest request) {
return request.getQueryString() == null || request.getQueryString().isEmpty()
? ""
: "?" + request.getQueryString();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
if (isLoggable()) {
ResponseLog responseLog = ResponseLog.builder()
.httpStatus(response.getStatus())
.contentType(response.getContentType())
.body(getBodyIfAvailable(response))
.respondedAt(Instant.now())
.traceId(MDC.get("traceId"))
.build();
loggingResponse(responseLog);
logPersistenceService.save(responseLog);
}
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
private boolean isLoggable() {
String traceId = MDC.get("traceId");
return traceId != null && traceId.isEmpty() == false;
}
private String getBodyIfAvailable(HttpServletResponse response) {
if (response instanceof ContentCachingResponseWrapper wrappedResponse) {
if (wrappedResponse.getContentType() != null && wrappedResponse.getContentType().contains(MimeTypeUtils.APPLICATION_JSON_VALUE)) {
return new String(wrappedResponse.getContentAsByteArray());
}
}
return null;
}
private void loggingResponse(ResponseLog responseLog) {
log.info("[RESPONSE] {} ({}) [{}]\nbody: {}",
responseLog.getHttpStatus(),
responseLog.getContentType(),
responseLog.getTraceId(),
responseLog.getBody());
}
}

View File

@@ -0,0 +1,59 @@
package com.myoa.engineering.sample.configuration.logging;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
public class RereadableRequestWrapper extends HttpServletRequestWrapper {
private final ByteArrayOutputStream contents = new ByteArrayOutputStream();
public RereadableRequestWrapper(HttpServletRequest request) {
super(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
super.getInputStream().transferTo(contents);
return new RereadableServletInputStream(contents);
}
public byte[] getContents() throws IOException {
return this.getInputStream().readAllBytes();
}
private static class RereadableServletInputStream extends ServletInputStream {
private final ByteArrayInputStream buffer;
public RereadableServletInputStream(final ByteArrayOutputStream contents) {
buffer = new ByteArrayInputStream(contents.toByteArray());
}
@Override
public boolean isFinished() {
return buffer.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener listener) {
throw new RuntimeException("Not implemented");
}
@Override
public int read() throws IOException {
return buffer.read();
}
}
}

View File

@@ -0,0 +1,31 @@
package com.myoa.engineering.sample.controller;
import com.myoa.engineering.sample.dto.TestDataDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@Slf4j
@RestController
@RequestMapping("/api/v1/loggingTest")
public class LoggingTestAPIController {
@GetMapping("/get")
public ResponseEntity<String> get() {
// log.info("[API called] get");
return ResponseEntity.ok("get");
}
@GetMapping("/getWithBody")
public ResponseEntity<TestDataDTO> getWithBody(@RequestBody TestDataDTO dto) {
// log.info("[API called] getWithRequestBody");
return ResponseEntity.ok(dto);
}
@PostMapping("/postWithBody")
public ResponseEntity<TestDataDTO> postWithBody(@RequestBody TestDataDTO dto) {
// log.info("[API called] postWithRequestBody");
return ResponseEntity.ok(dto);
}
}

View File

@@ -0,0 +1,10 @@
package com.myoa.engineering.sample.dto;
import lombok.Data;
@Data
public class TestDataDTO {
private String value1;
private Integer value2;
private Boolean value3;
}

View File

@@ -0,0 +1,21 @@
package com.myoa.engineering.sample.entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.Instant;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
@Setter
public abstract class Auditable {
@CreatedDate
private Instant createdAt;
}

View File

@@ -0,0 +1,29 @@
package com.myoa.engineering.sample.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
import java.time.Instant;
@Getter
@Setter
@NoArgsConstructor
@Builder
@AllArgsConstructor
@Entity
public class RequestLog extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String httpMethod;
private String uri;
private String queryString;
private String contentType;
private String body;
private Instant requestedAt;
private String traceId;
}

View File

@@ -0,0 +1,27 @@
package com.myoa.engineering.sample.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.*;
import java.time.Instant;
@Getter
@Setter
@NoArgsConstructor
@Builder
@AllArgsConstructor
@Entity
public class ResponseLog extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private Integer httpStatus;
private String contentType;
private String body;
private Instant respondedAt;
private String traceId;
}

View File

@@ -0,0 +1,9 @@
package com.myoa.engineering.sample.infra.repository;
import com.myoa.engineering.sample.entity.RequestLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RequestLogRepository extends JpaRepository<RequestLog, Long> {
}

View File

@@ -0,0 +1,9 @@
package com.myoa.engineering.sample.infra.repository;
import com.myoa.engineering.sample.entity.ResponseLog;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ResponseLogRepository extends JpaRepository<ResponseLog, Long> {
}

View File

@@ -0,0 +1,36 @@
package com.myoa.engineering.sample.service;
import com.myoa.engineering.sample.entity.RequestLog;
import com.myoa.engineering.sample.entity.ResponseLog;
import com.myoa.engineering.sample.infra.repository.RequestLogRepository;
import com.myoa.engineering.sample.infra.repository.ResponseLogRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service
public class LogPersistenceService {
private final RequestLogRepository requestLogRepository;
private final ResponseLogRepository responseLogRepository;
public LogPersistenceService(RequestLogRepository requestLogRepository,
ResponseLogRepository responseLogRepository) {
this.requestLogRepository = requestLogRepository;
this.responseLogRepository = responseLogRepository;
}
@Transactional
public void save(RequestLog requestLog) {
requestLogRepository.save(requestLog);
// log.info("requestLog saved. id: {}", requestLog.getId());
}
@Transactional
public void save(ResponseLog responseLog) {
responseLogRepository.save(responseLog);
// log.info("responseLog saved. id: {}", responseLog.getId());
}
}

View File

@@ -0,0 +1,22 @@
server:
port: 20090
spring:
h2:
console:
enabled: true
path: /h2
datasource:
url: jdbc:h2:mem:test
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
properties:
hibernate:
format_sql: true
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql: TRACE

View File

@@ -0,0 +1,13 @@
package com.myoa.engineering.sample;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringBootRequestResponseLoggingExampleApplicationTests {
@Test
void contextLoads() {
}
}

View File

@@ -0,0 +1,29 @@
package com.myoa.engineering.sample.configuration;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.util.AntPathMatcher;
class WebConfigurationTest {
private final AntPathMatcher sut = new AntPathMatcher();
@Test
void test_ant_style_pattern_matching() {
assertTrueAntPattern("/api/**", "/api/v1/loggingTest/get");
assertTrueAntPattern("/api/**", "/api/v1/loggingTest/getWithBody");
assertTrueAntPattern("/api/**", "/api/v1/loggingTest/postWithBody");
}
public void assertTrueAntPattern(String pattern, String assertString) {
Assertions.assertTrue(isMatchedAntStylePattern(pattern, assertString));
}
public void assertFalseAntPattern(String pattern, String assertString) {
Assertions.assertFalse(isMatchedAntStylePattern(pattern, assertString));
}
public boolean isMatchedAntStylePattern(String pattern, String assertString) {
return sut.match(pattern, assertString);
}
}