Kotlin + Spring Boot 3.0 + Java 17 LTS 환경에서 Whatap APM을 연동할 일이 생겼다. 이 글에서는 연동 방법과 몇가지 애먹은 부분을 기술한다.
에이전트 설치
wget https://service.whatap.io/agent/whatap.agent.java.tar.gz
tar -zxvf whatap.agent.java.tar.gz
Whatap 공식 문서 곳곳에서 $WHATAP_HOME
라고 되어 있는 것은 .jar 파일이 있는 경로를 말한다.
환경변수로 $WHATAP_HOME
를 설정해도 안먹으니까 주의하자.
이후, [whatap의 프로젝트 페이지 – 관리 – 에이전트 설치]를 참고해서 whatap.conf
를 수정한다.

그리고 whatap.conf
맨 아래에 weaving 항목을 추가한다. Whatap Agent 버전과 프레임워크(Tomcat, Spring Boot, Netty 등)에 따라 값이 달라지는것을 유의하여, 공식 문서를 참고해야 한다. Spring Boot 3.0의 경우에는 아래와 같이 추가한다.
weaving=spring-boot-3.0


JVM 매개변수 추가
이후 -javaagent
매개변수를 추가하여, 원래 프로그램을 실행하는 것 처럼 실행하면 된다. 핵심 부분은 -javaagent
를 whatap agent로 설정하는 것이다. Java 버전이 17 이상일 경우 --add-opens=java.base/java.lang=ALL-UNNAMED
를 추가한다.
java -javaagent:/path/to/whatap/whatap.agent-X.Y.Z.jar -Dwhatap.oname=AgentName --add-opens=java.base/java.lang=ALL-UNNAMED -jar ~~~~
whatap.oname=AgentName
의 AgentName
부분은 사용자가 Agent를 식별할 수 있게끔 수정하면 된다.
플러그인의 실행 로그 확인
.jar 파일이 있는 경로에 logs
이라는 디렉터리가 생긴다. 해당 디렉토리에서 로그를 확인 하면 된다.

플러그인
Java APM은 플러그인 기능을 지원한다. (https://docs.whatap.io/java/script-plugin)
플러그인의 전신은 https://github.com/scouter-project/scouter/blob/master/scouter.agent.java/plugin/readme_kr.md 이다. 개인적으로는 whatap의 문서보다 이쪽이 구조를 이해하기 더 편리했다.
플러그인을 이용하면 Login ID를 설정할 수 있다. Login ID는 트랜잭션 맵에서 사용자를 구별할 때 사용할 수 있다.

또는, 요청 상세 페이지에서 확인할 수 있다. 플러그인에 의해 로그인 ID
가 설정되지 않으면 해당 칸 부분이 아예 보이지 않는다. 그러므로 플러그인에서 데이터를 쏘지 않으면 볼 수 없다.

2025. 02. 15 현재에는 ‘트랜잭션 맵’ 하단에 TX trace가 바로 나오지 않는다. 대신에 맵 범위를 선택하면 나오는 팝업에서 확인할 수 있다.
로그인 ID 플러그인 생성
whatap-agent.jar
가 있는 디렉토리 아래에 plugin
이라는 디렉토리를 만들고, 그 안에 *.x
파일을 만들면 된다. 이 파일은 에이전트 시작시에 컴파일 된다. Syntax에 이상이 있으면 컴파일 단계에서 실패한다. logs
디렉토리의 로그 파일에 관련된 정보가 나오니까 참고하자. 당연하게도, 실행중에는 수정해봤자 영향이 없다.
우리는 /plugin/HttpServiceEnd.x
파일 이름을 사용한다. 파일 이름이 플러그인의 실행 시점을 결정한다 (PointCut와 같다)
Spring Boot 3.0 기준에서 HttpServiceEnd.x
또는 HttpServiceStart.x
에서 Session 관련 함수는 작동하지 않았다. 어떻게 해도 WrSession
객체가 null
로 나왔다. 혹시 Spring에서 에이전트로 데이터를 전달하고 싶으면 getAttritute()
를 사용하자.
@Component
class WhatapInterceptor : HandlerInterceptor {
override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean {
super.preHandle(request, response, handler)
val principal = SecurityContextHolder.getContext().authentication?.principal as Account
request.setAttribute("member", principal.username)
return true
}
}
인터셉터를 등록하기 위해서는 WebMvcConfigurer
를 써야한다.
@EnableWebSecurity
@Configuration
@EnableMethodSecurity
class ApiConfiguration : WebMvcConfigurer {
@Bean
fun authProviderManager(authProvider: SimpleCookieAuthProvider): ProviderManager {
val authManager = ProviderManager(listOf(authProvider))
return authManager
}
...
...
override fun addInterceptors(registry: InterceptorRegistry) {
registry.addInterceptor(WhatapInterceptor()).addPathPatterns("/**")
}
}
중요한건 request.setAttribute
로 값을 저장하는 것이다. 제대로 값이 저장됐다면 아래와 같이 HttpServiceEnd.x
에서 데이터를 받을 수 있다.
// HttpServiceEnd.x (플러그인)
Object userName = $req.getAttribute("userName");
if (userName != null) {
$ctx.login(cString(userName));
}
당연한 말이지만… HttpServiceStart.x
를 하면 요청이 시작되는 순간에 플러그인이 실행된다. 그때는 attribute를 설정할 수 없다.
참고로, whatap에 세션으로 데이터가 안받아진다고 문의 하니까 getParameter()
로 데이터를 땡겨와라고 답변 받았다. 물론 가능은 한데, 그렇게 해야하나 싶은 생각이 들었다. 매 페이지의 주소에 파라메터로 userName
같은걸 넣어야 하는데… 영 꺼림칙 했다.
답글 남기기