본문 바로가기
Cloud

[Tempo] Grafana Tempo 적용기 (2)

by VENUSIM 2024. 11. 19.

도입 배경

기존에 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 구성

서비스를 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가지 파이프라인으로 구성되어 있다.

  • 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
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
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

댓글