Java

최종 수정: 2026. 1. 16.

Java 계측

Java 애플리케이션에 OpenTelemetry를 적용하는 방법을 안내합니다.


개요

Java는 OpenTelemetry에서 가장 성숙한 지원을 제공합니다.

계측 옵션

방식 설명 코드 변경
APM Agent (eBPF) 커널 레벨 자동 감지 불필요
OTel Java Agent JVM 에이전트 자동 계측 불필요
SDK 수동 계측 코드에 직접 추가 필요

자동 계측 (권장)

OTel Operator 사용

가장 쉬운 방법은 OTel Operator를 통한 자동 주입입니다.

Deployment 설정

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-java-app
  namespace: production
spec:
  template:
    metadata:
      annotations:
        instrumentation.opentelemetry.io/inject-java: "skuber-observability/otel-instrumentation"
    spec:
      containers:
        - name: app
          image: my-java-app:1.0.0
          ports:
            - containerPort: 8080

지원 프레임워크

자동으로 계측되는 프레임워크:

카테고리 프레임워크
Spring Web MVC, Spring WebFlux, JAX-RS, Servlet
DB JDBC, Hibernate, JPA, MyBatis
HTTP 클라이언트 OkHttp, Apache HttpClient, WebClient
메시징 Kafka, RabbitMQ, JMS
gRPC gRPC Java
캐시 Redis (Jedis, Lettuce), Memcached

Java Agent 직접 사용

Kubernetes 외부 환경이나 더 세밀한 제어가 필요한 경우:

FROM openjdk:17-slim

# OTel Java Agent 다운로드
ADD https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar /opt/opentelemetry-javaagent.jar

COPY target/myapp.jar /app/myapp.jar

ENV JAVA_TOOL_OPTIONS="-javaagent:/opt/opentelemetry-javaagent.jar"
ENV OTEL_SERVICE_NAME="my-java-app"
ENV OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4317"

CMD ["java", "-jar", "/app/myapp.jar"]

수동 계측

비즈니스 로직에 커스텀 스팬을 추가하려면 수동 계측을 사용합니다.

의존성 추가

Maven

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>io.opentelemetry</groupId>
            <artifactId>opentelemetry-bom</artifactId>
            <version>1.35.0</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-api</artifactId>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-sdk</artifactId>
    </dependency>
    <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
    </dependency>
</dependencies>

Gradle

implementation platform('io.opentelemetry:opentelemetry-bom:1.35.0')
implementation 'io.opentelemetry:opentelemetry-api'
implementation 'io.opentelemetry:opentelemetry-sdk'
implementation 'io.opentelemetry:opentelemetry-exporter-otlp'

SDK 초기화

import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.exporter.otlp.trace.OtlpGrpcSpanExporter;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.resources.Resource;
import io.opentelemetry.sdk.trace.SdkTracerProvider;
import io.opentelemetry.sdk.trace.export.BatchSpanProcessor;
import io.opentelemetry.semconv.ResourceAttributes;

public class OtelConfig {

    public static OpenTelemetry initOpenTelemetry() {
        // 리소스 정의
        Resource resource = Resource.getDefault()
            .merge(Resource.builder()
                .put(ResourceAttributes.SERVICE_NAME, "order-service")
                .put(ResourceAttributes.SERVICE_VERSION, "1.0.0")
                .build());

        // OTLP Exporter 설정
        OtlpGrpcSpanExporter spanExporter = OtlpGrpcSpanExporter.builder()
            .setEndpoint("http://otel-collector:4317")
            .build();

        // Tracer Provider 설정
        SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
            .setResource(resource)
            .addSpanProcessor(BatchSpanProcessor.builder(spanExporter).build())
            .build();

        // OpenTelemetry SDK 빌드
        OpenTelemetrySdk openTelemetry = OpenTelemetrySdk.builder()
            .setTracerProvider(tracerProvider)
            .buildAndRegisterGlobal();

        // 종료 시 정리
        Runtime.getRuntime().addShutdownHook(new Thread(tracerProvider::close));

        return openTelemetry;
    }
}

트레이스 생성

기본 스팬 생성

import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.context.Scope;

public class OrderService {

    private final Tracer tracer;

    public OrderService(OpenTelemetry openTelemetry) {
        this.tracer = openTelemetry.getTracer("order-service", "1.0.0");
    }

    public Order createOrder(OrderRequest request) {
        // 스팬 시작
        Span span = tracer.spanBuilder("createOrder")
            .setAttribute("order.customer_id", request.getCustomerId())
            .setAttribute("order.items_count", request.getItems().size())
            .startSpan();

        try (Scope scope = span.makeCurrent()) {
            // 비즈니스 로직
            Order order = processOrder(request);

            // 결과 속성 추가
            span.setAttribute("order.id", order.getId());
            span.setAttribute("order.total", order.getTotal());

            return order;
        } catch (Exception e) {
            // 에러 기록
            span.recordException(e);
            span.setStatus(StatusCode.ERROR, e.getMessage());
            throw e;
        } finally {
            span.end();
        }
    }
}

중첩 스팬

public Order processOrder(OrderRequest request) {
    Span span = tracer.spanBuilder("processOrder").startSpan();

    try (Scope scope = span.makeCurrent()) {
        // 하위 스팬은 자동으로 부모와 연결됨
        validateInventory(request.getItems());
        calculatePricing(request);
        saveOrder(request);

        return createOrderResponse();
    } finally {
        span.end();
    }
}

private void validateInventory(List<Item> items) {
    Span span = tracer.spanBuilder("validateInventory").startSpan();
    try (Scope scope = span.makeCurrent()) {
        // 재고 확인 로직
    } finally {
        span.end();
    }
}

메트릭 생성

import io.opentelemetry.api.metrics.LongCounter;
import io.opentelemetry.api.metrics.Meter;

public class OrderMetrics {

    private final LongCounter ordersCreated;
    private final LongCounter ordersFailed;

    public OrderMetrics(OpenTelemetry openTelemetry) {
        Meter meter = openTelemetry.getMeter("order-service", "1.0.0");

        this.ordersCreated = meter.counterBuilder("orders.created")
            .setDescription("Number of orders created")
            .build();

        this.ordersFailed = meter.counterBuilder("orders.failed")
            .setDescription("Number of failed orders")
            .build();
    }

    public void recordOrderCreated(String customerId) {
        ordersCreated.add(1, Attributes.of(
            AttributeKey.stringKey("customer_id"), customerId
        ));
    }

    public void recordOrderFailed(String reason) {
        ordersFailed.add(1, Attributes.of(
            AttributeKey.stringKey("reason"), reason
        ));
    }
}

Spring Boot 통합

Spring Boot Starter

<dependency>
    <groupId>io.opentelemetry.instrumentation</groupId>
    <artifactId>opentelemetry-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

application.yml 설정

otel:
  service:
    name: order-service
  exporter:
    otlp:
      endpoint: http://otel-collector:4317
  traces:
    exporter: otlp
  metrics:
    exporter: otlp
  logs:
    exporter: otlp

어노테이션 기반 계측

import io.opentelemetry.instrumentation.annotations.SpanAttribute;
import io.opentelemetry.instrumentation.annotations.WithSpan;

@Service
public class OrderService {

    @WithSpan("createOrder")
    public Order createOrder(
            @SpanAttribute("customer.id") String customerId,
            @SpanAttribute("items.count") int itemCount) {
        // 자동으로 스팬이 생성됨
        return processOrder(customerId, itemCount);
    }
}

컨텍스트 전파

HTTP 클라이언트

import io.opentelemetry.context.Context;
import io.opentelemetry.context.propagation.TextMapSetter;

// RestTemplate에 컨텍스트 전파
RestTemplate restTemplate = new RestTemplate();
restTemplate.getInterceptors().add((request, body, execution) -> {
    TextMapSetter<HttpRequest> setter = (carrier, key, value) ->
        carrier.getHeaders().set(key, value);

    GlobalOpenTelemetry.getPropagators()
        .getTextMapPropagator()
        .inject(Context.current(), request, setter);

    return execution.execute(request, body);
});

환경 변수 설정

Kubernetes에서 환경 변수로 설정:

env:
  - name: OTEL_SERVICE_NAME
    value: "order-service"
  - name: OTEL_EXPORTER_OTLP_ENDPOINT
    value: "http://otel-collector:4317"
  - name: OTEL_TRACES_SAMPLER
    value: "parentbased_traceidratio"
  - name: OTEL_TRACES_SAMPLER_ARG
    value: "0.1"  # 10% 샘플링
  - name: OTEL_RESOURCE_ATTRIBUTES
    valueFrom:
      fieldRef:
        fieldPath: metadata.labels['app']

트러블슈팅

트레이스가 수집되지 않음

  1. Exporter 엔드포인트 확인
  2. 네트워크 연결 확인
  3. 로그에서 에러 확인:
    -Dotel.javaagent.debug=true

스팬이 연결되지 않음

  1. 컨텍스트 전파 확인
  2. Scope 사용 확인
  3. 비동기 코드에서 컨텍스트 전달 확인

다음 단계

  • 자동 계측 - OTel Operator 상세 설정
  • 트레이스 - 트레이스 분석 방법
  • 서비스 - 서비스 성능 모니터링