Log와 Trace 사이에 오작교 놓아주기 with OpenTelemetry Operator

zuckerfrei·2025년 8월 4일
0

Monitoring

목록 보기
7/8

1. 문제

signoz를 사용한 모니터링 환경을 구성하는 중이다.

signoz ui에서 log-trace 간의 연관관계가 존재하지 않아 분석에 한계가 있다.
스크린샷에서 보다시피 trace 화면에서 Go to related logs 버튼을 클릭하면, 연관된 로그가 존재하지 않는다고 나온다.


2. 원인

소스코드 분석 결과 SigNoz의 log-trace 연결 로직을 확인할 수 있었다.
getTraceToLogsQuery() 함수에서 trace_id 필드로 로그를 필터링하는 것이다.
그럼 연관 로그가 나오지 않는다는 것은, 필터링이 제대로 되지 않고 있다는 뜻이다.
이럴 때는 데이터를 확인해봐야겠다고 생각했다.

logs_v2 테이블에 trace_id, span_id 컬럼이 존재하는데, 현재 모든 로그 데이터의 이 컬럼이 비어있다.

  • Traces: 9,777,253개 존재
  • Logs: 3,819,297개 존재
  • 로그의 trace_id: 0개

trace_id, span_id가 존재하지 않아 trace와 연관관계가 생성되지 않는다.
이 trace_id, span_id는 애플리케이션에서 추가해야하는 작업이다.
이것은 sdk/Agent가 없이는 불가능하다.

자사 모니터링 시스템을 구축하는 상황이라면, 애플리케이션 코드 레벨에서 SDK를 심고 사용하면 된다.
그런데 우리같은 모니터링 솔루션이라면..?
고객사의 소스코드 수정을 할 수는 없을 것이다. 이러면 귀찮아서 아무도 안 쓸거다.


3. 해결

3-1. Admission Controller

이런 경우, SDK/Agent를 주입하는 방식으로 log-trace correlation을 만들어준다.
다른 상용 모니터링 솔루션(Datadog, New Relic, Splunk 등)들도 이런 방식을 채택한다.

Kubernetes Admission Controller를 사용하여 애플리케이션 파드에 sdk를 추가하는 방식을 사용한다.
짧게 말하자면, Pod가 생성되는 API를 중간에 가로채서 SDK/Agent를 주입하는 것이다.

Admission Webhook 동작 과정은 이러하다.

1. kubectl apply deployment.yaml
     ↓
2. Kubernetes API Server가 Deployment 생성
   ↓
3. Deployment Controller가 ReplicaSet 생성
   ↓
4. ReplicaSet Controller가 Pod 생성 API 호출
   ↓
5. MutatingAdmissionWebhook 호출 ← 여기서 가로챔!
   ↓
6. OpenTelemetry Operator가 annotation 확인
   ↓
7. Init Container + Volume + 환경변수 자동 추가
   ↓
8. 수정된 Pod 스펙으로 실제 생성
  • Admission Controller (상위 개념)
    • MutatingAdmissionWebhook (수정용)
    • ValidatingAdmissionWebhook (검증용)

3-2. 우리 상황에는?

signoz 공식 문서를 확인하면 이러한 log-trace correlation 이슈가 있을 경우 OTel SDK 사용을 권장한다.
Correlate Traces and Logs
https://github.com/open-telemetry/opentelemetry-operator

표준 otel operator에서도 Admission Controller 사용을 지원하기에, 이것을 사용하여 log-trace correlation을 구현하면 될 것 같았다.

모니터링 Agent 클러스터에 반드시 필요한 구성요소

찾아본 결과 다음과 같은 필수 구성요소 목록을 확인할 수 있었고, 순서대로 배포해서 테스트했다!

  1. cert-manager
  2. OpenTelemetry Operator
  3. Instrumentation CR
  4. 서비스 애플리케이션(java, node.js, python…)

1) cert-manager 배포

cert-manager는 OpenTelemetry Operator 때문에 필요하다.

  • 이유: OpenTelemetry Operator가 Admission Webhook TLS 인증서를 자동 생성하기 위해 사용
  • 역할: Webhook의 HTTPS 통신을 위한 인증서 자동 관리
  • 위치: OpenTelemetry Operator와 같은 클러스터에 있어야 함

2) otel operator 배포

그리고 OpenTelemetry Operator는 모니터링 대상 클러스터에 필수적으로 배포되어야 한다.

  • 이유: 애플리케이션 파드에 Agent를 자동 주입하는 역할
  • 동작: Kubernetes MutatingAdmissionWebhook으로 파드 생성 시 init container 추가
  • 위치: 각 모니터링 Agent 클러스터마다 설치 필요

3) OpenTelemetry Operator 설정 파일(Instrumentation CR)

  • MDC 로깅 통합 환경변수 추가
  • gRPC 프로토콜 명시적 설정
  • Java Agent 자동 주입 설정
apiVersion: opentelemetry.io/v1alpha1
kind: Instrumentation
metadata:
  name: signoz-instrumentation
  namespace: observability
spec:
  exporter:
    endpoint: http://192.168.254.246:4317  # 기존 Host 클러스터 엔드포인트
  propagators:
    - tracecontext
    - baggage
  sampler:
    type: parentbased_traceidratio
    argument: "0.1"  # 10% 샘플링으로 시작
  
  java:
    image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
    env:
      - name: OTEL_LOGS_EXPORTER
        value: otlp
      - name: OTEL_TRACES_EXPORTER
        value: otlp
      - name: OTEL_METRICS_EXPORTER
        value: otlp
      - name: OTEL_SERVICE_NAME
        value: "java-simple-server"
      - name: OTEL_RESOURCE_ATTRIBUTES
        value: "k8s.cluster.name=flash-cluster-1"
      # MDC 로깅 통합을 위한 설정
      - name: OTEL_JAVAAGENT_ENABLED_INSTRUMENTATIONS
        value: "logback-mdc,log4j-mdc"
      - name: OTEL_INSTRUMENTATION_LOGBACK_MDC_ADD_BAGGAGE
        value: "true"
      - name: OTEL_INSTRUMENTATION_COMMON_MDC_RESOURCE_ATTRIBUTES
        value: "true"
      # 로그에 trace context 정보 주입 활성화
      - name: OTEL_INSTRUMENTATION_LOGBACK_APPENDER_EXPERIMENTAL_CAPTURE_MDC_ATTRIBUTES
        value: "true"
      # OTLP 프로토콜 명시적 설정
      - name: OTEL_EXPORTER_OTLP_PROTOCOL
        value: "grpc"
      - name: OTEL_EXPORTER_OTLP_LOGS_PROTOCOL
        value: "grpc"
      - name: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL
        value: "grpc"
      - name: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
        value: "grpc"

4) Java 테스트 애플리케이션 배포

  • Spring Boot 로깅 패턴에 trace_id/span_id 추가
  • DEBUG 레벨 로깅 활성화
  • HTTP 요청 로깅 설정
apiVersion: apps/v1
kind: Deployment
metadata:
  name: java-simple-server
  namespace: observability
spec:
  replicas: 1
  selector:
    matchLabels:
      app: java-simple-server
  template:
    metadata:
      labels:
        app: java-simple-server
      annotations:
        instrumentation.opentelemetry.io/inject-java: "observability/signoz-instrumentation"
    spec:
      containers:
      - name: app
        # Spring Boot 기반 이미지 사용 (HTTP 서버가 내장됨)
        image: springio/gs-spring-boot-docker
        ports:
        - containerPort: 8080
        env:
        - name: OTEL_SERVICE_NAME
          value: "java-simple-server"
        - name: JAVA_OPTS
          value: "-Dlogging.level.root=INFO -Dlogging.level.org.springframework.web=DEBUG -Dlogging.level.org.apache.catalina=DEBUG"
        # Spring Boot 로깅 패턴에 trace_id/span_id 추가
        - name: LOGGING_PATTERN_LEVEL
          value: "trace_id=%mdc{trace_id} span_id=%mdc{span_id} %5p"
        - name: LOGGING_PATTERN_CONSOLE
          value: "%d{yyyy-MM-dd HH:mm:ss.SSS} %5p %pid --- [%15.15t] %-40.40logger{39} : trace_id=%mdc{trace_id} span_id=%mdc{span_id} %m%n"
        # HTTP 요청 로깅 활성화
        - name: LOGGING_LEVEL_WEB
          value: "DEBUG"
        - name: LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB_SERVLET_DISPATCHERSERVLET
          value: "DEBUG"
        resources:
          limits:
            memory: "512Mi"
            cpu: "200m"
          requests:
            memory: "256Mi"
            cpu: "100m"
        livenessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: java-simple-server-svc
  namespace: observability
spec:
  selector:
    app: java-simple-server
  ports:
  - port: 8080
    targetPort: 8080
  type: ClusterIP

3-3. 결과

드디어 log와 trace간의 연관관계가 잡히기 시작했따..!
Go to related logs 버튼을 누르면, 이 trace와 연관된 로그를 확인할 수 있었다.
단지 샘플앱이라 아주 기본적인 로깅밖에 없지만, 좀 더 복잡한? 앱을 배포하면 다양한 로그를 확인 할 수 있을 것이다.

로그 테이블도 확인해보니, 아까는 비어있던 trace_id 컬럼에도 데이터가 들어오기 시작했다.
이 trace_id가 log와 trace를 이어주는 오작교 역할을 하는 것이다..!

3-4. Admission Webhook 동작 확인

그런데 정말로 k8s Admission Webhook이 동작한건지 확인해보고 싶었다.

처음에는 샘플앱 deployment를 kubectl edit deploy 명령어로 확인해봤다.
엥.. 그런데 맨 첨에 배포한 yaml과 비교했을 때 별다른 차이를 알 수 없었다..
admission webhook 동작 안 한거 아냐? 그럼 어떻게 trace_id 생긴거지? 순간 혼란이 왔는데..

kubectl describe pod 명령어로 파드를 상세히 확인해보니,, webhook 동작의 흔적을 확인할 수 있었다.

  • Init Containers의 존재
  • Init Container와 App Container가 같은 경로(/otel-auto-instrumentation-java-app)를 마운트한 것
  • OTEL_*로 시작하는 환경변수 주입
# kubectl describe pod -n observability java-simple-server-b69588b47-7qtpc

Name:             java-simple-server-b69588b47-7qtpc
Namespace:        observability
Priority:         0
Service Account:  default
Node:             worker003/192.168.254.82
Start Time:       Mon, 04 Aug 2025 16:47:40 +0900
Labels:           app=java-simple-server
                  pod-template-hash=b69588b47
Annotations:      instrumentation.opentelemetry.io/inject-java: observability/signoz-instrumentation
                  kubectl.kubernetes.io/restartedAt: 2025-08-04T07:41:00Z
Status:           Running
IP:               10.233.66.168
IPs:
  IP:           10.233.66.168
Controlled By:  ReplicaSet/java-simple-server-b69588b47
Init Containers:
  opentelemetry-auto-instrumentation-java:
    Container ID:  containerd://a96ded5d1357eb57eac8458607cbdfb86f4d36f8078c707f8ab0de6ee8d2abe9
    Image:         ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java:latest
    Image ID:      ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-java@sha256:0103c0a251d7b40b021bb4afaa76d5bdbd7dbdec734076c3d5af520303fcc1ac
    Port:          <none>
    Host Port:     <none>
    Command:
      cp
      /javaagent.jar
      /otel-auto-instrumentation-java/javaagent.jar
    State:          Terminated
      Reason:       Completed
      Exit Code:    0
      Started:      Mon, 04 Aug 2025 16:47:41 +0900
      Finished:     Mon, 04 Aug 2025 16:47:41 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     500m
      memory:  256Mi
    Requests:
      cpu:        50m
      memory:     64Mi
    Environment:  <none>
    Mounts:
      /otel-auto-instrumentation-java from opentelemetry-auto-instrumentation-java (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-lm5ss (ro)
Containers:
  app:
    Container ID:   containerd://c43bb98ca11e4c7c50ac7db97136e505bd1e662915d794ad763bceee4cc7b567
    Image:          springio/gs-spring-boot-docker
    Image ID:       docker.io/springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08
    Port:           8080/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Mon, 04 Aug 2025 16:47:44 +0900
    Ready:          True
    Restart Count:  0
    Limits:
      cpu:     200m
      memory:  512Mi
    Requests:
      cpu:      100m
      memory:   256Mi
    Liveness:   http-get http://:8080/ delay=30s timeout=1s period=10s #success=1 #failure=3
    Readiness:  http-get http://:8080/ delay=10s timeout=1s period=5s #success=1 #failure=3
    Environment:
      OTEL_NODE_IP:                                                                (v1:status.hostIP)
      OTEL_POD_IP:                                                                 (v1:status.podIP)
      OTEL_SERVICE_NAME:                                                          java-simple-server
      JAVA_OPTS:                                                                  -Dlogging.level.root=INFO -Dlogging.level.org.springframework.web=DEBUG -Dlogging.level.org.apache.catalina=DEBUG
      LOGGING_PATTERN_LEVEL:                                                      trace_id=%mdc{trace_id} span_id=%mdc{span_id} %5p
      LOGGING_PATTERN_CONSOLE:                                                    %d{yyyy-MM-dd HH:mm:ss.SSS} %5p %pid --- [%15.15t] %-40.40logger{39} : trace_id=%mdc{trace_id} span_id=%mdc{span_id} %m%n
      LOGGING_LEVEL_WEB:                                                          DEBUG
      LOGGING_LEVEL_ORG_SPRINGFRAMEWORK_WEB_SERVLET_DISPATCHERSERVLET:            DEBUG
      OTEL_LOGS_EXPORTER:                                                         otlp
      OTEL_TRACES_EXPORTER:                                                       otlp
      OTEL_METRICS_EXPORTER:                                                      otlp
      OTEL_JAVAAGENT_ENABLED_INSTRUMENTATIONS:                                    logback-mdc,log4j-mdc
      OTEL_INSTRUMENTATION_LOGBACK_MDC_ADD_BAGGAGE:                               true
      OTEL_INSTRUMENTATION_COMMON_MDC_RESOURCE_ATTRIBUTES:                        true
      OTEL_INSTRUMENTATION_LOGBACK_APPENDER_EXPERIMENTAL_CAPTURE_MDC_ATTRIBUTES:  true
      OTEL_EXPORTER_OTLP_PROTOCOL:                                                grpc
      OTEL_EXPORTER_OTLP_LOGS_PROTOCOL:                                           grpc
      OTEL_EXPORTER_OTLP_TRACES_PROTOCOL:                                         grpc
      OTEL_EXPORTER_OTLP_METRICS_PROTOCOL:                                        grpc
      JAVA_TOOL_OPTIONS:                                                           -javaagent:/otel-auto-instrumentation-java-app/javaagent.jar
      OTEL_EXPORTER_OTLP_ENDPOINT:                                                http://192.168.254.246:4317
      OTEL_RESOURCE_ATTRIBUTES_POD_NAME:                                          java-simple-server-b69588b47-7qtpc (v1:metadata.name)
      OTEL_RESOURCE_ATTRIBUTES_NODE_NAME:                                          (v1:spec.nodeName)
      OTEL_PROPAGATORS:                                                           tracecontext,baggage
      OTEL_TRACES_SAMPLER:                                                        parentbased_traceidratio
      OTEL_TRACES_SAMPLER_ARG:                                                    0.1
      OTEL_RESOURCE_ATTRIBUTES:                                                   k8s.cluster.name=flash-cluster-1,k8s.container.name=app,k8s.deployment.name=java-simple-server,k8s.namespace.name=observability,k8s.node.name=$(OTEL_RESOURCE_ATTRIBUTES_NODE_NAME),k8s.pod.name=$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME),k8s.replicaset.name=java-simple-server-b69588b47,service.instance.id=observability.$(OTEL_RESOURCE_ATTRIBUTES_POD_NAME).app,service.namespace=observability
    Mounts:
      /otel-auto-instrumentation-java-app from opentelemetry-auto-instrumentation-java (rw)
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-lm5ss (ro)

...

Deployment에는 영향이 없고 정확히 pod 생성하는 API만 가로채서 작업하는것을 기억했다면..
먼저 deployment를 살펴보는 짓은 하지 않았을 것이다ㅠㅠ

문제는 각 언어별로 sdk/agent가 다르기 때문에, 각각 테스트가 필요하다는 것이다....
그리고 항상 100%는 아니고, 언어별로 log-trace 연결 가능한 범위가 다르기 때문에 이 점도 주의해야한다.

또 하나 느낀건.. 직접 써보니까 제약사항이 좀 많은 것 같다는 것이다.
아직은 Java, .NET 정도만 테스트를 해봤는데, 고객사에게 이러이러한 것은 준비되어야 한다고 명확히 안내가 나가야할 것 같다.

profile
무설탕 음료를 좋아합니다

0개의 댓글