Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Совсем недавно два стандарта – OpenTracing и OpenCensus – окончательно объединились в один. Появился новый стандарт распределенного трейсинга и мониторинга – OpenTelemetry. Но несмотря на то, что разработка библиотек идет полным ходом, реального опыта его использования пока не слишком много.
Илья Казначеев, который занимается разработкой восемь лет и работает backend-разработчиком в МТС, готов поделиться тем, как применять OpenTelemetry в Golang-проектах. На конференции Golang Live 2020 он рассказал о том, как настроить использование нового стандарта для трейсинга и мониторинга и подружить его с уже существующей в проекте инфраструктурой.
![](https://habrastorage.org/webt/r0/2s/m-/r02sm-hepn72bfotu2hrxww0010.png)
OpenTelemetry – стандарт, который появился относительно недавно: в конце прошлого года. При этом он получил широкое распространение и поддержку множества вендоров ПО для трейсинга и мониторинга.
Observability, или наблюдаемость, – термин из теории управления, который определяет, насколько можно судить о внутреннем состоянии системы по ее внешним проявлениям. В системной же архитектуре это обозначает набор подходов к наблюдению за состоянием системы в рантайме. К таким подходам относятся логирование, трейсинг и мониторинг.
![](https://habrastorage.org/webt/8b/uj/nh/8bujnhjkfnmmv3glopqv3qbwyne.png)
Для трейсинга и мониторинга существует множество вендорских решений. До недавнего времени было два открытых стандарта: OpenTracing от CNCF, который появился в 2016, и Open Census от Google, появившийся в 2018.
Это два довольно неплохих стандарта, которые конкурировали между собой некоторое время, пока в 2019 году они не решили объединиться в один новый стандарт, который называется OpenTelemetry.
![](https://habrastorage.org/webt/-n/77/rd/-n77rd0z6xw6e3gj9ls7ewavzv8.png)
Этот стандарт включает в себя распределенный трейсинг и мониторинг. Он совместим с первыми двумя. Более того, OpenTracing и Open Census прекращают поддержку в течение двух лет, что неотвратимо приближает нас к переходу на OpenTelemetry.
Сценарии использования
Стандарт предполагает широкие возможности совмещения всего со всем и является по сути активной прослойкой между источниками метрик и трейсов и их потребителями.
Давайте взглянем на основные сценарии.
Для распределенного трейсинга можно напрямую настроить подключение к Jaeger или к тому сервису, который вы используете.
![](https://habrastorage.org/webt/tu/my/yk/tumyykh5oqpg_-gralg_9h8cy-m.png)
Если трейсинг транслируется напрямую, можно использовать config и просто заменить библиотеку.
В случае, если ваше приложение уже использует OpenTracing, можно воспользоваться OpenTracing Bridge – оберткой, которая будет конвертировать запросы к OpenTracing API в OpenTelemetry API на верхнем уровне.
![](https://habrastorage.org/webt/la/3b/xr/la3bxrbvh2zbf6nk1pubujvzbha.png)
Для сбора метрик также можно настроить прямой доступ в Prometheus к порту для сбора метрик вашего приложения.
![](https://habrastorage.org/webt/2l/8k/l1/2l8kl1ck385o93-rqiehzewqudw.png)
Это пригодится, если у вас простая инфраструктура, и вы собираете метрики напрямую. Но стандарт предоставляет и более гибкие возможности.
Основной сценарий использования стандарта – это сбор метрик и трейсов через коллектор, который также запускается отдельным приложением либо контейнером в вашу инфраструктуру. Кроме того, можно взять готовый контейнер и установить его у себя.
Для этого достаточно настроить в приложении экспортер в формате OTLP. Это grpc-схема для передачи данных в формате OpenTracing. Со стороны коллектора можно настроить формат и параметры экспорта метрик и трейсов конечным потребителям, либо в другие форматы. Например, в OpenCensus.
![](https://habrastorage.org/webt/oa/w2/72/oaw272weoshgm5lygs4tytz92ws.png)
Коллектор позволяет подключать большое количество видов источников данных и множество приемников данных на выходе.
![](https://habrastorage.org/webt/te/5k/cn/te5kcnz9h8pkdd0nr_2papfrr98.png)
Таким образом, стандарт OpenTelemetry обеспечивает совместимость с многими открытыми и вендорскими стандартами.
Стандартный коллектор расширяем. Поэтому у большинства вендоров уже готовы экспортеры в их собственные решения, если они есть. Вы можете использовать OpenTelemetry, даже если применяете сбор метрик и трейсов от какого-то закрытого вендора. Таким образом решается проблема с vendor lock-in. Даже если что-то еще не появилось непосредственно для OpenTelemetry, это можно пробросить через OpenCensus.
Сам коллектор очень просто конфигурируется через банальный YAML конфиг:
![](https://habrastorage.org/webt/lj/pd/wg/ljpdwgue4h6eb6lzx2sqh6aurcm.png)
Здесь указываются ресиверы. В вашем приложении может быть какой-то другой источник (Kafka и т.д.):
![](https://habrastorage.org/webt/hs/dt/jf/hsdtjfffg5u53kwwdivb7e9b5pu.png)
Экспортеры – получатели данных.
Процессоры – методы обработки данных внутри коллектора:
![](https://habrastorage.org/webt/xj/bt/on/xjbtonlknk8vb5do0ibjayxoxmq.png)
И pipelines, которые непосредственно определяют, как будет обрабатываться каждый поток данных, который протекает внутри коллектора:
![](https://habrastorage.org/webt/te/s1/1v/tes11v7lgxrwarfz35stlprhno8.png)
Давайте рассмотрим один показательный пример.
![](https://habrastorage.org/webt/ky/9x/am/ky9xam8dsqhwdukqh5indpynfp4.png)
Допустим, у вас есть некий микросервис, к которому вы уже прикрутили OpenTelemetry и настроили его. И еще один сервис с аналогичной фрагментацией.
Пока все легко. Но появляются:
Все эти метрики можно объединить одним коллектором.
![](https://habrastorage.org/webt/tj/qa/na/tjqanakz_b61wyijkbaqj8um7-s.png)
Уже сейчас он поддерживает множество источников метрик и трейсов, которые используются в существующих приложениях. А на случай, если вы используете что-то экзотическое, можно реализовать собственный плагин. Но вряд ли это понадобится на практике. Потому что приложения, которые экспортируют метрики или трейсы, так или иначе используют либо какие-то распространенные стандарты, либо открытые стандарты типа OpenCensus.
Теперь мы хотим использовать эту информацию. В качестве экспортера трейсов можно указать Jaeger, а метрики отправлять в Prometheus, или что-то совместимое. Допустим, всеми любимую VictoriaMetrics.
Но что, если мы вдруг решили переехать в AWS и использовать местный трейсер X‑Ray? Не проблема. Это можно пробросить через OpenCensus, в котором есть экспортер для X‑Ray.
Таким образом, из этих кусков можно собрать всю вашу инфраструктуру для метрик и трейсов.
С теорией покончено. Поговорим о том, как использовать трейсинг на практике.
Инструментация Golang приложения: трейсинг
Сначала нужно создать корневой span, от которого и будет расти дерево вызовов.
ctx := context.Background()
tr := global.Tracer(«github.com/me/otel-demo»)
ctx, span := tr.Start(ctx, «root»)
span.AddEvent(ctx, «I am a root span!»)
doSomeAction(ctx, «12345»)
span.End()
Здесь указывается название вашего сервиса или библиотеки. В трейсе таким образом можно определить spans, которые лежат именно в рамках вашего приложения, и те, которые пошли в импортированные библиотеки.
Дальше создается корневой span с указанием названия:
…
ctx, span := tr.Start(ctx, «root»)
…
Выбирайте название, которое будет внятно описывать уровень трейса. Например, это может быть либо название метода (или класса и метода), либо слой архитектуры. Например, инфраструктурный уровень, уровень логики, уровень баз данных и т.д.
Данные о span также кладутся в контекст:
…
ctx, span := tr.Start(ctx, «root»)
span.AddEvent(ctx, «I am a root span!»)
doSomeAction(ctx, «12345»)
…
Поэтому в контекст нужно передавать методы, которые вы хотите трейсить.
Span представляет собой процесс на определенном уровне дерева вызова. В него можно положить атрибуты, логи и статусы ошибки, в случае ее возникновения. Span обязательно закрывать в конце. При закрытии рассчитывается его длительность.
ctx, span := tr.Start(ctx, «root»)
span.AddEvent(ctx, «I am a root span!»)
doSomeAction(ctx, «12345»)
span.End()
Так наш span выглядит в Jaeger:
![](https://habrastorage.org/webt/yo/s7/ox/yos7ox9umblnugebrizkgzycew4.png)
Его можно развернуть и посмотреть логи и атрибуты.
Далее можно получить тот же span из контекста, если не хочется задавать новый. Например, вы хотите писать один архитектурный слой в один span, а слой у вас разбросан по нескольким методам и нескольким уровням вызова. Вы его получаете, пишете в него, и потом он закрывается.
func doSomeAction(ctx context.Context, requestID string) {
span := trace.SpanFromContext(ctx)
span.AddEvent(ctx, «I am the same span!»)
…
}
Обратите внимание, что закрывать здесь его не нужно, потому что он закроется в том же методе, где был создан. Мы просто берем его из контекста.
Записываем сообщение в корневой span:
![](https://habrastorage.org/webt/pp/5z/m2/pp5zm2vf3zunqef7-zbvn2micai.png)
Иногда нужно создать новый дочерний span, чтобы он существовал отдельно.
func doSomeAction(ctx context.Context, requestID string) {
ctx, span := global.Tracer(«github.com/me/otel-demo»).
Start(ctx, «child»)
defer span.End()
span.AddEvent(ctx, «I am a child span!»)
…
}
Здесь мы получаем глобальный трейсер по имени библиотеки. Этот вызов можно обернуть в какой-нибудь метод, либо использовать глобальную переменную, потому что во всем вашем сервисе он будет одинаков.
Дальше создается дочерний span из контекста, и ему присваивается имя по аналогии с тем, как мы делали это в начале:
…
Start(ctx, «child»)
…
Не забудьте, что span нужно закрыть в конце метода, в котором он был создан.
…
ctx, span := global.Tracer(«github.com/me/otel-demo»).
Start(ctx, «child»)
defer span.End()
…
Пишем в него сообщения, которые попадают в дочерний span.
![](https://habrastorage.org/webt/5s/yz/5t/5syz5tphz6crzeidpruaayn1knq.png)
Здесь видно, что сообщения отображаются иерархически, и дочерний span находится под родительским. Он ожидаемо короче, потому что это был синхронный вызов.
В нем показываются атрибуты, которые можно писать в span:
func doSomeAction(ctx context.Context, requestID string) {
…
span.SetAttributes(label.String(«request.id», requestID))
span.AddEvent(ctx, «request validation ok»)
span.AddEvent(ctx, «entities loaded», label.Int64(«count», 123))
span.SetStatus(codes.Error, «insertion error»)
}
Например, сюда попал наш request. id:
![](https://habrastorage.org/webt/zy/9r/nk/zy9rnkdqgcybqabu4ukaajxn184.png)
Можно добавлять events:
…
span.AddEvent(ctx, «request validation ok»)
…
Кроме того, сюда можно добавлять label. Это работает примерно также, как структурный лог в виде logrus:
…
span.AddEvent(ctx, «entities loaded», label.Int64(«count», 123))
…
Здесь мы видим свое сообщение в логе span. Можно развернуть его и посмотреть labels. В нашем случае сюда добавился label count:
![](https://habrastorage.org/webt/vb/sf/__/vbsf__gxvocyj1iax7h_jew0g3a.png)
Потом это будет удобно использовать при фильтрации в поиске.
Если возникла ошибка, в span можно дописать статус. В этом случае он будет помечен как ошибочный.
…
span.SetStatus(codes.Error, «insertion error»)
…
Раньше стандарт использовал коды ошибок из OpenCensus, и они были из grpc. Сейчас оставили только OK, ERROR и UNSET. OK ставится по умолчанию, ERROR в случае ошибки добавляете вы.
Здесь видно, что трейс ошибки помечен красным значком. Есть код ошибки и сообщение о ней:
![](https://habrastorage.org/webt/yr/ai/va/yraiva8lepv6s7zzvg_qad9xsq0.png)
Нужно не забывать о том, что трейсинг – это не замена логов. Основной смысл в том, чтобы отслеживать протекание информации через распределенную систему, а для этого нужно класть трейсы в сетевые запросы и уметь читать их оттуда.
Трейсинг микросервисов
В OpenTelemetry уже есть множество set party реализаций interceptors и middleware для различных фреймворков и библиотек. Их можно найти в репозитории: github.com/open-telemetry/opentelemetry-go-contrib
Список фреймворков, для которых есть interceptors и middleware:
Как это использовать, посмотрим на примере стандартного http клиента и сервера.
middleware client
В клиенте просто добавляем interceptor в качестве транспорта, после чего наши запросы обогащаются на trace.id и необходимую для продолжения трейса информацию.
client := http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, _ := http.NewRequestWithContext(ctx, «GET», url, nil)
resp, err := client.Do(req)
middleware server
На сервере добавляется небольшой middleware с названием библиотеки:
http.Handle("/", otelhttp.NewHandler(
http.HandlerFunc(get), «root»))
err := http.ListenAndServe(addr, nil)
Дальше как обычно: получаете span из контекста, работаете с ним, пишите в него что-то, создаете дочерние spans, закрываете их и т.д.
Так выглядит простой запрос, проходящий через три сервиса:
![](https://habrastorage.org/webt/2o/b1/mk/2ob1mk9fzrcj92zx8pkfgvgugyk.png)
На скриншоте видна иерархия вызовов, разделение на сервисы, их длительность, последовательность. На каждый из них можно кликнуть и посмотреть более подробную информацию.
А так выглядит ошибка:
![](https://habrastorage.org/webt/nc/cq/9h/nccq9hbeawkirbnemciqg4nvfey.png)
Легко отследить, где она произошла, когда и сколько времени прошло.
В span можно посмотреть подробную информацию о контексте, в котором произошла ошибка:
![](https://habrastorage.org/webt/bz/4c/cw/bz4ccwfijif-iosb9z0ktptaab0.png)
Более того, поля, которые относятся ко всему span (различные id запроса, ключевые поля в таблице в запросе, еще какие-то мета данные, которые вы хотите положить) можно вкладывать в span при его создании. Грубо говоря, не нужно копипастить все эти поля в каждое место, где вы обрабатываете ошибку. Можно записать данные о ней в span.
middleware func
Вот небольшой бонус: как сделать middleware, чтобы можно было использовать его в качестве глобального middleware для таких вещей как Gorilla и Gin:
middleware := func(h http.Handler) http.Handler {
return otelhttp.NewHandler(h, «root»)
}
Инструментация Golang приложения: мониторинг
Пришло время поговорить о мониторинге.
Подключение к системе мониторинга настраивается аналогично тому, что делается для трейсинга.
Измерения делятся на два типа:
1. Синхронные, когда пользователь явно передает значения в момент вызова:
int64, float64
2. Асинхронные, которые SDK считывает в момент коллекта данных из приложения:
int64, float64
Сами метрики бывают:
− Аддитивные и монотонные (Counter, SumObserver), которые суммируют положительные числа, и они не уменьшаются.
− Аддитивные, но не монотонные (UpDownCounter, UpDownSumObserver), которые могут суммировать положительные и отрицательные числа.
− Неаддитивные (ValueRecorder, ValueObserver), которые просто записывают последовательность значений. Например, какое-то распределение.
В начале программы создается глобальный измеритель, которому указывается название библиотеки или сервиса.
meter := global.Meter(«github.com/ilyakaznacheev/otel-demo»)
floatCounter := metric.Must(meter).NewFloat64Counter(
«float_counter»,
metric.WithDescription(«Cumulative float counter»),
).Bind(label.String(«label_a», «some label»))
defer floatCounter.Unbind()
Дальше создается метрика:
…
floatCounter := metric.Must(meter).NewFloat64Counter(
«float_counter»,
metric.WithDescription(«Cumulative float counter»),
).Bind(label.String(«label_a», «some label»))
…
Ей указывается название:
…
«float_counter»,
…
Описание:
…
metric.WithDescription(«Cumulative float counter»),
…
Набор лейблов, по которым вы потом можете фильтровать запросы. Например, при построении дашбордов в Grafana:
…
).Bind(label.String(«label_a», «some label»))
…
В конце программы также нужно вызвать Unbind для каждой метрики, что освободит ресурсы и правильно ее закроет:
…
defer floatCounter.Unbind()
…
Записывать изменения просто:
var (
counter metric.BoundFloat64Counter
udCounter metric.BoundFloat64UpDownCounter
valueRecorder metric.BoundFloat64ValueRecorder
)
…
counter.Add(ctx, 1.5)
udCounter.Add(ctx, -2.5)
valueRecorder.Record(ctx, 3.5)
Это положительные числа для Counter, любые числа для UpDownCounter, которые он будет суммировать, и также любые числа для ValueRecorder. Для всех видов инструментов в Go поддерживаются int64 и float64.
Вот что мы получаем на выходе:
# HELP float_counter Cumulative float counter
# TYPE float_counter counter
float_counter{label_a=«some label»} 20
Это наша метрика с комментарием и заданным лейблом. Потом можно ее взять либо напрямую через Prometheus, либо экспортировать через OpenTelemetry коллектор, и дальше использовать там, где нам нужно.
Инструментация Golang приложения: библиотеки
Последнее, о чем хочется сказать – это возможность, которую стандарт дает для инструментирования библиотек.
Раньше, когда использовались OpenCensus и OpenTracing, вы не могли инструментировать ваши отдельные библиотеки, особенно опенсорсные. Потому что в этом случае у вас получался vendor lock-in. Тот, кто плотно работал с трейсингом, наверняка обращал внимание на то, что большие клиентские библиотеки, или крупные API к облачным сервисам, время от времени падают с трудно объяснимыми ошибками.
Тут бы очень пригодился трейсинг. Особенно в продуктиве, когда у вас происходит какая-то неясная ситуация, и очень хотелось бы знать, почему она произошла. Но все, что у вас есть – это сообщение об ошибке из вашей импортированной библиотеки.
OpenTelemetry решает эту проблему.
![](https://habrastorage.org/webt/rt/yn/qv/rtynqvux6e4u6pu5ym3j_q0occu.png)
Так как в стандарте SDK и API разделены, API трейсинга метрик можно использовать независимо от SDK и конкретных настроек экспорта данных. Более того, вы можете сначала инструментировать свои методы, а только потом настроить экспорт этих данных вовне.
Таким образом можно инструментировать импортируемую библиотеку, не заботясь о том, как и куда будут экспортированы эти данные. Это подойдет и для внутренних, и для открытых опенсорсных библиотек.
Не нужно заботиться о vendor lock-in, не нужно переживать по поводу того, как будет использована эта информация и будет ли использована вообще. Библиотеки и приложения инструментируются заранее, а конфигурация экспорта данных указывается при инициализации приложения.
Таким образом видно, что настройки конфигурации задаются в SDK приложении. Дальше нужно заняться экспортерами трейсинга и метрик. Это может быть один экспортер через OTLP, если вы экспортируете в OpenTelemetry коллектор. Потом все необходимые трейсы и метрики попадают в контекст, а он пропагируется по дереву вызова другим методом вниз.
Приложение наследует от корневого span остальные spans, просто используя OpenTelemetry API и данные, которые лежат в контексте. При этом импортируемые библиотеки получают на вход методы контекст, пытаются считать из этого метода информацию о корневом span. Если его нет, создают свой, и дальше инструментируют логику. Таким образом, вы можете сначала инструментировать свою библиотеку.
Более того, вы можете инструментировать все, но не настраивать экспортеры данных, и просто задеплоить это.
У вас это может работать в проде, и пока инфраструктура не устоялась, у вас не будет настроен трейсинг и мониторинг. Потом вы настроите их, развернете там коллектор, какие-то приложения для сбора этих данных, и у вас все заработает. Вам не нужно ничего менять непосредственно в самих методах.
Таким образом, если у вас какая-то опенсорсная библиотека, вы можете инструментировать ее при помощи OpenTelemetry. Потом люди, которые ее используют, настроят у себя OpenTelemetry и будут использовать эти данные.
В заключении хочу сказать, что стандарт OpenTelemetry многообещающий. Возможно, наконец это тот самый универсальный стандарт, который мы все хотели увидеть.
У нас в компании активно применяется стандарт OpenCensus для трейсинга и мониторинга микросервисного ландшафта компании. Планируется внедрение OpenTelemetry после его релиза.
Илья Казначеев, который занимается разработкой восемь лет и работает backend-разработчиком в МТС, готов поделиться тем, как применять OpenTelemetry в Golang-проектах. На конференции Golang Live 2020 он рассказал о том, как настроить использование нового стандарта для трейсинга и мониторинга и подружить его с уже существующей в проекте инфраструктурой.
![](https://habrastorage.org/webt/r0/2s/m-/r02sm-hepn72bfotu2hrxww0010.png)
OpenTelemetry – стандарт, который появился относительно недавно: в конце прошлого года. При этом он получил широкое распространение и поддержку множества вендоров ПО для трейсинга и мониторинга.
Observability, или наблюдаемость, – термин из теории управления, который определяет, насколько можно судить о внутреннем состоянии системы по ее внешним проявлениям. В системной же архитектуре это обозначает набор подходов к наблюдению за состоянием системы в рантайме. К таким подходам относятся логирование, трейсинг и мониторинг.
![](https://habrastorage.org/webt/8b/uj/nh/8bujnhjkfnmmv3glopqv3qbwyne.png)
Для трейсинга и мониторинга существует множество вендорских решений. До недавнего времени было два открытых стандарта: OpenTracing от CNCF, который появился в 2016, и Open Census от Google, появившийся в 2018.
Это два довольно неплохих стандарта, которые конкурировали между собой некоторое время, пока в 2019 году они не решили объединиться в один новый стандарт, который называется OpenTelemetry.
![](https://habrastorage.org/webt/-n/77/rd/-n77rd0z6xw6e3gj9ls7ewavzv8.png)
Этот стандарт включает в себя распределенный трейсинг и мониторинг. Он совместим с первыми двумя. Более того, OpenTracing и Open Census прекращают поддержку в течение двух лет, что неотвратимо приближает нас к переходу на OpenTelemetry.
Сценарии использования
Стандарт предполагает широкие возможности совмещения всего со всем и является по сути активной прослойкой между источниками метрик и трейсов и их потребителями.
Давайте взглянем на основные сценарии.
Для распределенного трейсинга можно напрямую настроить подключение к Jaeger или к тому сервису, который вы используете.
![](https://habrastorage.org/webt/tu/my/yk/tumyykh5oqpg_-gralg_9h8cy-m.png)
Если трейсинг транслируется напрямую, можно использовать config и просто заменить библиотеку.
В случае, если ваше приложение уже использует OpenTracing, можно воспользоваться OpenTracing Bridge – оберткой, которая будет конвертировать запросы к OpenTracing API в OpenTelemetry API на верхнем уровне.
![](https://habrastorage.org/webt/la/3b/xr/la3bxrbvh2zbf6nk1pubujvzbha.png)
Для сбора метрик также можно настроить прямой доступ в Prometheus к порту для сбора метрик вашего приложения.
![](https://habrastorage.org/webt/2l/8k/l1/2l8kl1ck385o93-rqiehzewqudw.png)
Это пригодится, если у вас простая инфраструктура, и вы собираете метрики напрямую. Но стандарт предоставляет и более гибкие возможности.
Основной сценарий использования стандарта – это сбор метрик и трейсов через коллектор, который также запускается отдельным приложением либо контейнером в вашу инфраструктуру. Кроме того, можно взять готовый контейнер и установить его у себя.
Для этого достаточно настроить в приложении экспортер в формате OTLP. Это grpc-схема для передачи данных в формате OpenTracing. Со стороны коллектора можно настроить формат и параметры экспорта метрик и трейсов конечным потребителям, либо в другие форматы. Например, в OpenCensus.
![](https://habrastorage.org/webt/oa/w2/72/oaw272weoshgm5lygs4tytz92ws.png)
Коллектор позволяет подключать большое количество видов источников данных и множество приемников данных на выходе.
![](https://habrastorage.org/webt/te/5k/cn/te5kcnz9h8pkdd0nr_2papfrr98.png)
Таким образом, стандарт OpenTelemetry обеспечивает совместимость с многими открытыми и вендорскими стандартами.
Стандартный коллектор расширяем. Поэтому у большинства вендоров уже готовы экспортеры в их собственные решения, если они есть. Вы можете использовать OpenTelemetry, даже если применяете сбор метрик и трейсов от какого-то закрытого вендора. Таким образом решается проблема с vendor lock-in. Даже если что-то еще не появилось непосредственно для OpenTelemetry, это можно пробросить через OpenCensus.
Сам коллектор очень просто конфигурируется через банальный YAML конфиг:
![](https://habrastorage.org/webt/lj/pd/wg/ljpdwgue4h6eb6lzx2sqh6aurcm.png)
Здесь указываются ресиверы. В вашем приложении может быть какой-то другой источник (Kafka и т.д.):
![](https://habrastorage.org/webt/hs/dt/jf/hsdtjfffg5u53kwwdivb7e9b5pu.png)
Экспортеры – получатели данных.
Процессоры – методы обработки данных внутри коллектора:
![](https://habrastorage.org/webt/xj/bt/on/xjbtonlknk8vb5do0ibjayxoxmq.png)
И pipelines, которые непосредственно определяют, как будет обрабатываться каждый поток данных, который протекает внутри коллектора:
![](https://habrastorage.org/webt/te/s1/1v/tes11v7lgxrwarfz35stlprhno8.png)
Давайте рассмотрим один показательный пример.
![](https://habrastorage.org/webt/ky/9x/am/ky9xam8dsqhwdukqh5indpynfp4.png)
Допустим, у вас есть некий микросервис, к которому вы уже прикрутили OpenTelemetry и настроили его. И еще один сервис с аналогичной фрагментацией.
Пока все легко. Но появляются:
- legacy-сервисы, которые работают через OpenCensus;
- база данных, которая отдает данные в своем формате (например, напрямую в Prometheus, как это делает PostgreSQL);
- еще какой-то сервис, работающий в контейнере и отдающий метрики в своем формате. Вам не хочется перебилдить этот контейнер и прикручивать sidecars, чтобы они переформатировали метрики. Вы хотите просто брать и отправлять их.
- железо, с которого вы тоже собираете метрики и хотите как-то использовать их.
Все эти метрики можно объединить одним коллектором.
![](https://habrastorage.org/webt/tj/qa/na/tjqanakz_b61wyijkbaqj8um7-s.png)
Уже сейчас он поддерживает множество источников метрик и трейсов, которые используются в существующих приложениях. А на случай, если вы используете что-то экзотическое, можно реализовать собственный плагин. Но вряд ли это понадобится на практике. Потому что приложения, которые экспортируют метрики или трейсы, так или иначе используют либо какие-то распространенные стандарты, либо открытые стандарты типа OpenCensus.
Теперь мы хотим использовать эту информацию. В качестве экспортера трейсов можно указать Jaeger, а метрики отправлять в Prometheus, или что-то совместимое. Допустим, всеми любимую VictoriaMetrics.
Но что, если мы вдруг решили переехать в AWS и использовать местный трейсер X‑Ray? Не проблема. Это можно пробросить через OpenCensus, в котором есть экспортер для X‑Ray.
Таким образом, из этих кусков можно собрать всю вашу инфраструктуру для метрик и трейсов.
С теорией покончено. Поговорим о том, как использовать трейсинг на практике.
Инструментация Golang приложения: трейсинг
Сначала нужно создать корневой span, от которого и будет расти дерево вызовов.
ctx := context.Background()
tr := global.Tracer(«github.com/me/otel-demo»)
ctx, span := tr.Start(ctx, «root»)
span.AddEvent(ctx, «I am a root span!»)
doSomeAction(ctx, «12345»)
span.End()
Здесь указывается название вашего сервиса или библиотеки. В трейсе таким образом можно определить spans, которые лежат именно в рамках вашего приложения, и те, которые пошли в импортированные библиотеки.
Дальше создается корневой span с указанием названия:
…
ctx, span := tr.Start(ctx, «root»)
…
Выбирайте название, которое будет внятно описывать уровень трейса. Например, это может быть либо название метода (или класса и метода), либо слой архитектуры. Например, инфраструктурный уровень, уровень логики, уровень баз данных и т.д.
Данные о span также кладутся в контекст:
…
ctx, span := tr.Start(ctx, «root»)
span.AddEvent(ctx, «I am a root span!»)
doSomeAction(ctx, «12345»)
…
Поэтому в контекст нужно передавать методы, которые вы хотите трейсить.
Span представляет собой процесс на определенном уровне дерева вызова. В него можно положить атрибуты, логи и статусы ошибки, в случае ее возникновения. Span обязательно закрывать в конце. При закрытии рассчитывается его длительность.
ctx, span := tr.Start(ctx, «root»)
span.AddEvent(ctx, «I am a root span!»)
doSomeAction(ctx, «12345»)
span.End()
Так наш span выглядит в Jaeger:
![](https://habrastorage.org/webt/yo/s7/ox/yos7ox9umblnugebrizkgzycew4.png)
Его можно развернуть и посмотреть логи и атрибуты.
Далее можно получить тот же span из контекста, если не хочется задавать новый. Например, вы хотите писать один архитектурный слой в один span, а слой у вас разбросан по нескольким методам и нескольким уровням вызова. Вы его получаете, пишете в него, и потом он закрывается.
func doSomeAction(ctx context.Context, requestID string) {
span := trace.SpanFromContext(ctx)
span.AddEvent(ctx, «I am the same span!»)
…
}
Обратите внимание, что закрывать здесь его не нужно, потому что он закроется в том же методе, где был создан. Мы просто берем его из контекста.
Записываем сообщение в корневой span:
![](https://habrastorage.org/webt/pp/5z/m2/pp5zm2vf3zunqef7-zbvn2micai.png)
Иногда нужно создать новый дочерний span, чтобы он существовал отдельно.
func doSomeAction(ctx context.Context, requestID string) {
ctx, span := global.Tracer(«github.com/me/otel-demo»).
Start(ctx, «child»)
defer span.End()
span.AddEvent(ctx, «I am a child span!»)
…
}
Здесь мы получаем глобальный трейсер по имени библиотеки. Этот вызов можно обернуть в какой-нибудь метод, либо использовать глобальную переменную, потому что во всем вашем сервисе он будет одинаков.
Дальше создается дочерний span из контекста, и ему присваивается имя по аналогии с тем, как мы делали это в начале:
…
Start(ctx, «child»)
…
Не забудьте, что span нужно закрыть в конце метода, в котором он был создан.
…
ctx, span := global.Tracer(«github.com/me/otel-demo»).
Start(ctx, «child»)
defer span.End()
…
Пишем в него сообщения, которые попадают в дочерний span.
![](https://habrastorage.org/webt/5s/yz/5t/5syz5tphz6crzeidpruaayn1knq.png)
Здесь видно, что сообщения отображаются иерархически, и дочерний span находится под родительским. Он ожидаемо короче, потому что это был синхронный вызов.
В нем показываются атрибуты, которые можно писать в span:
func doSomeAction(ctx context.Context, requestID string) {
…
span.SetAttributes(label.String(«request.id», requestID))
span.AddEvent(ctx, «request validation ok»)
span.AddEvent(ctx, «entities loaded», label.Int64(«count», 123))
span.SetStatus(codes.Error, «insertion error»)
}
Например, сюда попал наш request. id:
![](https://habrastorage.org/webt/zy/9r/nk/zy9rnkdqgcybqabu4ukaajxn184.png)
Можно добавлять events:
…
span.AddEvent(ctx, «request validation ok»)
…
Кроме того, сюда можно добавлять label. Это работает примерно также, как структурный лог в виде logrus:
…
span.AddEvent(ctx, «entities loaded», label.Int64(«count», 123))
…
Здесь мы видим свое сообщение в логе span. Можно развернуть его и посмотреть labels. В нашем случае сюда добавился label count:
![](https://habrastorage.org/webt/vb/sf/__/vbsf__gxvocyj1iax7h_jew0g3a.png)
Потом это будет удобно использовать при фильтрации в поиске.
Если возникла ошибка, в span можно дописать статус. В этом случае он будет помечен как ошибочный.
…
span.SetStatus(codes.Error, «insertion error»)
…
Раньше стандарт использовал коды ошибок из OpenCensus, и они были из grpc. Сейчас оставили только OK, ERROR и UNSET. OK ставится по умолчанию, ERROR в случае ошибки добавляете вы.
Здесь видно, что трейс ошибки помечен красным значком. Есть код ошибки и сообщение о ней:
![](https://habrastorage.org/webt/yr/ai/va/yraiva8lepv6s7zzvg_qad9xsq0.png)
Нужно не забывать о том, что трейсинг – это не замена логов. Основной смысл в том, чтобы отслеживать протекание информации через распределенную систему, а для этого нужно класть трейсы в сетевые запросы и уметь читать их оттуда.
Трейсинг микросервисов
В OpenTelemetry уже есть множество set party реализаций interceptors и middleware для различных фреймворков и библиотек. Их можно найти в репозитории: github.com/open-telemetry/opentelemetry-go-contrib
Список фреймворков, для которых есть interceptors и middleware:
- beego
- go-restful
- gin
- gocql
- mux
- echo
- http
- grpc
- sarama
- memcache
- mongo
- macaron
Как это использовать, посмотрим на примере стандартного http клиента и сервера.
middleware client
В клиенте просто добавляем interceptor в качестве транспорта, после чего наши запросы обогащаются на trace.id и необходимую для продолжения трейса информацию.
client := http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, _ := http.NewRequestWithContext(ctx, «GET», url, nil)
resp, err := client.Do(req)
middleware server
На сервере добавляется небольшой middleware с названием библиотеки:
http.Handle("/", otelhttp.NewHandler(
http.HandlerFunc(get), «root»))
err := http.ListenAndServe(addr, nil)
Дальше как обычно: получаете span из контекста, работаете с ним, пишите в него что-то, создаете дочерние spans, закрываете их и т.д.
Так выглядит простой запрос, проходящий через три сервиса:
![](https://habrastorage.org/webt/2o/b1/mk/2ob1mk9fzrcj92zx8pkfgvgugyk.png)
На скриншоте видна иерархия вызовов, разделение на сервисы, их длительность, последовательность. На каждый из них можно кликнуть и посмотреть более подробную информацию.
А так выглядит ошибка:
![](https://habrastorage.org/webt/nc/cq/9h/nccq9hbeawkirbnemciqg4nvfey.png)
Легко отследить, где она произошла, когда и сколько времени прошло.
В span можно посмотреть подробную информацию о контексте, в котором произошла ошибка:
![](https://habrastorage.org/webt/bz/4c/cw/bz4ccwfijif-iosb9z0ktptaab0.png)
Более того, поля, которые относятся ко всему span (различные id запроса, ключевые поля в таблице в запросе, еще какие-то мета данные, которые вы хотите положить) можно вкладывать в span при его создании. Грубо говоря, не нужно копипастить все эти поля в каждое место, где вы обрабатываете ошибку. Можно записать данные о ней в span.
middleware func
Вот небольшой бонус: как сделать middleware, чтобы можно было использовать его в качестве глобального middleware для таких вещей как Gorilla и Gin:
middleware := func(h http.Handler) http.Handler {
return otelhttp.NewHandler(h, «root»)
}
Инструментация Golang приложения: мониторинг
Пришло время поговорить о мониторинге.
Подключение к системе мониторинга настраивается аналогично тому, что делается для трейсинга.
Измерения делятся на два типа:
1. Синхронные, когда пользователь явно передает значения в момент вызова:
- Counter
- UpDownCounter
- ValueRecorder
int64, float64
2. Асинхронные, которые SDK считывает в момент коллекта данных из приложения:
- SumObserver
- UpDownSumObserver
- ValueObserver
int64, float64
Сами метрики бывают:
− Аддитивные и монотонные (Counter, SumObserver), которые суммируют положительные числа, и они не уменьшаются.
− Аддитивные, но не монотонные (UpDownCounter, UpDownSumObserver), которые могут суммировать положительные и отрицательные числа.
− Неаддитивные (ValueRecorder, ValueObserver), которые просто записывают последовательность значений. Например, какое-то распределение.
В начале программы создается глобальный измеритель, которому указывается название библиотеки или сервиса.
meter := global.Meter(«github.com/ilyakaznacheev/otel-demo»)
floatCounter := metric.Must(meter).NewFloat64Counter(
«float_counter»,
metric.WithDescription(«Cumulative float counter»),
).Bind(label.String(«label_a», «some label»))
defer floatCounter.Unbind()
Дальше создается метрика:
…
floatCounter := metric.Must(meter).NewFloat64Counter(
«float_counter»,
metric.WithDescription(«Cumulative float counter»),
).Bind(label.String(«label_a», «some label»))
…
Ей указывается название:
…
«float_counter»,
…
Описание:
…
metric.WithDescription(«Cumulative float counter»),
…
Набор лейблов, по которым вы потом можете фильтровать запросы. Например, при построении дашбордов в Grafana:
…
).Bind(label.String(«label_a», «some label»))
…
В конце программы также нужно вызвать Unbind для каждой метрики, что освободит ресурсы и правильно ее закроет:
…
defer floatCounter.Unbind()
…
Записывать изменения просто:
var (
counter metric.BoundFloat64Counter
udCounter metric.BoundFloat64UpDownCounter
valueRecorder metric.BoundFloat64ValueRecorder
)
…
counter.Add(ctx, 1.5)
udCounter.Add(ctx, -2.5)
valueRecorder.Record(ctx, 3.5)
Это положительные числа для Counter, любые числа для UpDownCounter, которые он будет суммировать, и также любые числа для ValueRecorder. Для всех видов инструментов в Go поддерживаются int64 и float64.
Вот что мы получаем на выходе:
# HELP float_counter Cumulative float counter
# TYPE float_counter counter
float_counter{label_a=«some label»} 20
Это наша метрика с комментарием и заданным лейблом. Потом можно ее взять либо напрямую через Prometheus, либо экспортировать через OpenTelemetry коллектор, и дальше использовать там, где нам нужно.
Инструментация Golang приложения: библиотеки
Последнее, о чем хочется сказать – это возможность, которую стандарт дает для инструментирования библиотек.
Раньше, когда использовались OpenCensus и OpenTracing, вы не могли инструментировать ваши отдельные библиотеки, особенно опенсорсные. Потому что в этом случае у вас получался vendor lock-in. Тот, кто плотно работал с трейсингом, наверняка обращал внимание на то, что большие клиентские библиотеки, или крупные API к облачным сервисам, время от времени падают с трудно объяснимыми ошибками.
Тут бы очень пригодился трейсинг. Особенно в продуктиве, когда у вас происходит какая-то неясная ситуация, и очень хотелось бы знать, почему она произошла. Но все, что у вас есть – это сообщение об ошибке из вашей импортированной библиотеки.
OpenTelemetry решает эту проблему.
![](https://habrastorage.org/webt/rt/yn/qv/rtynqvux6e4u6pu5ym3j_q0occu.png)
Так как в стандарте SDK и API разделены, API трейсинга метрик можно использовать независимо от SDK и конкретных настроек экспорта данных. Более того, вы можете сначала инструментировать свои методы, а только потом настроить экспорт этих данных вовне.
Таким образом можно инструментировать импортируемую библиотеку, не заботясь о том, как и куда будут экспортированы эти данные. Это подойдет и для внутренних, и для открытых опенсорсных библиотек.
Не нужно заботиться о vendor lock-in, не нужно переживать по поводу того, как будет использована эта информация и будет ли использована вообще. Библиотеки и приложения инструментируются заранее, а конфигурация экспорта данных указывается при инициализации приложения.
Таким образом видно, что настройки конфигурации задаются в SDK приложении. Дальше нужно заняться экспортерами трейсинга и метрик. Это может быть один экспортер через OTLP, если вы экспортируете в OpenTelemetry коллектор. Потом все необходимые трейсы и метрики попадают в контекст, а он пропагируется по дереву вызова другим методом вниз.
Приложение наследует от корневого span остальные spans, просто используя OpenTelemetry API и данные, которые лежат в контексте. При этом импортируемые библиотеки получают на вход методы контекст, пытаются считать из этого метода информацию о корневом span. Если его нет, создают свой, и дальше инструментируют логику. Таким образом, вы можете сначала инструментировать свою библиотеку.
Более того, вы можете инструментировать все, но не настраивать экспортеры данных, и просто задеплоить это.
У вас это может работать в проде, и пока инфраструктура не устоялась, у вас не будет настроен трейсинг и мониторинг. Потом вы настроите их, развернете там коллектор, какие-то приложения для сбора этих данных, и у вас все заработает. Вам не нужно ничего менять непосредственно в самих методах.
Таким образом, если у вас какая-то опенсорсная библиотека, вы можете инструментировать ее при помощи OpenTelemetry. Потом люди, которые ее используют, настроят у себя OpenTelemetry и будут использовать эти данные.
В заключении хочу сказать, что стандарт OpenTelemetry многообещающий. Возможно, наконец это тот самый универсальный стандарт, который мы все хотели увидеть.
У нас в компании активно применяется стандарт OpenCensus для трейсинга и мониторинга микросервисного ландшафта компании. Планируется внедрение OpenTelemetry после его релиза.