도입 배경
기존에 Prometheus & Prometail + Loki 를 통해 로그, VM 메트릭 등의 데이터를 수집하고 Grafana를 이용한 시각 화를 통해 모니터링 기능을 구축하였었다. 추가적으로 이용자의 활동, 병목 구간 분석 등의 니즈가 있었고 이를 해소하고자 Trace를 모니터링을 위한 Tempo 데모를 구성하였다.
그 과정에 너무 많은 삽질이 있었고, 정리가 필요해 보여 구성 과정에 대해 정리해 보려한다.
Opentelemetry
Tempo에 대해 서치를 진행하였고, 그 결과 Opentelemetry라는 오픈 소스와 시너지가 좋다는 결론이 도출 되었다. Opentelemetry는 Agent와 Collector가 존재하며, Agent로 만으로 구성이 가능하지만 추후 유지 보수와 고도화를 고려하였을 때 Collector를 이용하여 구성하는 것이 좋다는 결론을 끝으로 진행하였다.
구성
opentelemetry는 tempo에서 수는 trace 데이터 뿐만 아니라 metric, log 또한 수집하고 export하는 기능을 제공한다.

Agent 방식 채택
Opentelemetry는 Trace, Metric, Log 3가지에 대해 수집하여 Export하는 구성으로 기능을 제공한다.
대표적으로 Java 언어를 사용하는 App에 대해 구성하였고, 수집을 위해서는 Spring을 통한 구성 Agent 방식 두 가지가 있다.
Spring을 사용하지 않고 Servlet 혹은 Java로 구성된 Solution을 사용하고 있는 Application도 존재하여 Agent 방식을 채택하였다
Agent 구성
- Document
https://opentelemetry.io/docs/zero-code/java/agent/getting-started/ - Agent Jar
https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases
서비스를 Kubernetes 향으로 구성하고 있기에 Application이 컨테이너 화가 되기 위한 이미지 생성이 필요하여 Dockerfile을 통해 구성하였다.
Java Agent는 JVM(Java Virtual Machine)과 상호 작용하여 클래스 로딩 시점에 바이트코드를 조작하거나, 이미 로드된 클래스의 동작을 런타임에 수정할 수 있다. 이러한 기능은 성능 모니터링, 디버깅, 프로파일링, 코드 커버리지 분석, 그리고 AOP(Aspect-Oriented Programming) 구현 등 다양한 목적에 사용된다.
- Project 구조
Opentelemetry Github에서 제공하는 Agent를 다운 받아 프로젝트에 추가하였다.
- Dockerfile
Agent 심어주고, -javaagent 명령을 통해 실행하는 스크립트 생성 (하단 두 줄)
FROM openjdk:17.0.1-jdk-slim AS builder
COPY . /tmp
WORKDIR /tmp
RUN sed -i 's/\r$//' ./gradlew
RUN chmod +x ./gradlew
RUN ./gradlew clean
RUN ./gradlew bootjar
...
# OTLP Agent
COPY --from=builder /tmp/opentelemetry-javaagent.jar ./
CMD ["java", "-javaagent:opentelemetry-javaagent.jar", "-jar", "api-1.0.0-SNAPSHOT.jar"]
- 환경 변수 세팅
# 기본 값
# https://opentelemetry.io/docs/specs/otel/protocol/exporter/#specify-protocol
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
# 수집할 헤더 정의
# https://opentelemetry.io/docs/zero-code/java/agent/instrumentation/http/
OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_REQUEST_HEADERS=Authorization,Hmac-Id
OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_REQUEST_HEADERS=Authorization,Hmac-Id
OTEL_INSTRUMENTATION_HTTP_SERVER_CAPTURE_RESPONSE_HEADERS=Authorization,Hmac-Id
OTEL_INSTRUMENTATION_HTTP_CLIENT_CAPTURE_RESPONSE_HEADERS=Authorization,Hmac-Id
OTEL_INSTRUMENTATION_SERVLET_EXPERIMENTAL_CAPTURE_REQUEST_PARAMETERS=userId
# Metric, Trace, Log endpoint 설정
OTEL_METRICS_EXPORTER=prometheus
OTEL_TRACES_EXPORTER=otlp
OTEL_LOGS_EXPORTER=otlp
# Endpoint가 외부에 노출될 필요가 없고,
# 민감 정보이기에 보안을 위해 collector를 FQDN으로 호출하는 방식으로 Export 하도록 설정
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector-opentelemetry-collector.monitoring.svc.cluster.local:4318
OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://collector-opentelemetry-collector.monitoring.svc.cluster.local:4318/v1/metrics
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://collector-opentelemetry-collector.monitoring.svc.cluster.local:4318/v1/traces
OTEL_EXPORTER_OTLP_LOGS_ENDPOINT=http://collector-opentelemetry-collector.monitoring.svc.cluster.local:4318/v1/logs
# Trace에 노출될 service 이름
OTEL_SERVICE_NAME=vlmsapi-74f96cb55c-l2q4w
위 환경 변수를 ArgoCD 배포 환경에 적용하기 위해 Kustomize + HelmChart를 이용하였고 기존 구성에 영향을 미치지 않기 위해 operation add 를 통해 구성하였다.
- otlp-patch.yaml
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: OTEL_SERVICE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name # pod name으로 구성하기 위해
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: ...
value: ...# 위 환경 변수를 name, value 에 채워주면 된다.
OTLP helm patch 이슈 발생
이슈 발생 원인
otlp metric, trace, log에 대한 정보를 otlp로 전송하는 application에 대한 설정이 "개발" 환경 변수에만 적용되어 있는 상태
- 개발 환경에 agent를 심어 실행하지 않는 경우 정보를 수집할 수 없어 오류 발생 및 구동에 영향을 미침
- 개발 이외 agent를 심어 실행하는 경우 구동에는 영향 x , default 설정의 endpoint로 정보를 export 하지만 receive 하는 곳 없어 응답 없음으로 인해 오류 로그 출력 됨
해결 방안
Agent는 심어 이미지 생성 , agent 실행 옵션 제거
FROM openjdk:17.0.1-jdk-slim AS builder
COPY . /tmp
WORKDIR /tmp
RUN sed -i 's/\r$//' ./gradlew
RUN chmod +x ./gradlew
RUN ./gradlew clean
RUN ./gradlew bootjar
...
# OTLP Agent
COPY --from=builder /tmp/opentelemetry-javaagent.jar ./
CMD ["java", "-jar", "api-1.0.0-SNAPSHOT.jar"]
배포 시 agent 실행 옵션을 부여
- op: add
path: /spec/template/spec/containers/0/command
value: ["java"]
- op: add
path: /spec/template/spec/containers/0/args
value: ["-javaagent:opentelemetry-javaagent.jar", "-jar", "api-1.0.0-SNAPSHOT.jar"]
Collector 구성
Collector는 크게 receiver, processor, exporter 3가지 파이프라인으로 구성되어 있다.

- Helm
Helm을 ArgoCD를 이용하여 구성하였고,
document를 보면, image.repository / mode에 대한 parameter 를 제공해 주어 구성한다고 한다.
https://opentelemetry.io/docs/kubernetes/helm/collector/
https://github.com/open-telemetry/opentelemetry-collector-contrib/discussions/6971
- image repository
collector에 대한 배포판은 3가지가 있고, opentelemetry-collector(classic)은 핵심적인 구성 요소로만 되어 있고 opentelemetry-collector-contrib는 모든 구성 요소를 포함한다. opentelemetry-collector-k8s는 classic과 contrib의 구성 요소중 k8s cluster와 구성요소를 모니터링할 수 있도록 특별히 제작되었다.- otel/opentelemetry-operator:${version}
- otel/opentelemetry-collector:${version}
- otel/opentelemetry-collector-contrib:${version}
- otel/opentelemetry-collector-k8s:${version}
opentelemetry-collector-contrib로 구성하되 각 환경에 맞게 따로 배포판을 만들어서
사용(https://opentelemetry.io/docs/collector/custom-collector )할 수도 있다.

- Mode
deployment, statefulset, daemonset 3가지 방식으로 구성이 가능하다.
위에서 설명하였던 Collector의 구성은 configMap에 의해 구성되며 동작한다.
apiVersion: apps/v1
kind: Deployment
metadata:
...
spec:
...
selector:
...
strategy:
...
template:
metadata:
...
spec:
containers:
- args:
- '--config=/conf/relay.yaml'
env:
...
image: 'otel/opentelemetry-collector-contrib:0.111.0'
imagePullPolicy: IfNotPresent
livenessProbe:
...
name: opentelemetry-collector
ports:
...
readinessProbe:
...
volumeMounts:
- mountPath: /conf
name: opentelemetry-collector-configmap
...
volumes:
- configMap:
defaultMode: 420
items:
- key: relay
path: relay.yaml
name: collector-opentelemetry-collector
name: opentelemetry-collector-configmap
- ConfigMap 구성
otel/opentelemetry-collector-contrib 이미지를 사용한 이유는 prometheusremotewrite exporter의 기능을 사용하기 위함이다.
https://github.com/open-telemetry/opentelemetry-collector-contrib/tree/main/exporter
processor- attribute 설정
https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/processor/attributesprocessor/README.md - batch 설정
https://github.com/open-telemetry/opentelemetry-collector/blob/main/processor/batchprocessor/README.md - memory_limiter 설정
https://github.com/open-telemetry/opentelemetry-collector/tree/main/processor/memorylimiterprocessor
- attribute 설정
apiVersion: v1
data:
relay: |
exporters:
...
extensions:
health_check:
endpoint: ${env:MY_POD_IP}:13133
processors:
resource:
attributes:
- action: insert
key: service_name
from_attribute: service.name
- action: insert
key: service_namespace
from_attribute: service.namespace
- action: insert
key: service_version
from_attribute: service.version
- action: insert
key: deployment_environment
from_attribute: deployment.environment
- action: insert
key: loki.resource.labels
value: service_name,service_namespace,service_version,deployment_environment
batch:
send_batch_size: 10000
memory_limiter:
check_interval: 5s
limit_percentage: 80
spike_limit_percentage: 25
receivers:
...
service:
extensions:
- health_check
pipelines:
...
telemetry:
metrics:
address: ${env:MY_POD_IP}:8888
kind: ConfigMap
- Prometheus
- exporter
resource_to_telemetry_conversion: otel에서 수집한 메트릭 정보를 prometheus label화 역할을 수행
https://grafana.com/docs/alloy/latest/reference/components/otelcol/otelcol.exporter.prometheus/ - receivers
수신기의 의미지만 static_configs > targets 에 선언한 도메인을 통해 metrics_path에 선언한 endpoint를 통해 scrape_interval에 설정한 시간마다 metric 정보를 pull 해온다.
- exporter
apiVersion: v1
data:
relay: |
exporters:
...
prometheusremotewrite:
endpoint: "http://prometheus-infra-server:80/api/v1/write"
resource_to_telemetry_conversion:
enabled: true
extensions:
...
processors:
...
receivers:
...
prometheus:
config:
scrape_configs:
- job_name: "${job-name}"
scrape_interval: 5s
metrics_path: "${actuator-endpoint}"
static_configs:
- targets:
- "${app-name}.${namespace}.svc.cluster.local:${port}"
...
- job_name: opentelemetry-collector
scrape_interval: 10s
static_configs:
- targets:
- ${env:MY_POD_IP}:8888
service:
extensions:
- health_check
pipelines:
...
metrics:
exporters:
- prometheusremotewrite
processors:
- memory_limiter
- batch
receivers:
- otlp
- Tempo
tls/insecure : http를 사용하기 위한 옵션
apiVersion: v1
data:
relay: |
exporters:
otlphttp:
endpoint: http://grafana-tempo:4318
tls:
insecure: true
extensions:
...
processors:
...
receivers:
otlp:
protocols:
grpc:
endpoint: ${env:MY_POD_IP}:4317
http:
endpoint: ${env:MY_POD_IP}:4318
service:
extensions:
- health_check
pipelines:
...
traces:
exporters:
- otlphttp
processors:
- memory_limiter
- batch
receivers:
- otlp
- Loki
apiVersion: v1
data:
relay: |
exporters:
...
loki:
endpoint: http://${loki-endpoint}/loki/api/v1/push
tls:
insecure: true
processors:
...
receivers:
otlp:
protocols:
grpc:
endpoint: ${env:MY_POD_IP}:4317
http:
endpoint: ${env:MY_POD_IP}:4318
...
service:
extensions:
- health_check
pipelines:
logs:
exporters:
- loki
processors:
- memory_limiter
- resource
- batch
receivers:
- otlp
opentelemetry-collector/processor/memorylimiterprocessor at main · open-telemetry/opentelemetry-collector
OpenTelemetry Collector. Contribute to open-telemetry/opentelemetry-collector development by creating an account on GitHub.
github.com
opentelemetry-collector/processor/memorylimiterprocessor at main · open-telemetry/opentelemetry-collector
OpenTelemetry Collector. Contribute to open-telemetry/opentelemetry-collector development by creating an account on GitHub.
github.com
opentelemetry-collector/processor/memorylimiterprocessor at main · open-telemetry/opentelemetry-collector
OpenTelemetry Collector. Contribute to open-telemetry/opentelemetry-collector development by creating an account on GitHub.
github.com
Prometheus
기존 helm으로 구성된 prometheus 기본 설정에 web.enable-remote-write-receiver 옵션을 통해 otel collector에서 export하는 metric data를 수신할 수 있도록 활성화 해준다.
extraFlags[1] 로 설정한 이유는 기존 Helm에 extraFlags[0]을 사용 중이었기 때문이다.
project: default
source:
repoURL: 'https://prometheus-community.github.io/helm-charts'
targetRevision: 25.16.0
helm:
parameters:
- name: alertmanager.enabled
value: 'false'
values: 'server.extraFlags[1]: web.enable-remote-write-receiver'
chart: prometheus
destination:
server: 'https://kubernetes.default.svc'
namespace: monitoring
Opentelemetry , prometheus 설정이 끝났으니, metric endpoint를 제공하기 위한 설정을 한다.
- Spring
# build.gradle
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation "io.opentelemetry:opentelemetry-api:1.24.0"
implementation "io.micrometer:micrometer-registry-prometheus:1.10.5"
# application.yml
management:
server:
port: 9876
endpoints:
web:
exposure:
# promethues endpoint 만 활성화
include: prometheus
# 기본 path 변경 (default = "/actuator")
base-path: /metric
metrics:
tags:
application: app
distribution:
percentiles-histogram:
http:
server:
requests: 'true'
minimum-expected-value:
http.server.requests: 5ms
maximum-expected-value:
http.server.requests: 1000ms
// prometheus metric otel configuration
import io.prometheus.client.exemplars.tracer.otel_agent.OpenTelemetryAgentSpanContextSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
@Profile("test-profile")
public class PrometheusConfiguration {
@Bean
public OpenTelemetryAgentSpanContextSupplier openTelemetryAgentSpanContextSupplier() {
return new OpenTelemetryAgentSpanContextSupplier();
}
}
[ContextConfig.java] tomcat customizer 예외 발생
To allow a separate management port to be used, a top-level class or static inner class should be used instead
기존 코드는 익명의 하위 클래스는 인스턴스화 할 수 없으므로 actuator 관리 포트의 임베디드 컨테이너가 생성되지 않아 다음과 같이 변경하였다.
// BEFORE
@Bean
public TomcatServletWebServerFactory tomcatFactory() {
return new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
((StandardJarScanner) context.getJarScanner()).setScanManifest(false);
}
};
}
// AFTER
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> tomcatCustomizer() {
return (tomcat) -> tomcat.addContextCustomizers(context ->
((StandardJarScanner) context.getJarScanner()).setScanManifest(false));
}
org.springframework.boot.actuate.autoconfigure.web.ManagementContextFactory.java
- Servlet (솔루션 포함)
순수 servlet으로 구성되거나, 솔루션 형태로 소스를 수정할 수 없는 경우 spring에서 구성 방식을 이용할 수 없으므로 prometheus metric을 제공하기 위해 jmx_exporter를 직접 구성하는 방식을 택하였다.
https://github.com/prometheus/jmx_exporter/releases/tag/1.0.1
otlp 동일하게 agent 방식으로 구성하였으며, 차이점은 yaml을 통해 config를 해야하는 점과 agent를 두개 심어주고 spring의 설정을 인자로 넘겨줘야 한다는 점이다.
- op: add
path: /spec/template/spec/containers/0/env/-
value:
name: CATALINA_OPTS
value: "$(CATALINA_OPTS) -javaagent:/usr/local/tomcat/bin/opentelemetry-javaagent.jar -javaagent:/usr/local/tomcat/bin/jmx_prometheus_javaagent.jar=9876:/usr/local/tomcat/bin/jmx_prometheus_javaagent.yaml"
추후에 필요성을 느꼈을 때 커스텀 하기 위해 sample로 제공하는 config를 적용하였다.
https://github.com/prometheus/jmx_exporter/tree/main/example_configs
# jmx_prometheus_javaagent.yaml
startDelaySeconds: 5
ssl: false
lowercaseOutputName: false
lowercaseOutputLabelNames: false
whitelistObjectNames: ["java.lang:type=OperatingSystem", "Catalina:*"]
blacklistObjectNames: []
rules:
- pattern: 'Catalina<type=Server><>serverInfo: (.+)'
name: tomcat_serverinfo
value: 1
labels:
serverInfo: "$1"
type: COUNTER
- pattern: 'Catalina<type=GlobalRequestProcessor, name=\"(\w+-\w+)-(\d+)\"><>(\w+):'
name: tomcat_$3_total
labels:
port: "$2"
protocol: "$1"
help: Tomcat global $3
type: COUNTER
- pattern: 'Catalina<j2eeType=Servlet, WebModule=//([-a-zA-Z0-9+&@#/%?=~_|!:.,;]*[-a-zA-Z0-9+&@#/%=~_|]), name=([-a-zA-Z0-9+/$%~_-|!.]*), J2EEApplication=none, J2EEServer=none><>(requestCount|processingTime|errorCount):'
name: tomcat_servlet_$3_total
labels:
module: "$1"
servlet: "$2"
help: Tomcat servlet $3 total
type: COUNTER
- pattern: 'Catalina<type=ThreadPool, name="(\w+-\w+)-(\d+)"><>(currentThreadCount|currentThreadsBusy|keepAliveCount|connectionCount|acceptCount|acceptorThreadCount|pollerThreadCount|maxThreads|minSpareThreads):'
name: tomcat_threadpool_$3
labels:
port: "$2"
protocol: "$1"
help: Tomcat threadpool $3
type: GAUGE
- pattern: 'Catalina<type=Manager, host=([-a-zA-Z0-9+&@#/%?=~_|!:.,;]*[-a-zA-Z0-9+&@#/%=~_|]), context=([-a-zA-Z0-9+/$%~_-|!.]*)><>(processingTime|sessionCounter|rejectedSessions|expiredSessions):'
name: tomcat_session_$3_total
labels:
context: "$2"
host: "$1"
help: Tomcat session $3 total
type: COUNTER
Trace to log 설정
loki HTTP 429 Too Many Requests
로그 사이즈가 로키 설정에 부합하지 않은 경우 Collector에서 아래와 같은 로그 확인
Exporting failed. Will retry the request after interval. {"kind": "exporter", "data_type": "logs", "name": "loki", "error": "HTTP 429 \"Too Many Requests\": Ingestion rate limit exceeded for user fake (limit: 4194304 bytes/sec) while attempting to ingest '603' lines totaling '6332331' bytes, reduce log volume or contact your Loki administrator to see if the limit can be increased", "interval": "40.215348808s"}
stream 제한 2배로 증가 (default: 5000)
max_global_streams_per_user: 10000
ingestion 5배 증가
ingestion_rate_mb: 20 (default 4)
ingestion_burst_size_mb: 30 (default 6)
'Cloud' 카테고리의 다른 글
[ArgoCD] 계정 생성 (1) | 2024.12.27 |
---|---|
[ArgoCD] FE 배포 Helm 구성기 (1) | 2024.12.27 |
[Tempo] Grafana Tempo 도입 (1) (0) | 2024.11.18 |
[Kafka] CDC 도입기 (1) (0) | 2024.11.18 |
Kustomize, Helm Chart 시스템 구축 (0) | 2024.02.09 |
댓글