Bu örnekte Golang uygulama izlerini saklayıp görselleştireceğiz. Dışa aktarılan telemetri verileri ElasticSearch'te saklanacak ve Jaeger tarafından görselleştirilecek. Jaeger'in varsayılan depolama alanı bellektir ancak verimli olmayacaktır. Jaeger'i bir üretim Kubernetes kümesine kurmanın ve yönetmenin önerilen yolu, Jaeger Operatörü aracılığıyladır ve biz de bunu yapacağız. Jaeger varsayılan olarak verileri günlük endekslerde saklar ve endeksleri yuvarlamanıza olanak tanır - doc. Endeksleri döndürmenin otomatik bir yolu var ancak bazı nedenlerden dolayı beklendiği gibi davranmıyordu, bu yüzden belgede de gösterilen bu işlemi manuel olarak gerçekleştireceğim. Komutlar gönderinin altındadır. Bir önemli not daha: doğrudan verileri itmek yerine Kafka'yı (doc) kullanabilirsiniz. Verileri ElasticSearch'e aktaracağım ki bu muhtemelen daha iyi bir çalışma yöntemidir ancak bu konuya burada girmeyeceğim. Pek çok bilgiyi kapsayan belgeye göz atın.


Ben üretim (production) dağıtım stratejisini seçtim ancak isterseniz başka bir stratejiyi de seçebilirsiniz. Bunun için dokümanı kontrol edin.


Her zamanki gibi, örneğin tamamı iyileştirmelere ve ayarlamalara açıktır. Gönderiyi mümkün olduğunca küçük tutmak için bazı "sahip olunması gereken" ayarları dışarıda bıraktım.



ElasticSearch


Bu normalde internette bir yerde çalışıyor olurdu ama Jaeger'in Kubernetes içinden erişebilmesi için Docker sürümünü kullanacağım ve internete sunacağım.


$ @DOCKER_BUILDKIT=0 docker run \
--rm \
--env discovery.type=single-node \
--publish 9200:9200 \
--name trace-elastic \
docker.elastic.co/elasticsearch/elasticsearch:7.17.1

İnternet'e açım. Ortaya çıkan URL'yi daha sonra jaeger.yaml dosyasında kullanacağız.


$ ssh -p 443 -R0:localhost:9200 a.pinggy.io

http://rntsp-2a02-c7c-6502-900-24bc-b496-574f-783.a.free.pinggy.link
https://rntsp-2a02-c7c-6502-900-24bc-b496-574f-783.a.free.pinggy.link

Jaeger'i dağıtmadan önce aşağıda gösterildiği gibi okuma/yazma takma adları ve yazma dizinleri oluşturmak zorunludur. Bu tek seferlik bir süreçtir.


$ docker run -it --rm --net=host jaegertracing/jaeger-es-rollover:latest init http://localhost:9200 --shards 2 --replicas 1 --index-prefix prod

health status index                           id                     pri rep docs.count docs.deleted store.size creation.date.string
yellow open prod-jaeger-dependencies-000001 CtOkYcWMSraMbDCfKaSH3A 2 1 0 0 1.1kb 2024-06-01T15:47:14.738Z
yellow open prod-jaeger-span-000001 07konOTQSnGf8wbb6s_LSg 2 1 0 0 1.1kb 2024-06-01T15:47:13.273Z
yellow open prod-jaeger-service-000001 QwUbhxeFQ9O4R2zEojN6Bw 2 1 0 0 1.1kb 2024-06-01T15:47:14.056Z

alias                                              index
prod-jaeger-dependencies-read assigned to index prod-jaeger-dependencies-000001
prod-jaeger-dependencies-read assigned to index prod-jaeger-dependencies-000001
prod-jaeger-dependencies-write assigned to index prod-jaeger-dependencies-000001
prod-jaeger-service-read assigned to index prod-jaeger-service-000001
prod-jaeger-service-write assigned to index prod-jaeger-service-000001
prod-jaeger-span-read assigned to index prod-jaeger-span-000001
prod-jaeger-span-write assigned to index prod-jaeger-span-000001

Kubernetes


Yerel Kubernetes kümenizi başlatmak için Minikube'u çalıştırın.


$ minikube start --memory 4000 --cpus=2

Jaeger'ı hazırlayın - doc.


$ kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.6.1/cert-manager.yaml

$ kubectl get pods --namespace cert-manager
NAME READY STATUS RESTARTS AGE
cert-manager-5656f9c48-lz4sm 1/1 Running 0 103s
cert-manager-cainjector-765d9679c9-2btbx 1/1 Running 0 103s
cert-manager-webhook-586f8d6cf6-9pw8p 1/1 Running 0 103s


$ kubectl create namespace observability
$ kubectl create -n observability -f https://github.com/jaegertracing/jaeger-operator/releases/download/v1.56.0/jaeger-operator.yaml

$ kubectl -n observability get all
NAME READY STATUS RESTARTS AGE
pod/jaeger-operator-786c87cb64-vflww 2/2 Running 0 3m3s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/jaeger-operator-metrics ClusterIP 10.104.211.141 8443/TCP 3m4s
service/jaeger-operator-webhook-service ClusterIP 10.103.136.40 443/TCP 3m4s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jaeger-operator 1/1 1 1 3m5s

NAME DESIRED CURRENT READY AGE
replicaset.apps/jaeger-operator-786c87cb64 1 1 1 3m5s

Uygulama


Dockerfile


FROM golang:1.22.0-alpine3.19 as build
WORKDIR /api
COPY . .
RUN go mod verify
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -o ./bin/api main.go

FROM alpine:3.19
WORKDIR /api
COPY --from=build /api/bin/api bin/api
ENTRYPOINT ./bin/api

main.go


package main

import (
"context"
"log"
"net/http"
"os"

"playground/api"
"playground/trace"
)

func main() {
ctx := context.Background()

exp, err := trace.NewExporter(ctx, trace.ExporterConfig{
Type: os.Getenv("TYPE"),
Address: os.Getenv("JAEGER"),
})
if err != nil {
log.Fatalln(err)
}

pro, err := trace.NewProvider(ctx, trace.ProviderConfig{
Exporter: exp,
Service: os.Getenv("SVC"),
Version: os.Getenv("VER"),
Environment: os.Getenv("ENV"),
})
if err != nil {
log.Fatalln(err)
}
defer pro.Close(ctx)

rtr := http.NewServeMux()
rtr.HandleFunc("GET /api/v1/users/{id}", (api.User{}).Find)

log.Println(http.ListenAndServe(os.Getenv("HOST")+":"+os.Getenv("PORT"), rtr))
}

exporter.go


package trace

import (
"context"
"errors"

"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace"

sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

type ExporterConfig struct {
Type string
Address string
}

func NewExporter(ctx context.Context, cfg ExporterConfig) (sdktrace.SpanExporter, error) {
switch cfg.Type {
case "stdout":
return stdouttrace.New()
case "http":
return otlptracehttp.New(ctx,
otlptracehttp.WithInsecure(),
otlptracehttp.WithEndpoint(cfg.Address),
)
}

return nil, errors.New("invalid type")
}

provider.go


package trace

import (
"context"

"go.opentelemetry.io/otel"
"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.25.0"
)

type ProviderConfig struct {
Exporter sdktrace.SpanExporter
Service string
Version string
Environment string
}

type Provider struct {
provider *sdktrace.TracerProvider
}

func NewProvider(ctx context.Context, cfg ProviderConfig) (Provider, error) {
res := resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName(cfg.Service),
semconv.ServiceVersion(cfg.Version),
semconv.DeploymentEnvironment(cfg.Environment),
)

prp := propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
otel.SetTextMapPropagator(prp)

prv := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(cfg.Exporter),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(prv)

return Provider{
provider: prv,
}, nil
}

func (p Provider) Close(ctx context.Context) error {
return p.provider.Shutdown(ctx)
}

span.go


Daha fazla işlevsellik eklemek için bunu geliştirmenizi şiddetle öneririm. Span tipi birçok özellik ile birlikte gelir. Bu şimdilik sadece tembel bir uygulamadır.


package trace

import (
"context"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)

func Span(ctx context.Context, name string, opts ...trace.SpanStartOption) (context.Context, trace.Span) {
if opts == nil {
return otel.Tracer("").Start(ctx, name)
}

return otel.Tracer("").Start(ctx, name, opts...)
}

func Error(span trace.Span, err error) {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}

api.go


package api

import (
"context"
"errors"
"net/http"

"playground/trace"
)

type User struct{}

func (u User) Find(w http.ResponseWriter, r *http.Request) {
ctx, span := trace.Span(r.Context(), "user.Find")
defer span.End()

if err := u.isValid(ctx, r.PathValue("id")); err != nil {
trace.Error(span, errors.New("invalid user id"))

w.WriteHeader(http.StatusBadRequest)

return
}
}

func (u User) isValid(ctx context.Context, id string) error {
_, span := trace.Span(ctx, "user.isValid")
defer span.End()

if id != "b" {
return errors.New("invalid user id")
}

return nil
}

api.yaml


apiVersion: apps/v1
kind: Deployment

metadata:
name: api-deployment
namespace: default
labels:
app: api

spec:
replicas: 1
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: golang
image: you/api:latest
imagePullPolicy: Always
ports:
- containerPort: 8000
env:
- name: HOST
value: "0.0.0.0"
- name: PORT
value: "8000"
- name: ENV
value: "production"
- name: SVC
value: "api"
- name: VER
value: "v0.0.1"
- name: TYPE
value: "http"
- name: JAEGER
value: "jaeger-collector.observability:4318"

---

apiVersion: v1
kind: Service

metadata:
name: api-service
namespace: default

spec:
type: NodePort
selector:
app: api
ports:
- protocol: TCP
port: 80
targetPort: 8000

jaeger.yaml


apiVersion: jaegertracing.io/v1
kind: Jaeger

metadata:
name: jaeger
namespace: observability

spec:
strategy: production
collector:
maxReplicas: 2
resources:
limits:
cpu: 100m
memory: 128Mi
storage:
type: elasticsearch
options:
es:
server-urls: https://rntsp-2a02-c7c-6502-900-24bc-b496-574f-783.a.free.pinggy.link
version: 7
index-prefix: prod
use-aliases: true

Jaeger dağıtımı


$ kubectl apply -f jaeger.yaml

Kontrol etmedim ancak cronjob.batch/jaeger-es-index-cleaner aslında eski dizinleri temizliyor olabilir. Sadece kontrol edilmesi gerekiyor.


$ kubectl -n observability get all
NAME READY STATUS RESTARTS AGE
pod/jaeger-collector-7d4c468b9f-g8vq9 1/1 Running 0 2m46s
pod/jaeger-operator-786c87cb64-vflww 2/2 Running 0 6m51s
pod/jaeger-query-65f5979bc-btxln 2/2 Running 0 2m46s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/jaeger-collector ClusterIP 10.103.189.50 9411/TCP,14250/TCP,14267/TCP,14268/TCP,14269/TCP,4317/TCP,4318/TCP 2m47s
service/jaeger-collector-headless ClusterIP None 9411/TCP,14250/TCP,14267/TCP,14268/TCP,14269/TCP,4317/TCP,4318/TCP 2m47s
service/jaeger-operator-metrics ClusterIP 10.104.211.141 8443/TCP 6m52s
service/jaeger-operator-webhook-service ClusterIP 10.103.136.40 443/TCP 6m52s
service/jaeger-query ClusterIP 10.102.252.116 16686/TCP,16685/TCP,16687/TCP 2m47s

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/jaeger-collector 1/1 1 1 2m46s
deployment.apps/jaeger-operator 1/1 1 1 6m52s
deployment.apps/jaeger-query 1/1 1 1 2m46s

NAME DESIRED CURRENT READY AGE
replicaset.apps/jaeger-collector-7d4c468b9f 1 1 1 2m47s
replicaset.apps/jaeger-operator-786c87cb64 1 1 1 6m53s
replicaset.apps/jaeger-query-65f5979bc 1 1 1 2m47s

NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
horizontalpodautoscaler.autoscaling/jaeger-collector Deployment/jaeger-collector cpu: /90%, memory: /90% 1 5 1 79s

NAME SCHEDULE TIMEZONE SUSPEND ACTIVE LAST SCHEDULE AGE
cronjob.batch/jaeger-es-index-cleaner 55 23 * * * False 0 2m48s
cronjob.batch/jaeger-spark-dependencies 55 23 * * * False 0 2m48s

Uygulama Docker dağıtımı


Uygulama imajını DockerHub'a dağıtmamız gerekiyor.


$ docker build -t you/api:latest .
$ docker push you/api:latest

Uygulama dağıtımı


$ kubectl apply -f api.yaml

$ kubectl get all
NAME READY STATUS RESTARTS AGE
pod/api-deployment-fcdcc84d7-rkfbq 1/1 Running 0 11s

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/api-service NodePort 10.100.179.159 80:30512/TCP 11s
service/kubernetes ClusterIP 10.96.0.1 443/TCP 17m

NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/api-deployment 1/1 1 1 11s

NAME DESIRED CURRENT READY AGE
replicaset.apps/api-deployment-fcdcc84d7 1 1 1 11s

Jaeger kullanıcı arayüzüne erişin


Aşağıdaki komutu çalıştırdıktan sonra http://localhost:16686 adresini ziyaret edin.


$ kubectl port-forward -n observability service/jaeger-query 16686:16686
Forwarding from 127.0.0.1:16686 -> 16686

İzleme verilerini doldurma


Bazı sahte izleme verilerini dolduralım. Go uygulamamıza yerel ana bilgisayardan erişilebilmesi için ilk bağlantı noktasını iletin.


$ kubectl port-forward service/api-service 8888:80
Forwarding from 127.0.0.1:8888 -> 8000
Forwarding from [::1]:8888 -> 8000

$ curl -i http://localhost:8888/api/v1/users/a
HTTP/1.1 400 Bad Request
Date: Wed, 29 May 2024 20:49:51 GMT
Content-Length: 0

$ curl -i http://localhost:8888/api/v1/users/b
HTTP/1.1 200 OK
Date: Wed, 29 May 2024 20:50:02 GMT
Content-Length: 0

Elasticsearch endeks


$ curl -i https://rntsp-2a02-c7c-6502-900-24bc-b496-574f-783.a.free.pinggy.link/_cat/indices

health status index id pri rep docs.count docs.deleted store.size creation.date.string
yellow open prod-jaeger-dependencies-000001 CtOkYcWMSraMbDCfKaSH3A 2 1 0 0 1.1kb 2024-06-01T15:47:14.738Z
yellow open prod-jaeger-span-000001 07konOTQSnGf8wbb6s_LSg 2 1 13 0 18.8kb 2024-06-01T15:47:13.273Z
yellow open prod-jaeger-service-000001 QwUbhxeFQ9O4R2zEojN6Bw 2 1 0 0 1.1kb 2024-06-01T15:47:14.056Z






Endeks yuvarlama


Jaeger, dizinler için günlük dizin modelini kullanır ve aralığın zaman damgasına göre her gün için yeni bir dizin oluşturur. Bu model, bir dizinin diğerlerinden daha fazla veri içerebileceği parçalar üzerinden veri dağıtımında o kadar etkili olmayabilir.


Dizinler yuvarlanırken, bir dizin takma adı, verilen yapılandırma koşullarına göre yeni bir dizine aktarılır. Okumak için bir alias, yazmak için başka bir alias vardır. Okuma aliası, bir grup salt okunur dizine işaret eder ve yazma aliası, bir yazma dizinine işaret eder. Şimdi komutlara geçelim. İdeal olarak bu komutları manuel olarak çalıştırmak yerine otomatikleştirmelisiniz. Örneğin, sizin için günde bir kez olacak şekilde Kubernetes'te aşağıdakileri gerçekleştiren bir cron işi oluşturun. Aşağıdaki her komutta ipuçları için --help etiketi bulunur.


Yeni bir dizine geçiş


Bu komut, yazma aliasını koşula göre yeni bir dizine döndürür. Ayrıca, yeni verilerin arama için kullanılabilir hale gelmesi için okuma aliasına yeni bir dizin ekler. Burada, geçerli yazma dizininin yaşı 10 saniyeden eski olduğu sürece aliası yeni bir dizine aktarırız.


$ docker run -it --rm --net=host jaegertracing/jaeger-es-rollover:latest rollover http://localhost:9200 --conditions '{"max_age": "10s"}' --index-prefix prod

Okuma aliasından eski dizinleri kaldırın


Bu, verilen birimlere göre (1 saniye sonra) eski verileri arama için kullanılamaz hale getirecektir.


$ docker run -it --rm --net=host jaegertracing/jaeger-es-rollover:latest lookback http://localhost:9200 --unit-count 1 --unit seconds --index-prefix prod

Eski verileri kaldırma


Bu, eski endeksleri silerek geçmiş verileri siler. Burada 1 günden eski endeksleri kaldırıyoruz.


$ docker run -it --rm --net=host jaegertracing/jaeger-es-index-cleaner:latest 0 http://localhost:9200 --rollover --index-prefix prod

Sonuç


Sadece bir not: lookback komutundan hemen önce yeni dizini doldurmak için API'yi bir kez aradım.


health status index                           id                     pri rep docs.count docs.deleted store.size creation.date.string
yellow open prod-jaeger-dependencies-000002 atOkYcWMSrasdfsdfaSH3A 2 1 0 0 1.1kb 2024-06-01T15:47:14.738Z
yellow open prod-jaeger-span-000002 a7konOTQSnGf8hgfhs_LSg 2 1 2 0 4.8kb 2024-06-01T15:47:13.273Z
yellow open prod-jaeger-service-000002 awUbjyutj9O4R2zEojN6Bw 2 1 0 0 1.1kb 2024-06-01T15:47:14.056Z