04. Agent Design
목적
Java Agent의 내부 구조, 계측(instrumentation) 방식, 설정 방법을 정의하여 이후 계측 대상을 추가하거나 다른 언어의 Agent를 만들 때 참고할 수 있도록 한다.
범위
agent/java-agent 모듈의 구조와 동작 방식. Collector와의 통신 프로토콜은 docs/07-api-design.md를 참고한다.
설계
패키지 구조 (io.apmx.agent)
io.apmx.agent
├── AgentBootstrap premain 진입점
├── config
│ ├── AgentConfig 시스템 프로퍼티 기반 설정
│ └── AgentContext 프로세스 전역에서 읽는 설정 홀더
├── instrument
│ ├── ServletInstrumentation ByteBuddy AgentBuilder 설정 (HTTP 진입점)
│ ├── ServletAdvice 실제 계측 코드 (Advice)
│ ├── ClientIpResolver 요청 IP 판별 (X-Forwarded-For 우선, 없으면 getRemoteAddr())
│ ├── HeaderRedactor 요청/응답 헤더 수집 + 민감 헤더 블랙리스트 redaction
│ ├── QueryStringRedactor Query string 수집 + 민감 파라미터 값 마스킹
│ ├── ContentLengthParser Content-Length 헤더 안전 파싱 (예외 없이 -1 폴백)
│ ├── MethodTraceInstrumentation ByteBuddy AgentBuilder 설정 (@ApmxTrace 메서드)
│ ├── MethodTraceAdvice 실제 계측 코드 (Advice)
│ ├── SpanContext 스레드별 활성 Trace/Span 스택 (ThreadLocal)
│ ├── ArgumentFormatter Span 인자값 안전 포맷팅 (안전 타입만 값, 나머지는 클래스명)
│ ├── JdbcInstrumentation ByteBuddy AgentBuilder 설정 (JDBC 자동 계측 3개 지점)
│ ├── JdbcPrepareAdvice Connection#prepareStatement → SQL 텍스트 기록
│ ├── JdbcParameterAdvice PreparedStatement#setXxx → 바인딩 파라미터 기록(마스킹 적용)
│ ├── JdbcExecuteAdvice Statement/PreparedStatement#execute* → SQL Span 생성
│ ├── JdbcStatementRegistry Statement 인스턴스별 SQL 텍스트/파라미터 보관 (WeakHashMap)
│ ├── JdbcSpanFormatter SQL Span의 className/methodName/args 포맷팅(컬럼 길이 준수)
│ ├── RestTemplateInstrumentation ByteBuddy AgentBuilder 설정 (RestTemplate#doExecute)
│ ├── RestTemplateAdvice 실제 계측 코드 (Advice) → 외부 HTTP 호출 Span 생성
│ ├── RedisInstrumentation ByteBuddy AgentBuilder 설정 (Jedis 커맨드 메서드 목록)
│ └── RedisAdvice 실제 계측 코드 (Advice) → Redis Span 생성
├── resource
│ ├── ResourceMetricCollector ScheduledExecutorService로 10초마다 JVM 리소스 메트릭 수집
│ └── ResourceMetricSnapshot 수집된 메트릭 1건의 DTO (JSON 직렬화 포함)
├── sender
│ ├── TraceSender Trace Queue + 백그라운드 스레드
│ ├── LogSender 로그 Queue + 배치 전송 백그라운드 스레드
│ ├── HeartbeatSender Agent 등록/heartbeat 주기 전송 스레드 (최신 리소스 메트릭 스냅샷 동봉)
│ └── CollectorClient Collector로의 HTTP JSON 전송 (엔드포인트별 인스턴스)
├── trace
│ ├── TraceData 전송용 DTO (수동 JSON 직렬화, Span 목록 포함)
│ ├── SpanData Trace에 중첩되는 Span DTO
│ ├── JsonWriter DTO들이 공유하는 JSON 직렬화 헬퍼
│ ├── RequestStartInfo ServletAdvice enter → exit 사이에 넘기는 임시 데이터
│ └── MethodSpanStart MethodTraceAdvice enter → exit 사이에 넘기는 임시 데이터
├── log
│ ├── SystemStreamCapture System.out/err 를 pass-through 래퍼로 교체
│ ├── LineCapturingOutputStream 원본으로 모든 바이트를 그대로 흘리면서 줄 단위 캡처
│ ├── LogLevelDetector 줄 앞부분의 레벨 토큰 감지 (없으면 stdout=INFO, stderr=ERROR)
│ └── LogRecordData 캡처된 로그 1줄 DTO
└── internal
└── AgentLogger stderr 기반 최소 로거 (대상 앱 로깅 프레임워크에 의존하지 않음)Advice 클래스가 호출하는 모든 헬퍼(ClientIpResolver, HeaderRedactor, QueryStringRedactor, ContentLengthParser, SpanContext, ArgumentFormatter)는 반드시 public이어야 한다. ByteBuddy는 Advice의 바이트코드를 대상 애플리케이션 클래스에 그대로 복사하므로, 헬퍼가 package-private이면 대상 클래스가 다른 패키지에 있을 때 IllegalAccessError가 발생한다 (기존 AgentConfig/AgentContext/ TraceSender가 public인 이유와 동일).
계측 대상
HTTP 요청의 시작/종료는 jakarta.servlet.http.HttpServlet#service(ServletRequest, ServletResponse) 하나만 계측한다. Spring MVC의 DispatcherServlet도 결국 HttpServlet을 상속하므로 이 지점 하나로 Servlet 기반 요청을 관측할 수 있다.
그 아래 계층(DB 호출, 외부 HTTP 호출, Redis 호출)은 아래 라이브러리의 정해진 지점을 자동 계측한다. 라이브러리를 쓰기만 하면 별도 코드 수정 없이 Span이 생긴다.
| 대상 | 계측 지점 | Span 종류 |
|---|---|---|
| JDBC | PreparedStatement/Statement의 execute/executeQuery/executeUpdate/executeLargeUpdate | SQL |
| HTTP Client | RestTemplate#doExecute (모든 public 메서드가 결국 이 지점을 거친다) | 외부 HTTP 호출 |
| Redis | redis.clients.jedis.Jedis의 커맨드 메서드 중 자주 쓰는 일부(get/set/hset 등) | Redis 커맨드 |
여기에 더해 기존의 @ApmxTrace 어노테이션 기반 opt-in 계측(임의의 커스텀 메서드)을 그대로 병행 지원한다. 즉 Span 생성 방식은 "자동계측(라이브러리 hook) + opt-in(커스텀 메서드) 병행"이다 - 기존에는 opt-in만 지원했으나(.claude/decisions.md 2026-07-06 항목), 실제 서비스에서 가장 흔한 병목 지점(DB/외부 API/캐시 호출)까지 매번 어노테이션을 붙여야 하는 부담을 줄이기 위해 자동 계측을 병행하는 것으로 정책을 변경했다(.claude/decisions.md 2026-07-09 항목 참고). 다만 "전수 계측 금지" 원칙 자체는 유지한다 - 각 라이브러리에서 실제로 필요한 훅 지점 하나(또는 소수의 커맨드 메서드)만 선택적으로 계측하고, 라이브러리 전체를 무차별로 감싸지 않는다.
WebClient(리액티브 HTTP Client)와 OkHttp, Lettuce(Redis)는 아직 계측하지 않는다 (TODO 참고).
ByteBuddy의 Advice를 사용해 바이트코드를 대상 클래스에 inline 방식으로 삽입한다 (별도 클래스 로딩 없이 메서드 본문에 직접 복사됨). JDBC/RestTemplate/Redis Advice는 모두 SpanContext(같은 스레드의 활성 HTTP Trace)가 있을 때만 Span을 기록한다 - "Trace 없이 Span 없음" 원칙은 자동 계측에도 동일하게 적용된다.
왜 jakarta.servlet-api를 shaded jar에 그대로 포함했는가
Advice 클래스(ServletAdvice)는 Agent 자신의 클래스로더(시스템 클래스로더)에서 로드되어 바이트코드 검증을 거친다. 이때 ServletRequest/ServletResponse 같은 타입이 클래스패스에 없으면 계측 시점에 NoClassDefFoundError가 발생한다. 따라서 jakarta.servlet-api는 compileOnly가 아닌 implementation으로 선언하고, shadowJar에서 relocate하지 않은 채 그대로 포함시켰다. Agent jar는 JVM 기동 시 시스템 클래스패스에 올라가므로, 이후 애플리케이션 클래스로더가 같은 타입을 요청하면 부모 우선(parent-first) 위임 규칙에 따라 Agent가 번들한 버전을 그대로 사용하게 된다. ByteBuddy는 반대로 relocate하여 대상 애플리케이션이 별도 버전의 ByteBuddy(예: Mockito가 포함한 버전)를 갖고 있어도 충돌하지 않도록 했다.
데이터 흐름 (요청 1건 기준)
ServletAdvice#onEnter: Trace ID 생성(UUID), 시작 시각/Method/URI/요청 IP(ClientIpResolver) 기록- 대상 애플리케이션의 실제 요청 처리
ServletAdvice#onExit: 종료 시각, 응답 상태 코드, 예외 여부를 확인하고TraceData생성 후TraceSender.offer()호출 (non-blocking)TraceSender의 백그라운드 스레드가 Queue에서 꺼내CollectorClient로 HTTP 전송
장애 격리 원칙 구현
TraceSender.offer()는 Queue가 가득 차면 즉시 드롭하고 리턴한다 (블로킹 없음).CollectorClient.send()의 모든 예외는 내부에서 잡아 로깅만 하고 전파하지 않는다.AgentBootstrap.premain()전체가try/catch(Throwable)로 감싸져 있어, Agent 초기화 자체가 실패해도 대상 애플리케이션의main()은 정상적으로 계속 실행된다.- Servlet에서 발생한 예외(
@Advice.Thrown)는 Trace 기록용으로만 사용하고, 원래 흐름대로 다시 던져진다 (Agent가 예외를 삼키지 않음).
장애 격리 검증 (수동 테스트 결과)
Collector 프로세스를 강제 종료한 상태에서 curl http://localhost:9000/hello 호출 시 16ms 내외로 정상 응답(200)을 받는 것을 확인했다. Agent의 전송 실패는 apmx.agent.debug=true 설정 시 stderr 로그로만 남는다.
설정 (JVM 시스템 프로퍼티)
| 프로퍼티 | 기본값 | 설명 |
|---|---|---|
apmx.service.name | unknown-service | Trace에 기록될 서비스명 |
apmx.collector.url | http://localhost:8080 | Collector 기본 URL. Agent가 내부적으로 /api/v1/ingest/traces를 붙여서 전송한다 |
apmx.queue.capacity | 1000 | 내부 Queue 최대 크기 |
apmx.collector.timeout.ms | 2000 | Collector 연결/응답 타임아웃(ms) |
apmx.agent.debug | false | true일 경우 stderr에 상세 로그 출력 |
apmx.header.capture.enabled | false | true일 경우 요청/응답 헤더를 수집한다 (기본 비활성화 - 오버헤드/민감정보 위험 때문에 옵트인) |
apmx.header.exclude | (빈 문자열) | 콤마로 구분한 추가 제외 헤더명 (대소문자 무시). 내장 블랙리스트(Authorization, Cookie, Set-Cookie, Proxy-Authorization, WWW-Authenticate, X-Api-Key, X-Auth-Token)는 이 프로퍼티로도 비활성화할 수 없다 |
apmx.remote-addr.capture.enabled | true | false로 설정하면 요청 IP(remoteAddr)를 수집하지 않는다. IP는 개인정보로 분류될 수 있어(AGENTS.md 13.4) 헤더/Span과 동일하게 끌 수 있는 옵트아웃을 제공한다 - 다만 다른 두 기능과 달리 하위 호환을 위해 기본값은 true(기존 동작 유지)다 |
apmx.query.capture.enabled | true | false로 설정하면 query string을 수집하지 않는다. 수집 시에도 password/token/secret 등 민감 파라미터 값은 QueryStringRedactor가 항상 ***로 마스킹한다 |
apmx.log.capture.enabled | true | false로 설정하면 stdout/stderr 로그 수집을 끈다 |
apmx.log.queue.capacity | 5000 | 로그 Queue 최대 크기. 가득 차면 새 로그를 드롭한다 |
apmx.log.batch.size | 100 | 로그 배치 전송 시 1회 최대 건수 |
apmx.agent.id | {serviceName}@{hostName} | Collector가 Agent를 식별하는 안정적인 ID. 재시작해도 같은 값이 유지되어야 Collector 쪽 이름/그룹 설정이 이어진다 |
apmx.agent.name | apmx.agent.id 값 | 실행 시 보고하는 Agent 이름. Collector(Console)에서 표시 이름으로 덮어쓸 수 있다 |
apmx.heartbeat.interval.ms | 30000 | Agent 등록/heartbeat 전송 주기(ms) |
apmx.sql.parameter.masking.enabled | true | false로 설정하면 JDBC Span의 바인딩 파라미터 값을 마스킹하지 않고 그대로 기록한다. SQL 쿼리문 자체는 이 설정과 무관하게 항상 그대로 기록된다 |
헤더 수집과 민감정보 redaction
apmx.header.capture.enabled=true일 때만 HeaderRedactor가 요청 헤더(ServletAdvice#onEnter)와 응답 헤더(ServletAdvice#onExit)를 수집한다. AGENTS.md 13절("개인정보, 인증 토큰, 쿠키 수집 금지")을 지키기 위해 내장 블랙리스트를 코드 레벨에서 항상 적용하며, 설정으로는 블랙리스트를 늘릴 수만 있다(줄일 수 없음). 큰 객체 그래프를 피하기 위해 헤더 개수(최대 20개)와 값 총 길이(최대 2KB)에도 상한을 둔다.
요청/응답 상세 수집
Trace에는 URI 외에 다음 상세 정보가 함께 기록된다.
queryString: 요청 query string.QueryStringRedactor가 키 이름에password/passwd/pwd/secret/token/auth/apikey/api_key/session/credential이 포함된 파라미터의 값을 항상***로 마스킹하고, 전체 길이를 1KB로 절단한다.requestContentType/requestContentLength: 요청의 Content-Type과 Content-Length (선언되지 않은 경우-1로 전송, Collector가null로 정규화).responseContentType/responseContentLength: 응답의 Content-Type과 Content-Length 헤더.
요청/응답 본문(body)은 절대 수집하지 않는다 (CLAUDE.md Trace Model: No request body, No response body). 상세 수집은 요청 라인/헤더 수준의 메타데이터로 한정한다.
로그 수집 (System.out / System.err 포함)
apmx.log.capture.enabled=true(기본값)일 때 SystemStreamCapture가 System.out/System.err를 pass-through 래퍼(LineCapturingOutputStream)로 교체한다. 대부분의 로깅 프레임워크 (Logback/Log4j의 콘솔 Appender)와 순수 System.out.println 출력이 모두 이 지점을 지나므로 별도의 프레임워크 연동 없이 애플리케이션 로그를 수집할 수 있다.
- 원본 스트림에는 모든 바이트가 그대로 전달된다 - 콘솔 출력은 Agent 유무와 무관하게 동일하다.
- 줄 단위(
\n)로 캡처하며, 한 줄은 8KB로 절단한다. 빈 줄은 무시한다. [apmx-agent]접두어(Agent 자신의 로그)는 캡처하지 않는다 - Collector 전송 실패 로그가 다시 수집되는 피드백 루프 방지.LogLevelDetector가 줄 앞부분(80자)에서ERROR/WARN/INFO/DEBUG/TRACE토큰을 찾고, 없으면 stdout은INFO, stderr는ERROR로 기록한다.LogSender가 별도 Queue(기본 5000)와 백그라운드 데몬 스레드로 최대 100건씩 배치 전송한다. Queue가 가득 차면 드롭한다 - Trace와 동일한 장애 격리 원칙.
Agent 등록과 Heartbeat
HeartbeatSender가 시작 직후, 이후 apmx.heartbeat.interval.ms(기본 30초) 주기로 POST /api/v1/ingest/agents에 Agent 정보를 전송한다: agentId, agentName, serviceName, hostName, language, jvmVersion, pid, agentVersion, startTime, resourceMetrics (아래 "JVM 리소스 메트릭 수집" 참고 - 신규 ingest API를 만드는 대신 기존 heartbeat payload에 얹어서 보낸다).
agentId기본값은{serviceName}@{hostName}으로 재시작 후에도 동일하다. Collector는 이 ID로 upsert하므로 Collector에서 지정한 표시 이름/그룹이 재시작 후에도 유지된다.- 전송 실패는 무시하고 다음 주기에 재시도한다. Collector는 heartbeat가 90초(3회분) 이상 끊긴 Agent를 비활성(inactive)으로 표시한다.
JVM 리소스 메트릭 수집
ResourceMetricCollector가 ScheduledExecutorService(단일 데몬 스레드)로 heartbeat와는 별도로 10초 주기로 JVM 리소스 메트릭 스냅샷을 수집해 메모리에 최신 1건만 들고 있는다. HeartbeatSender는 heartbeat를 보낼 때마다 이 최신 스냅샷을 읽어 resourceMetrics 필드로 동봉한다(최대 10초 정도 오래된 값일 수 있다). 아직 스냅샷이 없으면(기동 직후) null을 보낸다.
java.lang.management.ManagementFactory의 MXBean만 사용한다(리플렉션/외부 라이브러리 없음):
| 필드 | MXBean | 설명 |
|---|---|---|
cpuUsage | com.sun.management.OperatingSystemMXBean#getProcessCpuLoad() | 프로세스 CPU 사용률(0.0~1.0). 값을 못 구하면 -1 |
heapUsed/heapMax | MemoryMXBean#getHeapMemoryUsage() | Heap 사용량/최대치(byte) |
threadCount | ThreadMXBean#getThreadCount() | 현재 스레드 총 개수 |
gcCount/gcTime | GarbageCollectorMXBean#getCollectionCount()/getCollectionTime() | 전체 GC(들)의 누적 실행 횟수/시간(ms) 합계 |
구현 범위(중요): 이번 구현은 위 6개 필드까지만 수집·저장한다. 아래 항목은 "수집 데이터"로 목표에는 포함되지만 이번 구현에는 없으며 TODO로 남긴다.
- Non-Heap Memory 사용량
- Thread 상태별 카운트(RUNNABLE/BLOCKED/WAITING 등) -
ThreadMXBean#getThreadInfo()로 구할 수 있으나 매 10초마다 전체 스레드의ThreadInfo를 순회하는 비용이 있어 이번 범위에서 제외했다. - Thread Pool active/max - Tomcat/Jetty 등 WAS마다 다른 자체 MBean을 통해야 해서 JDK 표준 MXBean만으로는 구할 수 없다. 범용적으로 구하려면 WAS별 어댑터가 필요하므로 범위 밖으로 남긴다.
자동 계측: JDBC / RestTemplate / Redis
세 자동 계측 모두 leaf span으로 기록한다 - MethodTraceAdvice처럼 SpanContext의 스택에 스스로를 push하지는 않고, SpanContext.currentParentSpanId()로 현재 부모만 읽어 Span의 parentSpanId로 사용한다(중첩 호출 추적이 필요 없는 지점이라 스택 관리를 단순화했다).
Span의 className/methodName/args 필드는 @ApmxTrace Span과 동일한 SpanData/span 테이블을 그대로 재사용한다(스키마 변경 없음, docs/06-storage-design.md 참고) - 컬럼 길이 제약(method_name 128자, args 1024자)에 맞춰 아래처럼 매핑한다.
| 계측 | className | methodName | args |
|---|---|---|---|
| JDBC | JDBC | SQL 첫 키워드(SELECT/INSERT/...) | <SQL 텍스트> | params=[...] (1024자로 절단) |
| HTTP Client | HTTP_CLIENT | HTTP Method(GET/POST/...) | 요청 URL |
| Redis | REDIS | 커맨드명 대문자(GET/SET/...) | ArgumentFormatter로 포맷한 커맨드 인자 |
JDBC: 표준 java.sql 인터페이스(Connection/PreparedStatement/Statement)를 매칭하므로 드라이버 종류를 가리지 않는다.
JdbcPrepareAdvice가Connection#prepareStatement(String sql, ...)의 SQL 텍스트를JdbcStatementRegistry에 반환된PreparedStatement인스턴스 키로 기록한다.JdbcParameterAdvice가setXxx(int parameterIndex, ...)계열(2개 이상 인자, 첫 인자가int인 메서드)의 바인딩 값을 파라미터 인덱스 순으로 기록한다.JdbcExecuteAdvice가execute/executeQuery/executeUpdate/executeLargeUpdate호출 시점에 SQL 텍스트(PreparedStatement는 레지스트리에서, 평범한Statement는 인자로 받은 SQL 문자열에서)와 파라미터를 모아 Span을 만든다.executeBatch는 이번 범위에서 제외했다 (TODO 참고).JdbcStatementRegistry는Statement인스턴스를 키로 쓰는WeakHashMap기반이라 별도 정리(cleanup) 없이 Statement가 GC되면 함께 회수된다.
RestTemplate: RestTemplate#doExecute(URI, HttpMethod, RequestCallback, ResponseExtractor) 하나만 계측한다 - getForObject/postForEntity/exchange 등 모든 public 메서드가 내부적으로 이 메서드를 거치므로 이 지점 하나로 충분하다. RestTemplateAdvice는 HttpMethod 인자를 Object로 느슨하게 타입 선언하고 toString()만 사용한다 - Advice 클래스가 Agent 자신의 클래스로더에서 로드되므로(위 "왜 jakarta.servlet-api를..." 절과 동일한 이유), Spring Web 타입을 정확히 선언하면 Agent가 Spring Web을 번들해야 하는데, 이렇게 느슨한 타입으로 우회하면 그 의존성 자체가 필요 없다.
Redis: redis.clients.jedis.Jedis(동기 클라이언트) 클래스의 커맨드 메서드 중 자주 쓰는 것만 골라 계측한다(get/set/hset/lpush/sadd/zadd 등, 전체 목록은 RedisInstrumentation.REDIS_COMMANDS). "Never instrument everything"(CLAUDE.md Java Rules) 원칙에 따라 Jedis의 모든 public 메서드(연결 관리용 close/auth/select 등 포함)를 계측하지 않고, 실제 커맨드로 보이는 메서드만 화이트리스트로 선택했다. RedisAdvice도 @Advice.Origin ("#m")(메서드명 문자열)과 ArgumentFormatter(안전 타입만 값)만 사용해 Jedis 타입을 전혀 참조하지 않으므로, RestTemplate과 마찬가지로 Jedis를 Agent에 번들할 필요가 없다. Lettuce는 아직 계측하지 않는다(TODO).
SQL 파라미터 마스킹
JDBC Span에 기록되는 바인딩 파라미터 값은 apmx.sql.parameter.masking.enabled(기본값 true, 기존 apmx.query.capture.enabled의 query string 마스킹과 동일한 "기본적으로 안전한 값" 기조)에 따라 마스킹 여부가 갈린다. SQL 쿼리문 자체(JdbcSpanFormatter.keyword/전체 SQL 텍스트)는 마스킹 대상이 아니다 - 오직 바인딩 파라미터 값만 대상이다.
true(기본값): 파라미터 값 전체를QueryStringRedactor.MASK("***", 기존 query string 마스킹과 동일한 상수를 재사용한다)로 마스킹한다.false:ArgumentFormatter.formatValue()(안전 타입은 값 그대로, 그 외는 클래스명만 -@ApmxTraceSpan의 인자 포맷팅과 동일한 유틸리티를 재사용한다)로 바인딩 값을 그대로 기록한다.setNull(int, int)는 타입 코드를 값처럼 오인하지 않도록 항상NULL리터럴로 기록한다.
두 옵션 모두 새 유틸리티 클래스를 만드는 대신 기존 query string 마스킹/인자 포맷팅에 쓰던 QueryStringRedactor/ArgumentFormatter를 그대로 재사용한다.
메서드 호출 스택(Span) 계측
common 모듈의 @io.apmx.common.ApmxTrace 어노테이션을 붙인 메서드는 그대로 opt-in 계측 대상이다 - 위 자동 계측(JDBC/RestTemplate/Redis) 대상이 아닌 임의의 커스텀 메서드를 추적하고 싶을 때 사용한다. "전수 계측 금지" 원칙에 따라 패키지/클래스 전체를 무조건 계측하는 기능은 여전히 제공하지 않는다.
MethodTraceInstrumentation은ServletInstrumentation과 동일한AgentBuilder패턴으로@ApmxTrace가 붙은 메서드만MethodTraceAdvice를 적용한다.SpanContext(스레드별 ThreadLocal)가 현재 스레드에 활성 HTTP Trace가 있는지 추적한다. 활성 Trace가 없는 스레드(예: 백그라운드 잡, 스케줄러)에서@ApmxTrace메서드가 호출되면 아무 Span도 기록하지 않고 조용히 통과한다 - "Trace 없이 Span 없음" 원칙.- 중첩 호출은
SpanContext의 스택으로 부모-자식 관계(parentSpanId)를 추적한다. 최상위 Span의 부모는 HTTP Trace의traceId자신이다. ArgumentFormatter가 인자값을 안전하게 축약한다:String/Number/Boolean/Character는 값 그대로(최대 200자로 절단) 기록하고, 그 외 모든 객체(커스텀 클래스, 컬렉션 등)는toString()을 호출하지 않고 클래스명만 기록한다(<com.example.User>형태). 전체 인자 포맷 문자열도 500자로 상한을 둔다.- 대상 애플리케이션은
@ApmxTrace를 사용하려면compileOnly(project(":common"))(또는 퍼블리시된io.apmx:common아티팩트)을 컴파일 의존성으로 추가하면 된다. Agent 자체는implementation(project(":common"))으로 해당 클래스를 shaded jar에 포함해 시스템 클래스패스에 올려두므로, 런타임에는 애플리케이션이 별도로 이 의존성을 갖지 않아도 된다.
예시
java \
-javaagent:agent/java-agent/build/libs/java-agent-0.1.0-SNAPSHOT.jar \
-Dapmx.service.name=example-app \
-Dapmx.collector.url=http://localhost:8080 \
-jar examples/example-app/build/libs/example-app-0.1.0-SNAPSHOT.jarexamples/example-app의 ExampleController#findOrder에 @ApmxTrace를 붙여 두었다. GET /orders/{orderId} 호출 후 GET /api/v1/traces/{traceId} 응답의 spans 배열에서 확인할 수 있다.
TODO
- JDBC
executeBatchSpan 미지원 (execute/executeQuery/executeUpdate/executeLargeUpdate만 계측) - WebClient(리액티브), OkHttp HTTP Client 자동 계측 미지원 (RestTemplate만 지원)
- Lettuce Redis 자동 계측 미지원 (Jedis만 지원)
- JVM 리소스 메트릭 중 Non-Heap 사용량, Thread 상태별 카운트, Thread Pool active/max 미수집 (CPU/Heap/GC/총 스레드 개수만 수집 - 위 "JVM 리소스 메트릭 수집" 절 참고)
resource_metrics조회 API/Console 화면 없음 (Collector에 저장만 하고 조회 API는 아직 없음)- Servlet Filter 계측 방식 추가 검토 (WAR 배포 등 HttpServlet 상속 구조가 아닌 경우 대응)
- Forward/Error 재디스패치(
/error) 시 중복 Trace가 생기는 문제 개선 (예: 예외 발생 시 원본 요청 Trace와 Spring의 에러 페이지 처리(BasicErrorController) Trace가 별도로 2건 기록됨 - 현재는 알려진 제약사항으로만 남김) - Batch 전송(N건 모아서 전송) 지원
- Trace ID를 응답 헤더로 노출해 클라이언트 상관관계 분석 지원
- Span 계측을 스레드 경계를 넘는 비동기 호출(
@Async, 별도 스레드풀)까지 확장하는 방안 검토 (현재는 같은 스레드에서 동기적으로 실행되는 메서드만 지원) HeaderRedactor(헤더 개수 20/총 길이 2KB)와ArgumentFormatter(인자 200자/전체 500자)의 크기 상한이 현재 코드 상수로 고정되어 있다 - 필요 시 시스템 프로퍼티로 노출하는 방안 검토