Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ dependencies {
implementation 'org.hibernate.validator:hibernate-validator'
implementation 'com.amazonaws:aws-java-sdk-s3:1.12.782'
implementation 'org.springframework.boot:spring-boot-starter-websocket'

// Database Proxy
implementation 'net.ttddyy:datasource-proxy:1.11.0'
implementation 'net.ttddyy.observation:datasource-micrometer:1.2.0'
Comment on lines +72 to +74
Copy link

@coderabbitai coderabbitai bot Jan 1, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

datasource-proxy net.ttddyy latest version Maven Central 2025

💡 Result:

Latest Maven Central version: 1.11.0 (released July 14, 2025).

Maven dependency:

net.ttddyy
datasource-proxy
1.11.0

Sources: [Maven Repository (1.11.0)] [1], [javadoc.io (1.11.0)] [2], [Maven Central index/metadata] [3].

References

  1. https://mvnrepository.com/artifact/net.ttddyy/datasource-proxy/1.11.0
  2. https://javadoc.io/doc/net.ttddyy/datasource-proxy/1.11.0
  3. https://maven-eu.nuxeo.org/nexus/content/groups/public/net/ttddyy/datasource-proxy/

🌐 Web query:

datasource-micrometer net.ttddyy.observation latest version Maven Central 2025

💡 Result:

The latest Maven Central version is net.ttddyy.observation:datasource-micrometer: 2.0.1. [1][2]

Sources:

  • mvnrepository entry for datasource-micrometer 2.0.1. [1]
  • Official docs (installation snippet showing version 2.0.1). [2]

🌐 Web query:

datasource-proxy 1.11.0 security vulnerability CVE

💡 Result:

I found no public CVE that specifically affects the net.ttddyy datasource‑proxy 1.11.0 library itself. Maven listings for datasource‑proxy 1.11.0 show reported vulnerabilities coming from its dependencies (examples listed there include CVE-2025-68161, CVE-2024-38819, CVE-2023-6378), not a direct CVE for datasource‑proxy. [1]

Note: There is a separate, unrelated Grafana "datasource proxy" advisory (CVE-2025-3454) that affects Grafana’s server API — not the net.ttddyy datasource‑proxy Java library. Do not confuse the two. [2]

Sources:

  • Maven / mvnrepository entry for net.ttddyy:datasource-proxy 1.11.0 (vulnerability list). [1]
  • Grafana security advisory (CVE-2025-3454) — unrelated Grafana product issue. [2]

References:
[1] https://mvnrepository.com/artifact/net.ttddyy/datasource-proxy/1.11.0
[2] https://grafana.com/security/security-advisories/cve-2025-3454/


🌐 Web query:

datasource-micrometer 1.2.0 security vulnerability CVE

💡 Result:

I found no public CVE specifically affecting net.ttddyy.observation:datasource-micrometer version 1.2.0. The Maven listing for that artifact/version shows no CVE entry for the artifact itself. [1]

There are unrelated CVEs for other metric libraries (for example CVE-2020-1960 affecting Apache Flink's JMX reporter), but those do not apply to datasource-micrometer. [2][3]

If you want, I can:

  • scan OSS advisories (GitHub Security Advisories, NVD) for any recent updates, or
  • check your SBOM/dependency tree for transitive vulnerabilities that might affect your build. Which would you prefer?

Sources:
[1] mvnrepository — net.ttddyy.observation:datasource-micrometer 1.2.0.
[2] Micrometer security policy / advisories.
[3] NVD / GitLab advisory — CVE-2020-1960 (example of an unrelated metrics CVE).


데이터소스 프록시 의존성이 추가되었습니다. datasource-micrometer 버전 업그레이드를 권장합니다.

변경 내용:

  1. datasource-proxy:1.11.0 - 최신 버전이며, 쿼리 인터셉션을 위한 핵심 프록시 라이브러리입니다.
  2. datasource-micrometer:1.2.0 - 최신 버전은 2.0.1이므로 업그레이드를 고려하세요. Micrometer와의 통합을 위한 라이브러리입니다.

DataSourceProxyConfig 및 QueryMetricsListener와 연동되어 쿼리별 메트릭 수집을 지원합니다. 현재 datasource-micrometer는 1.2.0 사용 중이지만, 2.0.1이 최신 버전으로 이용 가능합니다. 특정 보안 취약점은 발견되지 않았으나, 최신 버전으로 업그레이드하면 개선 사항과 안정성을 함께 얻을 수 있습니다.

🤖 Prompt for AI Agents
In build.gradle around lines 72 to 74, the datasource-micrometer dependency is
pinned to 1.2.0 but should be upgraded to the current 2.0.1; update the
implementation coordinate from
'net.ttddyy.observation:datasource-micrometer:1.2.0' to
'net.ttddyy.observation:datasource-micrometer:2.0.1' and then run a quick build
and tests to verify compatibility (adjust any import or API usages in
DataSourceProxyConfig/QueryMetricsListener if the newer API changed).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

datasource-micrometer 의존성이 내부적으로 datasource-proxy 를 포함하기에, datasource-proxy 의존성은 제거해도 될 듯 합니다.

버전은 1.2.x 그대로 사용하면 될 듯 합니다. 메이저 버전이 1인 경우 마이너 버전 2가 최신이고, 저희 Spring Boot 버전과 호환됩니다. 메이저 버전 2는 Spring Boot 4.x에서 지원하네요. [참고1], [참고2]

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@whqtker 정확한 지적 감사합니다! 제 분석이 잘못되었네요.

말씀하신 내용 정리:

  1. 중복 의존성 제거 필요

    • datasource-micrometer가 내부적으로 datasource-proxy를 포함하고 있어, Line 72의 implementation 'net.ttddyy:datasource-proxy:1.11.0' 라인은 제거해도 됩니다.
  2. 버전 1.2.0이 적절함

    • 현재 프로젝트가 Spring Boot 3.1.5를 사용 중이므로, datasource-micrometer:1.2.0이 올바른 버전입니다.
    • 2.x 버전은 Spring Boot 4.x용이므로 업그레이드하면 안 됩니다.

참고 자료 제공해주셔서 이해가 명확해졌습니다. 중복 의존성을 제거하면 의존성 관리가 더 깔끔해질 것 같습니다!


✏️ Learnings added
Learnt from: whqtker
Repo: solid-connection/solid-connect-server PR: 602
File: build.gradle:72-74
Timestamp: 2026-01-02T08:56:21.107Z
Learning: In the solid-connect-server project using Spring Boot 3.1.5, datasource-micrometer 1.2.x is the correct version. Version 2.x is for Spring Boot 4.x and should not be used with Spring Boot 3.x.

Learnt from: whqtker
Repo: solid-connection/solid-connect-server PR: 602
File: build.gradle:72-74
Timestamp: 2026-01-02T08:56:21.107Z
Learning: The datasource-micrometer dependency (net.ttddyy.observation:datasource-micrometer) internally includes datasource-proxy as a transitive dependency, so explicit datasource-proxy dependency is redundant and should be removed to avoid duplication.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

}

tasks.named('test', Test) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example.solidconnection.common.config.datasource;

import com.example.solidconnection.common.listener.QueryMetricsListener;
import javax.sql.DataSource;
import lombok.RequiredArgsConstructor;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@RequiredArgsConstructor
@Configuration
public class DataSourceProxyConfig {

private final QueryMetricsListener queryMetricsListener;

@Bean
@Primary
public DataSource proxyDataSource(DataSourceProperties props) {
DataSource dataSource = props.initializeDataSourceBuilder().build();

return ProxyDataSourceBuilder
.create(dataSource)
.listener(queryMetricsListener)
.name("main")
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
package com.example.solidconnection.common.config.web;

import com.example.solidconnection.common.filter.HttpLoggingFilter;
import com.example.solidconnection.common.interceptor.ApiPerformanceInterceptor;
import com.example.solidconnection.common.interceptor.RequestContextInterceptor;
import com.example.solidconnection.common.resolver.AuthorizedUserResolver;
import com.example.solidconnection.common.resolver.CustomPageableHandlerMethodArgumentResolver;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
Expand All @@ -14,6 +21,9 @@ public class WebMvcConfig implements WebMvcConfigurer {

private final AuthorizedUserResolver authorizedUserResolver;
private final CustomPageableHandlerMethodArgumentResolver customPageableHandlerMethodArgumentResolver;
private final HttpLoggingFilter httpLoggingFilter;
private final ApiPerformanceInterceptor apiPerformanceInterceptor;
private final RequestContextInterceptor requestContextInterceptor;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
Expand All @@ -22,4 +32,21 @@ public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers)
customPageableHandlerMethodArgumentResolver
));
}

@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(apiPerformanceInterceptor)
.addPathPatterns("/**");

registry.addInterceptor(requestContextInterceptor)
.addPathPatterns("/**");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

필터에서 /actuator 경로에 대해 처리하지 않도록 설정되었고, 필터가 최우선적으로 실행되긴 하지만 인터셉터 등록 시에도 명시적으로 /actuator 경로를 무시하도록 하는 것이 좋을 거 같습니다 !

}

@Bean
public FilterRegistrationBean<HttpLoggingFilter> customHttpLoggingFilter() {
FilterRegistrationBean<HttpLoggingFilter> filterBean = new FilterRegistrationBean<>();
filterBean.setFilter(httpLoggingFilter);
filterBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return filterBean;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import com.example.solidconnection.common.response.ErrorResponse;
import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -24,62 +25,96 @@
public class CustomExceptionHandler {

@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponse> handleCustomException(CustomException ex) {
log.error("커스텀 예외 발생 : {}", ex.getMessage());
protected ResponseEntity<ErrorResponse> handleCustomException(
CustomException ex,
HttpServletRequest request
) {
request.setAttribute("exceptionHandlerLogged", true);
Long userId = (Long) request.getAttribute("userId");
log.error("커스텀 예외 발생 userId : {} msg: {}", userId, ex.getMessage());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외 로그에 userId를 추가로 받도록 하신 이유가 있나요 ?

ErrorResponse errorResponse = new ErrorResponse(ex);
return ResponseEntity
.status(ex.getCode())
.body(errorResponse);
}

@ExceptionHandler(InvalidFormatException.class)
public ResponseEntity<Object> handleInvalidFormatException(InvalidFormatException ex) {
public ResponseEntity<Object> handleInvalidFormatException(
InvalidFormatException ex,
HttpServletRequest request
) {
request.setAttribute("exceptionHandlerLogged", true);
Long userId = (Long) request.getAttribute("userId");
String errorMessage = ex.getValue() + " 은(는) 유효하지 않은 값입니다.";
log.error("JSON 파싱 예외 발생 : {}", errorMessage);
log.error("JSON 파싱 예외 발생 userId: {} msg: {}", userId, errorMessage);
ErrorResponse errorResponse = new ErrorResponse(JSON_PARSING_FAILED, errorMessage);
return ResponseEntity
.status(JSON_PARSING_FAILED.getCode())
.body(errorResponse);
}

@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) {
public ResponseEntity<ErrorResponse> handleValidationExceptions(
MethodArgumentNotValidException ex,
HttpServletRequest request
) {
request.setAttribute("exceptionHandlerLogged", true);
Long userId = (Long) request.getAttribute("userId");

List<String> errors = new ArrayList<>();
ex.getBindingResult()
.getFieldErrors()
.forEach(fieldError -> errors.add(fieldError.getDefaultMessage()));

String errorMessage = errors.toString();
log.error("입력값 검증 예외 발생 : {}", errorMessage);
log.error("입력값 검증 예외 발생 userId : {} msg: {}", userId, errorMessage);
ErrorResponse errorResponse = new ErrorResponse(INVALID_INPUT, errorMessage);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}

@ExceptionHandler(DataIntegrityViolationException.class)
public ResponseEntity<Object> handleDataIntegrityViolationException(DataIntegrityViolationException ex) {
log.error("데이터 무결성 제약조건 위반 예외 발생 : {}", ex.getMessage());
public ResponseEntity<Object> handleDataIntegrityViolationException(
DataIntegrityViolationException ex,
HttpServletRequest request
) {
request.setAttribute("exceptionHandlerLogged", true);
Long userId = (Long) request.getAttribute("userId");

log.error("데이터 무결성 제약조건 위반 예외 발생 userId : {} msg : {}", userId, ex.getMessage());
ErrorResponse errorResponse = new ErrorResponse(DATA_INTEGRITY_VIOLATION, "데이터 무결성 제약조건 위반 예외 발생");
return ResponseEntity
.status(DATA_INTEGRITY_VIOLATION.getCode())
.body(errorResponse);
}

@ExceptionHandler(JwtException.class)
public ResponseEntity<Object> handleJwtException(JwtException ex) {
public ResponseEntity<Object> handleJwtException(
JwtException ex,
HttpServletRequest request
) {
request.setAttribute("exceptionHandlerLogged", true);
Long userId = (Long) request.getAttribute("userId");

String errorMessage = ex.getMessage();
log.error("JWT 예외 발생 : {}", errorMessage);
log.error("JWT 예외 발생 userId : {} msg : {}", userId, errorMessage);
ErrorResponse errorResponse = new ErrorResponse(JWT_EXCEPTION, errorMessage);
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(errorResponse);
}

@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleOtherException(Exception ex) {
public ResponseEntity<Object> handleOtherException(
Exception ex,
HttpServletRequest request
) {
request.setAttribute("exceptionHandlerLogged", true);
Long userId = (Long) request.getAttribute("userId");

String errorMessage = ex.getMessage();
log.error("서버 내부 예외 발생 : {}", errorMessage);
log.error("서버 내부 예외 발생 userId : {} , msg : {}", userId, errorMessage);
ErrorResponse errorResponse = new ErrorResponse(NOT_DEFINED_ERROR, errorMessage);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.example.solidconnection.common.filter;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

@Slf4j
@RequiredArgsConstructor
@Component
public class HttpLoggingFilter extends OncePerRequestFilter {

private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

private static final List<String> EXCLUDE_PATTERNS = List.of(
"/actuator/**"
);

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {

// 1) traceId 부여
String traceId = generateTraceId();
MDC.put("traceId", traceId);

boolean excluded = isExcluded(request);

// 2) 로깅 제외 대상이면 그냥 통과 (traceId는 유지: 추후 하위 레이어 로그에도 붙음)
if (excluded) {
try {
filterChain.doFilter(request, response);
} finally {
MDC.clear();
}
return;
}

printRequestUri(request);

try {
filterChain.doFilter(request, response);

Boolean alreadyExceptionLogging = (Boolean) request.getAttribute("exceptionHandlerLogged");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CustomExceptionHandler에서의 로그와 HttpLoggingFilter에서의 로그 형태가 서로 다른 목적을 가지고 있다고 생각해서 개인적으로 둘 다 있으면 좋을 거 같긴 합니다 ..! 음 근게 이건 다른 분들의 의견도 궁금하긴 하네요

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추가로 exceptionHandlerLogged를 상수로 분리하면 좋을 거 같은데 ... CustomExceptionHandler에도 사용되어서 조금 까다롭네요 ㅠㅠ

if (alreadyExceptionLogging == null || !alreadyExceptionLogging) {
printResponse(request, response);
}

} finally {
MDC.clear();
}
}

private boolean isExcluded(HttpServletRequest req) {
String path = req.getRequestURI();
for (String p : EXCLUDE_PATTERNS) {
if (PATH_MATCHER.match(p, path)) {
return true;
}
}
return false;
}

private String generateTraceId() {
return java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16);
}

private void printResponse(
HttpServletRequest request,
HttpServletResponse response
) {
Long userId = (Long) request.getAttribute("userId");
String uri = buildDecodedRequestUri(request);
HttpStatus status = HttpStatus.valueOf(response.getStatus());

log.info("[RESPONSE] {} userId = {}, ({})", uri, userId, status);
}

private void printRequestUri(HttpServletRequest request) {
String methodType = request.getMethod();
String uri = buildDecodedRequestUri(request);
log.info("[REQUEST] {} {}", methodType, uri);
}

private String decodeQuery(String rawQuery) {
if (rawQuery == null) {
return null;
}
try {
return URLDecoder.decode(rawQuery, StandardCharsets.UTF_8);
} catch (IllegalArgumentException e) {
return rawQuery;
}
}

private String buildDecodedRequestUri(HttpServletRequest request) {
String path = request.getRequestURI();
String query = decodeQuery(request.getQueryString());
return (query == null || query.isBlank()) ? path : path + "?" + query;
}
}
Loading
Loading