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 처리없이 간단하게 분산추적데이터를 모을 수 있다.