In coming
저번에 저희는 Observability 와 관련된 기본 개념들을 학습하였는데요.
간단히 요약을 해드리면
Observability 는 오류가 왜 발생했는지 이해하는 것을 목표로 하고 있고, system 의 내부 구조를 모르는 사람마저 근본 원인을 찾을 수 있게 하도록 관측(instrumentation) 하는 것을 의미합니다.
Observability 를 구성하는 가장 중요시되는 요소는 아래와 같습니다.
- Metric
- Trace
- Log
따라서 application 을 운영한다면, 위에 언급된 3가지 요소 data 를 생산할 수 있는 기반을 만들고, observability 를 위한 데이터 저장소로 보내는 것이 중요하죠.
https://huisam.tistory.com/entry/observability
위 내용에 대한 복습이 끝났다면 이번에는 spring boot web application 기반으로 어떻게 observability 를 실현하는지
구체적인 방법에 대해 한번 소개해드리겠습니다 :)
Integration with spring boot
우선 시작하기전에 간단하게 기술 set 부터 소개해드릴게요.
- Web application: Spring Boot
- Observability framework: Opentelemetry
- Metric storage: Prometheus
- Trace storage: Tempo
- Log storage: Loki
- Monitoring UI: Grafana
- Perfomance Test Tool: Locust
Spring Boot Application 기반으로 web server 를 구축하고, Observability 실현을 위해 Opentelemetry java agent 기반으로 관측 데이터(metric,trace,log) 를 추출하여 Observability storage 쪽으로 데이터를 보내고자 합니다.
전체 시스템 흐름도는 아래와 같습니다.
Opentelemetry java agent 를 넣어주게 되면, application 의 bytecode 를 변경하여 proxy 패턴처럼 instrumentation 데이터를 생산하고, export 하는 기능이 완성되게 됩니다.
bytecode 를 조작하는 library 는 bytebuddy 를 사용하고 있으며, 해당 라이브러리를 사용하는 대표적인 예로는 pinpoint 가 있습니다.
어떤식으로 java agent 를 심어주면 될까요?
val agent = configurations.create("agent")
dependencies {
// ... library
agent("io.opentelemetry.javaagent:opentelemetry-javaagent:2.7.0") // 1번
}
jib {
from {
image = "eclipse-temurin:21-jdk"
platforms {
platform {
architecture = "arm64"
os = "linux"
}
}
}
extraDirectories {
paths {
path { // 2번
setFrom(layout.buildDirectory.dir("agent"))
into = "/otelagent"
}
}
}
container {
mainClass = "com.huisam.orderapplication.OrderApplicationKt"
jvmFlags = listOf(
"-javaagent:/otelagent/opentelemetry-javaagent.jar" // 3번
)
}
}
tasks.named("jibDockerBuild").configure {
dependsOn(copyAgent)
}
위 코드를 설명드리면,
- opentelemetry agent 를 받아올 수 잇는 라이브러리명을 지정하여 동적으로 다운로드 받습니다.
- jib 로 docker image 를 만드는 과정에서 agent 에 대한 library path 를 설정하여 binary 를 복사합니다.
- 복사된 binary 가 인식되어, jvm flag 로 java agent 를 명시하여 작동시킵니다.
그렇게 되면 instrumentation(관측) 되는 application 에 대한 준비는 끝났습니다. 나머지는 띄울 때 환경변수를 더 지정하면 되는데요.
docker compose.yml 로 설정된 것을 한번 확인해봅시다.
services:
order-application:
image: order-application:0.0.1-SNAPSHOT
container_name: order-application
environment:
OTEL_SERVICE_NAME: "order-application"
OTEL_RESOURCE_ATTRIBUTES: "service=order-application,env=dev"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector:4317"
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_INSTRUMENTATION_MICROMETER_ENABLED: true
ports:
- "8080:8080"
depends_on:
- postgres-order
- collector
product-application:
image: product-application:0.0.1-SNAPSHOT
container_name: product-application
environment:
OTEL_SERVICE_NAME: "product-application"
OTEL_RESOURCE_ATTRIBUTES: "service=product-application,env=dev"
OTEL_EXPORTER_OTLP_ENDPOINT: "http://collector:4317"
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
OTEL_INSTRUMENTATION_MICROMETER_ENABLED: true
ports:
- "8081:8080"
depends_on:
- postgres-product
- collector
환경변수로는 지정하고자 하는 application 이름과 Opentelemetry collector 의 endpoint 를 지정해주고, 데이터를 보낼 protocol 을 정의하기만 하면 됩니다.
- Opentelemetry collector 는 데이터를 수집하고, 주어진 송/수신 정책에 따라 Observability storage 로 데이터를 보내는 역할을 하고 있습니다.
- Protocol 은 크게 2가지가 있으며 https, grpc 중에서 하나를 골라 지정하면 됩니다.
Opentelemetry collector 에 대한 자세한 내용은 아래 링크를 참고해주세요!
https://opentelemetry.io/docs/collector/
Opentelemetry Collector 는 아래와 같이 설정값을 넣습니다.
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
# batch metrics before sending to reduce API usage
batch:
send_batch_max_size: 1000
send_batch_size: 100
timeout: 10s
exporters:
prometheusremotewrite:
endpoint: "http://prometheus:9090/api/v1/write"
prometheus:
endpoint: "0.0.0.0:8889"
enable_open_metrics: true
otlp/tempo:
endpoint: "http://tempo:4317"
tls:
insecure: true
loki:
endpoint: "http://loki:3100/loki/api/v1/push"
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheusremotewrite]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/tempo]
logs:
receivers: [otlp]
processors: [batch]
exporters: [loki]
10초의 주기로 prometheus, tempo, loki 저장소에 보내는 설정값을 추가하고, pipeline 을 지정하여 데이터가 저장될 수 있도록 정의하였습니다. 디테일한 설명은 아래와 같습니다.
- receiver: 데이터를 수신 받을 endpoint
- processor: 데이터를 가공하는 방법 및 주기
- exporter: 데이터를 저장소 쪽으로 송신할 endpoint 및 설정
실제 저장소 설정에 대한 소개는 추가적으로 드리지는 않겠습니다.
instrumentation(관측) 데이터를 전송하는 pipeline 이 완성되었고, 이제 UI 상으로 확인하는 일만 남았는데요.
이전 글에 소개되었던 Correlation 을 활용하여 어떻게 각 관측 데이터를 연결지어 볼 수 있는지 한번 알아볼게요.
apiVersion: 1
datasources:
- name: Prometheus
// ...
jsonData:
httpMethod: GET
exemplarTraceIdDestinations: // 1번 metric to trace
- datasourceUid: tempo
name: trace_id
- name: Loki
// ...
jsonData:
derivedFields: // 2번 log to trace
- datasourceUid: tempo
matcherRegex: '"traceid":"(\w+)"'
url: '$${__value.raw}'
name: traceId
- name: Tempo
// ...
jsonData:
httpMethod: GET
tracesToMetrics: // 3번 trace to metric
datasourceUid: prometheus
tags: [ { key: 'service.name', value: 'job' }, { key: 'method' }, { key: 'uri' }, { key: 'outcome' }, { key: 'status' }, { key: 'exception' } ]
queries:
- name: 'Requests'
query: 'sum(rate(http_server_requests_seconds_count{$$__tags}[10m]))'
spanStartTimeShift: '-10m'
spanEndTimeShift: '10m'
serviceMap:
datasourceUid: prometheus
nodeGraph:
enabled: true
tracesToLogsV2: // 4번 trace to log
datasourceUid: loki
spanStartTimeShift: '-1h'
spanEndTimeShift: '1h'
filterByTraceID: true
filterBySpanID: true
tags: [ { key: 'service.name', value: 'job' } ]
되게 길죠? ㅎㅎ
주석 처리한 항목에 대해서만 조금 더 deep 하게 봐보죠
(1) Metric to Trace
grafana 에서 metric 데이터를 한번 찾으러 가봅시다
위에서 확인해보면 time x 축 기반으로 시간의 흐름으로 구성되어 있는데요. 특정 시간의 trace 를 찾아낼 수 있도록 sampling 을 해주기 때문에 찾아볼 수 있게 되었네요.
exemplarTraceIdDestinations 에서 설정해주었기 떄문에 sampling 되었다고 볼 수 있습니다.
그래서 metric 을 보는 화면에서 시간 데이터를 근거로 특정 시간의 trace 데이터를 찾을 수 있게 되었습니다~!
(2) Log to Trace
또 grafana 에 접근해서 이번에는 log 데이터를 확인해봅시다
시간 기반으로 로그 데이터가 노출되는 것을 볼 수 있는데요. 로그항목을 조금 더 상세히 들어가보면
위 로그 데이터를 클릭을 해볼 수 있는데요. 클릭하게 되면?
이렇게 기본 field 정보와 위에서 yaml 에 명시된 derrivedFields 기반으로 traceId 를 가져오는 것을 볼 수 있습니다.
당연하게도 log 데이터에 trace context 정보들이 포함되도록 해놓았기 때문에 둘간의 연결고리를 찾게 되어 찾을 수 있게 되는 것이죠.
(3) Trace to Metric
먼저 grafana 에 접근해서 trace 데이터를 확인해보아야 합니다.
그러면 위와 같이 특정 trace 에서 링크를 제공할 수 있게 되었는데요. 위 requests 버튼을 누르면?
위와 같이 특정 시간 전후로 요청양을 확인해볼 수 있습니다. 이는 trace 정보로부터 앞뒤 시간을 정의하여 찾을 수 있게 되는 것이죠.
traceToMetrics 설정하에 정의되어 있는 것을 볼 수 있네요.
(4) Trace to Log
다시 trace 데이터에 접근해봅시다
이번에는 Related logs 버튼을 눌러볼 것인데요. 누르게 되면 아래와 같이 나옵니다.
traceId 와 spanId 를 기반으로 loki 에 query 를 하여 특정 시점의 로그를 정확하게 찾을 수 있는 것이죠. 위 trace 에서 하나의 bar 로 구분되어 있는 것이 span 단위라고 이해하면 됩니다.
혹시 span 에 해당하는 로그가 없다면 당연히 찾을 수 없겠죠? ㅎㅎ
해당 설정은 traceToLogsV2 설정으로 확인해볼 수 있으니 참고해주세요 :)
이렇게 해서 최종적으로 내가 운영하고 있는 web application 에서 observability 를 실현할 수 있게 되었네요.
물론 correlation 은 모두 명시하지는 않았지만, 이렇게 각 데이터 간의 연결고리를 활용하여 연결 지어 볼 수 있습니다.
각 데이터를 대표하는 속성은 아래와 같습니다.
- Metric: 시간 단위
- Trace: 요청 단위
- Log: Event 단위(특정 span의 behavior)
Reference
물론 github 에 opensource 로 올려놓았으니 잘 이해가 안된다거나 직접 해보고 싶은 분들은 아래 repository 링크를 참고해주세요~!
https://github.com/huisam/spring-observability
'Developer > Observability' 카테고리의 다른 글
Observability - Concept with grafana example (feat. Prometheus, Tempo, Loki) (0) | 2024.07.29 |
---|