Ilya Kaznacheev, quien ha estado desarrollando durante ocho años y trabaja como desarrollador backend en MTS, está listo para compartir cómo usar OpenTelemetry en proyectos de Golang. En la conferencia Golang Live 2020, habló sobre cómo configurar el uso de un nuevo estándar para rastreo y monitoreo y hacerlo amigo de la infraestructura ya existente en el proyecto.

OpenTelemetry es un estándar relativamente reciente, a fines del año pasado. Al mismo tiempo, recibió una amplia distribución y apoyo de muchos proveedores de software para seguimiento y monitoreo.
Observabilidad, u observabilidad, es un término de la teoría del control que determina cuánto se puede juzgar el estado interno de un sistema por sus manifestaciones externas. En la arquitectura del sistema, esto significa un conjunto de enfoques para monitorear el estado del sistema en tiempo de ejecución. Estos enfoques incluyen registro, rastreo y monitoreo.

Hay muchas soluciones de proveedores para seguimiento y monitoreo. Hasta hace poco, había dos estándares abiertos: OpenTracing de CNCF, que apareció en 2016, y Open Census, de Google, que apareció en 2018.
Estos son dos estándares bastante buenos que compitieron entre sí por un tiempo, hasta que en 2019 decidieron fusionarse en un nuevo estándar llamado OpenTelemetry.

Este estándar incluye seguimiento y supervisión distribuidos. Es compatible con los dos primeros. Además, OpenTracing y Open Census han dejado de brindar soporte en dos años, lo que inevitablemente nos acerca a movernos a OpenTelemetry.
Casos de uso
El estándar asume amplias oportunidades para combinar todo con todo y es, de hecho, una capa activa entre las fuentes de métricas y trazas y sus consumidores.
Echemos un vistazo a los principales escenarios.
Para el rastreo distribuido, puede configurar directamente una conexión a Jaeger o cualquier servicio que esté utilizando.

Si el seguimiento se transmite directamente, puede usar config y simplemente reemplazar la biblioteca.
En caso de que su aplicación ya use OpenTracing, puede usar OpenTracing Bridge, un contenedor que convertirá las solicitudes a la API de OpenTracing a la API de OpenTelemetry en el nivel superior.

Para recopilar métricas, también puede configurar Prometheus para acceder directamente al puerto de métricas de su aplicación.

Esto es útil si tiene una infraestructura simple y recopila métricas directamente. Pero el estándar también proporciona más flexibilidad.
El escenario principal para usar el estándar es recopilar métricas y rastreos a través de un recopilador, que también es lanzado por una aplicación o contenedor separado en su infraestructura. Además, puede tomar un contenedor listo para usar e instalarlo en casa.
Para ello basta con configurar el exportador en formato OTLP en la aplicación. Es un esquema grpc para la transmisión de datos en formato OpenTracing. Desde el lado del recopilador, puede configurar el formato y los parámetros para exportar métricas y seguimientos a usuarios finales u otros formatos. Por ejemplo, en OpenCensus.

El recopilador le permite conectar una gran cantidad de tipos de fuentes de datos y muchos sumideros de datos en la salida.

Por lo tanto, el estándar OpenTelemetry proporciona compatibilidad con muchos estándares de proveedores y de código abierto.
El colector estándar es ampliable. Por lo tanto, la mayoría de los proveedores ya tienen exportadores listos para sus propias soluciones, si las hay. Puede utilizar OpenTelemetry incluso si recopila métricas y seguimientos de algún proveedor propietario. Esto resuelve el problema del bloqueo del proveedor. Incluso si algo aún no ha aparecido directamente para OpenTelemetry, se puede reenviar a través de OpenCensus.
El colector en sí es muy fácil de configurar a través de la configuración banal de YAML: los

receptores se especifican aquí. Su aplicación puede tener otra fuente (Kafka, etc.):

Exportadores: destinatarios de los datos.
Procesadores: métodos para procesar datos dentro del recopilador:

Y canalizaciones, que definen directamente cómo se manejará cada flujo de datos que fluye dentro de un recopilador:

veamos un ejemplo ilustrativo.

Digamos que tienes un microservicio al que ya le has atornillado OpenTelemetry y lo has configurado. Y un servicio más con una fragmentación similar.
Hasta ahora todo es fácil. Pero hay:
- servicios heredados que se ejecutan a través de OpenCensus;
- una base de datos que envía datos en su propio formato (por ejemplo, directamente a Prometheus, como lo hace PostgreSQL);
- algún otro servicio que funcione en un contenedor y proporcione métricas en su propio formato. No desea reconstruir este contenedor y estropear los sidecars para que vuelvan a formatear las métricas. Solo desea recogerlos y enviarlos.
- hardware del que también recopila métricas y desea utilizarlas de alguna manera.
Todas estas métricas se pueden combinar en un recopilador.

Ya es compatible con muchas fuentes de métricas y seguimientos que se utilizan en aplicaciones existentes. Y en caso de que esté usando algo exótico, puede implementar su propio complemento. Pero es poco probable que esto sea necesario en la práctica. Porque las aplicaciones que exportan métricas o trazas, de una forma u otra, utilizan algunos estándares comunes o estándares abiertos como OpenCensus.
Ahora queremos usar esta información. Puede especificar a Jaeger como exportador de trazas y enviar métricas a Prometheus o algo compatible. Digamos las VictoriaMetrics favoritas de todos.
Pero, ¿qué pasa si de repente decidimos cambiarnos a AWS y usar el trazador de rayos X local? No hay problema. Esto se puede enviar a través de OpenCensus, que tiene un exportador para X - Ray.
Así, a partir de estas piezas puedes ensamblar toda tu infraestructura para métricas y trazas.
Se acabó la teoría. Hablemos de cómo utilizar el rastreo en la práctica.
Instrumentación de la aplicación Golang: rastreo
Primero, necesita crear un tramo raíz, a partir del cual crecerá el árbol de llamadas.
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()
Este es el nombre de su servicio o biblioteca. De esta manera, en el seguimiento, puede definir intervalos que se encuentran dentro de su aplicación y aquellos que fueron a las bibliotecas importadas.
A continuación, se crea un tramo raíz con el nombre:
ctx, span := tr.Start(ctx, "root")
Elija un nombre que describa claramente el nivel de seguimiento. Por ejemplo, puede ser el nombre de un método (o clase y método) o una capa de arquitectura. Por ejemplo, capa de infraestructura, capa lógica, capa de base de datos, etc.
Los datos de tramo también se ponen en contexto:
ctx, span := tr.Start(ctx, "root") span.AddEvent(ctx, "I am a root span!") doSomeAction(ctx, "12345")
Por lo tanto, debe pasar los métodos que desea rastrear al contexto.
Span representa un proceso en un nivel específico en el árbol de llamadas. Puede poner atributos, registros y estados de error en él, si ocurre. El tramo debe cerrarse al final. Cuando está cerrado, se calcula su duración.
ctx, span := tr.Start(ctx, "root") span.AddEvent(ctx, "I am a root span!") doSomeAction(ctx, "12345") span.End()
Así es como se ve nuestro intervalo en Jaeger:

puede expandirlo y ver los registros y atributos.
Luego, puede obtener el mismo intervalo del contexto si no desea establecer uno nuevo. Por ejemplo, desea escribir una capa arquitectónica en un tramo y su capa está dispersa en varios métodos y varios niveles de llamada. Lo obtienes, escribes en él y luego se cierra.
func doSomeAction(ctx context.Context, requestID string) { span := trace.SpanFromContext(ctx) span.AddEvent(ctx, "I am the same span!") ... }
Tenga en cuenta que no es necesario cerrarlo aquí, porque se cerrará con el mismo método en el que se creó. Simplemente lo estamos sacando de contexto.
Escribir un mensaje en el intervalo raíz: a

veces es necesario crear un intervalo secundario nuevo para que exista por separado.
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!") ... }
Aquí obtenemos un rastreador global llamado biblioteca. Esta llamada puede estar envuelta en algún método, o puede usar una variable global, porque será la misma en todo su servicio.
A continuación, se crea un intervalo secundario a partir del contexto y se le asigna un nombre, similar a cómo lo hicimos al principio:
Start(ctx, "child")
Recuerde cerrar el intervalo al final del método en el que fue creado.
ctx, span := global.Tracer("github.com/me/otel-demo"). Start(ctx, "child") defer span.End()
Escribimos mensajes en él que caen en el intervalo de niños.

Aquí puede ver que los mensajes se muestran jerárquicamente y el intervalo secundario está debajo del principal. Se espera que sea más corto porque fue una llamada sincrónica.
Muestra los atributos que se pueden escribir en el intervalo:
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") }
Por ejemplo, nuestra solicitud llegó aquí. id:

puede agregar eventos:
span.AddEvent(ctx, "request validation ok")
Además, puede agregar una etiqueta aquí. Esto funciona de la misma manera que un registro estructurado en forma de logrus:
span.AddEvent(ctx, "entities loaded", label.Int64("count", 123))
Aquí vemos nuestro mensaje en el registro de intervalo. Puedes expandirlo y ver etiquetas. En nuestro caso, aquí se agregó un recuento de etiquetas:

Entonces será conveniente usarlo al filtrar en una búsqueda.
Si ocurre un error, puede agregar un estado al intervalo. En este caso, se marcará como inválido.
span.SetStatus(codes.Error, "insertion error")
El estándar solía usar códigos de error de OpenCensus y eran de grpc. Ahora solo quedan OK, ERROR y UNSET. OK es el valor predeterminado, ERROR se agrega en caso de error.
Aquí puede ver que el seguimiento del error está marcado con un icono rojo. Hay un código de error y un mensaje al respecto:

No debemos olvidar que el rastreo no reemplaza los registros. El punto principal es rastrear el flujo de información a través de un sistema distribuido, y para ello es necesario poner trazas en las solicitudes de red y poder leerlas desde allí. Microservicios de
seguimiento
OpenTelemetry ya tiene muchas implementaciones de interceptores y middleware para varios marcos y bibliotecas. Se pueden encontrar en el repositorio: github.com/open-telemetry/opentelemetry-go-contrib
Lista de marcos para los que existen interceptores y middleware:
- beego
- tranquilo
- Ginebra
- gocql
- mux
- eco
- http
- grpc
- sarama
- memcache
- mongo
- macaron
Veamos cómo usar esto usando un cliente y servidor http estándar como ejemplo.
cliente de middleware
En el cliente, simplemente agregamos un interceptor como transporte, luego de lo cual nuestras solicitudes se enriquecen con trace.id y la información necesaria para continuar con el rastreo.
client := http.Client{ Transport: otelhttp.NewTransport(http.DefaultTransport), } req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req)
servidor de middleware
Se agrega un pequeño middleware con el nombre de la biblioteca en el servidor:
http.Handle("/", otelhttp.NewHandler( http.HandlerFunc(get), "root")) err := http.ListenAndServe(addr, nil)
Luego, como de costumbre: obtenga un intervalo del contexto, trabaje con él, escriba algo en él, cree intervalos secundarios, ciérrelos, etc.
Así es como se ve una solicitud simple, pasando por tres servicios:

La captura de pantalla muestra la jerarquía de llamadas, división en servicios, su duración, secuencia. Puede hacer clic en cada uno de ellos y ver información más detallada.
Y así es como se ve el error:

es fácil rastrear dónde sucedió, cuándo y cuánto tiempo ha pasado.
En el intervalo, puede ver información detallada sobre el contexto en el que se produjo el error:

Además, los campos que hacen referencia a todo el intervalo (varios ID de solicitud, campos clave en la tabla de la solicitud, algunos otros metadatos que desea poner) se pueden anidar en el intervalo cuando se crea. En términos generales, no es necesario que copie y pegue todos estos campos en todos los lugares donde maneja un error. Puede escribir datos sobre él para abarcar.
middleware func
Aquí hay una pequeña ventaja: cómo hacer middleware para que pueda usarlo como middleware global para cosas como Gorilla y Gin:
middleware := func(h http.Handler) http.Handler { return otelhttp.NewHandler(h, "root") }
Instrumentación de aplicaciones de Golang: Monitoreo
Es hora de hablar sobre monitoreo.
La conexión al sistema de monitorización se configura de la misma forma que para el rastreo.
Las mediciones se dividen en dos tipos:
1. Sincrónicas, cuando el usuario pasa valores explícitamente en el momento de la llamada:
- Mostrador
- UpDownCounter
- ValueRecorder
int64, float64
2. Asíncrono, que el SDK lee en el momento de la recopilación de datos de la aplicación:
- SumObserver
- UpDownSumObserver
- ValueObserver
int64, float64
Las métricas en sí mismas son:
- Aditivo y monótono (Counter, SumObserver) que suman números positivos y no disminuyen.
- Aditivo pero no monótono (UpDownCounter, UpDownSumObserver), que puede sumar números positivos y negativos.
- No aditivo (ValueRecorder, ValueObserver) que simplemente registra una secuencia de valores. Por ejemplo, algún tipo de distribución.
Al inicio del programa se crea un contador global, al que se le indica el nombre de la biblioteca o servicio.
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()
A continuación, se crea una métrica:
floatCounter := metric.Must(meter).NewFloat64Counter( "float_counter", metric.WithDescription("Cumulative float counter"), ).Bind(label.String("label_a", "some label"))
Se le da un nombre:
"float_counter",
Descripción:
… metric.WithDescription("Cumulative float counter"), …
Un conjunto de etiquetas por las que luego puede filtrar solicitudes. Por ejemplo, al crear paneles en Grafana:
… ).Bind(label.String("label_a", "some label")) …
Al final del programa, también debe llamar a Unbind para cada métrica, lo que liberará recursos y lo cerrará correctamente:
… defer floatCounter.Unbind() …
Grabar cambios es simple:
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)
Estos son números positivos para Counter, cualquier número para UpDownCounter que sumará y también cualquier número para ValueRecorder. Para todo tipo de instrumentos, Go admite int64 y float64.
Esto es lo que obtenemos en la salida:
# HELP float_counter Cumulative float counter # TYPE float_counter counter float_counter{label_a="some label"} 20
Esta es nuestra métrica con un comentario y una etiqueta determinada. Luego, puede llevarlo directamente a través de Prometheus o exportarlo a través del recopilador de OpenTelemetry y luego usarlo donde lo necesitemos.
Instrumentación de la aplicación Golang: Bibliotecas
Lo último que quiero decir es la capacidad que proporciona el estándar para instrumentar bibliotecas.
Anteriormente, al usar OpenCensus y OpenTracing, no podía instrumentar sus bibliotecas individuales, especialmente las de código abierto. Porque en este caso, tienes un bloqueo de proveedor. Cualquiera que haya trabajado en estrecha colaboración con el seguimiento probablemente prestó atención al hecho de que las grandes bibliotecas de clientes o las grandes API para servicios en la nube se bloquean de vez en cuando con errores difíciles de explicar.
El rastreo sería muy útil aquí. Especialmente en productividad, cuando tienes algún tipo de situación poco clara y realmente me gustaría saber por qué sucedió. Pero todo lo que tiene es un mensaje de error de su biblioteca importada.
OpenTelemetry resuelve este problema.

Dado que el SDK y la API están separados en el estándar, la API de seguimiento de métricas se puede utilizar independientemente del SDK y la configuración de exportación de datos específicos. Además, primero puede instrumentar sus métodos y solo luego configurar la exportación de estos datos al exterior.
De esta manera, puede instrumentar la biblioteca importada sin preocuparse por cómo y dónde se exportarán los datos. Esto funcionará tanto para bibliotecas internas como de código abierto.
No es necesario preocuparse por el bloqueo del proveedor, no es necesario preocuparse por cómo se utilizará esta información o si se utilizará en absoluto. Las bibliotecas y las aplicaciones se instrumentan de antemano y la configuración de exportación de datos se especifica cuando se inicializa la aplicación.
Por lo tanto, puede ver que los ajustes de configuración están establecidos en la aplicación SDK. A continuación, debe tratar con los exportadores de seguimiento y métricas. Puede ser un exportador a través de OTLP si está exportando al recopilador OpenTelemetry. Luego, todos los rastreos y métricas necesarios entran en el contexto y se propagan por el árbol de llamadas mediante otro método.
La aplicación hereda el resto de los tramos del tramo raíz, simplemente usando la API de OpenTelemetry y los datos que están en el contexto. En este caso, las bibliotecas importadas reciben los métodos de contexto como entrada, intente leer información sobre el intervalo raíz de este método. Si no está ahí, crean el suyo y luego instruyen la lógica. De esta manera, puede instrumentar su biblioteca primero.
Además, puede instrumentar todo, pero no configurar los exportadores de datos y simplemente implementarlos.
Esto puede funcionar para usted en producción y, hasta que se establezca la infraestructura, no tendrá configurado el seguimiento y la supervisión. Luego, los configura, implementa un recopilador allí, algunas aplicaciones para recopilar estos datos y todo funcionará para usted. No es necesario cambiar nada directamente en los propios métodos.
Por lo tanto, si tiene una biblioteca de código abierto, puede instrumentarla usando OpenTelemetry. Luego, las personas que lo usan configurarán OpenTelemetry y usarán estos datos.
En conclusión, me gustaría decir que el estándar OpenTelemetry es prometedor. Quizás, finalmente, este sea el mismo estándar universal que todos queríamos ver.
Nuestra empresa utiliza activamente el estándar OpenCensus para rastrear y monitorear el panorama de microservicios de la empresa. Está previsto implementar OpenTelemetry después de su lanzamiento.