본문 바로가기
카테고리 없음

OpenTelemetry with Go

by litaro 2021. 7. 20.

 

https://opentelemetry.io/

 

OpenTelemetry

The OpenTelemetry Project Site

opentelemetry.io

 

Jeager를 하면서 https://litaro.tistory.com/entry/Jaeger-with-Go OpenTracing 사이트에 가보니, 이제 OpenTelemetry 로 merge가 되는 중이라고 했다. 아 또 새롭게 공부해야하나...

다행히 OpenTelemetry는 완전 바꾸는 것이 아니라 OpenTracing과 OpenCesus의 장점을 잘 통합해서 Traces, Metrics, Loggs의 기능을 표준화된 API로 만들겠다는 것이었다. 사용자입장에서는 하나의 표준 API가 나오게 된다면 뒷단의 실체 구현체들을 필요에 따라서 코드 수정없이 바꿀수 있으니 좋은 방향인것 같다.

OpenTelemetry is a set of APIs, SDKs, tooling and integrations that are designed for the creation and management of telemetry data such as traces, metrics, and logs.

 

그럼 OpenTelemetry를 어떻게 사용하는지 해보자.

 

OpenTelemetry 구조


그림처럼 Otel Collector가 애플리케이션과 Backend의 실체 구현체 (예를 들어, Jaeger or Prometheus) 중간에 들어가서 분산추적 데이터를 전달하게 된다. 이러한 Otel Collector를 중간에 활용하는 방법의 장점은, 여러 backend를 하나의 collector로 연동가능하고 코드 수정 없이 backends를 변경할 수 있다는 것이다. 

Collector를 사용하는 방법은 두가지인데, 하나는 같은 Host (kubernetes 라면 같은 Pod)에 side car 형태로 실행되는 Agent 이고, 다른 하나는 독립적으로 실행되는 Standalone Service 형태이다.

Single OpenTelemetry Collector Gateway Running Local Node OpenTelemetry Collector Agents and OpenTelemetry Collector Gateway
테스트 용도
적은 수의 Pod 사용 (<100)
상당수의 Pod를 실행하는 경우
트래픽 양이 많은 경우
Client가 UDP 프로토콜을 사용하는 경우

Collector의 구조는 아래와 같다.

  • Receivers: Data를 Collector로 수집하는 부분
  • Processors : Data를 처리하는 부분
  • Exporters: Data를 backend 분석 tool로 보내는 부분

 

OpenTelemetry Collector 설치


목표: docker desktop 에 opentelemetry 배포하고 local server에서 연동

이미 Jeager는 https://litaro.tistory.com/entry/Jaeger-with-Go  에서 설치한 상태에서 OpenTelemetry로 연동해보자.

참고: https://github.com/open-telemetry/opentelemetry-go/tree/main/example/otel-collector

OpenTelemetry Config

OpenTelemetry 내부 구조 설정 Receivers, Processors, Exporters 세가지 설정을 하게 된다.

나는 Traces 용으로 Jaeger만 붙여보는것이니깐 아래와 같이 간단한 설정을 하면 된다.

apiVersion: v1
kind: ConfigMap
metadata:
  name: otel-collector-conf
  namespace: observability
  labels:
    app: opentelemetry
    component: otel-collector-conf
data:
  otel-collector-config: |
    receivers:
      # Make sure to add the otlp receiver.
      # This will open up the receiver on port 4317
      otlp:
        protocols:
          grpc:
            endpoint: "0.0.0.0:4317"
    processors:
    extensions:
      health_check: {}
    exporters:
      jaeger:
        endpoint: "jaeger-collector.observability.svc.cluster.local:14250" ✅
        insecure: true
      logging:
 
    service:
      extensions: [health_check]
      pipelines:
        traces:
          receivers: [otlp]
          processors: []
          exporters: [jaeger]

OpenTelemetry Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: otel-collector
  namespace: observability
  labels:
    app: opentelemetry
    component: otel-collector
spec:
  selector:
    matchLabels:
      app: opentelemetry
      component: otel-collector
  minReadySeconds: 5
  progressDeadlineSeconds: 120
  replicas: 1 #TODO - adjust this to your own requirements
  template:
    metadata:
      labels:
        app: opentelemetry
        component: otel-collector
    spec:
      containers:
        - command:
            - "/otelcol"
            - "--config=/conf/otel-collector-config.yaml"
            # Memory Ballast size should be max 1/3 to 1/2 of memory.
            - "--mem-ballast-size-mib=683"
          env:
            - name: GOGC
              value: "80"
          image: otel/opentelemetry-collector:latest
          name: otel-collector
          resources:
            limits:
              cpu: 1
              memory: 2Gi
            requests:
              cpu: 200m
              memory: 400Mi
          ports:
            - containerPort: 4317 # Default endpoint for otlp receiver.
          volumeMounts:
            - name: otel-collector-config-vol
              mountPath: /conf
          livenessProbe:
            httpGet:
              path: /
              port: 13133 # Health Check extension default port.
          readinessProbe:
            httpGet:
              path: /
              port: 13133 # Health Check extension default port.
      volumes:
        - configMap:
            name: otel-collector-conf
            items:
              - key: otel-collector-config
                path: otel-collector-config.yaml
          name: otel-collector-config-vol

OpenTelemetry Service

apiVersion: v1
kind: Service
metadata:
  name: otel-collector
  namespace: observability
  labels:
    app: opentelemetry
    component: otel-collector
spec:
  ports:
    - name: otlp # Default endpoint for otlp receiver.
      port: 4317
      protocol: TCP
      targetPort: 4317
      nodePort: 30080
  selector:
    component: otel-collector
  type: NodePort ✅ Local Server에서 Kubernetes 에 배포된 Collector 접근하기 위해 NodePort로 배포

 

배포 결과 아래와 같다.

 

서비스 코드 적용


                           -----> name-service (service B)
my-service (service A) ---|
                           -----> greeting-service (service C)

Trace Provider 생성 (공통)

OpenTelemetry에서는 다양한 3rd-party library를 제공한다. 이를 활용하면 mongodb나 gin, http 를 통해 별도 추가 코드 없이 context만 전달하면 요청 path나 db operation 정보가 추가되어 Trace 를 한눈에 잘 파악하도록 해준다. 

func initProvider() (*sdktrace.TracerProvider, error) {
	ctx := context.Background()

	res, err := resource.New(ctx,
		resource.WithAttributes(
			// the service name used to display traces in backends
			semconv.ServiceNameKey.String(service),
		),
	)
	if err != nil {
		return nil, err
	}

	// Set up a trace exporter
	traceExporter, err := otlptracegrpc.New(ctx,
		otlptracegrpc.WithInsecure(),
		otlptracegrpc.WithEndpoint("localhost:30080"), ✅ nodeport로 외부로 열어준 port
		otlptracegrpc.WithDialOption(grpc.WithBlock()),
	)
	if err != nil {
		return nil, err
	}

	// Register the trace exporter with a TracerProvider, using a batch
	// span processor to aggregate spans before export.
	bsp := sdktrace.NewBatchSpanProcessor(traceExporter)
	tracerProvider := sdktrace.NewTracerProvider(
		sdktrace.WithSampler(sdktrace.AlwaysSample()),
		sdktrace.WithResource(res),
		sdktrace.WithSpanProcessor(bsp),
	)
	return tracerProvider, nil
}

func main() {
	tp, err := initProvider()
	if err != nil {
		log.Fatal(err)
	}

	// Register our TracerProvider as the global so any imported
	// instrumentation in the future will default to using it.
	otel.SetTracerProvider(tp)    
	✅ Remote Process에서도 TraceContext 활용하도록 Propagater 생성
	propagator := propagation.NewCompositeTextMapPropagator(propagation.Baggage{}, propagation.TraceContext{})
	otel.SetTextMapPropagator(propagator)

	ctx := context.Background()
	defer tp.Shutdown(ctx) ✅ 애플리케이션 종료시 OpenTelemetry 도 정리

	r := gin.Default()
	r.Use(otelgin.Middleware(service)) ✅ gin middleware 로 별도 span 추가 없이 path정보가 쌓인다.
}

Client Side (Service A)

func main() {
...
	r.GET("/users/:id", sayHello)
	_ = r.Run(":8080")
}

func sayHello(c *gin.Context) {
	// Pass the built-in `context.Context` object from http.Request to OpenTelemetry APIs
	// where required. It is available from gin.Context.Request.Context()
	bag, _ := baggage.Parse("client=my-service") ✅ 다른 서비스에 전달할 Key,Value 데이터 추가
	ctx := c.Request.Context()
	ctx = baggage.ContextWithBaggage(ctx, bag)
	id := c.Param("id")
	name := getName(ctx, id)
	greetings := getGreetings(ctx)
	c.JSON(http.StatusOK, fmt.Sprintf("%s %s", greetings, name))
}

func getGreetings(ctx context.Context) string {
	// Pass the built-in `context.Context` object from http.Request to OpenTelemetry APIs
	// where required. It is available from gin.Context.Request.Context()
	✅ Span 추가
	tracer :=  otel.Tracer("test-tracer")
	ctx, span := tracer.Start(ctx, "get greeting", trace.WithAttributes(semconv.PeerServiceKey.String("greeting-service")))
	defer span.End()

	// Request
	✅ otelhttp library를 사용하면 알아서 trace context를 전달한다.
	client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
	req, _ := http.NewRequestWithContext(ctx, "GET", "http://localhost:7778", nil)

	res, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	body, _ := ioutil.ReadAll(res.Body)
	_ = res.Body.Close()
	return string(body)
}

Server Side (Service B, C)

func main() {
...

	r := gin.Default()
	r.Use(otelgin.Middleware(service))
	r.GET("", func(c *gin.Context) {
		ctx := c.Request.Context()
		span := trace.SpanFromContext(ctx) ✅ 전달된 context 기반으로 Span 추가
		bag := baggage.FromContext(ctx)
		uk := attribute.Key("client")
		✅ span에 message와 baggage로 전달된 client 정보 추가
		span.AddEvent("saying hello - request from", trace.WithAttributes(uk.String(bag.Member("client").Value())))
		c.JSON(http.StatusOK, "Hello~!")
	})
	_ = r.Run(":7778")
}

 

실행 결과


http://localhost:80 으로 접속해서 확인해 보면 아래와 같이 Trace가 잘 나오는것을 확인할수 있다.

  • gin middleware 를 사용해서 각 요청 Span이 자동생성되어 표시
  • otelhttp 를 통해 'HTTP GET' Span 표시
  • 별도로 추가한 Span 표시

  • Span에 추가한 Baggage 정보 표시
  • Span에 추가한 message 표시

 

정리


  • OpenTelemetry는 Traces, Metrics, Logs 를 위한 데이터를 생성하고 관리하는 CNCF Open Source Project다.
  • OpenTelemetry 는 실제 데이터를 수집하여 처리하는 Tracer의 역할을 하는것이 아니라 이와 연동하기 위한 표준화된 API, SDK와 Tool을 제공한다.
  • API와 SDK 만 이용해서 Jaeger나 Zipkin과 같은 Tracer를 직접 연동할 수도 있다.
  • OpenTelemetry Collector를 이용하면 여러개의 Backend를 연동할 수 있고, 코드 변경없이 배포 설정 파일 수정으로 Backend 를 변경할 수 있어 편리하다.
  • OpenTelemetry Collector를 sidecar나 demonset 의 Agent 형태로 Host에 같이 배포하여 중앙에 독립 Service인 OpenTelemetry Collector Gateway 에 전달하는 방법과 애플리케이션이 직접 OpenTelemetry Collecter Gateway로 전달하는 방법이 있다. 각자의 상황에 맞게 구조를 잡으면 된다.
  • OpenTelemetry Collector config map 배포시 어떤 Tracer와 연동할지 설정하면 된다.
  • 애플리케이션의 구현 방법은 기존 OpenTracing 과 동일하게 Provider (연동할 OpenTelemetry Collecter Endpoint)를 설정 하고 필요한 Span을 설정하고 다른 서비스 연동 시 Context Propagate 를 잘 해주면된다. 이때,  OpenTelemetry에서 제공하는 3rd party Go Libraries 를 활용하면 직접 Span 생성이나 Context 처리없이 간단하게 분산추적데이터를 모을 수 있다.