OpenTelemetry en pratique

Plus récemment, deux standards - OpenTracing et OpenCensus - ont finalement fusionné en un seul. Un nouveau standard pour le traçage et la surveillance distribués est apparu - OpenTelemetry. Mais malgré le fait que le développement des bibliothèques bat son plein, il n'y a pas beaucoup d'expérience réelle de son utilisation.



Ilya Kaznacheev, qui développe depuis huit ans et travaille en tant que développeur backend chez MTS, est prêt à partager comment utiliser OpenTelemetry dans les projets Golang. Lors de la conférence Golang Live 2020, il a expliqué comment mettre en place l'utilisation d'une nouvelle norme de traçage et de surveillance et se lier d'amitié avec l'infrastructure déjà existante dans le projet.





OpenTelemetry est une norme relativement récente, à la fin de l'année dernière. En même temps, il a reçu une large distribution et un soutien de la part de nombreux fournisseurs de logiciels de traçage et de surveillance.



L'observabilité, ou observabilité, est un terme de la théorie du contrôle qui détermine dans quelle mesure on peut juger de l'état interne d'un système par ses manifestations externes. Dans l'architecture du système, cela signifie un ensemble d'approches pour surveiller l'état du système au moment de l'exécution. Ces approches comprennent la journalisation, le traçage et la surveillance.







Il existe de nombreuses solutions de fournisseurs pour le traçage et la surveillance. Jusqu'à récemment, il y avait deux standards ouverts: OpenTracing de CNCF, qui est apparu en 2016, et Open Census, de Google, qui est apparu en 2018.



Ce sont deux très bonnes normes qui se sont concurrencées pendant un certain temps, jusqu'à ce qu'en 2019, elles décident de fusionner en une nouvelle norme appelée OpenTelemetry.







Cette norme inclut le traçage et la surveillance distribués. Il est compatible avec les deux premiers. De plus, OpenTracing et Open Census ont interrompu le support dans les deux ans, nous rapprochant inévitablement du passage à OpenTelemetry.



Scénarios d'utilisation



La norme suppose de nombreuses possibilités de combiner tout avec tout et est, en fait, une couche active entre les sources de métriques et de traces et leurs consommateurs.

Jetons un coup d'œil aux principaux scénarios.



Pour le traçage distribué, vous pouvez directement configurer une connexion à Jaeger ou à tout autre service que vous utilisez.







Si le traçage est diffusé directement, vous pouvez utiliser config et simplement remplacer la bibliothèque.



Si votre application utilise déjà OpenTracing, vous pouvez utiliser OpenTracing Bridge, un wrapper qui convertira les requêtes vers l'API OpenTracing en API OpenTelemetry au niveau supérieur.







Pour collecter des métriques, vous pouvez également configurer Prometheus pour accéder directement au port de métriques de votre application.







Ceci est utile si vous disposez d'une infrastructure simple et que vous collectez des métriques directement. Mais la norme offre également plus de flexibilité.



Le scénario principal d'utilisation de la norme consiste à collecter des métriques et des traces via un collecteur, qui est également lancé par une application ou un conteneur distinct dans votre infrastructure. De plus, vous pouvez emporter un conteneur prêt à l'emploi et l'installer chez vous.



Pour ce faire, il suffit de configurer l'exportateur au format OTLP dans l'application. Il s'agit d'un schéma grpc pour la transmission de données au format OpenTracing. Du côté du collecteur, vous pouvez configurer le format et les paramètres pour l'exportation des métriques et des traces vers les utilisateurs finaux ou vers d'autres formats. Par exemple, dans OpenCensus.







Le collecteur vous permet de connecter un grand nombre de types de sources de données et de nombreux puits de données en sortie







Ainsi, la norme OpenTelemetry offre une compatibilité avec de nombreuses normes open source et des fournisseurs.



Le collecteur standard est extensible. Par conséquent, la plupart des fournisseurs ont déjà des exportateurs prêts pour leurs propres solutions, le cas échéant. Vous pouvez utiliser OpenTelemetry même si vous collectez des métriques et des traces auprès d'un fournisseur propriétaire. Cela résout le problème du verrouillage du fournisseur. Même si quelque chose n'est pas encore apparu directement pour OpenTelemetry, il peut être transféré via OpenCensus.



Le collecteur lui-même est très facile à configurer grâce à la configuration banale YAML: les







récepteurs sont spécifiés ici. Votre application peut avoir une autre source (Kafka, etc.):







Exportateurs - destinataires des données.

Processeurs - méthodes de traitement des données à l'intérieur du collecteur:







Et les pipelines, qui définissent directement la manière dont chaque flux de données qui circule à l'intérieur d'un collecteur sera géré:







Regardons un exemple illustratif.







Supposons que vous ayez un microservice auquel vous avez déjà vissé OpenTelemetry et configuré. Et un autre service avec une fragmentation similaire.



Jusqu'à présent, tout est facile. Mais il y a:



  • les services hérités qui fonctionnent via OpenCensus;
  • une base de données qui envoie des données dans son propre format (par exemple, directement à Prometheus, comme le fait PostgreSQL);
  • un autre service qui fonctionne dans un conteneur et fournit des métriques dans son propre format. Vous ne voulez pas reconstruire ce conteneur et bousiller les side-cars pour qu'ils reformatent les métriques. Vous voulez juste les récupérer et les envoyer.
  • matériel à partir duquel vous collectez également des métriques et souhaitez les utiliser d'une manière ou d'une autre.


Toutes ces mesures peuvent être combinées dans un seul collecteur.







Il prend déjà en charge de nombreuses sources de métriques et de traces utilisées dans les applications existantes. Et si vous utilisez quelque chose d'exotique, vous pouvez implémenter votre propre plugin. Mais il est peu probable que cela soit nécessaire dans la pratique. Parce que les applications qui exportent des métriques ou des traces, d'une manière ou d'une autre, utilisent soit des normes communes, soit des normes ouvertes comme OpenCensus.



Nous voulons maintenant utiliser ces informations. Vous pouvez spécifier Jaeger comme exportateur de traces et envoyer des métriques à Prometheus, ou quelque chose de compatible. Disons que tout le monde est le favori VictoriaMetrics.



Mais que se passe-t-il si nous décidons soudainement de passer à AWS et d'utiliser le traceur X-Ray local? Aucun problème. Cela peut être transmis via OpenCensus, qui dispose d'un exportateur pour X-Ray.



Ainsi, à partir de ces éléments, vous pouvez assembler toute votre infrastructure pour les métriques et les traces.



La théorie est terminée. Parlons de la façon d'utiliser le traçage dans la pratique.



Instrumentation de l'application Golang: traçage



Tout d'abord, vous devez créer un span racine, à partir duquel l'arborescence des appels se développera.



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()
      
      





C'est le nom de votre service ou bibliothèque. De cette façon, vous pouvez définir des étendues dans la trace qui se trouvent dans le cadre de votre application, et celles qui sont allées aux bibliothèques importées.



Ensuite, un span racine est créé avec le nom:



ctx, span := tr.Start(ctx, "root")
      
      





Choisissez un nom qui décrira clairement le niveau de trace. Par exemple, il peut s'agir du nom d'une méthode (ou d'une classe et d'une méthode) ou d'une couche d'architecture. Par exemple, couche infrastructure, couche logique, couche base de données, etc.



Les données span sont également mises en contexte:



ctx, span := tr.Start(ctx, "root")
span.AddEvent(ctx, "I am a root span!")
doSomeAction(ctx, "12345")

      
      





Par conséquent, vous devez transmettre les méthodes que vous souhaitez tracer dans le contexte.



Span représente un processus à un niveau spécifique dans l'arborescence des appels. Vous pouvez y mettre des attributs, des journaux et des statuts d'erreur, si cela se produit. Span doit être fermé à la fin. Lorsqu'il est fermé, sa durée est calculée.



ctx, span := tr.Start(ctx, "root")
span.AddEvent(ctx, "I am a root span!")
doSomeAction(ctx, "12345")
span.End()
      
      





Voici à quoi ressemble notre span dans Jaeger: vous







pouvez l'étendre et voir les logs et les attributs.



Ensuite, vous pouvez obtenir la même étendue à partir du contexte si vous ne souhaitez pas en définir une nouvelle. Par exemple, vous souhaitez écrire une couche architecturale dans une travée et votre couche est dispersée sur plusieurs méthodes et plusieurs niveaux d'appel. Vous l'obtenez, écrivez-y, puis il se ferme.



func doSomeAction(ctx context.Context, requestID string) {
      span := trace.SpanFromContext(ctx)
      span.AddEvent(ctx, "I am the same span!")
      ...
}
      
      





Notez que vous n'avez pas besoin de le fermer ici, car il se fermera dans la même méthode où il a été créé. Nous prenons simplement cela hors de son contexte.



Ecrire un message sur le span racine:







Parfois, vous devez créer un nouveau span enfant pour qu'il existe séparément.



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!")
   ...
}
      
      





Ici, nous obtenons un traceur global nommé library. Cet appel peut être encapsulé dans une méthode, ou vous pouvez utiliser une variable globale, car ce sera le même dans tout votre service.



Ensuite, un span enfant est créé à partir du contexte et un nom lui est attribué, comme nous l'avons fait au début:



   Start(ctx, "child")
      
      





N'oubliez pas de fermer la plage à la fin de la méthode dans laquelle elle a été créée.



  ctx, span := global.Tracer("github.com/me/otel-demo"). 
      Start(ctx, "child") 
   defer span.End()
      
      





Nous y écrivons des messages qui tombent dans la plage enfant.







Ici, vous pouvez voir que les messages sont affichés hiérarchiquement et que l'étendue enfant se trouve sous le parent. Il devrait être plus court car il s'agissait d'un appel synchrone.



Il montre les attributs qui peuvent être écrits dans le 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")
}
      
      





Par exemple, notre demande est arrivée ici. id:







vous pouvez ajouter des événements:



   span.AddEvent(ctx, "request validation ok")
      
      





Vous pouvez également ajouter une étiquette ici. Cela fonctionne à peu près de la même manière qu'un journal structuré sous la forme de logrus:



span.AddEvent(ctx, "entities loaded", label.Int64("count", 123))
      
      





Ici, nous voyons notre message dans le journal span. Vous pouvez le développer et voir les étiquettes. Dans notre cas, un nombre d'étiquettes a été ajouté ici:







il sera alors pratique de l'utiliser lors d'un filtrage dans une recherche.



Si une erreur se produit, vous pouvez ajouter un état à la plage. Dans ce cas, il sera marqué comme invalide.



  span.SetStatus(codes.Error, "insertion error")
      
      





Le standard utilisait les codes d'erreur d'OpenCensus et ils provenaient de grpc. Maintenant, il ne reste plus que OK, ERROR et UNSET. OK est la valeur par défaut, ERROR est ajouté en cas d'erreur.



Ici, vous pouvez voir que la trace d'erreur est marquée d'une icône rouge. Il y a un code d'erreur et un message à ce sujet:







il ne faut pas oublier que le traçage ne remplace pas les journaux. Le point principal est de suivre le flux d'informations à travers un système distribué, et pour cela vous devez mettre des traces dans les requêtes réseau et pouvoir les lire à partir de là.



Trace microservices



OpenTelemetry a déjà de nombreuses implémentations de parties d'intercepteurs et de middleware pour divers frameworks et bibliothèques. Ils peuvent être trouvés dans le référentiel: github.com/open-telemetry/opentelemetry-go-contrib



Liste des frameworks pour lesquels il existe des intercepteurs et middleware:



  • beego
  • se reposer
  • Gin
  • gocql
  • mux
  • écho
  • http
  • grpc
  • sarama
  • Memcache
  • Mongo
  • macaron


Voyons comment l'utiliser en utilisant un client et un serveur http standard comme exemple.



client middleware



Dans le client, nous ajoutons simplement un intercepteur en tant que transport, après quoi nos requêtes sont enrichies avec trace.id et les informations nécessaires pour poursuivre la trace.



client := http.Client{
      Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := client.Do(req)
      
      





serveur middleware



Un petit middleware avec le nom de la bibliothèque est ajouté sur le serveur:



http.Handle("/", otelhttp.NewHandler(
      http.HandlerFunc(get), "root"))
err := http.ListenAndServe(addr, nil)
      
      





Ensuite, comme d'habitude: récupérez un span à partir du contexte, travaillez avec lui, écrivez quelque chose dedans, créez des span enfants, fermez-les, etc.



Voici à quoi ressemble une simple requête, passant par trois services:







La capture d'écran montre la hiérarchie des appels, la division en services, leur durée, leur séquence. Vous pouvez cliquer sur chacun d'eux et voir des informations plus détaillées.



Et voici à quoi ressemble l'erreur:







il est facile de suivre où elle s'est produite, quand et combien de temps s'est écoulé.

Dans span, vous pouvez voir des informations détaillées sur le contexte dans lequel l'erreur s'est produite:







De plus, les champs qui font référence à l'ensemble de l'étendue (divers identifiants de demande, champs clés de la table dans la demande, certaines autres métadonnées que vous souhaitez mettre) peuvent être imbriqués dans l'étendue lors de sa création. En gros, vous n’avez pas besoin de copier et coller tous ces champs à chaque endroit où vous gérez une erreur. Vous pouvez écrire des données à son sujet pour s'étendre.



middleware func



Voici un petit bonus: comment créer un middleware pour pouvoir l'utiliser comme middleware global pour des choses comme Gorilla et Gin:



middleware := func(h http.Handler) http.Handler {
      return otelhttp.NewHandler(h, "root")
}
      
      





Instrumentation d'application Golang: surveillance



Il est temps de parler de surveillance.



La connexion au système de surveillance est configurée de la même manière que pour le traçage.



Les mesures sont divisées en deux types:



1. Synchrone, lorsque l'utilisateur transmet explicitement des valeurs au moment de l'appel:



  • Compteur
  • UpDownCounter
  • ValeurRecorder


int64, float64



2. Asynchrone, que le SDK lit au moment de la collecte des données depuis l'application:



  • SumObserver
  • UpDownSumObserver
  • ValueObserver


int64, float64



Les métriques elles-mêmes sont:



  • Additif et monotone (Counter, SumObserver) qui additionne les nombres positifs et ne diminue pas.
  • Additif mais pas monotone (UpDownCounter, UpDownSumObserver), qui peut additionner des nombres positifs et négatifs.
  • Non additif (ValueRecorder, ValueObserver) qui enregistre simplement une séquence de valeurs. Par exemple, une sorte de distribution.


Au début du programme, un compteur global est créé, auquel le nom de la bibliothèque ou du service est indiqué.



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()
      
      





Ensuite, une métrique est créée:



floatCounter := metric.Must(meter).NewFloat64Counter(
         "float_counter",
         metric.WithDescription("Cumulative float counter"),
   ).Bind(label.String("label_a", "some label"))
      
      





Elle reçoit un nom:



   "float_counter",
      
      





La description:




         metric.WithDescription("Cumulative float counter"),
      
      





Un ensemble d'étiquettes par lequel vous pouvez ensuite filtrer les demandes. Par exemple, lors de la création de tableaux de bord dans Grafana:




    ).Bind(label.String("label_a", "some label"))

      
      





À la fin du programme, vous devez également appeler Unbind pour chaque métrique, ce qui libérera des ressources et la fermera correctement:




defer floatCounter.Unbind()

      
      





L'enregistrement des modifications est 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)

      
      





Ce sont des nombres positifs pour Counter, tous les nombres pour UpDownCounter qu'il additionnera, ainsi que tous les nombres pour ValueRecorder. Pour tous les types d'instruments, Go prend en charge int64 et float64.



Voici ce que nous obtenons à la sortie:



# HELP float_counter Cumulative float counter
# TYPE float_counter counter
float_counter{label_a="some label"} 20
      
      





Ceci est notre métrique avec un commentaire et une étiquette donnée. Ensuite, vous pouvez le prendre soit directement via Prometheus, soit l'exporter via le collecteur OpenTelemetry, puis l'utiliser partout où nous en avons besoin.



Golang Application Instrumentation: Bibliothèques



La dernière chose que je veux dire est la capacité que la norme fournit pour les bibliothèques d'instrumentation.



Auparavant, lors de l'utilisation d'OpenCensus et d'OpenTracing, vous ne pouviez pas instrumenter vos bibliothèques individuelles, en particulier celles open source. Parce que dans ce cas, vous avez un verrouillage du fournisseur. Quiconque a travaillé en étroite collaboration avec le traçage a probablement prêté attention au fait que les grandes bibliothèques clientes, ou les grandes API pour les services cloud, plantent de temps en temps avec des erreurs difficiles à expliquer.



Le traçage serait très utile ici. Surtout dans la productivité, lorsque vous avez une sorte de situation peu claire, et j'aimerais vraiment savoir pourquoi cela s'est produit. Mais tout ce que vous avez est un message d'erreur de votre bibliothèque importée.



OpenTelemetry résout ce problème.







Étant donné que le SDK et l'API sont séparés dans la norme, l'API de suivi des métriques peut être utilisée indépendamment du SDK et des paramètres d'exportation de données spécifiques. De plus, vous pouvez d'abord instrumenter vos méthodes, et ensuite seulement configurer l'exportation de ces données vers l'extérieur.

De cette façon, vous pouvez instrumenter la bibliothèque importée sans vous soucier de savoir comment et où les données seront exportées. Cela fonctionnera pour les bibliothèques internes et open source.



Pas besoin de s'inquiéter du verrouillage du fournisseur, pas besoin de s'inquiéter de la façon dont ces informations seront utilisées ou si elles seront utilisées du tout. Les bibliothèques et les applications sont instrumentées à l'avance et la configuration d'exportation des données est spécifiée lors de l'initialisation de l'application.



Ainsi, vous pouvez voir que les paramètres de configuration sont définis dans l'application SDK. Ensuite, vous devez traiter avec les exportateurs de traçage et de métriques. Il peut s'agir d'un exportateur via OTLP si vous exportez vers le collecteur OpenTelemetry. Ensuite, toutes les traces et métriques nécessaires tombent dans le contexte, et elles sont propagées dans l'arborescence des appels par une autre méthode.



L'application hérite du reste des étendues de l'étendue racine, en utilisant simplement l'API OpenTelemetry et les données qui se trouvent dans le contexte. Dans ce cas, les bibliothèques importées reçoivent les méthodes de contexte en entrée, essayez de lire les informations sur l'étendue racine à partir de cette méthode. Si ce n'est pas là, ils créent le leur, puis ils instruisent la logique. De cette façon, vous pouvez d'abord instrumenter votre bibliothèque.



De plus, vous pouvez tout instrumenter, mais pas configurer les exportateurs de données, et simplement le déployer.



Cela peut fonctionner pour vous en production, et tant que l'infrastructure n'est pas installée, vous n'aurez pas configuré le traçage et la surveillance. Ensuite, vous les configurez, y déployez un collecteur, des applications pour collecter ces données, et tout fonctionnera pour vous. Vous n'avez pas besoin de changer quoi que ce soit directement dans les méthodes elles-mêmes.



Ainsi, si vous disposez d'une bibliothèque open source, vous pouvez l'instrumenter à l'aide d'OpenTelemetry. Ensuite, les personnes qui l'utilisent configureront OpenTelemetry et utiliseront ces données.



En conclusion, je tiens à dire que le standard OpenTelemetry est prometteur. Peut-être, enfin, c'est la même norme universelle que nous voulions tous voir.



Notre entreprise utilise activement la norme OpenCensus pour tracer et surveiller le paysage des microservices de l'entreprise. Il est prévu d'implémenter OpenTelemetry après sa sortie.



All Articles