프로젝트

Opentelemetry 를 이용한 메트릭,트레이스,로그 수집

신민석 2025. 5. 29. 18:35

🎯 Observability 와 메트릭, 트레이스, 로그 수집


 

Observability 는 오류가 왜 발생했는지를 추적하고 분석하는 것을 목표로 가지고 있습니다. 즉 어떤 요청이 들어왔을때 어느 계층, 어느 부분에서 문제가 발생하고 개선해야지는 찾아야 하는 것을 의미합니다. 

 

 

이러한 Observability 를 구성하는 요소는 크게 3가지 입니다. Metric, Trace, Log 따라서 애플리케이션을 운영한다면 이 3가지 요소를 모니터링 하는 환경을 만들고 시스템의 병목 지점을 분석할 수 있어야 합니다. 이제 각각의 정보들을 어떻게 수집했는지에 대해 설명드리겠습니다.

 

 

Metric/Trace/Log 수집 흐름

 

 

로그 수집

 

저는 Opentelemetry 에서 로그를 수집하는게 아닌, 별도의 Promtail 설정을 통해 수집했습니다. Opentelemetry 는 로그보다는 트레이스,메트릭 중심의 데이터를 수집하는데 특화 되어 있고 Exporter + Processor 설정을 추가적으로 작업해야 합니다. 반대로 Promtail 을 통해 Loki 로 로그를 보내는 방식은 Promtail 만 설치하면 환경 설정이 끝나고 강력한 텍스트 검색에 대한 기능 을 지원 해주기 때문에 로그는 Promtail -> Loki 를 통해 수집하도록 만들었습니다.

 

 

애플리케이션(Spring) -> 별도의 로그 파일 저장 -> Promtail 에서 로그 긁어옴 -> Loki 로 전송 -> Grafana 에서 확인

 

트레이스 수집 

 

트레이스는 Opentelemetry 를 통해 감지하고 Opentelemetry Collector 로 보낸 뒤 Tempo 에서 모니터링 하는 방식을 선택했습니다. 하지만 여기서 의문점이 하나 있었습니다.

 

왜 Opentelemetry 에서 바로 Tempo 로 트레이스를 전송하지 않는거지 ? 

 

물론 기술적으로는 가능하다고 합니다. 하지만 Opentelemetry 에서 Opentelemetry Collector 로 보내는 이유는 여러 저장소로 보낼 수 있는 파이프라인 역할 을 하기 때문입니다. 만약 Tempo 외에 다른 저장소로 동시에 트레이스나 메트릭을 보낼수도 있고 샘플링, 필터링, 집계등을 거쳐 데이터를 가공할 수 있는 장점도 있습니다. 이러한 Collector 는 데이터를 모으고 가공해 여러 백엔드로 보낼 수 있게 해주는 허브 같은 존재 라서 운영과 확장성, 유지 보수가 쉽다는 장점이 있습니다. 이러한 이유 때문에 OpenTelemetry → OpenTelemetry Collector → Tempo 순으로 구성하는 게 일반적이고 권장되는 아키텍처입니다.

 

Opentelemetry 를 이용해 트레이스, 메트릭을 수집하는 방법은 크게 두가지가 있습니다.

 

1.SDK 를 이용한 수집

2.Java agent 를 이용한 수집

 

 

SDK 를 이용해 Opentelemtry 를 설정하면 커스터마이징에 좀 더 자유롭지만 코드를 직접 작성해서 수집해야 하며, 자동 추적은 안 됩니다. 반대로 Java agent 를 이용해 수집하면 트레이스와 메트릭을 자동수집하고 별도의 코드 작성이 필요 없다는 장점때문에 저는 2번 방법을 선택했습니다.

 

 

javaagent는 JVM이 애플리케이션을 실행하기 직전에 자동으로 로드되어, 클래스 로딩 과정에 개입하여 바이트코드를 조작합니다. 이를 통해 코드 수정 없이 런타임에 트레이스 수집을 자동화할 수 있게 됩니다.

 

 

 

애플리케이션(Spring) -> Opentelemetry 자동 감지 -> Opentelemetry Collector 전송 -> Opentelemetry Collector 가 Tempo 로 push  -> Grafana 에서 확인

 

메트릭 수집 

 

메트릭은 CPU 사용률, 메모리 사용량등 수치적으로 나타낼 수 있는 데이터 입니다. 메트릭은 트레이스를 수집하는 방식과 동일하게 Opentelemetry 에서 자동으로 수집해 이를 Opentelemetry Collector 로 보낸 뒤 Prometheus 가 긁어와 Grafana 에서 시각화 하는방식을 선택했습니다.

 

 

애플리케이션(Spring) -> Opentelemetry 자동 감지 -> Opentelemetry Collector 전송 -> Opentelemetry Collector 에서  Promethus 가 긁어옴 -> Grafana 에서 확인

 

 

🎯 Opentelemetry 를 이용한 애플리케이션 트레이스 수집


 

Opentelemetry java agent 등록 (build.gradle)

 

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.5.0'
	id 'io.spring.dependency-management' version '1.1.7'
	id 'com.google.cloud.tools.jib' version '3.3.1'
}

group = 'com.ticketon'
version = '0.0.1-SNAPSHOT'

configurations {
	agent
	compileOnly {
		extendsFrom annotationProcessor
	}
}

java {
	toolchain {
		languageVersion = JavaLanguageVersion.of(21)
	}
}

repositories {
	mavenCentral()
}

bootRun {
	jvmArgs = [
			"-javaagent:${buildDir}/agent/opentelemetry-javaagent.jar",
			"-Dotel.metrics.exporter=otlp",
			"-Dotel.exporter.otlp.metrics.endpoint=http://localhost:4317",
			"-Dotel.exporter.otlp.protocol=grpc",
			"-Dotel.exporter.otlp.endpoint=http://localhost:4317",
			"-Dotel.resource.attributes=service.name=ticketon-service",
			"-Dotel.javaagent.debug=true",
			"-Dotel.logs.exporter=none"
	]
}

bootRun.dependsOn("copyAgent")

dependencies {
	agent 'io.opentelemetry.javaagent:opentelemetry-javaagent:2.7.0'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'com.mysql:mysql-connector-j'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}

tasks.register("copyAgent", Copy) {
	from(configurations.agent)
	into(layout.buildDirectory.dir("agent"))
	rename { 'opentelemetry-javaagent.jar' }
}

jib {
	from {
		image = "eclipse-temurin:21-jdk"
		platforms {
			platform {
				architecture = "arm64"
				os = "linux"
			}
		}
	}
	extraDirectories {
		paths {
			path {
				setFrom(layout.buildDirectory.dir("agent"))
				into = "/otelagent"
			}
		}
	}
	container {
		mainClass = "com.huisam.orderapplication.OrderApplicationKt"
		jvmFlags = [
				"-javaagent:${buildDir}/agent/opentelemetry-javaagent.jar",
				"-Dotel.metrics.exporter=otlp",
				"-Dotel.exporter.otlp.metrics.endpoint=http://localhost:4317",
				"-Dotel.exporter.otlp.protocol=grpc",
				"-Dotel.exporter.otlp.endpoint=http://localhost:4317",
				"-Dotel.resource.attributes=service.name=ticketon-service",
				"-Dotel.javaagent.debug=true",
				"-Dotel.logs.exporter=none"
		]
	}
}

jibDockerBuild.dependsOn("copyAgent")

test {
	useJUnitPlatform()
}

 

 

Gradle 설정에서는 OpenTelemetry Java Agent를 사용하여 애플리케이션의 트레이스, 메트릭, 리소스 정보 등을 자동으로 수집하고 OTLP Collector로 전송하는 구성이며, bootRun과 jib을 통해 로컬 실행 및 Docker 이미지 생성 시에도 agent가 작동하게 해둔 상태입

니다.

 

 

bootRun.jvmArgs

jvmArgs = [
    "-javaagent:${buildDir}/agent/opentelemetry-javaagent.jar",
    "-Dotel.metrics.exporter=otlp",
    "-Dotel.exporter.otlp.metrics.endpoint=http://localhost:4317",
    "-Dotel.exporter.otlp.protocol=grpc",
    "-Dotel.exporter.otlp.endpoint=http://localhost:4317",
    "-Dotel.resource.attributes=service.name=ticketon-service",
    "-Dotel.javaagent.debug=true",
    "-Dotel.logs.exporter=none"
]

 

JVM에 OTel 에이전트를 주입합니다. Agent가 자동으로 Spring, JDBC, Web 요청 등을 감지해서 trace 와 metric 생성 하기 위한 설정입니다.

 

jvmFlags

jvmFlags = [
    "-javaagent:${buildDir}/agent/opentelemetry-javaagent.jar",
    ...
]

 

위의 bootRun.jvmArgs 설정과 완전히 동일한 설정을 Docker 컨테이너에 적용하는 부분입니다. 이렇게 해야 Docker에서도 agent가 자동 트레이싱 & 메트릭 수집을 하게 됩니다.

 

copyAgent Task

tasks.register("copyAgent", Copy) {
	from(configurations.agent)
	into(layout.buildDirectory.dir("agent"))
	rename { 'opentelemetry-javaagent.jar' }
}

 

이 Task는 OpenTelemetry Agent 라이브러리를 build/agent/ 디렉토리에 복사합니다.
bootRun과 jibDockerBuild 실행 전에 이 작업이 필요하므로 dependsOn("copyAgent")로 연결되어 있습니다.

 

 

여기까지가 OpenTelemetry 를 Java Agent 에 등록하는 설정입니다.

 

 

자! 이제 OpenTelemetry 에서 수집한 트레이스, 메트릭을 OpenTelemetry Collector 로 보내고, 트레이스는 Tempo, 메트릭은 Prometheus 로 보내는 설정에 대해 말씀드리겠습니다.

 

 

docker-compose.yml

version: '3.7'

services:
  tempo:
    image: grafana/tempo:2.4.1
    container_name: tempo
    ports:
      - "3200:3200"    # 메트릭, 상태 확인 등

    volumes:
      - ./tempo/tempo-config.yml:/etc/tempo-config.yml
    command: --config.file=/etc/tempo-config.yml
    networks:
      - monitoring

  otel-collector:
    image: otel/opentelemetry-collector-contrib:0.93.0
    container_name: otel-collector
    ports:
      - "4317:4317"  # OTLP gRPC 수신 포트 (애플리케이션이 Collector로 보낼 때 사용)
      - "4319:4319"  # 일반적으로 OTLP gRPC exporter용 포트 아님 (혼동 주의)
      - "8889:8889"  # Prometheus metrics endpoint (ex: /metrics)
    volumes:
      - ./otel/otel-collector-config.yml:/etc/otel/otel-collector-config.yml
    command: --config=/etc/otel/otel-collector-config.yml
    networks:
      - monitoring
    depends_on:
      - tempo 

  loki:
    image: grafana/loki:2.9.4
    container_name: loki
    ports:
      - "3100:3100"
    command: -config.file=/etc/loki/local-config.yml
    volumes:
      - ./loki/loki-config.yml:/etc/loki/local-config.yml
    networks:
      - monitoring

  prometheus:
    image: prom/prometheus
    container_name: prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - monitoring

  grafana:
    image: grafana/grafana
    container_name: grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin
    depends_on:
      - prometheus
      - loki
    networks:
      - monitoring
    volumes:
      - grafana-storage:/var/lib/grafana

  promtail:
    image: grafana/promtail:2.9.4
    container_name: promtail
    command: -config.file=/etc/promtail/config.yml
    ports:
      - "9080:9080"
    volumes:
      - ./loki/promtail-config.yml:/etc/promtail/config.yml
      - /Users/sinminseok12/Desktop/Ticket-on/ticketon/logs:/var/log
    depends_on:
      - loki
    networks:
      - monitoring

networks:
  monitoring:
    driver: bridge


volumes:
  grafana-storage:

 

 

실행할 docker-compose 파일입니다. 총 6개의 이미지가 있는데 각각의 이미지 port 가 충돌하지 않게 조심해야 합니다. 

 

./otel/otel-collector-config.yml

receivers:
  otlp:
    protocols:
      http:
      grpc:

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889" # otlp 에서 prometheus 로 보내는 엔드포인트 

  otlp/tempo:
    endpoint: tempo:4317 # otlp 에서 tempo 로 보내는 포트가 4317 
    tls:
      insecure: true

service:
  pipelines:
    metrics:
      receivers: [otlp]
      exporters: [prometheus]

    traces:
      receivers: [otlp]
      exporters: [otlp/tempo]

 

exporters 를 확인해보면, prometheus 와 otpl/tempo 설정이 있는데 Openteletry Collectir 에서 수집한 메트릭, 트레이스 정보를 각각 Prometheus, Tempo 로 보내는 설정입니다.

 

./prometheus/prometheus.yml

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'otel-collector'
    static_configs:
      - targets: ['otel-collector:8889']

 

targets: ['otel-collector:8889'] 는 Collector 가 prometheus exporter 로 노출하는 endposit 에 요청합니다. 즉 otel-collector-config.yml 에서 설정한 exporter 로 부터 메트릭을 긁어와 Prometheus 에서 수집하는 설정입니다.

 

 

 

./tempo/tempo-config.yml

auth_enabled: false

server:
  http_listen_port: 3200
  grpc_listen_port: 4318 # gRPC 요청을 수신하는 기본 포트

distributor: # OTLP(오픈텔레메트리 프로토콜) 수신을 위해 gRPC 서버를 띄우는 포트
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317 # OTLP gRPC 프로토콜을 통해 들어오는 데이터를 받기 위한 "리시버(receiver)"가 4317
        http:
          endpoint: 0.0.0.0:55681 # OTLP HTTP 프로토콜 수신 포트

ingester:
  lifecycler:
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
  trace_idle_period: 10s
  max_block_duration: 5m

compactor:
  compaction:
    compaction_window: 1h

storage:
  trace:
    backend: local
    local:
      path: /tmp/tempo
    wal:
      path: /tmp/tempo/wal


querier: {}

query_frontend: {}

memberlist:
  join_members: ["127.0.0.1"]  # ring 구성에 필수

overrides:
  metrics_generator_processors: []

 

 

 

auth_enabled : false

 

인증 사용 여부를 나타냅니다. 개발 환경에서는 false 로 설정하고 운영 환경에서는 별도의 인증 설정이 필요합니다. 

 

server:
  http_listen_port: 3200
  grpc_listen_port: 4318

 

 

Tempo 자체가 노출하는 API 서버 설정입니다. http_listen_port 는 Tempo 의 HTTP 상태를 확인하기 위한 설정입니다. grpc_listen_port 는 Tempo 가 gRPC 로 trace 요청을 받을 때 사용하는 포트 입니다.

 

distributor:
  receivers:
    otlp:
      protocols:
        grpc:
          endpoint: 0.0.0.0:4317
        http:
          endpoint: 0.0.0.0:55681

 

Trace 를 수신하기 위한 설정입니다. OTel Agent 나 Collector 가 트레이스를 Tempo 로 보낼 수 있게 해주는 핵심 설정입니다.

 

 

- endpoint: 0.0.0.0:4317 → Tempo가 gRPC로 OTLP 데이터를 받는 수신 포트

- endpoint: 0.0.0.0:55681 → Tempo가 HTTP로 OTLP 데이터를 받는 수신 포트

 

 

Tempo가 OTLP 리시버 역할을 하기 위해 열어둔 포트 설정입니다. OpenTelemetry Collector는 이 주소(4317 또는 55681)로 trace 데이터를 Push합니다.

 

 

 

 

즉, OTel Agent나 Collector가 트레이스를 Tempo로 보낼 수 있게 해주는 핵심 부분입니다.

 

 

storage:
  trace:
    backend: local
    local:
      path: /tmp/tempo
    wal:
      path: /tmp/tempo/wal

 

 

Trace 를 어디에 저장할지를 정의합니다. backend: local 는 로컬 디스크에 저장함을 의미하고 각각 저장 위치(path: /tmp/tempo), WAL 저장 경로 (wal.path: /tmp/tempo/wal) 를 의미합니다.

 

 

자! 여기까지 OpenTelemetry를 통해 메트릭트레이스를 수집해 각각 PrometheusTempo로 보내는 설정을 완료했습니다.
로그 관련 설정은 따로 설명드릴 내용이 많지 않아, 아래 GitHub 저장소를 통해 전체 코드를 참고해주시면 감사하겠습니다.

 

 

이렇게  Observability(가시성) 요소들을 어떻게 수집하고 관리할 수 있는지 실제로 설정해보며 이해해보았습니다. 메트릭은Prometheus에서 수집하고, 트레이스는 Tempo로 전송하며, 로그는 Promtail → Loki를 통해 처리하게 되면 Grafana라는 단일 대시보드에서 메트릭, 로그, 트레이스를 모두 확인할 수 있는 완전한 모니터링 환경이 갖춰지게 됩니다.

 

 

물론 프로젝트의 성격이나 개발자의 스타일, 인프라 환경에 따라 Observability를 구축하고 관리하는 방식은 정말 다양합니다.
저 같은 경우는 가능한 단순하면서도 표준을 따르는 방식으로,

  • OpenTelemetry Agent를 사용한 무관섭 방식의 데이터 수집,
  • Collector를 통한 중앙 집중형 라우팅과 필터링,
  • Grafana를 중심으로 한 통합 관측 플랫폼 구성  을 선호합니다.

 

 

관측 가능성은 단순한 로그 수집이 아니라, 시스템의 상태를 명확하게 파악하고 빠르게 대응할 수 있게 해주는 핵심 도구입니다. 이렇게 설정한 모니터링 환경을 통해 다양한 부하 테스트를 진행해보며 성능을 개선하고 안정적인 애플리케이션을 만드는 연습도 필요할 것 같습니다.