следовательность, критический компонент, отслеживает запросы через сложные системы. Эта видимость revels блокирующие узкие места и ошибки, позволяя быстрее решать проблемы. В предыдущей статье нашей серии Go веб-сервисов мы исследовали значимость наблюдаемости. Сегодня мы сосредотачиваемся на слежении. Jaeger собирает, хранит и визуализирует следы из распределенных систем. Он предоставляет важные Insights о потоке запросов между сервисами. Благодаря интеграции Jaeger с OpenTelemetry, разработчики могут объединить свой подход к слежению, обеспечивая последовательность и всестороннюю видимость. Эта интеграция упрощает диагностику проблем с производительностью и улучшает надежность системы. В этой статье мы настроим Jaeger, интегрируем его с OpenTelemetry в нашем приложении и исследуем визуализацию следов для более глубоких Insights.
Мотивация
Компонент, на который мы работаем, предполагает, что Jaeger Dashboard выглядит так:
Когда мы переходим в различные части приложения (на frontend Onehub), следы различных запросов собираются (от точки, когда нажимается grpc-gateway) с оSummary каждого из них. Мы даже можем заглянуть в одну из следов для более детального просмотра. Посмотрим на первый POST
запрос (на создание/отправку сообщения в топике):
Здесь мы видим все компоненты, которые касаются Create
запроса, а также их входные/выходные times и время, прошедшее внутри и снаружи методов. Очень мощно.
Подготовка
TL;DR: Чтобы увидеть действие вживую и проверить остальную часть блога:
- Исходник для этого находится в ветке PART11_TRACING.
- Создайте все необходимые вещи (после того, как вы вытащите ветку):
make build
- Мы разделили docker-compose на две части (это будет объяснено ниже), поэтому убедитесь, что у вас два окна запускаются.
- Терминал 1:
make updb dblogs
- Терминал 2:
make up logs
- Перейдите по адресу localhost:7080 и начните работу.
Высокоуровневая OVERVIEW
Наша система в настоящее время:
С использованием инструментирования OpenTelemetry, наша система будет развиваться в направлении:
Как мы упомянули ранее, представляет собой довольно тяжелое для каждого сервиса использовать отдельные клиенты, чтобы отправлять данные определенным поставщикам. Instead with an OTel collector running separately, we can ensure that all (interested) services can simply send metrics/logs/traces to this collector, which can then export to them various backends as required — in this case, Jaeger for traces.
Давайте начать.
Установите сборщик OTel
Первым шагом является добавление нашего сборщика OTel, выполняющегося в среде Docker, вместе с Jaeger, чтобы они были доступны.
Примечание: Мы разделили нашу исходную всеобъемлющую конфигурацию docker-compose.yml
на две части:
- db-docker-compose.yml: Содержит все компоненты, связанные с базами данных и инфраструктурой (не с приложениями), такие как базы данных (Postgres, Typesense) и сервисы мониторинга (сборщик OTel, Jaeger, Prometheus и т. д.).
- docker-compose.yml: Содержит все сервисы, связанные с приложением (Nginx, gRPC Gateway, dbsync, frontend и т.д.)
Два среда docker-compose соединены shared network (onehubnetwork
), через который сервисы в этих средах могут взаимодействовать. Благодаря этому разделению нам достаточно перезапустить подмножество сервисов после изменений, что ускоряет наше разработку.
вернемся к нашему настройке: в нашем db-docker-compose.yml
, добавим следующие сервисы:
services
...
otel-collector
networks
onehubnetwork
image otel/opentelemetry-collector-contrib0.105.0
command"--config=/etc/otel-collector.yaml"
volumes
./configs/otel-collector.yaml:/etc/otel-collector.yaml
environment
POSTGRES_DB $ POSTGRES_DB
POSTGRES_USER $ POSTGRES_USER
POSTGRES_PASSWORD $ POSTGRES_PASSWORD
jaeger
networks
onehubnetwork
image jaegertracing/all-in-one1.59
container_name jaeger
environment
QUERY_BASE_PATH'/jaeger'
COLLECTOR_OTLP_GRPC_HOST_PORT'0.0.0.0:4317'
COLLECTOR_OTLP_HTTP_HOST_PORT'0.0.0.0:4318'
COLLECTOR_OTLP_ENABLEDtrue
prometheus
networks
onehubnetwork
image prom/prometheus v2.53.1
command
'--config.file=/etc/prometheus/prometheus.yml'
'--web.external-url=/prometheus/'
'--web.route-prefix=/prometheus/'
volumes
./configs/prometheus.yaml:/etc/prometheus/prometheus.yml
ports
9090:9090
Это достаточно просто, это устанавливает два сервиса в нашем окружении Docker:
otel-collector
: Сink всех сигналов (метрики/логгирование/traces), отправляемых различными наблюдаемыми сервисами (нам будет добавляться в этот список по мере необходимости), он использует стандартную образ OTel вместе с нашей custom OTel конфигурацией (ниже), описывая различные потоки наблюдаемости (то есть, как сигналы должны быть приняты, обработаны и экспортированы по различным способам).jaeger
: наш Jaeger экземпляр, который будет принимать и хранить трассировки (экспортируемыеotel-collector
), этот сервис также выступает в качестве хранения, а также панели приложения (UI), которое мы экспортируем под префиксом HTTP path /jaeger, чтобы оно было доступно через nginx.prometheus
: хотя это не требуется для этой записи, мы также экспортируем металлы, чтобы они могли быть извлечены Prometheus. Мы не будем детально обсуждать это в этой статье.
несколько заметок:
- хотя это не требуется для этой записи, мы передаем
POSTGRES
данные подключения (как environment variables) дляotel-collector
, чтобы он мог извлекать health metrics Postgres. - Jaeger (с v1.35) поддерживает OTLP нативно.
- beauty OTLP в том, что могут быть соединены в цепочку OTel сборщики, образуя сеть сборчиков/преобразователей/передатчиков/подсистем распределения и т.п.
- OTLP может быть обслужен либо через gRPC- или HTTP-endpoint (на портах 4317 и 4318 соответственно).
- По умолчанию услуги OTLP запускаются на
localhost:4317/4318
. Это вполне адекватно, если Jaeger запущен на том же хосте/подкамере, где работают службы (мониторимые). Однако, учитывая, что Jaeger запущен на отдельном подкамере, их нужно связать с внешними адресами (0.0.0.0). Этого не было ясно в документации, и это привело к значительному количеству неудач. COLLECTOR_OTLP_ENABLED: true
теперь является default и не требует явного определения.
Конфигурация OTel
OTel также должен быть настроен специфическими приемниками, процессорами и экспортерами. мы это сделаем в configs/otel-collector.yaml.
Добавление приемников
Нам нужно сообщить сборщику OTel, какие приемники должны быть активированы. Это определено в секции receivers
:
receivers
otlp
protocols
http
endpoint 0.0.0.04318
grpc
endpoint 0.0.0.04317
postgresql
endpoint postgres5432
transport tcp
username $ POSTGRES_USER
password $ POSTGRES_PASSWORD
databases
$ POSTGRES_DB
collection_interval 10s
tls
insecuretrue
Это активирует получатель OTLP на портах 4317 и 4318 (grpc
, http
соответственно). Existuje много различных получателей, которые могут быть запущены. As пример, мы также добавили получатель “postgresql
”, который активно собирает метрики Postgres ( хотя это неpertinentно для этого поста). Получатели могут быть основаны на pull или push-а. Pull-ориентированные получатели периодически собирают определенные цели (например, postgres
), в то время как push-ориентированные получатели прослушивают и “получают” send метрики/логи/следы от приложений с использованием OTel клиентского SDK.
That’s it. Now our collector is ready to receive (or scrape) the appropriate metrics.
Add Processors
Processors in OTel are a way to transform, map, batch, filter, and/or enrich received signals before exporting them. For example, processors can sample metrics, filter logs, or even batch signals for efficiency. By default, no processors are added (making the collector a pass-through). We will ignore this for now.
Add Exporters
Теперь пришло время определить, где мы хотим, чтобы сигналы были экспортированы: на сервера backend, наилучшим соответствием для соответствующих сигналов. Precisely like receivers, exporters can also be either pull- or push-based. Push-based exporters are used to emit signals to another receiver that acts in push mode. These are outbound. Pull-based exporters expose endpoints that can be scraped by other pull-based receivers (for example, prometheus
). We will add an exporter of each kind: one for tracing and one for Prometheus to scrape from (though Prometheus is not the topic of this post):
exporters
otlp/jaeger
endpoint jaeger4317
tls
insecuretrue
prometheus
endpoint 0.0.0.09090
namespace onehub
debug
Here we have an exporter to Jaeger running the OTLP collector, as indicated by otlp/jaeger
. This exporter will push traces regularly to Jaeger. We are also adding a “scraper” endpoint on port 9090 which Prometheus will scrape regularly from.
The “debug” exporter simply is used for dumping signals to standard output/error streams.
Define Pipelines
The receiver, processor, and exporter sections simply define the modules that will be enabled by the collector. They are still not invoked. To actually invoke/activate them, they must be referred to as “pipelines”. Pipelines define how signals flow through and are processed by the collector. Our pipeline definitions (in the services
section) will clarify this:
service
extensions health_check
pipelines
traces
receivers otlp
processors
exporters otlp/jaeger debug
metrics
receivers otlp
processors
exporters prometheus debug
Here we are defining two pipelines. Note how similar the pipelines are but allow two different exporting modes (Jaeger and Prometheus). Now we are seeing the power of OTel and in creating pipelines within it.
traces
:
- Receive signals from client SDKs
- No processing
- Экспорт трассировок в консоль и Jaeger
метрики
:
- Получение сигналов от клиентских SDK-ов
- Непредсказуемое
- Экспорт метрик в консоль и Prometheus (через暴露 endpoint для его скачивания).
Просмотр Dashboards через Nginx
Jaeger обеспечивает Dashboard для визуализации трассировок о происходящих запросах. Это можно увидеть в браузере, включив следующее в нашемфайле конфигурации Nginx. Также, хотя это не тема этого поста, мы также экспортируем UI Prometheus через nginx по префиксу HTTP /prometheus
.
...
location ~ ^/jaeger
if ($request_method = OPTIONS ) return 200;
proxy_pass http://jaeger:16686; # Note that JaegerUI starts on port 16686 by default
proxy_pass_request_headers on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host-With-Port $http_host;
proxy_set_header Connection '';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
proxy_http_version 1.1;
chunked_transfer_encoding off;
location ~ ^/prometheus
if ($request_method = OPTIONS ) return 200;
proxy_pass http://prometheus:9090;
proxy_pass_request_headers on;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header Host-With-Port $http_host;
proxy_set_header Connection '';
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-HTTPS on;
proxy_set_header Authorization $http_authorization;
proxy_pass_header Authorization;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Prefix /;
proxy_http_version 1.1;
chunked_transfer_encoding off;
...
Визуализация трассировок в Jaeger
UI Jaeger довольно гибка и содержит множество возможностей для исследования. Перейдите к UI Jaeger в браузере. Вы увидите гибкую систему для поиска и анализа трассировок. Ознакомьтесь с основными разделами, включая поисковую строку и список трассировок. Можете искать различные трассировки по критериям поиска и отфильтровывать по сервисам, продолжительности операций, компонентам и т. д.
Анализируйте траектории трассировок в различных запросах, чтобы понять последовательность операций. Каждая Span представляет единицу работы, показывая время начала и окончания, продолжительность и связанные метаданные. Этот детальный вид очень помогает обнаружить проблемы с производительностью и ошибки внутри трассировки.
Интеграция клиентского SDK
Пока что мы настроили свои системы для визуализации, потребления сигналов и т. д. Однако наши услуги до сих пор не обновлены для испускания сигналов в OTel. Здесь мы интегрируемся с (Golang) клиентским SDK в различных частях нашего кода. Документация SDK SDK является отличным местом, где можно сначала familiarize yourself с некоторыми из понятий.
Ключевые понятия, с которыми мы будем работать, описаны ниже.
Ресурсы
Ресурсы являются энтитетом, который производит сигнал. В нашем случае спектр ресурса – это二进制, используемый для хранения услуг. В настоящее время у нас есть единственный ресурс для всего Onehub сервиса, но это может быть разделено позже.
Это определено в cmd/backend/obs.go. Обратите внимание, что клиентский SDK не требовал от нас исчерпывающего описания ресурса. Стандартный помощник (sdktrace.WithResource
) позволяет создать определение ресурса, инferring наиболее полезные части (такие как имя процесса, имя контейнера и т. д.) в runtime.
Мы только изменили одно: переопределили переменную среды OTEL_RESOURCE_ATTRIBUTES: service.name=onehub.backend,service.version=0.0.1
для сервиса onehub
в docker-compose.yml.
Пропагация контекстов
Пропагация контекста является очень важной темой в области наблюдаемости. различные столбы становятся экспоненциально мощными, когда мы можем коррелировать сигналы с каждого из них для идентификации проблем в нашем системе. рассмотрим контексты как extra биты данных, которые могут быть привязаны к сигналам: т.е., могут быть “соединены” на уникальном образе, чтобы связать различные сигналы с определенной группой (например, запросом).
Провайдеры/Экспортеры
Для каждого сигнала OTel предоставляет интерфейс Провайдера (например, TracerProvider
для экспорта Spans/следов, MeterProvider
для экспорта метрик, LoggerProvider
для экспорта логов и т. д.). для каждого из этих интерфейсов может быть несколько реализаций, например, Debug провайдер для отправки в stdout/err потоки, OTel провайдер для экспорта в другой OTel точку в цепочке, или даже прямой через различные экспортеры.然而, в нашем случае, мы хотим отложить выбор любого производителя из наших услуг и, скорее всего, отправить все сигналы в OTel сборщик, работающий в нашей среде.
Чтобы абстрагировать это, мы создадим тип “OTELSetup
”, который отслеживает различные провайдеры, которые мы можем использовать или заменить. В cmd/backend/obs.go, у нас есть:
type OTELSetup[C any] struct {
ctx context.Context
shutdownFuncs []ShutdownFunc
Resource *resource.Resource
Context C
SetupPropagator func(o *OTELSetup[C])
SetupTracerProvider func(o *OTELSetup[C]) (trace.TracerProvider, ShutdownFunc, error)
SetupMeterProvider func(o *OTELSetup[C]) (otelmetric.MeterProvider, ShutdownFunc, error)
SetupLogger func(o *OTELSetup[C]) (logr.Logger, ShutdownFunc, error)
SetupLoggerProvider func(o *OTELSetup[C]) (*log.LoggerProvider, ShutdownFunc, error)
}
Данный файл является простым обёртки, отслеживающей общие аспекты, необходимые для SDK OTel. Здесь мы имеем провайдеров (Логгер, Трассировщик иМетрики) а также способы для предоставления контекста (для трассировки). Общей ресурс используемый всеми провайдерами также определён здесь. Функции остановки интересны. Это функции, которые вызываются провайдерами, когда основной экспортёр завершён (гладко или вследствие выхода). Сама обёртка принимает общий для всех провайдеров ресурс.
В репозитории содержится две реализации этого:
- Сигналы о приостановке для стандартного вывода/ошибок – cmd/backend/stdout.go
- Экспорт сигналов в другого коллектора OTel – cmd/backend/otelcol.go
Мы будем инициализировать второе в нашем приложении. Мы не будем углубляться в детали конкретных реализаций, так как они были взяты из примеров в SDK с небольшими исправлениями и рефакторингом. Специфически, посмотрите на примерotel-collector для насpegпередачи идей.
Инициализируйте провайдеров OTel.
Основной принцип включения коллектора в наши услуги заключается в том, что какая-то определённая связанная с OTel “контекст” начинает своё существование на всех “входных” узлах. Если этот контекст создан в начале, то он будет отправлен всем целям, указанным здесь, и затем распространяться (до тех пор, пока мы делаем все правильно).
Принимая во внимание простую API-запрос ListTopics
(api/vi/topics
), наш запрос идет по следующему пути и обратно:
[ Browser ] ---> [ Nginx ] ---> [ gRPC Gateway ] ---> [ gRPC Service ] ---> [ Database ]
В нашем случае, входные узлы здесь находятся в начале, когда gRPC Gateway получает API-запрос от Nginx (мы могли бы начать отслеживать их с точки, когда HTTP-запрос попадает в Nginx, чтобы даже выделить задержки в Nginx, но мы подождем немного).
Необходимое:
- gRPC Gateway получает запрос.
- Он создает “пользовательскую” для OTel специфическую instancу
context.Context
. - Создает пользовательскую связь с соответствующим gRPC-сервисом (например,
TopicService
), передавая этот контекст вместо стандартного. - Соответствующий сервис использует этот контекст при испускании следов.
Посмотрим на это шаг за шагом.
Инициализируйте и подготовьте SDK OTel к использованию
В main.go, сначала инициализируем соединение с коллектором:
func main() {
flag.Parse()
// Handle SIGINT (CTRL+C) gracefully.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
collectorAddr := cmdutils.GetEnvOrDefault("OTEL_COLLECTOR_ADDR", "otel-collector:4317")
conn, err := grpc.NewClient(collectorAddr,
// Note the use of insecure transport here. TLS is recommended in production.
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Println("failed to create gRPC connection to collector: %w", err)
return
}
setup := NewOTELSetupWithCollector(conn)
err = setup.Setup(ctx)
if err != nil {
log.Println("error setting up otel: ", err)
}
defer func() {
err = setup.Shutdown(context.Background())
}()
ohdb := OpenOHDB()
srvErr := make(chan error, 2)
httpSrvChan := make(chan bool)
grpcSrvChan := make(chan bool)
go startGRPCServer(*addr, ohdb, srvErr, httpSrvChan)
go startGatewayServer(ctx, *gw_addr, *addr, srvErr, grpcSrvChan)
// Wait for interruption.
select {
case err = <-srvErr:
log.Println("Server error: ", err)
// Error when starting HTTP server or GRPC server
return
case <-ctx.Done():
// Wait for first CTRL+C.
// Stop receiving signal notifications as soon as possible.
stop()
}
// When Shutdown is called, ListenAndServe immediately returns ErrServerClosed.
httpSrvChan <- true
grpcSrvChan <- true
...
}
- Строки 8-16: Мы создаем соединение с
otel-collector
, запущенным в нашем окружении Docker. - Строки 17-21: Затем мы настраиваем OTel с тракером и провайдерами метрик, чтобы наш сборщик теперь мог отправлять все следы и метрики (помните, раньше мы определили приемники в конфигурации OTel).
- Строки 23-25: Мы устанавливаем финализаторы для очистки соединений/провайдеров OTel при остановке.
- Строка 27: Затем мы устанавливаем нашу базу данных и соединения, как и раньше.
- Строки 29 и выше: раньше мы просто запускали сервисы GRPC и Gateway в фоновом режиме, и это было всё. Мы не были слишком заинтересованы в их возвращаемых или выходных статусах. Чтобы создать более устойчивую систему, важно обладать лучшим представлением о жизненном цикле сервисов, которые мы запускаем. Так что сейчас мы передаем каналы “allback” для каждого из сервисов, которые мы запускаем. когда серверы завершают свою работу, соответствующие методы будут вызываться обратно через эти каналы, доступные им, для уведомления о том, что они ушли гладко. Наш целый бинарник закроется, когда уйдет любой из этих сервисов.
Как пример, посмотрим, как сервис Gateway использует этот канал.
Вместо того, чтобы запустить HTTP-сервер (для grpc-gateway), как:
1 http.ListenAndServe(gw_addr, mux)
Мы теперь имеем:
server := &http.Server{
Addr: gw_addr,
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: otelhttp.NewHandler(mux, "gateway", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s %s", operation, r.Method, r.URL.Path)
})),
}
go func() {
<-stopChan
if err := server.Shutdown(context.Background()); err != nil {
log.Fatalln(err)
}
}()
srvErr <- server.ListenAndServe()
Обратите внимание на строки 9-14, где остановка сервера наблюдается в отдельном горутине и на строке 15, где при ошибке при выходе сервера она возвращается через канал “notification”, который был передан как аргумент этому методу.
Теперь различные части наших услуг имеют доступ к “активному” соединению OTLP, которое может использоваться в любое время, когда необходимо отправлять сигналы.
Международное программное обеспечение для gRPC Gateway
В приведенном выше примере http.Server
образца, используемого для запуска gRPC Gateway, используетカスタム обработчик: http.Handler
из пакета OTel HTTP. Этот обработчик принимает существующий экземпляр http.Handler
, декоратор его с контекстами OTel и обеспечивает их распространение в любое другое вниз строки, которое вызывается.
server := &http.Server{
Addr: gw_addr,
BaseContext: func(_ net.Listener) context.Context { return ctx },
Handler: otelhttp.NewHandler(mux, "gateway", otelhttp.WithSpanNameFormatter(func(operation string, r *http.Request) string {
return fmt.Sprintf("%s %s %s", operation, r.Method, r.URL.Path)
})),
}
Наш HTTP-обработчик прост:
- Ряд 4: Мы создаем новый специфичный для OTel-обработчик для обработки HTTP-запросов.
- Ряд 5: Мы устанавливаем опцию
SpanFormatter
, чтобы следы могли быть уникально идентифицированы по методу и пути HTTP-запросов. Без этогоSpanNameFormatter
, default “name” для наших следов на шлюзе просто будет"gateway"
, приводя к тому, что все следы выглядят так:
Обёртка шлюза для gRPC-вызовов с использованием OTel
По умолчанию gRPC Gateway библиотека создает “простой” контекст при создании/управлении соединениями с базовыми gRPC-сервисами. после всего, шлюз не знает о OTel. в этом режиме, соединение (из пользователя/браузера) с gRPC Gateway и соединение от gateway к gRPC сервису будут трактоваться как два различных следа.
Таким образом, важно избавиться от обязательства создания gRPC-соединений от шлюза и вместо этого предоставить соединение, уже содержащее информацию о OTel. Мы это сделаем сейчас.
Перед интеграцией с OTel мы регистрировали обработчикゲート웨イ для наших gRPC следующим образом:
ctx := context.Background()
mux := runtime.NewServeMux() // Not showing the interceptors
opts := []grpc.DialOption{grpc.WithInsecure()}
// grpc_addr = ":9090"
v1.RegisterTopicServiceHandlerFromEndpoint(ctx, mux, grpc_addr, opts)
v1.RegisterMessageServiceHandlerFromEndpoint(ctx, mux, grpc_addr, opts)
// And other servers
Теперь передача другого соединения стала простой:
mux := // Creat the mux runtime as usual
// Use the OpenTelemetry gRPC client interceptor for tracing
trclient := grpc.WithStatsHandler(otelgrpc.NewClientHandler())
conn, err := grpc.NewClient(grpc_addr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
trclient)
if err != nil {
srvErr <- err
}
err = v1.RegisterTopicServiceHandler(ctx, mux, conn)
if err != nil {
srvErr <- err
}
// And the Message and User server too...
Что мы сделали, это сначала создали client
(Line 6), который выступает в качестве фабрики соединений для нашего gRPC сервера. Клиент довольно прост. Используется только gRPC ClientHandler
(otelgrpc.NewClientHandler
) для создания соединения. Это гарантирует, что контекст текущего просмотра, который начался с новым HTTP-запросом, теперь распространяется через этот обработчик на gRPC сервер.
Вот всё. Теперь мы должны начинать видеть новый запрос кゲート웨イ и запрос отゲート웨э к gRPC в едином консолидированном просмотре, а не как два разных просмотра.
Запуск и окончание секций
Мы практически there. До сих пор:
- Мы включили OTel коллектор и Jaeger для приёма и хранения данных трэйса (секций) (в docker-compose).
- Мы настроили базовый OTel коллектор (работает как отдельный подкаст) в качестве нашего “провайдера” трасеров, метрик и логов (то есть OTel интеграция нашего приложения будет использовать этот конечный точки для отложения всех сигналов).
- Мы обернули HTTP-обработчикゲート웨а, чтобы он был OTel-совместимым, чтобы трассировки и их контексты были созданы и распространены.
- Мы изменили (gRPC) клиент вゲート웨е, чтобы он теперь обертывал контекст OTel из нашего OTel настройки, а не использовал стандартный контекст.
- Мы создали глобальные экземпляры трассера/замера/логера, чтобы мы могли отправлять фактические сигналы с их помощью.
Теперь我们需要生成 span-ы для всех “интересных” мест в нашем коде. Возьмем метод ListTopics
например (в services/topics.go):
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
results, err := s.DB.ListTopics(ctx, "", 100)
if err != nil {
return nil, err
}
resp = &protos.ListTopicsResponse{Topics: gfn.Map(results, TopicToProto)}
return
}
Мы вызываем базу данных, чтобы забрать темы и возвратить их. Аналогично с методом доступа к базе данных (в datastore/topicds.go):
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
query := tdb.storage.Model(&Topic{}).Order("name asc")
if pageKey != "" {
count := 0
query = query.Offset(count)
}
if pageSize <= 0 || pageSize > tdb.MaxPageSize {
pageSize = tdb.MaxPageSize
}
query = query.Limit(pageSize)
err = query.Find(&out).Error
return out, err
}
Тут нам, в основном, интересно, сколько времени тратится на каждый из этих методов. Мы просто создаем span-ы в каждом из них, и дело кончено. Наши добавления к методам сервиса и datastore (соответственно) выглядят так:
services/topics.go
:
func (s *TopicService) ListTopics(ctx context.Context, req *protos.ListTopicsRequest) (resp *protos.ListTopicsResponse, err error) {
ctx, span := Tracer.Start(ctx, "ListTopics")
defer span.End()
... rest of the code to query the DB and return a proto response
}
datastore/topicds.go
:
func (tdb *OneHubDB) ListTopics(ctx context.Context, pageKey string, pageSize int) (out []*Topic, err error) {
_, span := Tracer.Start(ctx, "db.ListTopics")
defer span.End()
... rest of the code to fetch rows from the DB and return them
}
Общий шаблон таков:
1. Создать span:
ctx, span := Tracer.Start(ctx, "<span name>")
В этом случае указанный контекст (ctx
) “обернут” и возвращается новый контекст. Мы можем (и должны) передать этот новый обернутый контекст в дальнейшие методы. Мы и делаем это, когда вызываем метод datastore ListTopics
.
2. Завершить span:
defer span.End()
Завершение span-а (как только метод возвращает) уверенно записывает правильные временные показатели/коды и т. д. Мы также можем делать другие вещи, как добавлять теги и статусы в это, если необходимо, чтобы переносить больше информации для помощи в отладке.
Вот и всё. Вы можете увидеть свои красивые трассировки в Jaeger и получить больше и больше INSIGHTS о производительности ваших запросов, от начала до конца!
Заключение
В этой записи мы привели много информации, но все же мы еще только на колокольчике вокруг OTel и трассировки. Вместо того, чтобы перегрузить это (уже перегруженное) посте, мы будем представлять новые концепции и сложные детали в будущих постах. В настоящее время попробуйте эту идею в своих собственных сервисах и экспериментируйте с другими экспортерами и получателями в репозитории otel-contrib.
Source:
https://dzone.com/articles/tracing-with-opentelemetry-and-jaeger