OpenTelemetry to Jaeger, Grafana, ELK¶
OpenTelemetry에는 다양한 오픈 소스 조합 방식이 있습니다. 세 가지 플랫폼/아키텍처를 통해 OpenTelemetry가 서로 다른 기술 아키텍처에서 어떻게 배포되는지 각각 소개하고 시연합니다.
OpenTelemetry¶
OTEL은 OpenTelemetry의 약칭으로, CNCF의 관측 가능성 프로젝트입니다. 관측 가능성 영역의 표준화 방안을 제공하여 관측 데이터의 데이터 모델, 수집, 처리, 내보내기 등의 표준화 문제를 해결하고, 특정 벤더에 종속되지 않는 서비스를 제공하는 것을 목표로 합니다.
OpenTelemetry는 트레이스, 메트릭, 로그 등 관측 데이터(향후 새로운 관측 데이터 유형이 등장할 수 있음)를 관리하기 위한 표준 및 도구의 집합입니다. 현재 업계 표준으로 자리 잡았습니다.
OTLP¶
OTLP(OpenTelemetry Protocol)는 OpenTelemetry 네이티브 텔레메트리 신호 전송 프로토콜입니다. OpenTelemetry 프로젝트의 구성 요소가 Zipkin v2 또는 Jaeger Thrift 프로토콜 형식을 지원하지만, 이는 모두 서드파티 기여 라이브러리 형태로 제공됩니다. OTLP만이 OpenTelemetry 공식에서 네이티브로 지원하는 형식입니다. OTLP의 데이터 모델 정의는 ProtoBuf를 기반으로 완성되었습니다. OTLP 텔레메트리 데이터를 수집할 수 있는 백엔드 서비스를 구현해야 한다면 해당 내용을 이해해야 하며, 관련 코드 저장소는 opentelemetry-proto(https://github.com/open-telemetry/opentelemetry-proto)를 참고하시기 바랍니다.
OpenTelemetry-Collector¶
OpenTelemetry Collector(이하 "otel-collector")는 텔레메트리 데이터를 수신, 처리 및 내보내는 방법에 대해 벤더에 종속되지 않는 구현을 제공합니다. Jaeger, Prometheus 등과 같은 오픈 소스 관측 가능성 데이터 형식을 하나 이상의 오픈 소스 또는 상용 백엔드로 전송하기 위해 여러 에이전트/컬렉터를 실행, 운영 및 유지 관리할 필요가 없습니다. 또한 컬렉터는 최종 사용자가 자신의 데이터를 제어할 수 있도록 합니다. 컬렉터는 기본 위치에서 계측 라이브러리가 텔레메트리 데이터를 내보내는 곳입니다.
OpenTelemetry-Java¶
OpenTelemetry를 기반으로 Java 언어로 개발된 SDK로, 다양한 익스포터를 통해 데이터를 여러 관측 플랫폼으로 푸시할 수 있습니다.
OpenTelemetry-JS¶
OpenTelemetry에서 출시한 프론트엔드 JS 기반 분산 추적입니다.
아키텍처¶
아키텍처 설명¶
-
애플리케이션 서버와 클라이언트는 메트릭, 트레이스 데이터를 otlp-exporter를 통해 otel-collector로 푸시합니다.
-
front-app은 프론트엔드 트레이스로, 트레이스 정보를 otel-collector로 푸시하고 애플리케이션 서비스 API에 접근합니다.
-
otel-collector는 데이터를 수집 및 변환한 후 Jaeger, Zipkin으로 데이터를 푸시합니다.
-
동시에 Prometheus는 otel-collector에서 데이터를 풀합니다.
로그 전송 방식은 두 가지입니다.
방식 1: OTLP를 통한 로그 전송
애플리케이션 서버와 클라이언트는 로그를 otlp-exporter를 통해 otel-collector로 푸시한 후, otel-collector의 익스포터를 통해 Elasticsearch로 전송합니다. OpenTelemetry 로그 부분은 아직 안정적이지 않으므로, 로그는 otel-collector를 거치지 않고 별도로 처리하는 것을 권장합니다. 테스트 과정에서 로그와 메트릭을 동시에 구성하면 otel-collector에서 충돌 문제가 발생하는 것을 확인했으며, 이는 공식 수정을 기다려야 합니다.
방식 2: Logback-logstash를 통한 로그 전송
애플리케이션 서버와 클라이언트는 로그를 Logback-logstash를 통해 logstash로 푸시합니다.
otel-collector는 4개의 익스포터를 구성했습니다.
prometheus:
endpoint: "0.0.0.0:8889"
const_labels:
label1: value1
zipkin:
endpoint: "http://otel_collector_zipkin:9411/api/v2/spans"
format: proto
jaeger:
endpoint: otel_collector_jaeger:14250
tls:
insecure: true
elasticsearch:
endpoints: "http://192.168.0.17:9200"
참고: 모든 애플리케이션은 동일한 머신(IP: 192.168.0.17)에 배포됩니다. 애플리케이션과 일부 미들웨어가 별도로 배포되는 경우 해당 IP를 수정해야 합니다. 클라우드 서버를 사용하는 경우 관련 포트를 개방하여 접근 실패를 방지하시기 바랍니다.
설치 및 배포¶
OpenTelemetry-Collector 설치¶
소스 코드 주소¶
https://github.com/lrwh/observable-demo/tree/main/opentelemetry-collector-to-all
otel-collector-config.yaml 구성¶
컬렉터 구성을 추가합니다. 1개의 리시버(otlp)와 4개의 익스포터(prometheus, zipkin, jaeger, elasticsearch)를 구성합니다.
receivers:
otlp:
protocols:
grpc:
http:
cors:
allowed_origins:
- http://*
- https://*
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
const_labels:
label1: value1
zipkin:
endpoint: "http://otel_collector_zipkin:9411/api/v2/spans"
format: proto
jaeger:
endpoint: otel_collector_jaeger:14250
tls:
insecure: true
elasticsearch:
endpoints: "http://192.168.0.17:9200"
processors:
batch:
extensions:
health_check:
pprof:
endpoint: :1888
zpages:
endpoint: :55679
service:
extensions: [pprof, zpages, health_check]
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [zipkin, jaeger]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [batch]
exporters: [elasticsearch]
docker-compose를 통해 otel-collector 설치
version: '3.3'
services:
jaeger:
image: jaegertracing/all-in-one:1.29
container_name: otel_collector_jaeger
ports:
- 16686:16686
- 14250
- 14268
zipkin:
image: openzipkin/zipkin:latest
container_name: otel_collector_zipkin
ports:
- 9411:9411
# Collector
otel-collector:
image: otel/opentelemetry-collector:0.50.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "1888:1888" # pprof extension
- "8888:8888" # Prometheus metrics exposed by the collector
- "8889:8889" # Prometheus exporter metrics
- "13133:13133" # health_check extension
- "4317:4317" # OTLP gRPC receiver
- "4318:4318" # OTLP http receiver
- "55670:55679" # zpages extension
depends_on:
- jaeger
- zipkin
prometheus:
container_name: prometheus
image: prom/prometheus:latest
volumes:
- ./prometheus.yaml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
grafana:
container_name: grafana
image: grafana/grafana
ports:
- "3000:3000"
Prometheus 구성¶
scrape_configs:
- job_name: 'otel-collector'
scrape_interval: 10s
static_configs:
- targets: ['otel-collector:8889']
- targets: ['otel-collector:8888']
컨테이너 시작¶
시작 상태 확인¶
Docker로 ELK 설치¶
Docker를 사용하여 ELK를 설치하는 것은 간편하며, 관련 컴포넌트 버전은 7.16.2입니다.
이미지 Pull¶
docker pull docker.elastic.co/elasticsearch/elasticsearch:7.16.2
docker pull docker.elastic.co/logstash/logstash:7.16.2
docker pull docker.elastic.co/kibana/kibana:7.16.2
디렉터리 구성¶
# Linux 전용 설정
sysctl -w vm.max_map_count=262144
sysctl -p
# Linux 설정 종료
mkdir -p ~/elk/elasticsearch/plugins
mkdir -p ~/elk/elasticsearch/data
mkdir -p ~/elk/logstash
chmod 777 ~/elk/elasticsearch/data
Logstash 구성¶
input {
tcp {
mode => "server"
host => "0.0.0.0"
port => 4560
codec => json_lines
}
}
output {
elasticsearch {
hosts => "es:9200"
index => "springboot-logstash-demo-%{+YYYY.MM.dd}"
}
}
tcp : TCP 프로토콜 port: TCP 포트 codec: JSON 행 파싱
Docker-compose 구성¶
version: '3'
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.16.2
container_name: elasticsearch
volumes:
- ~/elk/elasticsearch/plugins:/usr/share/elasticsearch/plugins #플러그인 파일 마운트
- ~/elk/elasticsearch/data:/usr/share/elasticsearch/data #데이터 파일 마운트
environment:
- "cluster.name=elasticsearch" #클러스터 이름을 elasticsearch로 설정
- "discovery.type=single-node" #단일 노드 모드로 시작
- "ES_JAVA_OPTS=-Xms512m -Xmx512m" #JVM 메모리 크기 설정
- "ingest.geoip.downloader.enabled=false" # (Dynamic, Boolean) true인 경우 Elasticsearch가 ingest.geoip.downloader.endpoint에서 GeoIP2 데이터베이스 업데이트를 자동으로 다운로드하고 관리합니다. false인 경우 Elasticsearch가 업데이트를 다운로드하지 않고 다운로드된 모든 데이터베이스를 삭제합니다. 기본값은 true입니다.
ports:
- 9200:9200
logstash:
image: docker.elastic.co/logstash/logstash:7.16.2
container_name: logstash
volumes:
- ~/elk/logstash/logstash.conf:/usr/share/logstash/pipeline/logstash.conf #logstash 구성 파일 마운트
depends_on:
- elasticsearch #kibana는 elasticsearch가 시작된 후 시작
links:
- elasticsearch:es #es 도메인으로 elasticsearch 서비스에 접근 가능
ports:
- 4560:4560
kibana:
image: docker.elastic.co/kibana/kibana:7.16.2
container_name: kibana
depends_on:
- elasticsearch #kibana는 elasticsearch가 시작된 후 시작
links:
- elasticsearch:es #es 도메인으로 elasticsearch 서비스에 접근 가능
environment:
- "elasticsearch.hosts=http://es:9200" #elasticsearch 접근 주소 설정
ports:
- 5601:5601
컨테이너 시작¶
시작 상태 확인¶
Springboot 애플리케이션 연동 (APM)¶
소스 코드 주소¶
https://github.com/lrwh/observable-demo/tree/main/springboot-server
서버 시작¶
java -javaagent:opentelemetry-javaagent-1.13.1.jar \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4350 \
-Dotel.resource.attributes=service.name=server,username=liu \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.propagators=b3 \
-jar springboot-server.jar --client=true
클라이언트 시작¶
java -javaagent:opentelemetry-javaagent-1.13.1.jar \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4350 \
-Dotel.resource.attributes=service.name=client,username=liu \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.propagators=b3 \
-jar springboot-client.jar
파라미터 설명¶
otel.traces.exporter: otlp # 익스포터 유형을 otlp로 구성, 기본값은 otlp입니다.
otel.exporter.otlp.endpoint: otlp 익스포터 엔드포인트 (grpc)
otel.resource.attributes: 태그 구성
otel.metrics.exporter: otlp # 메트릭 익스포터 유형 구성, 기본값은 none입니다.
otel.logs.exporter: otlp # 로그 익스포터 유형 구성, 기본값은 none입니다.
otel.propagators: 트레이스 전파기 구성
OpenTelemetry 로그 부분은 아직 성숙하고 안정적이지 않으므로 프로덕션 환경에서 사용을 권장하지 않습니다. 테스트 과정에서도 일부 버그가 발견되었으며, 학습용으로만 사용하시기 바랍니다.
Springboot 애플리케이션 연동 (로그)¶
방식 1: OTLP를 통한 로그 전송¶
애플리케이션 서버와 클라이언트는 로그를 otlp-exporter를 통해 otel-collector로 푸시한 후, otel-collector의 익스포터를 통해 Elasticsearch로 전송합니다.
시작 파라미터 수정¶
애플리케이션 시작 시 파라미터 -Dotel.logs.exporter=otlp를 추가해야 합니다.
java -javaagent:opentelemetry-javaagent-1.13.1.jar \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4350 \
-Dotel.resource.attributes=service.name=server,username=liu \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.propagators=b3 \
-jar springboot-server.jar --client=true
java -javaagent:opentelemetry-javaagent-1.13.1.jar \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4350 \
-Dotel.resource.attributes=service.name=client,username=liu \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.propagators=b3 \
-jar springboot-client.jar
방식 2: Logstash-logback을 통한 로그 전송¶
주로 Logstash-logback에서 제공하는 소켓 방식을 통해 로그를 Logstash로 업로드합니다. 코드를 일부 조정해야 합니다.
1. 프로젝트 Maven에 Logstash-logback 추가¶
<dependency>
<groupId>net.logstash.logback</groupId>
<artifactId>logstash-logback-encoder</artifactId>
<version>7.0.1</version>
</dependency>
2. logback-logstash.xml 추가¶
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="30 seconds">
<!-- 일부 파라미터는 properties 파일에서 가져옴 -->
<springProperty scope="context" name="logName" source="spring.application.name" defaultValue="localhost.log"/>
<!-- 구성 후 동적으로 로그 레벨 수정 가능 -->
<jmxConfigurator />
<property name="log.pattern" value="%d{HH:mm:ss} [%thread] %-5level %logger{10} [traceId=%X{trace_id} spanId=%X{span_id} userId=%X{user-id}] %msg%n" />
<springProperty scope="context" name="logstashHost" source="logstash.host" defaultValue="logstash"/>
<springProperty scope="context" name="logstashPort" source="logstash.port" defaultValue="4560"/>
<!-- %m 출력 정보, %p 로그 레벨, %t 스레드명, %d 날짜, %c 클래스 전체 이름 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/${logName}/${logName}.log</file> <!-- 사용 방법 -->
<append>true</append>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>logs/${logName}/${logName}-%d{yyyy-MM-dd}.log.%i</fileNamePattern>
<maxFileSize>64MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>1GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- LOGSTASH 출력 설정 -->
<appender name="LOGSTASH" class="net.logstash.logback.appender.LogstashTcpSocketAppender">
<!-- logStash 서비스 주소 구성 -->
<destination>${logstashHost}:${logstashPort}</destination>
<!-- 로그 출력 인코딩 -->
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp>
<timeZone>UTC+8</timeZone>
</timestamp>
<pattern>
<pattern>
{
"podName":"${podName:-}",
"namespace":"${k8sNamespace:-}",
"severity": "%level",
"serverName": "${logName:-}",
"traceId": "%X{trace_id:-}",
"spanId": "%X{span_id:-}",
"pid": "${PID:-}",
"thread": "%thread",
"class": "%logger{40}",
"message": "%message\n%exception"
}
</pattern>
</pattern>
</providers>
</encoder>
<!-- 연결 유지 -->
<keepAliveDuration>5 minutes</keepAliveDuration>
</appender>
<!-- error 레벨만 출력 -->
<logger name="net.sf.json" level="ERROR" />
<logger name="org.springframework" level="ERROR" />
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="LOGSTASH"/>
</root>
</configuration>
3. application-logstash.yml 추가¶
4. 재패키징¶
5. 서비스 시작¶
java -javaagent:opentelemetry-javaagent-1.13.1.jar \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4350 \
-Dotel.resource.attributes=service.name=server,username=liu \
-Dotel.metrics.exporter=otlp \
-Dotel.propagators=b3 \
-jar springboot-server.jar --client=true \
--spring.profiles.active=logstash \
--logstash.host=localhost \
--logstash.port=4560
java -javaagent:opentelemetry-javaagent-1.13.1.jar \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4350 \
-Dotel.resource.attributes=service.name=client,username=liu \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.propagators=b3 \
-jar springboot-client.jar \
--spring.profiles.active=logstash \
--logstash.host=localhost \
--logstash.port=4560
JS 연동 (RUM)¶
소스 코드 주소¶
https://github.com/lrwh/observable-demo/tree/main/opentelemetry-js
OTLPTraceExporter 구성¶
const otelExporter = new OTLPTraceExporter({
// optional - url default value is http://localhost:55681/v1/traces
url: 'http://192.168.91.11:4318/v1/traces',
headers: {},
});
여기서 url은 otel-collector의 otlp 수신 주소(HTTP 프로토콜)입니다.
server_name 구성¶
const providerWithZone = new WebTracerProvider({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'front-app',
}),
}
);
설치¶
시작¶
기본 포트는8090입니다.
APM과 RUM 연동¶
APM과 RUM은 주로 헤더 파라미터를 통해 연동됩니다. 일관성을 유지하기 위해 통일된 전파기(Propagator)를 구성해야 합니다. 여기서 RUM은 B3를 사용하므로 APM도 B3를 구성해야 하며, APM 시작 파라미터에 -Dotel.propagators=b3를 추가하면 됩니다.
APM과 로그 연동¶
APM과 로그는 주로 로그에 traceId와 spanId를 심어서 연동합니다. 로그 연동 방식에 따라 심는 방식이 다릅니다.
UI 표시¶
프론트엔드 URL에 접근하여 트레이스 정보를 생성합니다.
ELK 로그 표시¶
ELK는 ElasticSearch, Logstash, Kibana의 약칭입니다.
OTLP를 통한 로그 전송¶
로그 source 부분을 펼친 내용
"_source": {
"@timestamp": "2022-05-18T08:39:20.661000000Z",
"Body": "this is method3,null",
"Resource.container.id": "7478",
"Resource.host.arch": "amd64",
"Resource.host.name": "cluster-ecs07",
"Resource.os.description": "Linux 3.10.0-1160.15.2.el7.x86_64",
"Resource.os.type": "linux",
"Resource.process.command_line": "/usr/java/jdk1.8.0_111/jre:bin:java -javaagent:opentelemetry-javaagent-1.13.1.jar -Dotel.traces.exporter=otlp -Dotel.exporter.otlp.endpoint=http://localhost:4350 -Dotel.resource.attributes=service.name=server,username=liu -Dotel.metrics.exporter=otlp -Dotel.logs.exporter=otlp -Dotel.propagators=b3",
"Resource.process.executable.path": "/usr/java/jdk1.8.0_111/jre:bin:java",
"Resource.process.pid": 5728,
"Resource.process.runtime.description": "Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 25.111-b14",
"Resource.process.runtime.name": "Java(TM) SE Runtime Environment",
"Resource.process.runtime.version": "1.8.0_111-b14",
"Resource.service.name": "server",
"Resource.telemetry.auto.version": "1.13.1",
"Resource.telemetry.sdk.language": "java",
"Resource.telemetry.sdk.name": "opentelemetry",
"Resource.telemetry.sdk.version": "1.13.0",
"Resource.username": "liu",
"SeverityNumber": 9,
"SeverityText": "INFO",
"SpanId": "bb890485f7b6ba05",
"TraceFlags": 1,
"TraceId": "b4841a6b3ec9aa93d7f002393a156ff5"
},
otlp 프로토콜을 통해 로그에 traceId와 spanId가 자동으로 심어집니다.
Logstash-logback을 통한 로그 전송¶
로그 source 부분을 펼친 내용
"_source": {
"@timestamp": "2022-05-18T13:43:34.790Z",
"port": 55630,
"serverName": "otlp-server",
"namespace": "k8sNamespace_IS_UNDEFINED",
"message": "this is tag\n",
"@version": "1",
"severity": "INFO",
"thread": "http-nio-8080-exec-1",
"pid": "3975",
"host": "gateway",
"class": "c.z.o.server.controller.ServerController",
"traceId": "a7360264491f074a1b852cfcabb10fdb",
"spanId": "e4a8f1c4606ca598",
"podName": "podName_IS_UNDEFINED"
},
Logstash-logback 방식을 사용하려면 traceId와 spanId를 수동으로 심어야 합니다.
Prometheus & Grafana 메트릭 표시¶
Jaeger, Zipkin 트레이스 표시¶
다음 문서에서는 OpenTelemetry가 grafana 관련 컴포넌트를 기반으로 어떻게 관측 가능성을 구현하는지 소개합니다.









