07. API Design
목적
Collector가 제공하는 REST API 계약을 정의한다. Agent(ingest)와 Console/클라이언트(조회)가 모두 이 문서를 기준으로 삼는다.
범위
collector 모듈에 구현된 실제 엔드포인트. AGENTS.md의 초안(8, 9절)을 기준으로 하되, 구현 과정에서 확정된 세부사항(페이징 파라미터, 에러 응답 형식 등)을 반영했다.
설계
공통사항
- Base URL:
http://<collector-host>:<port>(기본 포트 8080) - Content-Type:
application/json - 시간은 epoch milliseconds, duration은 milliseconds
Ingest Trace
POST /api/v1/ingest/tracesRequest:
{
"traceId": "abc123",
"serviceName": "order-service",
"hostName": "was-01",
"language": "java",
"runtime": "spring-boot",
"method": "GET",
"uri": "/orders",
"queryString": "page=2&size=20",
"remoteAddr": "203.0.113.5",
"requestContentType": null,
"requestContentLength": -1,
"responseContentType": "application/json",
"responseContentLength": 512,
"statusCode": 200,
"startTime": 1710000000000,
"endTime": 1710000000150,
"error": false,
"errorMessage": null,
"requestHeaders": { "Accept": "application/json" },
"responseHeaders": { "Content-Type": "application/json" },
"spans": [
{
"spanId": "span-1",
"parentSpanId": "abc123",
"className": "com.example.OrderService",
"methodName": "placeOrder",
"args": "[42]",
"startTime": 1710000000010,
"endTime": 1710000000060,
"error": false,
"errorMessage": null
}
]
}remoteAddr는 선택 필드다(구버전 Agent 호환을 위해 검증 대상 아님, apmx.remote-addr.capture.enabled=false인 Agent가 보내면 null). X-Forwarded-For 헤더가 있으면 첫 번째 IP, 없으면 소켓 레벨 remote address를 사용한다.
requestHeaders/responseHeaders는 선택 필드다. Agent의 apmx.header.capture.enabled가 false(기본값)이면 전송되지 않는다. Authorization/Cookie/Set-Cookie 등 민감 헤더는 Agent에서 이미 제외된 상태로 도착하므로 Collector는 그대로 저장/노출한다.
queryString은 선택 필드다(apmx.query.capture.enabled=false이거나 query string이 없으면 null). Agent가 민감 파라미터(password, token 등) 값을 ***로 마스킹한 뒤 전송한다.
requestContentType/requestContentLength/responseContentType/responseContentLength도 선택 필드다. Content-Length를 알 수 없으면 Agent는 -1을 보내고 Collector는 null로 정규화해 저장한다. 요청/응답 본문(body)은 어떤 경우에도 수집하지 않는다.
spans는 선택 필드다(계측된 지점이 없으면 빈 배열이거나 아예 생략됨). @ApmxTrace로 계측된 커스텀 메서드 외에, JDBC/RestTemplate/Redis 자동 계측 Span도 같은 배열에 같은 형식으로 담겨 온다 - className에 JDBC/HTTP_CLIENT/REDIS 중 하나가 오면 자동 계측 Span이다 (docs/04-agent-design.md의 "자동 계측: JDBC / RestTemplate / Redis" 절 참고). 같은 traceId가 이미 존재해 Trace 저장이 무시되는 경우(idempotent) Span도 저장하지 않는다.
Response (200 OK):
{ "success": true, "message": null }검증 실패 시 (400 Bad Request):
{ "success": false, "message": "invalid trace payload" }같은 traceId가 이미 존재하면 조용히 무시하고 success: true를 반환한다 (idempotent).
Ingest Logs
POST /api/v1/ingest/logsAgent가 캡처한 stdout/stderr 로그를 배치로 전송한다 (1회 최대 1000건).
Request:
{
"serviceName": "order-service",
"hostName": "was-01",
"logs": [
{ "timestamp": 1710000000000, "level": "INFO", "source": "stdout", "message": "order saved id=42" },
{ "timestamp": 1710000000010, "level": "ERROR", "source": "stderr", "message": "boom" }
]
}level은 선택 필드다. 없거나 비어 있으면INFO로 저장된다.source는stdout/stderr중 하나. 그 외 값은stdout으로 정규화된다.message는 8KB를 초과하면 절단된다.
Response는 Trace ingest와 동일한 { "success": true, "message": null } 형식이다.
Ingest Agent (등록 + Heartbeat)
POST /api/v1/ingest/agentsAgent가 시작 시, 이후 apmx.heartbeat.interval.ms(기본 30초) 주기로 자신의 정보를 보고한다. 같은 agentId가 이미 있으면 보고 필드와 lastSeenTime만 갱신한다(upsert). Collector에서 지정한 표시 이름/그룹은 유지된다.
Request:
{
"agentId": "order-service@was-01",
"agentName": "order-was-01",
"serviceName": "order-service",
"hostName": "was-01",
"language": "java",
"jvmVersion": "17.0.10",
"pid": 4242,
"agentVersion": "0.1.0-SNAPSHOT",
"startTime": 1710000000000,
"resourceMetrics": {
"timestamp": 1710000000000,
"cpuUsage": 0.35,
"heapUsed": 134217728,
"heapMax": 536870912,
"threadCount": 42,
"gcCount": 12,
"gcTime": 340
}
}resourceMetrics는 선택 필드다 - Agent가 첫 10초 수집 주기를 아직 마치지 못했으면 null이거나 생략된다(구버전 Agent 호환을 위해 검증 대상 아님). 신규 ingest API를 만들지 않고 기존 heartbeat payload에 얹어서 보내며, docs/04-agent-design.md의 "JVM 리소스 메트릭 수집" 절을 참고한다.
Response는 { "success": true, "message": null } 형식이다.
Service List
GET /api/v1/services["order-service", "example-app"]Service Summary
GET /api/v1/services/{serviceName}/summary{
"serviceName": "order-service",
"totalCount": 120,
"avgDurationMs": 42.5,
"errorCount": 3,
"errorRate": 0.025
}데이터가 없는 서비스명이면 404 Not Found.
URI Statistics
GET /api/v1/services/{serviceName}/uris[
{
"uri": "/orders",
"method": "GET",
"count": 80,
"avgDurationMs": 38.1,
"errorCount": 1,
"errorRate": 0.0125
}
]요청 건수(count) 내림차순으로 정렬된다.
Trace List
GET /api/v1/traces?serviceName=order-service&page=0&size=20serviceName: 선택. 없으면 전체 서비스의 Trace를 최신순으로 반환한다.page: 0부터 시작, 기본값 0size: 기본값 20, 최대 200
[
{
"traceId": "abc123",
"serviceName": "order-service",
"method": "GET",
"uri": "/orders",
"statusCode": 200,
"startTime": 1710000000000,
"durationMs": 150,
"error": false
}
]Trace Detail
GET /api/v1/traces/{traceId}{
"traceId": "abc123",
"serviceName": "order-service",
"hostName": "was-01",
"language": "java",
"runtime": "spring-boot",
"method": "GET",
"uri": "/orders",
"queryString": "page=2&size=20",
"remoteAddr": "203.0.113.5",
"requestContentType": null,
"requestContentLength": null,
"responseContentType": "application/json",
"responseContentLength": 512,
"statusCode": 200,
"startTime": 1710000000000,
"endTime": 1710000000150,
"durationMs": 150,
"error": false,
"errorMessage": null,
"requestHeaders": { "Accept": "application/json" },
"responseHeaders": { "Content-Type": "application/json" },
"spans": [
{
"spanId": "span-1",
"parentSpanId": "abc123",
"className": "com.example.OrderService",
"methodName": "placeOrder",
"args": "[42]",
"startTime": 1710000000010,
"endTime": 1710000000060,
"durationMs": 50,
"error": false,
"errorMessage": null
}
]
}존재하지 않는 traceId이면 404 Not Found.
Log List
GET /api/v1/logs?serviceName=order-service&level=ERROR&page=0&size=100serviceName: 선택. 없으면 전체 서비스의 로그를 최신순으로 반환한다.level: 선택 (ERROR/WARN/INFO/DEBUG/TRACE등).page: 0부터 시작, 기본값 0 /size: 기본값 100, 최대 500
[
{
"serviceName": "order-service",
"hostName": "was-01",
"timestamp": 1710000000010,
"level": "ERROR",
"source": "stderr",
"message": "boom"
}
]Log Service List
GET /api/v1/logs/services로그를 보낸 적이 있는 서비스명 목록. Console의 로그 필터 드롭다운에 사용된다.
["order-service", "example-app"]Agent List
GET /api/v1/agents[
{
"agentId": "order-service@was-01",
"name": "결제 WAS 1호기",
"agentName": "order-was-01",
"displayName": "결제 WAS 1호기",
"groupName": "prod",
"serviceName": "order-service",
"hostName": "was-01",
"language": "java",
"jvmVersion": "17.0.10",
"pid": 4242,
"agentVersion": "0.1.0-SNAPSHOT",
"startTime": 1710000000000,
"lastSeenTime": 1710000030000,
"active": true
}
]name: 유효 이름.displayName(Collector 오버라이드)이 있으면 그 값, 없으면agentName(Agent가 실행 시 보고한 이름).active: 마지막 heartbeat가 90초 이내면true.
Agent Update (이름/그룹 변경)
PATCH /api/v1/agents/{agentId}Request:
{ "displayName": "결제 WAS 1호기", "groupName": "prod" }두 필드 모두 "원하는 오버라이드 상태 전체"를 보낸다. null 또는 빈 문자열은 오버라이드 해제를 의미한다 (displayName 해제 시 Agent가 보고한 이름으로 되돌아가고, groupName 해제 시 그룹 없음으로 표시된다). 응답은 갱신된 Agent 1건(Agent List와 같은 형식). 존재하지 않는 agentId이면 404 Not Found.
예시
README.md의 "로컬 실행" 절에 curl 예시가 포함되어 있다.
TODO
- 인증/인가 (현재 MVP는 무인증)
- Batch ingest API
- 조회 API에 시간 범위 필터(
from/to) 추가