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/otelhttp

SDK 초기화

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
}

트러블슈팅

트레이스가 수집되지 않음

  1. Exporter 연결 확인:

    // 디버그 로깅 추가
    import "go.opentelemetry.io/otel/sdk/trace"
    
    tp := trace.NewTracerProvider(
        trace.WithBatcher(exporter,
            trace.WithBatchTimeout(5*time.Second),
        ),
    )
  2. 컨텍스트 전파 확인:

    // ctx가 올바르게 전달되는지 확인
    ctx, span := tracer.Start(ctx, "operation")
    defer span.End()

스팬이 연결되지 않음

  1. 모든 함수에 context.Context 전달 확인
  2. HTTP/gRPC 계측 라이브러리 사용 확인
  3. tracer.Start(ctx, ...) 사용 확인

다음 단계

  • 자동 계측 - APM Agent (eBPF) 상세 설정
  • 트레이스 - 트레이스 분석 방법
  • 서비스 - 서비스 성능 모니터링