Saltar a contenido

Andreani.Arq.Observability

Autor José Menéndez.

Objetivo

El objetivo de esta librería es facilitar y hacer transparente el cambio entre proveedores de observabilidad, como Elastic APM y OpenTelemetry.

Arquitectura de paquetes

arquitectura de paquetes

Atención

La Aplicación debe utilizar la implementación correspondiente a su estrategia de observabilidad. Por defecto es ElasticApm. En caso de necesitar cambiar a OpenTelemetry, se debe reemplazar las librerias de observabilidad de ElasticApm a OpenTelemetry.

Instalación

  1. Instalar en el proyecto Application la dependencia con Observability
    dotnet add package Andreani.Arq.Observability
    
  2. Instalar en el proyecto WebApi la dependencia con Observability.ElasticApm o Observability.OpenTelemetry
    dotnet add package Andreani.Arq.Observability.ElasticApm
    dotnet add package Andreani.Arq.Observability.OpenTelemetry
    

Intefaces

    public interface ICustomTracer
    {
        public bool IsConfigured { get; }
        public bool IsConfigEnabled { get; }
        public ICustomTransaction StartTransaction(string name, string type, string? links = null);
        public ICustomTransaction GetCurrentTransaction();
        public ICustomSpan GetCurrentSpan();
        public T CaptureTransaction<T>(string name, string type, Func<ICustomTransaction, T> func, string? links = null);
        public T CaptureTransaction<T>(string name, string type, Func<T> func, string? links = null);
        public void CaptureTransaction(string name, string type, Action<ICustomTransaction> func, string? links = null);
        public void CaptureTransaction(string name, string type, Action func, string? links = null);
        public Task<T> CaptureTransactionAsync<T>(string name, string type, Func<ICustomTransaction, Task<T>> func, string? links = null);
        public Task<T> CaptureTransactionAsync<T>(string name, string type, Func<Task<T>> func, string? links = null);
        public Task CaptureTransactionAsync(string name, string type, Func<ICustomTransaction,Task> func, string? links = null);
        public Task CaptureTransactionAsync(string name, string type, Func<Task> func, string? links = null);
    }
    public interface ICustomSpan
    {
        public string? Links { get; }
        public void End();

        public ICustomSpan StartSpan(string name, string type = "Internal", string? subType = null);

        public T CaptureSpan<T>(string name, string type, Func<ICustomSpan, T> func, string? subType = null);

        public T CaptureSpan<T>(string name, string type, Func<T> func, string? subType = null);

        public void CaptureSpan(string name, string type, Action<ICustomSpan> func, string? subType = null);

        public void CaptureSpan(string name, string type, Action func, string? subType = null);

        public Task<T> CaptureSpanAsync<T>(string name, string type, Func<ICustomSpan, Task<T>> func, string? subType = null);

        public Task<T> CaptureSpanAsync<T>(string name, string type, Func<Task<T>> func, string? subType = null);

        public Task CaptureSpanAsync(string name, string type, Func<ICustomSpan,Task> func, string? subType = null);

        public Task CaptureSpanAsync(string name, string type, Func<Task> func, string? subType = null);

        public void SetTag(string key, string value);

        public void CaptureException(Exception exception);

        public void CaptureError(string message, string type = "log");
    }
    public interface ICustomTransaction : ICustomSpan
    {

    }

Configuración

Para configurar Observability en nuestro proyecto, debemos tener las siguientes variables de entorno:

Observability:
  ServerUrls: "https://localhost:8200"
  ServiceName: "ServiceName"
name descripción requerido default
ServiceName El nombre del servicio
ServerUrls La url del servicio
Environment La variable de entorno. Opciones Development, QA, Production. no Development
ServiceVersion La versión del servicio no 1.0
Enabled Es para activar o desactivar la observabilidad no true
Token El token para conectarse al servicio en caso de ser necesario autenticarse. no ""

Luego en nuestro archivo Program, debemos utilizar los métodos AddObservability() y UseObservability() de la siguiente manera:

var builder = WebApplication.CreateBuilder(args);

builder.Host.ConfigureAndreaniWebHost(args)
            .AddObservability();
//...
var app = builder.Build();

app.UseObservability();
app.ConfigureAndreani();
//...
app.Run();

Metodos de configuración

-AddObservability(): Este método se encarga de inyectar las interfaces y configurar el proveedor, por ejemplo, ElasticApm si utilizamos la librería Andreani.Arq.Observability.ElasticApm.

-UseObservability(): Este método se encarga de agregar configuraciones extras y necesarias para la conexión con el proveedor.

-Metodos que extienden de AddObservability():

  • .WithMongo(): Este método se encarga de configurar la observabilidad en Mongo, en caso de que estemos utilizando la librería Andreani.Arq.Mongo en nuestro proyecto.

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Host.ConfigureAndreaniWebHost(args)
                .AddObservability()
                .WithMongo();
    //...
    var app = builder.Build();
    
    app.UseObservability();
    app.ConfigureAndreani();
    //...
    app.Run();
    
  • .WithRedis(): Este método se encarga de configurar la observabilidad en Redis en caso de que estemos utilizando la librería Andreani.Arq.Redis en nuestro proyecto.

    var builder = WebApplication.CreateBuilder(args);
    
    builder.Host.ConfigureAndreaniWebHost(args)
                .AddObservability()
                .WithRedis();
    //...
    var app = builder.Build();
    
    app.UseObservability();
    app.ConfigureAndreani();
    //...
    app.Run();
    

Uso de Prometheus con Opentelemtry

En el caso de Andreani.Arq.Observability.Opentelemetry se debe agregar app.MapPrometheusScrapingEndpoint() después del app.ConfigureAndreani(), esto se agrega para mapear el endpoint al que Prometheus arroja las metricas.

Transacciones

Como crear Transacciones

Para crear una nueva transacción, podemos utilizar el método StarTransaction() o CaptureTransaction() de la interfaz ICustomTracer. Primero, debemos crear una variable de tipo ICustomTracer y recibirla en el constructor de nuestro handler. A continuación, se muestra un ejemplo de cómo usarlo:

StarTransaction():

    public record struct ListPerson : IRequest<Response<List<Person>>>
    {
    }

    public class ListPersonHandler : IRequestHandler<ListPerson, Response<List<Person>>>
    {
        private readonly IMongoRepository<Person> _query;
        private readonly ICustomTracer _tracer;

        public ListPersonHandler(IMongoRepository<Person> query, ICustomTracer tracer)
        {
            _query = query;
            _tracer = tracer;
        }

        public async Task<Response<List<Person>>> Handle(ListPerson request, CancellationToken cancellationToken)
        {
            List<Person> result= new();
            var trans = _tracer.StartTransaction(name:"ListPerson", type:"query");
            try
            {
               result = await _query.GetAllAsync();

            }
            catch (Exception ex)
            {
                trans.CaptureException(ex);
            }
            finally
            {
              trans.End();
            }

            return new Response<List<Person>>
            {
                Content = result,
                StatusCode = System.Net.HttpStatusCode.OK
            };
        }
    }

Warning

Siempre que se utilice **`StarTransaction()`**, se debe cerrar la transacción con el método **`End()`**

Tip

Como se muestra en el ejemplo, podemos utilizar **`CaptureException()`** en caso de que el código genere una excepción para capturarla en la transacción.

CaptureTransaction():

    public record struct ListPerson : IRequest<Response<List<Person>>>
    {
    }

    public class ListPersonHandler : IRequestHandler<ListPerson, Response<List<Person>>>
    {
        private readonly IMongoRepository<Person> _query;
        private readonly ICustomTracer _tracer;

        public ListPersonHandler(IMongoRepository<Person> query, ICustomTracer tracer)
        {
            _query = query;
            _tracer = tracer;
        }

        public async Task<Response<List<Person>>> Handle(ListPerson request, CancellationToken cancellationToken)
        {
            List<Person> result= await _tracer.CaptureTransaction(name:"ListPerson", type:"query",
                async ()=>
            {
               return await _query.GetAllAsync();
            });

            return new Response<List<Person>>
            {
                Content = result,
                StatusCode = System.Net.HttpStatusCode.OK
            };
        }
    }

Seguimiento de la transacción

Esta funcionalidad está diseña para cuando queremos continuar la transacción en un sistema externo. En ICustomTransaction, tenemos la propiedad Links, que guarda los datos de la transacción en un string para realizar un seguimiento de la misma en varias APIs o servicios.

    var links= _tracer.GetCurrentTransaction().Links

Luego, pasamos el string a la transacción utilizando el método StartTransaction(string name, string type, string? links = null) o CaptureTransaction, ambos métodos tienen el parámetro opcional string? links.

      var transaction2 = _tracer.StartTransaction("Transaction2", "request",links);

Spans

Como crear un span

Para crear un nuevo span, podemos utilizar el método StarSpan() o CaptureSpan() de la interfaz ICustomTransaction o ICustomSpan. Podemos utilizar ICustomTracer para crear una transacción, como se mostró anteriormente. Aquí hay un ejemplo de cómo usar ambas funciones:

StarSpan():

    public record struct ListPerson : IRequest<Response<List<Person>>>
    {
    }

    public class ListPersonHandler : IRequestHandler<ListPerson, Response<List<Person>>>
    {
        private readonly IMongoRepository<Person> _query;
        private readonly ICustomTracer _tracer;

        public ListPersonHandler(IMongoRepository<Person> query, ICustomTracer tracer)
        {
            _query = query;
            _tracer = tracer;
        }

        public async Task<Response<List<Person>>> Handle(ListPerson request, CancellationToken cancellationToken)
        {
            List<Person> result= new();
            var trans = _tracer.StartTransaction(name:"ListPerson", type:"query");
            var span = trans.StartSpan("GetAllPerson");
            try
            {
               result = await _query.GetAllAsync();

            }
            catch (Exception ex)
            {
                span.CaptureException(ex);
                trans.CaptureException(ex);
            }
            finally
            {
                span.End();
                trans.End();
            }

            return new Response<List<Person>>
            {
                Content = result,
                StatusCode = System.Net.HttpStatusCode.OK
            };
        }
    }

Warning

Siempre que se utilice **`StarSpan()`**, se debe cerrar el span con el método **`End()`**

Tip

Como se muestra en el ejemplo, podemos utilizar **`CaptureException()`** en caso de que el código genere una excepción para capturarla en la transacción.

CaptureSpan():

    public record struct ListPerson : IRequest<Response<List<Person>>>
    {
    }

    public class ListPersonHandler : IRequestHandler<ListPerson, Response<List<Person>>>
    {
        private readonly IMongoRepository<Person> _query;
        private readonly ICustomTracer _tracer;

        public ListPersonHandler(IMongoRepository<Person> query, ICustomTracer tracer)
        {
            _query = query;
            _tracer = tracer;
        }

        public async Task<Response<List<Person>>> Handle(ListPerson request, CancellationToken cancellationToken)
        {
            List<Person> result= await _tracer.CaptureSpan(name:"GetAllPerson","Internal",
                async ()=>
            {
               return await _query.GetAllAsync();
            });

            return new Response<List<Person>>
            {
                Content = result,
                StatusCode = System.Net.HttpStatusCode.OK
            };
        }
    }

Tags

Las etiquetas (Atributos) permiten adjuntar pares clave/valor a un archivo para que contenga más información sobre la operación actual de la que se realiza el seguimiento.

Atributos

Los atributos son pares clave-valor que contienen metadatos que se pueden usar para anotar un Span para llevar información sobre la operación que está rastreando.

Por ejemplo, si un intervalo realiza un seguimiento de una operación que agrega un elemento a la propiedad de un usuario carrito de compras en un sistema de comercio electrónico, puede capturar la identificación del usuario, la identificación de el artículo que se va a añadir al carrito y el ID del carrito.

Podemos agregar tags(labels) a una transacción o un span utilizando el método SetTag(string key, string value).

Transacciones:

    var trans = _tracer.GetCurrentTransaction();
    trans.SetTag("TagKey", "TagValue");

Spans:

    var span = _tracer.GetCurrentSpan();
    span.SetTag("TagKey", "TagValue");

Captura de errores

Podemos capturar errores en una transacción o un span utilizando el método CaptureError(string message, string type = "log")

Transacciones:

    var trans = _tracer.GetCurrentTransaction();
    trans.CaptureError("Error message");

Spans:

    var span = _tracer.GetCurrentSpan();
    span.CaptureError("Error message");

Para más ejemplos de como utilizar Andreani.Arq.Observability.OpenTelemetry o Andreani.Arq.Observability.ElasticApm, visete lo ejemplos de platform-net examples.