Opentelemetry Instrumentation
В данном практическом занятии опробуем инструментирование приложения с помощью библиотек opentelemetry для языка golang.
Vagrant
Для работы будем использовать следующий Vagrantfile
:
Vagrant.configure("2") do |config|
config.vm.define "otel" do |c|
c.vm.box = "ubuntu/lunar64"
c.vm.hostname = "otel"
c.vm.network "forwarded_port", guest: 8888, host: 8888
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, с помощью которых в дальнейшем будут развернуты остальные компоненты.
Collector
Развернем коллектор и компоненты для телеметрии при помощи docker-compose,
для этого зададим конфигурацию в файл config.yaml
:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
exporters:
debug:
verbosity: detailed
otlp:
endpoint: jaeger:4317
tls:
insecure: true
prometheusremotewrite:
endpoint: http://prometheus:9090/api/v1/write
service:
pipelines:
logs:
receivers: [otlp]
processors: [batch]
exporters: [debug]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheusremotewrite,debug]
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp,debug]
И сам compose.yaml
:
services:
otel-collector:
container_name: collector
image: otel/opentelemetry-collector-contrib:0.86.0
ports:
- 4317:4317
configs:
- source: collector
target: /etc/otelcol-contrib/config.yaml
prometheus:
container_name: prometheus
image: prom/prometheus:v2.50.1
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --web.enable-remote-write-receiver
ports:
- 9090:9090
jaeger:
container_name: jaeger
image: jaegertracing/all-in-one:1.56
ports:
- "16686:16686"
grafana:
container_name: grafana
image: grafana/grafana:10.4.0
ports:
- 8888:3000
configs:
collector:
file: ./config.yaml
После чего запустим:
$ docker compose up -d
[+] Running 5/5
✔ Network vagrant_default Created 0.0s
✔ Container collector Started 0.5s
✔ Container prometheus Started 0.5s
✔ Container jaeger Started 0.6s
✔ Container grafana Started 0.6s
Мы получим работающий коллектор с конфигурацией, отправляющей метрики и трейсы
в prometheus
и jaeger
соответственно, а также grafana
, с помощью которой
сможем визуализировать информацию в них.
Application
Добавим приложение, которое будет обрабатывать http
запросы и, используя
библиотеки opentelemetry, собирать метрики и трейсы, после чего отправлять их
в коллектор. Приложение взято из занятия по jaeger
, дополненное метриками:
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/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
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()
conn, err := grpc.NewClient("collector:4317",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatal("connect to collector", err)
}
tr, err := initTracer(ctx, conn, service)
if err != nil {
log.Fatal("init tracer", err)
}
_, err = initMeter(ctx, conn, service)
if err != nil {
log.Fatal("init meter", err)
}
http.Handle("/", newHandler(service, tr))
http.ListenAndServe(":8080", nil)
}
func initTracer(ctx context.Context, conn *grpc.ClientConn, svc string) (trace.Tracer, error) {
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 initMeter(ctx context.Context, conn *grpc.ClientConn, svc string) (metric.MeterProvider, error) {
exp, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithGRPCConn(conn))
if err != nil {
return nil, err
}
res, err := resource.New(ctx,
resource.WithAttributes(
semconv.ServiceName(svc),
),
)
if err != nil {
return nil, err
}
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithResource(res),
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exp)),
)
otel.SetMeterProvider(mp)
return mp, 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:
app1:
container_name: app1
image: app
build: .
environment:
- NAME=app1
ports:
- "8080:8080"
otel-collector:
container_name: collector
image: otel/opentelemetry-collector-contrib:0.86.0
ports:
- 4317:4317
configs:
- source: collector
target: /etc/otelcol-contrib/config.yaml
prometheus:
container_name: prometheus
image: prom/prometheus:v2.50.1
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --web.enable-remote-write-receiver
ports:
- 9090:9090
jaeger:
container_name: jaeger
image: jaegertracing/all-in-one:1.56
ports:
- "16686:16686"
grafana:
container_name: grafana
image: grafana/grafana:10.4.0
ports:
- 8888:3000
configs:
collector:
file: ./config.yaml
Запустим контейнер и цикл запросов, чтобы наше приложение начало отправлять метрики и трейсы в коллектор:
$ docker compose up -d
[+] Running 6/6
✔ Network vagrant_default Created 0.0s
✔ Container app1 Started 0.8s
✔ Container prometheus Started 0.5s
✔ Container collector Started 0.7s
✔ Container jaeger Started 0.7s
✔ Container grafana Started 0.8s
$ while :;do curl localhost:8080;done
Visualization
По адресу localhost:8888 доступен интерфейс
grafana. Авторизуемся под учетными данными admin:admin
и перейдем в раздел
datasources.
Metrics
В нем добавим в качестве источника метрик prometheus:
После чего нажмем кнопку Save & test
и перейдем в Explore data
. В данном
разделе можем сделать поиск по метрике http_server_duration_milliseconds_count
,
чтобы убедиться, что метрики нашего приложения попадают в хранилище:
Как видно, метрики сервера автоматически сгенерированы библиотекой инструментирования opentelemetry при обработке http запросов.
Traces
Добавим еще один датасорс jaeger для трейсов:
После сохранения также перейдем в Explore data
, чтобы увидеть трейсы нашего
приложения:
Как видно, библиотека инструментирования opentelemetry добавляет информацию о запросе в атрибуты спана.
Dashboard
Создадим новый дашборд в разделе dashboards.
Добавим в него переменную code
для выбора http кода в метриках и трейсах:
И добавим переменную для выбора traceid
:
После чего добавим визуализацию для трейсов как ранее делали в практике по
jaeger
, добавив переменную в теги:
И также добавив override для выбора traceid
:
А также добавим визуализации с деталями по трейсу:
Добавим на дашборд метрики для отображения частоты запросов:
И среднее время запросов:
После чего с помощью переменных мы можем манипулировать данными в наших визуализациях для удобного анализа проблем в приложении: