Skip to content

04. Agent Design

목적

Java Agent의 내부 구조, 계측(instrumentation) 방식, 설정 방법을 정의하여 이후 계측 대상을 추가하거나 다른 언어의 Agent를 만들 때 참고할 수 있도록 한다.

범위

agent/java-agent 모듈의 구조와 동작 방식. Collector와의 통신 프로토콜은 docs/07-api-design.md를 참고한다.

설계

패키지 구조 (io.apmx.agent)

text
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/ TraceSenderpublic인 이유와 동일).

계측 대상

HTTP 요청의 시작/종료는 jakarta.servlet.http.HttpServlet#service(ServletRequest, ServletResponse) 하나만 계측한다. Spring MVC의 DispatcherServlet도 결국 HttpServlet을 상속하므로 이 지점 하나로 Servlet 기반 요청을 관측할 수 있다.

그 아래 계층(DB 호출, 외부 HTTP 호출, Redis 호출)은 아래 라이브러리의 정해진 지점을 자동 계측한다. 라이브러리를 쓰기만 하면 별도 코드 수정 없이 Span이 생긴다.

대상계측 지점Span 종류
JDBCPreparedStatement/Statementexecute/executeQuery/executeUpdate/executeLargeUpdateSQL
HTTP ClientRestTemplate#doExecute (모든 public 메서드가 결국 이 지점을 거친다)외부 HTTP 호출
Redisredis.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-apicompileOnly가 아닌 implementation으로 선언하고, shadowJar에서 relocate하지 않은 채 그대로 포함시켰다. Agent jar는 JVM 기동 시 시스템 클래스패스에 올라가므로, 이후 애플리케이션 클래스로더가 같은 타입을 요청하면 부모 우선(parent-first) 위임 규칙에 따라 Agent가 번들한 버전을 그대로 사용하게 된다. ByteBuddy는 반대로 relocate하여 대상 애플리케이션이 별도 버전의 ByteBuddy(예: Mockito가 포함한 버전)를 갖고 있어도 충돌하지 않도록 했다.

데이터 흐름 (요청 1건 기준)

  1. ServletAdvice#onEnter: Trace ID 생성(UUID), 시작 시각/Method/URI/요청 IP(ClientIpResolver) 기록
  2. 대상 애플리케이션의 실제 요청 처리
  3. ServletAdvice#onExit: 종료 시각, 응답 상태 코드, 예외 여부를 확인하고 TraceData 생성 후 TraceSender.offer() 호출 (non-blocking)
  4. 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.nameunknown-serviceTrace에 기록될 서비스명
apmx.collector.urlhttp://localhost:8080Collector 기본 URL. Agent가 내부적으로 /api/v1/ingest/traces를 붙여서 전송한다
apmx.queue.capacity1000내부 Queue 최대 크기
apmx.collector.timeout.ms2000Collector 연결/응답 타임아웃(ms)
apmx.agent.debugfalsetrue일 경우 stderr에 상세 로그 출력
apmx.header.capture.enabledfalsetrue일 경우 요청/응답 헤더를 수집한다 (기본 비활성화 - 오버헤드/민감정보 위험 때문에 옵트인)
apmx.header.exclude(빈 문자열)콤마로 구분한 추가 제외 헤더명 (대소문자 무시). 내장 블랙리스트(Authorization, Cookie, Set-Cookie, Proxy-Authorization, WWW-Authenticate, X-Api-Key, X-Auth-Token)는 이 프로퍼티로도 비활성화할 수 없다
apmx.remote-addr.capture.enabledtruefalse로 설정하면 요청 IP(remoteAddr)를 수집하지 않는다. IP는 개인정보로 분류될 수 있어(AGENTS.md 13.4) 헤더/Span과 동일하게 끌 수 있는 옵트아웃을 제공한다 - 다만 다른 두 기능과 달리 하위 호환을 위해 기본값은 true(기존 동작 유지)다
apmx.query.capture.enabledtruefalse로 설정하면 query string을 수집하지 않는다. 수집 시에도 password/token/secret 등 민감 파라미터 값은 QueryStringRedactor가 항상 ***로 마스킹한다
apmx.log.capture.enabledtruefalse로 설정하면 stdout/stderr 로그 수집을 끈다
apmx.log.queue.capacity5000로그 Queue 최대 크기. 가득 차면 새 로그를 드롭한다
apmx.log.batch.size100로그 배치 전송 시 1회 최대 건수
apmx.agent.id{serviceName}@{hostName}Collector가 Agent를 식별하는 안정적인 ID. 재시작해도 같은 값이 유지되어야 Collector 쪽 이름/그룹 설정이 이어진다
apmx.agent.nameapmx.agent.id실행 시 보고하는 Agent 이름. Collector(Console)에서 표시 이름으로 덮어쓸 수 있다
apmx.heartbeat.interval.ms30000Agent 등록/heartbeat 전송 주기(ms)
apmx.sql.parameter.masking.enabledtruefalse로 설정하면 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(기본값)일 때 SystemStreamCaptureSystem.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 리소스 메트릭 수집

ResourceMetricCollectorScheduledExecutorService(단일 데몬 스레드)로 heartbeat와는 별도로 10초 주기로 JVM 리소스 메트릭 스냅샷을 수집해 메모리에 최신 1건만 들고 있는다. HeartbeatSender는 heartbeat를 보낼 때마다 이 최신 스냅샷을 읽어 resourceMetrics 필드로 동봉한다(최대 10초 정도 오래된 값일 수 있다). 아직 스냅샷이 없으면(기동 직후) null을 보낸다.

java.lang.management.ManagementFactory의 MXBean만 사용한다(리플렉션/외부 라이브러리 없음):

필드MXBean설명
cpuUsagecom.sun.management.OperatingSystemMXBean#getProcessCpuLoad()프로세스 CPU 사용률(0.0~1.0). 값을 못 구하면 -1
heapUsed/heapMaxMemoryMXBean#getHeapMemoryUsage()Heap 사용량/최대치(byte)
threadCountThreadMXBean#getThreadCount()현재 스레드 총 개수
gcCount/gcTimeGarbageCollectorMXBean#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자)에 맞춰 아래처럼 매핑한다.

계측classNamemethodNameargs
JDBCJDBCSQL 첫 키워드(SELECT/INSERT/...)<SQL 텍스트> | params=[...] (1024자로 절단)
HTTP ClientHTTP_CLIENTHTTP Method(GET/POST/...)요청 URL
RedisREDIS커맨드명 대문자(GET/SET/...)ArgumentFormatter로 포맷한 커맨드 인자

JDBC: 표준 java.sql 인터페이스(Connection/PreparedStatement/Statement)를 매칭하므로 드라이버 종류를 가리지 않는다.

  • JdbcPrepareAdviceConnection#prepareStatement(String sql, ...)의 SQL 텍스트를 JdbcStatementRegistry에 반환된 PreparedStatement 인스턴스 키로 기록한다.
  • JdbcParameterAdvicesetXxx(int parameterIndex, ...) 계열(2개 이상 인자, 첫 인자가 int인 메서드)의 바인딩 값을 파라미터 인덱스 순으로 기록한다.
  • JdbcExecuteAdviceexecute/executeQuery/executeUpdate/executeLargeUpdate 호출 시점에 SQL 텍스트(PreparedStatement는 레지스트리에서, 평범한 Statement는 인자로 받은 SQL 문자열에서)와 파라미터를 모아 Span을 만든다. executeBatch는 이번 범위에서 제외했다 (TODO 참고).
  • JdbcStatementRegistryStatement 인스턴스를 키로 쓰는 WeakHashMap 기반이라 별도 정리(cleanup) 없이 Statement가 GC되면 함께 회수된다.

RestTemplate: RestTemplate#doExecute(URI, HttpMethod, RequestCallback, ResponseExtractor) 하나만 계측한다 - getForObject/postForEntity/exchange 등 모든 public 메서드가 내부적으로 이 메서드를 거치므로 이 지점 하나로 충분하다. RestTemplateAdviceHttpMethod 인자를 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()(안전 타입은 값 그대로, 그 외는 클래스명만 - @ApmxTrace Span의 인자 포맷팅과 동일한 유틸리티를 재사용한다)로 바인딩 값을 그대로 기록한다. setNull(int, int)는 타입 코드를 값처럼 오인하지 않도록 항상 NULL 리터럴로 기록한다.

두 옵션 모두 새 유틸리티 클래스를 만드는 대신 기존 query string 마스킹/인자 포맷팅에 쓰던 QueryStringRedactor/ArgumentFormatter를 그대로 재사용한다.

메서드 호출 스택(Span) 계측

common 모듈의 @io.apmx.common.ApmxTrace 어노테이션을 붙인 메서드는 그대로 opt-in 계측 대상이다 - 위 자동 계측(JDBC/RestTemplate/Redis) 대상이 아닌 임의의 커스텀 메서드를 추적하고 싶을 때 사용한다. "전수 계측 금지" 원칙에 따라 패키지/클래스 전체를 무조건 계측하는 기능은 여전히 제공하지 않는다.

  • MethodTraceInstrumentationServletInstrumentation과 동일한 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에 포함해 시스템 클래스패스에 올려두므로, 런타임에는 애플리케이션이 별도로 이 의존성을 갖지 않아도 된다.

예시

bash
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.jar

examples/example-appExampleController#findOrder@ApmxTrace를 붙여 두었다. GET /orders/{orderId} 호출 후 GET /api/v1/traces/{traceId} 응답의 spans 배열에서 확인할 수 있다.

TODO

  • JDBC executeBatch Span 미지원 (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자)의 크기 상한이 현재 코드 상수로 고정되어 있다 - 필요 시 시스템 프로퍼티로 노출하는 방안 검토