Go
최종 수정: 2026. 1. 16.
Go 계측
Go 애플리케이션에 OpenTelemetry를 적용하는 방법을 안내합니다.
개요
Go는 컴파일 언어 특성상 자동 계측이 제한적이지만, OpenTelemetry SDK를 통해 효과적으로 계측할 수 있습니다.
계측 옵션
| 방식 | 설명 | 코드 변경 |
|---|---|---|
| APM Agent (eBPF) | 커널 레벨 자동 감지 | 불필요 |
| SDK 수동 계측 | 코드에 직접 추가 | 필요 |
| 라이브러리 계측 | 프레임워크별 래퍼 | 최소 |
참고: Go는 컴파일 언어이므로 OTel Operator의 자동 주입이 제한적입니다. APM Agent (eBPF) 또는 수동 계측을 권장합니다.
APM Agent (eBPF) (권장)
Go 애플리케이션은 APM Agent를 통해 코드 변경 없이 계측할 수 있습니다.
장점
- ✅ 코드 변경 불필요
- ✅ 빌드 과정 변경 불필요
- ✅ HTTP, gRPC 자동 감지
- ✅ 네트워크 플로우 시각화
확인 방법
# APM Agent 파드 상태 확인
kubectl get pods -n skuber-observability -l app.kubernetes.io/name=skuber-apm-agent
# Go 애플리케이션 감지 확인
kubectl logs -n skuber-observability -l app.kubernetes.io/name=skuber-apm-agent | grep "Go"수동 계측
더 상세한 트레이싱이 필요한 경우 OpenTelemetry SDK를 사용합니다.
의존성 추가
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/sdk
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc
go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttpSDK 초기화
package main
import (
"context"
"log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
// OTLP Exporter 생성
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("otel-collector:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
// 리소스 정의
res, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("order-service"),
semconv.ServiceVersion("1.0.0"),
attribute.String("environment", "production"),
),
)
if err != nil {
return nil, err
}
// TracerProvider 생성
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
// 전역 TracerProvider 설정
otel.SetTracerProvider(tp)
// 컨텍스트 전파자 설정
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
),
)
return tp, nil
}
func main() {
ctx := context.Background()
// 트레이서 초기화
tp, err := initTracer(ctx)
if err != nil {
log.Fatal(err)
}
defer func() {
if err := tp.Shutdown(ctx); err != nil {
log.Printf("Error shutting down tracer provider: %v", err)
}
}()
// 애플리케이션 시작
startServer()
}트레이스 생성
기본 스팬 생성
package order
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
var tracer = otel.Tracer("order-service")
type OrderService struct{}
func (s *OrderService) CreateOrder(ctx context.Context, customerID string, items []Item) (*Order, error) {
// 스팬 시작
ctx, span := tracer.Start(ctx, "CreateOrder",
trace.WithAttributes(
attribute.String("customer.id", customerID),
attribute.Int("items.count", len(items)),
),
)
defer span.End()
// 비즈니스 로직
order, err := s.processOrder(ctx, customerID, items)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// 결과 속성 추가
span.SetAttributes(
attribute.String("order.id", order.ID),
attribute.Float64("order.total", order.Total),
)
return order, nil
}중첩 스팬
func (s *OrderService) processOrder(ctx context.Context, customerID string, items []Item) (*Order, error) {
ctx, span := tracer.Start(ctx, "processOrder")
defer span.End()
// 하위 스팬은 자동으로 부모와 연결됨
if err := s.validateInventory(ctx, items); err != nil {
return nil, err
}
pricing, err := s.calculatePricing(ctx, items)
if err != nil {
return nil, err
}
order, err := s.saveOrder(ctx, customerID, items, pricing)
if err != nil {
return nil, err
}
return order, nil
}
func (s *OrderService) validateInventory(ctx context.Context, items []Item) error {
ctx, span := tracer.Start(ctx, "validateInventory",
trace.WithAttributes(
attribute.Int("items.count", len(items)),
),
)
defer span.End()
// 재고 확인 로직
for _, item := range items {
if err := s.checkStock(ctx, item); err != nil {
span.RecordError(err)
return err
}
}
return nil
}메트릭 생성
package metrics
import (
"context"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
)
var (
meter = otel.Meter("order-service")
ordersCreated metric.Int64Counter
orderValue metric.Float64Histogram
)
func InitMetrics(ctx context.Context) (*sdkmetric.MeterProvider, error) {
exporter, err := otlpmetricgrpc.New(ctx,
otlpmetricgrpc.WithEndpoint("otel-collector:4317"),
otlpmetricgrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(
sdkmetric.NewPeriodicReader(exporter),
),
)
otel.SetMeterProvider(mp)
// 메트릭 초기화
ordersCreated, _ = meter.Int64Counter("orders.created",
metric.WithDescription("Number of orders created"),
)
orderValue, _ = meter.Float64Histogram("order.value",
metric.WithDescription("Order value distribution"),
metric.WithUnit("USD"),
)
return mp, nil
}
// 메트릭 기록
func RecordOrderCreated(ctx context.Context, customerType string) {
ordersCreated.Add(ctx, 1, metric.WithAttributes(
attribute.String("customer.type", customerType),
))
}
func RecordOrderValue(ctx context.Context, value float64, currency string) {
orderValue.Record(ctx, value, metric.WithAttributes(
attribute.String("currency", currency),
))
}프레임워크별 통합
net/http
package main
import (
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func main() {
// HTTP 핸들러 래핑
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 핸들러 로직
w.Write([]byte("Hello, World!"))
})
// otelhttp로 계측
wrappedHandler := otelhttp.NewHandler(handler, "my-server")
http.ListenAndServe(":8080", wrappedHandler)
}Gin
package main
import (
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin"
)
func main() {
r := gin.Default()
// OTel 미들웨어 추가
r.Use(otelgin.Middleware("order-service"))
r.POST("/orders", func(c *gin.Context) {
ctx := c.Request.Context()
// 커스텀 스팬 추가
ctx, span := tracer.Start(ctx, "processOrderRequest")
defer span.End()
// 비즈니스 로직
order := createOrder(ctx, c.Request.Body)
c.JSON(200, order)
})
r.Run(":8080")
}Echo
package main
import (
"github.com/labstack/echo/v4"
"go.opentelemetry.io/contrib/instrumentation/github.com/labstack/echo/otelecho"
)
func main() {
e := echo.New()
// OTel 미들웨어 추가
e.Use(otelecho.Middleware("order-service"))
e.POST("/orders", func(c echo.Context) error {
ctx := c.Request().Context()
order := createOrder(ctx, c.Request().Body)
return c.JSON(200, order)
})
e.Start(":8080")
}gRPC
package main
import (
"net"
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
func main() {
lis, _ := net.Listen("tcp", ":50051")
// gRPC 서버에 OTel 인터셉터 추가
server := grpc.NewServer(
grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
)
// 서비스 등록
pb.RegisterOrderServiceServer(server, &orderServer{})
server.Serve(lis)
}HTTP 클라이언트 계측
표준 http.Client
package main
import (
"context"
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
func callExternalService(ctx context.Context, url string) (*http.Response, error) {
// OTel 계측된 HTTP 클라이언트
client := http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
return client.Do(req)
}gRPC 클라이언트
package main
import (
"context"
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
)
func newGRPCClient(ctx context.Context, addr string) (*grpc.ClientConn, error) {
return grpc.DialContext(ctx, addr,
grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)
}환경 변수 설정
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% 샘플링환경 변수 읽기
func getOTLPEndpoint() string {
endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
if endpoint == "" {
return "otel-collector:4317"
}
return endpoint
}트러블슈팅
트레이스가 수집되지 않음
Exporter 연결 확인:
// 디버그 로깅 추가 import "go.opentelemetry.io/otel/sdk/trace" tp := trace.NewTracerProvider( trace.WithBatcher(exporter, trace.WithBatchTimeout(5*time.Second), ), )컨텍스트 전파 확인:
// ctx가 올바르게 전달되는지 확인 ctx, span := tracer.Start(ctx, "operation") defer span.End()
스팬이 연결되지 않음
- 모든 함수에
context.Context전달 확인 - HTTP/gRPC 계측 라이브러리 사용 확인
tracer.Start(ctx, ...)사용 확인
다음 단계
- 자동 계측 - APM Agent (eBPF) 상세 설정
- 트레이스 - 트레이스 분석 방법
- 서비스 - 서비스 성능 모니터링