OpenTelemetry(OTel)는 클라우드 네이티브 소프트웨어를 위한 관측성 프레임워크입니다. Traces, Metrics, Logs의 세 가지 신호를 생성, 수집, 관리하기 위한 벤더 중립적 표준을 제공합니다. CNCF의 두 번째로 활발한 프로젝트로, 업계 표준으로 자리잡고 있습니다.
OpenTelemetry란?
OpenTelemetry는 OpenTracing과 OpenCensus 프로젝트가 합쳐져 탄생했습니다:
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.api.trace.Span;
import io.opentelemetry.api.trace.SpanKind;
import io.opentelemetry.api.trace.StatusCode;
import io.opentelemetry.api.trace.Tracer;
import io.opentelemetry.api.common.Attributes;
import io.opentelemetry.api.common.AttributeKey;
import io.opentelemetry.context.Context;
import io.opentelemetry.context.Scope;
@Service
public class OrderService {
private final Tracer tracer;
public OrderService(OpenTelemetry openTelemetry) {
this.tracer = openTelemetry.getTracer("order-service", "1.0.0");
}
public Order processOrder(OrderRequest request) {
// 부모 스팬 생성
Span parentSpan = tracer.spanBuilder("processOrder")
.setSpanKind(SpanKind.SERVER)
.setAttribute("order.id", request.getOrderId())
.setAttribute("customer.id", request.getCustomerId())
.startSpan();
try (Scope scope = parentSpan.makeCurrent()) {
// 비즈니스 로직
parentSpan.addEvent("Order validation started");
// 자식 스팬 - 재고 확인
Order order = checkInventory(request);
// 자식 스팬 - 결제 처리
processPayment(order);
parentSpan.addEvent("Order processing completed");
parentSpan.setStatus(StatusCode.OK);
return order;
} catch (Exception e) {
parentSpan.setStatus(StatusCode.ERROR, e.getMessage());
parentSpan.recordException(e);
throw e;
} finally {
parentSpan.end();
}
}
private Order checkInventory(OrderRequest request) {
Span span = tracer.spanBuilder("checkInventory")
.setSpanKind(SpanKind.INTERNAL)
.startSpan();
try (Scope scope = span.makeCurrent()) {
span.setAttribute("product.count", request.getItems().size());
// 재고 확인 로직
Order order = inventoryService.check(request);
span.setAttribute("inventory.available", true);
return order;
} catch (InsufficientStockException e) {
span.setAttribute("inventory.available", false);
span.setStatus(StatusCode.ERROR, "Insufficient stock");
throw e;
} finally {
span.end();
}
}
private void processPayment(Order order) {
Span span = tracer.spanBuilder("processPayment")
.setSpanKind(SpanKind.CLIENT) // 외부 서비스 호출
.setAttribute("payment.method", order.getPaymentMethod())
.setAttribute("payment.amount", order.getTotalAmount())
.startSpan();
try (Scope scope = span.makeCurrent()) {
// 결제 처리
PaymentResult result = paymentGateway.charge(order);
span.setAttribute("payment.transaction_id", result.getTransactionId());
span.setStatus(StatusCode.OK);
} catch (PaymentException e) {
span.setStatus(StatusCode.ERROR, e.getMessage());
span.recordException(e);
throw e;
} finally {
span.end();
}
}
}
from opentelemetry import trace
from opentelemetry.trace import SpanKind, Status, StatusCode
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import Resource, SERVICE_NAME
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from functools import wraps
# TracerProvider 설정
resource = Resource.create({SERVICE_NAME: "user-service"})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317"))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("user-service", "1.0.0")
# 데코레이터를 사용한 계측
def traced(span_name=None, kind=SpanKind.INTERNAL):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
name = span_name or func.__name__
with tracer.start_as_current_span(name, kind=kind) as span:
try:
result = func(*args, **kwargs)
span.set_status(Status(StatusCode.OK))
return result
except Exception as e:
span.set_status(Status(StatusCode.ERROR, str(e)))
span.record_exception(e)
raise
return wrapper
return decorator
class UserService:
@traced("get_user", kind=SpanKind.SERVER)
def get_user(self, user_id: str) -> dict:
span = trace.get_current_span()
span.set_attribute("user.id", user_id)
# 데이터베이스 조회
user = self._fetch_from_db(user_id)
span.set_attribute("user.found", user is not None)
span.add_event("User fetched from database")
return user
@traced("fetch_from_db", kind=SpanKind.CLIENT)
def _fetch_from_db(self, user_id: str) -> dict:
span = trace.get_current_span()
span.set_attribute("db.system", "postgresql")
span.set_attribute("db.operation", "SELECT")
span.set_attribute("db.statement", f"SELECT * FROM users WHERE id = '{user_id}'")
# 실제 DB 쿼리
with self.db.cursor() as cursor:
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
result = cursor.fetchone()
return result
@traced("create_user", kind=SpanKind.SERVER)
def create_user(self, user_data: dict) -> dict:
span = trace.get_current_span()
span.set_attribute("user.email", user_data.get("email"))
# 검증
with tracer.start_as_current_span("validate_user_data") as validation_span:
self._validate(user_data)
validation_span.add_event("Validation passed")
# 저장
with tracer.start_as_current_span("save_to_db", kind=SpanKind.CLIENT) as db_span:
db_span.set_attribute("db.system", "postgresql")
user = self._save_to_db(user_data)
db_span.set_attribute("user.id", user["id"])
# 이벤트 발행
with tracer.start_as_current_span("publish_event", kind=SpanKind.PRODUCER) as event_span:
event_span.set_attribute("messaging.system", "kafka")
event_span.set_attribute("messaging.destination", "user-events")
self._publish_event("user.created", user)
return user