signoz를 사용한 모니터링 환경을 구성하는 중이다.
signoz ui에서 log-trace 간의 연관관계가 존재하지 않아 분석에 한계가 있다.
스크린샷에서 보다시피 trace 화면에서 Go to related logs 버튼을 클릭하면, 연관된 로그가 존재하지 않는다고 나온다.


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

trace_id, span_id가 존재하지 않아 trace와 연관관계가 생성되지 않는다.
이 trace_id, span_id는 애플리케이션에서 추가해야하는 작업이다.
이것은 sdk/Agent가 없이는 불가능하다.
자사 모니터링 시스템을 구축하는 상황이라면, 애플리케이션 코드 레벨에서 SDK를 심고 사용하면 된다.
그런데 우리같은 모니터링 솔루션이라면..?
고객사의 소스코드 수정을 할 수는 없을 것이다. 이러면 귀찮아서 아무도 안 쓸거다.
이런 경우, SDK/Agent를 주입하는 방식으로 log-trace correlation을 만들어준다.
다른 상용 모니터링 솔루션(Datadog, New Relic, Splunk 등)들도 이런 방식을 채택한다.
Kubernetes Admission Controller를 사용하여 애플리케이션 파드에 sdk를 추가하는 방식을 사용한다.
짧게 말하자면, Pod가 생성되는 API를 중간에 가로채서 SDK/Agent를 주입하는 것이다.
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 스펙으로 실제 생성
signoz 공식 문서를 확인하면 이러한 log-trace correlation 이슈가 있을 경우 OTel SDK 사용을 권장한다.
Correlate Traces and Logs
https://github.com/open-telemetry/opentelemetry-operator
표준 otel operator에서도 Admission Controller 사용을 지원하기에, 이것을 사용하여 log-trace correlation을 구현하면 될 것 같았다.
찾아본 결과 다음과 같은 필수 구성요소 목록을 확인할 수 있었고, 순서대로 배포해서 테스트했다!
cert-manager는 OpenTelemetry Operator 때문에 필요하다.
그리고 OpenTelemetry Operator는 모니터링 대상 클러스터에 필수적으로 배포되어야 한다.
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"
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
드디어 log와 trace간의 연관관계가 잡히기 시작했따..!
Go to related logs 버튼을 누르면, 이 trace와 연관된 로그를 확인할 수 있었다.
단지 샘플앱이라 아주 기본적인 로깅밖에 없지만, 좀 더 복잡한? 앱을 배포하면 다양한 로그를 확인 할 수 있을 것이다.


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

그런데 정말로 k8s Admission Webhook이 동작한건지 확인해보고 싶었다.
처음에는 샘플앱 deployment를 kubectl edit deploy 명령어로 확인해봤다.
엥.. 그런데 맨 첨에 배포한 yaml과 비교했을 때 별다른 차이를 알 수 없었다..
admission webhook 동작 안 한거 아냐? 그럼 어떻게 trace_id 생긴거지? 순간 혼란이 왔는데..
kubectl describe pod 명령어로 파드를 상세히 확인해보니,, webhook 동작의 흔적을 확인할 수 있었다.
# 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 정도만 테스트를 해봤는데, 고객사에게 이러이러한 것은 준비되어야 한다고 명확히 안내가 나가야할 것 같다.