Jaeger E2E
В данном практическом занятии рассмотрим возможность сквозной трассировки через несколько приложений с помощью jaeger.
Vagrant
Vagrant.configure("2") do |config|
config.vm.define "jaeger" do |c|
c.vm.box = "ubuntu/lunar64"
c.vm.hostname = "jaeger"
c.vm.network "forwarded_port", guest: 8888, host: 8888
c.vm.network "forwarded_port", guest: 8889, host: 8889
c.vm.provision "shell", inline: <<-SHELL
apt-get update -q
apt-get install -yq docker.io docker-compose-v2
usermod -a -G docker vagrant
SHELL
end
end
Данная конфигурация установит на виртуальную машину docker и docker compose, с помощью которых в дальнейшем будут развернуты остальные компоненты.
Развернем jaeger
с помощью следующего compose.yaml
:
services:
jaeger:
container_name: jaeger
image: jaegertracing/all-in-one
ports:
- "8889:14268"
- "8888:16686"
$ docker compose up -d
[+] Running 2/2
✔ Network vagrant_default Created 0.1s
✔ Container jaeger Started 0.3s
После чего по адресу localhost:8888/search будет доступен интерфейс.
Application
Создадим приложение на языке golang для обработки http запросов, которое будет обрабатывать запрос случайное время и иногда возвращать ошибку. Для инструментирования http запросов воспользуемся библиотеками opentelemetry.
package main
import (
"context"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"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.24.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
service := os.Getenv("NAME")
ctx := context.Background()
tr, err := initTracer(ctx, service)
if err != nil {
log.Fatal("init tracer", err)
}
http.Handle("/", newHandler(service, tr))
http.ListenAndServe(":8080", nil)
}
func initTracer(ctx context.Context, svc string) (trace.Tracer, error) {
conn, err := grpc.NewClient("jaeger:4317",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, err
}
exp, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
if err != nil {
return nil, err
}
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(svc),
),
)
if err != nil {
return nil, err
}
prv := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(prv)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
return prv.Tracer("tracer"), nil
}
func sendReq(ctx context.Context, tr trace.Tracer, url string) error {
client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("response code: %d", resp.StatusCode)
}
return nil
}
func newHandler(name string, tr trace.Tracer) http.Handler {
return otelhttp.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("request %s\n", r.URL.Path)
num := rand.Intn(5) + 1
time.Sleep(time.Duration(num) * time.Second)
if num%3 == 0 {
w.WriteHeader(http.StatusInternalServerError)
}
}), name)
}
И Dockerfile
к нему:
FROM golang:1.21 as build
WORKDIR /src
COPY . /src/
RUN go mod init test && go mod tidy
RUN CGO_ENABLED=0 go build -o /bin/app ./main.go
FROM scratch
COPY --from=build /bin/app /app
ENTRYPOINT ["/app"]
После чего добавим в наш compose.yaml
:
services:
jaeger:
container_name: jaeger
image: jaegertracing/all-in-one
ports:
- "8889:14268"
- "8888:16686"
app1:
container_name: app1
image: app
build: .
environment:
- NAME=app1
ports:
- "8080:8080"
Запустим и сделаем несколько запросов:
$ docker compose up -d --force-recreate
[+] Running 2/2
✔ Container app1 Started 0.6s
✔ Container jaeger Started 0.7s
$ curl localhost:8080
$ curl localhost:8080
$ curl localhost:8080
После чего в интерфейсе jaeger сможем наблюдать наши запросы:
Если раскрыть подробную информацию о трейсе, то в ней будет видно дополнительную информацию, которую автоматически вносит библиотека инструментирования:
Distributed tracing
Добавим в приложение возможность отправки запросов в другие сервисы, таким образом
что путь в url
запроса будет указывать в какой сервис пойти далее:
package main
import (
"context"
"fmt"
"io"
"log"
"math/rand"
"net/http"
"os"
"strings"
"time"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/codes"
"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.24.0"
"go.opentelemetry.io/otel/trace"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
service := os.Getenv("NAME")
ctx := context.Background()
tr, err := initTracer(ctx, service)
if err != nil {
log.Fatal("init tracer", err)
}
http.Handle("/", newHandler(service, tr))
http.ListenAndServe(":8080", nil)
}
func initTracer(ctx context.Context, svc string) (trace.Tracer, error) {
conn, err := grpc.NewClient("jaeger:4317",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
return nil, err
}
exp, err := otlptracegrpc.New(ctx, otlptracegrpc.WithGRPCConn(conn))
if err != nil {
return nil, err
}
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(svc),
),
)
if err != nil {
return nil, err
}
prv := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(res),
)
otel.SetTracerProvider(prv)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
return prv.Tracer("tracer"), nil
}
func sendReq(ctx context.Context, tr trace.Tracer, url string) error {
client := http.Client{Transport: otelhttp.NewTransport(http.DefaultTransport)}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("response code: %d", resp.StatusCode)
}
return nil
}
func newHandler(name string, tr trace.Tracer) http.Handler {
return otelhttp.NewHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
log.Printf("request %s\n", r.URL.Path)
path := strings.Split(r.URL.Path, "/")
if len(path) > 1 && len(path[1]) > 0 {
log.Printf("send request to %s\n", path[1])
if err := sendReq(ctx, tr,
fmt.Sprintf(
"http://%s:8080/%s", path[1], strings.Join(path[2:], "/"),
)); err != nil {
log.Printf("send request error %s", err)
span := trace.SpanFromContext(ctx)
span.SetStatus(codes.Error, "error span")
span.RecordError(fmt.Errorf("error span"))
}
}
num := rand.Intn(5) + 1
time.Sleep(time.Duration(num) * time.Second)
if num%3 == 0 {
w.WriteHeader(http.StatusInternalServerError)
}
}), name)
}
И добавим несколько сервисов в compose.yaml
:
services:
jaeger:
container_name: jaeger
image: jaegertracing/all-in-one
ports:
- "8889:14268"
- "8888:16686"
app1:
container_name: app1
image: app
build: .
environment:
- NAME=app1
ports:
- "8080:8080"
app2:
container_name: app2
image: app
build: .
environment:
- NAME=app2
app3:
container_name: app3
image: app
build: .
environment:
- NAME=app3
Запустим с пересборкой и отправим несколько запросов:
$ docker compose up -d --build
[+] Running 4/4
✔ Container app2 Started 0.9s
✔ Container app3 Started 0.9s
✔ Container jaeger Running 0.0s
✔ Container app1 Started 0.9s
$ curl localhost:8080/app2
$ curl localhost:8080/app3
$ curl localhost:8080/app1
В интерфейсе jaeger теперь можно наблюдать взаимодействие с несколькими сервисами в наших трейсах:
В последнем трейсе сервис отправляет запрос сам в себя, так что взаимодействие происходит только с одним сервисом:
В двух других же можно проследить взаимодействие между сервисами, которые выделяются разным цветом в трейсе:
Трейс в себе может хранить информацию о сквозном прохождении через любое количество сервисов. Отправим такой запрос:
$ curl localhost:8080/app3/app2/app1/app2/app3
Тогда наш трейс будет выглядеть следующим образом:
На нем видно весь путь прохождения запроса через все сервисы, время обработки каждым, а также в каком месте возникала ошибка.
Также в правом углу можно переключить на другие варианты визуализации.
Например в виде графа:
Или в виде flamegraph:
Либо общую статистику трейса, которую можно сгруппировать, например, по сервисам:
Таким образом jaeger позволяет детально анализировать прохождение запроса в сложной распределенной микросервисной архитектуре.