자동 계측과 수동 계측의 차이를 이해하고, Python/Node.js/Go 각 언어별 SDK 활용법과 커스텀 스팬/메트릭 생성을 실습합니다.
자동 계측은 코드 변경 없이 프레임워크와 라이브러리의 텔레메트리를 수집하는 방법입니다. HTTP 서버/클라이언트, 데이터베이스 드라이버, 메시지 큐 라이브러리 등을 자동으로 감지하여 스팬과 메트릭을 생성합니다.
# 자동 계측 패키지 설치
pip install opentelemetry-distro opentelemetry-exporter-otlp
# 사용 중인 라이브러리의 계측 패키지 자동 설치
opentelemetry-bootstrap -a install
# 자동 계측으로 애플리케이션 실행
opentelemetry-instrument \
--service_name user-service \
--exporter_otlp_endpoint http://collector:4317 \
python app.py자동 계측의 장점은 빠른 도입입니다. 기존 코드를 전혀 수정하지 않아도 HTTP 요청, DB 쿼리, 외부 API 호출의 스팬이 자동 생성됩니다.
수동 계측은 개발자가 직접 코드에 OTel API를 호출하여 텔레메트리를 생성하는 방법입니다. 비즈니스 로직에 특화된 스팬, 커스텀 메트릭, 도메인별 속성을 추가할 때 필요합니다.
실무에서는 자동 계측과 수동 계측을 함께 사용하는 것이 가장 효과적입니다. 자동 계측으로 인프라 수준의 텔레메트리(HTTP, DB, 메시징)를 확보하고, 수동 계측으로 비즈니스 로직의 핵심 구간(결제 처리, 주문 검증, AI 추론 등)을 보강합니다.
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.flask import FlaskInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
import logging
def init_telemetry(
service_name: str,
service_version: str,
collector_endpoint: str = "http://collector:4317",
environment: str = "production",
):
"""OpenTelemetry 3대 신호(트레이스, 메트릭, 로그) 통합 초기화"""
resource = Resource.create({
"service.name": service_name,
"service.version": service_version,
"deployment.environment": environment,
})
# --- Traces ---
trace_provider = TracerProvider(resource=resource)
trace_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint=collector_endpoint),
max_queue_size=2048,
max_export_batch_size=512,
)
)
trace.set_tracer_provider(trace_provider)
# --- Metrics ---
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint=collector_endpoint),
export_interval_millis=15000,
)
metric_provider = MeterProvider(
resource=resource,
metric_readers=[metric_reader],
)
metrics.set_meter_provider(metric_provider)
# --- Logs ---
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(
OTLPLogExporter(endpoint=collector_endpoint)
)
)
handler = LoggingHandler(level=logging.INFO, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
# --- 자동 계측 ---
FlaskInstrumentor().instrument()
RequestsInstrumentor().instrument()
SQLAlchemyInstrumentor().instrument()
return trace_provider, metric_provider, logger_providerfrom flask import Flask, jsonify, request
from telemetry import init_telemetry
from opentelemetry import trace, metrics
import logging
# 텔레메트리 초기화
init_telemetry(
service_name="order-service",
service_version="2.0.0",
environment="production",
)
app = Flask(__name__)
tracer = trace.get_tracer("order-service.handlers")
meter = metrics.get_meter("order-service.business")
logger = logging.getLogger("order-service")
# 비즈니스 메트릭
order_counter = meter.create_counter("orders.created.count")
order_amount = meter.create_histogram("orders.amount")
@app.route("/orders", methods=["POST"])
def create_order():
data = request.json
with tracer.start_as_current_span("validate-order") as span:
span.set_attribute("order.item_count", len(data["items"]))
validate_order(data)
with tracer.start_as_current_span("process-payment") as span:
span.set_attribute("payment.method", data["payment_method"])
span.set_attribute("payment.amount", data["total"])
result = process_payment(data)
# 비즈니스 메트릭 기록
order_counter.add(1, {"payment.method": data["payment_method"]})
order_amount.record(data["total"], {"currency": "KRW"})
logger.info("주문 생성 완료", extra={
"order_id": result["order_id"],
"total": data["total"],
})
return jsonify(result), 201import { NodeSDK } from "@opentelemetry/sdk-node";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
import { PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { HttpInstrumentation } from "@opentelemetry/instrumentation-http";
import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express";
import { PgInstrumentation } from "@opentelemetry/instrumentation-pg";
import { Resource } from "@opentelemetry/resources";
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
const sdk = new NodeSDK({
resource: new Resource({
[ATTR_SERVICE_NAME]: "user-service",
[ATTR_SERVICE_VERSION]: "1.0.0",
"deployment.environment": "production",
}),
traceExporter: new OTLPTraceExporter({
url: "http://collector:4317",
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: "http://collector:4317",
}),
exportIntervalMillis: 15000,
}),
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
new PgInstrumentation(),
],
});
sdk.start();
// 종료 시 정리
process.on("SIGTERM", () => {
sdk.shutdown().then(() => process.exit(0));
});Node.js에서 OTel SDK 초기화는 반드시 애플리케이션 코드보다 먼저 실행되어야 합니다. require 또는 import로 모듈이 로드되기 전에 계측이 설정되어야 자동 계측이 올바르게 동작합니다. --require 플래그로 초기화 스크립트를 먼저 로드하는 방법이 일반적입니다.
node --require ./telemetry.js app.jsimport { trace, metrics, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("user-service.handlers");
const meter = metrics.getMeter("user-service.business");
const loginCounter = meter.createCounter("user.login.count");
async function handleLogin(req: Request, res: Response) {
const span = tracer.startSpan("authenticate-user");
try {
span.setAttribute("user.email_domain", extractDomain(req.body.email));
const user = await authenticateUser(req.body);
loginCounter.add(1, { method: req.body.auth_method });
span.setStatus({ code: SpanStatusCode.OK });
res.json({ token: generateToken(user) });
} catch (error) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
span.recordException(error);
res.status(401).json({ error: "Authentication failed" });
} finally {
span.end();
}
}package telemetry
import (
"context"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)
func InitTelemetry(ctx context.Context, serviceName, serviceVersion string) (func(), error) {
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(serviceName),
semconv.ServiceVersion(serviceVersion),
semconv.DeploymentEnvironment("production"),
),
)
if err != nil {
return nil, err
}
// Trace Exporter
traceExporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("collector:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithResource(res),
sdktrace.WithBatcher(traceExporter,
sdktrace.WithBatchTimeout(5*time.Second),
sdktrace.WithMaxQueueSize(2048),
),
)
otel.SetTracerProvider(tp)
// Metric Exporter
metricExporter, err := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithEndpoint("collector:4317"),
otlpmetricgrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
mp := metric.NewMeterProvider(
metric.WithResource(res),
metric.WithReader(
metric.NewPeriodicReader(metricExporter,
metric.WithInterval(15*time.Second),
),
),
)
otel.SetMeterProvider(mp)
// 종료 함수 반환
shutdown := func() {
tp.Shutdown(ctx)
mp.Shutdown(ctx)
}
return shutdown, nil
}package handlers
import (
"context"
"net/http"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/metric"
)
var (
tracer = otel.Tracer("order-service.handlers")
meter = otel.Meter("order-service.business")
orderCounter metric.Int64Counter
)
func init() {
var err error
orderCounter, err = meter.Int64Counter("orders.processed.count",
metric.WithDescription("Total processed orders"),
)
if err != nil {
panic(err)
}
}
func HandleCreateOrder(ctx context.Context, order Order) error {
ctx, span := tracer.Start(ctx, "create-order")
defer span.End()
span.SetAttributes(
attribute.String("order.id", order.ID),
attribute.Float64("order.total", order.Total),
attribute.Int("order.item_count", len(order.Items)),
)
if err := validateOrder(ctx, order); err != nil {
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
return err
}
if err := processPayment(ctx, order); err != nil {
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
return err
}
orderCounter.Add(ctx, 1,
metric.WithAttributes(
attribute.String("payment.method", order.PaymentMethod),
),
)
span.SetStatus(codes.Ok, "")
return nil
}Java는 JVM의 바이트코드 조작 기능을 활용한 에이전트 방식의 자동 계측을 지원합니다. 코드 변경이 전혀 필요 없으며, JVM 시작 시 에이전트를 주입하면 됩니다.
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=payment-service \
-Dotel.exporter.otlp.endpoint=http://collector:4317 \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-jar payment-service.jarJava 에이전트는 Spring Boot, Servlet, JDBC, Hibernate, Kafka, gRPC 등 수십 개의 라이브러리를 자동으로 계측합니다. Kubernetes에서는 Init Container로 에이전트 JAR을 주입하는 패턴이 일반적입니다.
서비스 간 컨텍스트를 수동으로 전파하는 방법입니다. 자동 계측이 지원하지 않는 커스텀 프로토콜이나 비동기 처리에서 필요합니다.
from opentelemetry import trace, context
from opentelemetry.propagate import inject, extract
tracer = trace.get_tracer("propagation-example")
# --- 발신측: 컨텍스트를 캐리어에 주입 ---
def send_message(message: dict):
with tracer.start_as_current_span("send-message") as span:
# 현재 컨텍스트를 메시지 헤더에 주입
headers = {}
inject(headers)
message["_trace_headers"] = headers
queue.publish(message)
# --- 수신측: 캐리어에서 컨텍스트를 추출 ---
def receive_message(message: dict):
# 메시지 헤더에서 컨텍스트 추출
ctx = extract(message.get("_trace_headers", {}))
# 추출된 컨텍스트를 부모로 하는 새 스팬 생성
with tracer.start_as_current_span(
"process-message",
context=ctx,
) as span:
span.set_attribute("message.type", message["type"])
process(message)이번 장에서는 OTel SDK를 활용한 실전 계측을 학습했습니다. 자동 계측으로 인프라 수준의 텔레메트리를 빠르게 확보하고, 수동 계측으로 비즈니스 로직에 특화된 관측을 추가하는 조합 전략을 다루었습니다. Python, Node.js, Go, Java 각 언어별 SDK 설정과 활용법을 실습했으며, 커스텀 프로토콜에서의 컨텍스트 전파도 직접 구현해 보았습니다.
다음 장에서는 OTel Collector의 아키텍처를 심층적으로 다룹니다. Receiver, Processor, Exporter로 구성되는 파이프라인 설계, 핵심 프로세서 활용법, Kubernetes 환경에서의 배포 전략을 학습합니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
OTel Collector의 Receiver/Processor/Exporter 파이프라인, 핵심 프로세서 활용법, Kubernetes 환경에서의 DaemonSet/Deployment 배포를 학습합니다.
OpenTelemetry 로그 데이터 모델, 로그-트레이스 상관관계, 기존 로거 브릿지(Python logging, Go slog), 구조화 로그와 로그 레벨 전략을 학습합니다.
Jaeger로 분산 추적을 시각화하고, Prometheus로 메트릭을 저장/쿼리하며, Grafana로 통합 대시보드를 구성합니다. Docker Compose로 전체 스택을 실습합니다.