Skip to content

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/traces

Request:

json
{
  "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.enabledfalse(기본값)이면 전송되지 않는다. 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도 같은 배열에 같은 형식으로 담겨 온다 - classNameJDBC/HTTP_CLIENT/REDIS 중 하나가 오면 자동 계측 Span이다 (docs/04-agent-design.md의 "자동 계측: JDBC / RestTemplate / Redis" 절 참고). 같은 traceId가 이미 존재해 Trace 저장이 무시되는 경우(idempotent) Span도 저장하지 않는다.

Response (200 OK):

json
{ "success": true, "message": null }

검증 실패 시 (400 Bad Request):

json
{ "success": false, "message": "invalid trace payload" }

같은 traceId가 이미 존재하면 조용히 무시하고 success: true를 반환한다 (idempotent).

Ingest Logs

POST /api/v1/ingest/logs

Agent가 캡처한 stdout/stderr 로그를 배치로 전송한다 (1회 최대 1000건).

Request:

json
{
  "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로 저장된다.
  • sourcestdout/stderr 중 하나. 그 외 값은 stdout으로 정규화된다.
  • message는 8KB를 초과하면 절단된다.

Response는 Trace ingest와 동일한 { "success": true, "message": null } 형식이다.

Ingest Agent (등록 + Heartbeat)

POST /api/v1/ingest/agents

Agent가 시작 시, 이후 apmx.heartbeat.interval.ms(기본 30초) 주기로 자신의 정보를 보고한다. 같은 agentId가 이미 있으면 보고 필드와 lastSeenTime만 갱신한다(upsert). Collector에서 지정한 표시 이름/그룹은 유지된다.

Request:

json
{
  "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
json
["order-service", "example-app"]

Service Summary

GET /api/v1/services/{serviceName}/summary
json
{
  "serviceName": "order-service",
  "totalCount": 120,
  "avgDurationMs": 42.5,
  "errorCount": 3,
  "errorRate": 0.025
}

데이터가 없는 서비스명이면 404 Not Found.

URI Statistics

GET /api/v1/services/{serviceName}/uris
json
[
  {
    "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=20
  • serviceName: 선택. 없으면 전체 서비스의 Trace를 최신순으로 반환한다.
  • page: 0부터 시작, 기본값 0
  • size: 기본값 20, 최대 200
json
[
  {
    "traceId": "abc123",
    "serviceName": "order-service",
    "method": "GET",
    "uri": "/orders",
    "statusCode": 200,
    "startTime": 1710000000000,
    "durationMs": 150,
    "error": false
  }
]

Trace Detail

GET /api/v1/traces/{traceId}
json
{
  "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=100
  • serviceName: 선택. 없으면 전체 서비스의 로그를 최신순으로 반환한다.
  • level: 선택 (ERROR/WARN/INFO/DEBUG/TRACE 등).
  • page: 0부터 시작, 기본값 0 / size: 기본값 100, 최대 500
json
[
  {
    "serviceName": "order-service",
    "hostName": "was-01",
    "timestamp": 1710000000010,
    "level": "ERROR",
    "source": "stderr",
    "message": "boom"
  }
]

Log Service List

GET /api/v1/logs/services

로그를 보낸 적이 있는 서비스명 목록. Console의 로그 필터 드롭다운에 사용된다.

json
["order-service", "example-app"]

Agent List

GET /api/v1/agents
json
[
  {
    "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:

json
{ "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) 추가