Compartir a través de


Creación de métricas

Este artículo se aplica a: ✔️ .NET Core 6 y versiones posteriores ✔️ .NET Framework 4.6.1 y versiones posteriores

Las aplicaciones .NET se pueden instrumentar mediante las API de System.Diagnostics.Metrics para realizar un seguimiento de las métricas importantes. Algunas métricas se incluyen en las bibliotecas estándar de .NET, pero es posible que quiera agregar nuevas métricas personalizadas que sean relevantes para las aplicaciones y bibliotecas. En este tutorial, agregará nuevas métricas y comprenderá qué tipos de métricas están disponibles.

Nota

.NET tiene algunas API de métricas anteriores, es decir, EventCounters y System.Diagnostics.PerformanceCounter, que no se tratan aquí. Para obtener más información sobre estas alternativas, consulte Comparación de las API de métricas.

Creación de una métrica personalizada

requisitos previos: SDK de .NET Core 6 o una versión posterior

Cree una nueva aplicación de consola que haga referencia al paquete NuGet System.Diagnostics.DiagnosticSource versión 8 o posterior. Las aplicaciones que tienen como destino .NET 8+ incluyen esta referencia de forma predeterminada. A continuación, actualice el código de Program.cs para que coincida con:

> dotnet new console
> dotnet add package System.Diagnostics.DiagnosticSource
using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each second that sells 4 hats
            Thread.Sleep(1000);
            s_hatsSold.Add(4);
        }
    }
}

El tipo System.Diagnostics.Metrics.Meter es el punto de entrada para que una biblioteca cree un grupo de instrumentos con nombre. Los instrumentos registran las medidas numéricas necesarias para calcular las métricas. Aquí usamos CreateCounter para crear un instrumento Counter denominado "hatco.store.hats_sold". Durante cada transacción fingida, el código llama a Add para registrar la cantidad de sombreros vendidos, 4 en este caso. El instrumento "hatco.store.hats_sold" define implícitamente algunas métricas que se podrían calcular a partir de estas medidas, como el número total de sombreros vendidos o sombreros vendidos por segundo. En última instancia, es necesario que las herramientas de recopilación de métricas determinen qué métricas se deben calcular y cómo realizar esos cálculos, pero cada instrumento tiene algunas convenciones predeterminadas que transmiten la intención del desarrollador. En el caso de los instrumentos Counter, la convención es que las herramientas de recopilación muestren el recuento total o la velocidad a la que aumenta el recuento.

El parámetro genérico int en Counter<int> y CreateCounter<int>(...) define que este contador debe poder almacenar valores hasta Int32.MaxValue. Puede usar cualquiera de byte, short, int, long, float, doubleo decimal en función del tamaño de los datos que necesite almacenar y si se necesitan valores fraccionarios.

Ejecute la aplicación y déjela en ejecución por ahora. Veremos las métricas a continuación.

> dotnet run
Press any key to exit

Procedimientos recomendados

  • Para el código que no está diseñado para su uso en un contenedor de inserción de dependencias (DI), cree el medidor una vez y almacénelo en una variable estática. Para el uso de las variables estáticas de bibliotecas compatibles con DI se consideran antipatrones y el ejemplo de DI de inserción de dependencias siguiente muestra un enfoque más idiomático. Cada biblioteca o subcomponente de biblioteca puede (y a menudo debería) crear su propio Meter. Considere la posibilidad de crear un nuevo medidor en lugar de reutilizar uno existente si prevé que los desarrolladores de aplicaciones le gustaría poder habilitar y deshabilitar fácilmente los grupos de métricas por separado.

  • El nombre pasado al Meter constructor debe ser único para distinguirlo de otros medidores. Recomendamos las directrices de nomenclatura de OpenTelemetry, que utilizan nombres jerárquicos con puntos. Los nombres de ensamblado o los nombres de espacio de nombres para el código que se instrumenta suelen ser una buena opción. Si un ensamblado agrega instrumentación para el código en un segundo ensamblado independiente, el nombre debe basarse en el ensamblado que define el medidor, no en el ensamblado cuyo código se está instrumentando.

  • .NET no aplica ningún esquema de nomenclatura para los instrumentos, pero se recomienda seguir las instrucciones de nomenclatura de OpenTelemetry, que usan nombres jerárquicos de puntos en minúsculas y un carácter de subrayado ('_') como separador entre varias palabras en el mismo elemento. No todas las herramientas de métricas conservan el nombre del medidor como parte del nombre final de la métrica, por lo que es beneficioso que el nombre del instrumento sea único globalmente por sí mismo.

    Nombres de instrumento de ejemplo:

    • contoso.ticket_queue.duration
    • contoso.reserved_tickets
    • contoso.purchased_tickets
  • Las API para crear instrumentos y registrar medidas son seguras para los subprocesos. En las bibliotecas de .NET, la mayoría de los métodos de instancia requieren sincronización cuando se invoca en el mismo objeto desde varios subprocesos, pero eso no es necesario en este caso.

  • Las APIs de Instrumentos para registrar medidas (Add en este ejemplo) funcionan típicamente en <10 ns cuando no se recopilan datos, o de decenas a cientos de nanosegundos cuando una biblioteca o herramienta de recopilación de alto rendimiento recopila medidas. Esto permite que estas API se usen liberalmente en la mayoría de los casos, pero tenga cuidado con el código que es extremadamente sensible al rendimiento.

Visualización de la nueva métrica

Hay muchas opciones para almacenar y ver las métricas. En este tutorial se usa la herramienta dotnet-counters, que resulta útil para el análisis ad hoc. También puede ver el tutorial de recopilación de métricas para conocer otras alternativas. Si la herramienta dotnet-counters aún no está instalada, use el SDK para instalarla.

> dotnet tool update -g dotnet-counters
You can invoke the tool using the following command: dotnet-counters
Tool 'dotnet-counters' (version '7.0.430602') was successfully installed.

Mientras la aplicación de ejemplo sigue en ejecución, use dotnet-counters para supervisar el nuevo contador:

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)                          4

Como se esperaba, puedes ver que la tienda HatCo está vendiendo constantemente 4 sombreros cada segundo.

Obtener un medidor mediante la inserción de dependencias

En el ejemplo anterior, el medidor se obtuvo al construirlo con new y asignarlo a un campo estático. Usar estáticos de esta manera no es un buen enfoque cuando se utiliza la inyección de dependencias (DI). En el código que usa di, como ASP.NET Core o aplicaciones con Host genérico, cree el objeto medidor mediante IMeterFactory. A partir de .NET 8, los hosts se registrarán automáticamente IMeterFactory en el contenedor de servicios o puede registrar manualmente el tipo en cualquier IServiceCollection mediante una llamada a AddMetrics. El generador de medidores integra métricas con DI, manteniendo los medidores en diferentes colecciones de servicios aisladas entre sí incluso si usan un nombre idéntico. Esto resulta especialmente útil para las pruebas para que varias pruebas que se ejecuten en paralelo solo observen las medidas producidas desde el mismo caso de prueba.

Para obtener un medidor en un tipo diseñado para la inserción de dependencias, agregue un parámetro IMeterFactory al constructor y luego llame a Create. En este ejemplo se muestra el uso de IMeterFactory en una aplicación ASP.NET Core.

Defina un tipo para contener los instrumentos:

public class HatCoMetrics
{
    private readonly Counter<int> _hatsSold;

    public HatCoMetrics(IMeterFactory meterFactory)
    {
        var meter = meterFactory.Create("HatCo.Store");
        _hatsSold = meter.CreateCounter<int>("hatco.store.hats_sold");
    }

    public void HatsSold(int quantity)
    {
        _hatsSold.Add(quantity);
    }
}

Registre el tipo con el contenedor de inserción de dependencias en Program.cs.

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<HatCoMetrics>();

Inserte el tipo de métrica y los valores de registro cuando sea necesario. Dado que el tipo de métrica está registrado en DI, puede utilizarse con controladores MVC, API mínimas o cualquier otro tipo creado por DI:

app.MapPost("/complete-sale", ([FromBody] SaleModel model, HatCoMetrics metrics) =>
{
    // ... business logic such as saving the sale to a database ...

    metrics.HatsSold(model.QuantitySold);
});

Procedimientos recomendados

  • System.Diagnostics.Metrics.Meter implementa IDisposable, pero IMeterFactory administra automáticamente la duración de los objetos Meter que crea y los elimina cuando se elimina el contenedor de DI. No es necesario agregar código adicional para invocar Dispose() en el Metery no tendrá ningún efecto.

Tipos de instrumentos

Hasta ahora solo hemos demostrado un instrumento de Counter<T>, pero hay más tipos de instrumentos disponibles. Los instrumentos difieren de dos maneras:

  • Cálculos de métricas predeterminadas: las herramientas que recopilan y analizan las medidas del instrumento calcularán métricas predeterminadas diferentes en función del instrumento.
  • Almacenamiento de datos agregados: la mayoría de las métricas útiles necesitan agregar datos de muchas medidas. Una opción es que el autor de la llamada proporcione medidas individuales en momentos arbitrarios y la herramienta de recopilación administra la agregación. Como alternativa, el llamante puede gestionar las medidas agregadas y proporcionarlas bajo demanda en una devolución de llamada.

Tipos de instrumentos disponibles actualmente:

  • Counter (CreateCounter): este instrumento realiza un seguimiento de un valor que aumenta con el tiempo, y el autor de la llamada comunica los incrementos mediante Add. La mayoría de las herramientas calcularán el total y la tasa de cambio en el total. En el caso de las herramientas que solo muestren una cosa, se recomienda que sea la tasa de cambio. Por ejemplo, supongamos que el autor de la llamada invoca Add() una vez cada segundo con valores sucesivos 1, 2, 4, 5, 4, 3. Si la herramienta de recopilación se actualiza cada tres segundos, el total después de tres segundos es 1+2+4=7 y el total después de seis segundos es 1+2+4+5+4+3=19. La tasa de cambio es (current_total - previous_total), por lo que en tres segundos la herramienta informa de 7-0=7 y después de seis segundos, informa de 19-7=12.

  • upDownCounter (CreateUpDownCounter): este instrumento realiza un seguimiento de un valor que puede aumentar o disminuir con el tiempo. El autor de la llamada informa de los incrementos y decrementos mediante Add. Por ejemplo, supongamos que el autor de la llamada invoca Add() una vez cada segundo con valores sucesivos 1, 5, -2, 3, -1, -3. Si la herramienta de recopilación se actualiza cada tres segundos, el total después de tres segundos es 1+5-2=4 y el total después de seis segundos es 1+5-2+3-1-3=3=3.

  • ObservableCounter (CreateObservableCounter): este instrumento es similar al contador, salvo que el autor de la llamada ahora es responsable de mantener el total agregado. El autor de la llamada proporciona un delegado de devolución de llamada cuando se crea ObservableCounter, y se invoca la devolución de llamada cada vez que las herramientas necesitan observar el total actual. Por ejemplo, si una herramienta de recopilación se actualiza cada tres segundos, la función devolución de llamada también se invocará cada tres segundos. La mayoría de las herramientas tendrán disponible tanto el total como la tasa de cambio del total. Si solo se puede mostrar una, se recomienda que sea la tasa de cambio. Si la función de devolución devuelve 0 en la llamada inicial, 7 cuando se llama de nuevo después de tres segundos y 19 cuando se llama después de seis segundos, la herramienta informará de esos valores sin cambios como totales. Para la tasa de cambio, la herramienta mostrará 7-0=7 después de tres segundos y 19-7=12 después de seis segundos.

  • ObservableUpDownCounter (CreateObservableUpDownCounter): este instrumento es similar a UpDownCounter, salvo que el autor de la llamada ahora es responsable de mantener el total agregado. El autor de la llamada proporciona un delegado de devolución de llamada cuando se crea ObservableUpDownCounter, y se invoca la devolución de llamada cada vez que las herramientas necesitan observar el total actual. Por ejemplo, si una herramienta de recopilación se actualiza cada tres segundos, la función devolución de llamada también se invocará cada tres segundos. El valor devuelto por la función de retorno (callback) se mostrará sin cambios en la herramienta de recopilación como el total.

  • medidor (CreateGauge): este instrumento permite al autor de la llamada establecer el valor actual de la métrica mediante el método Record. El valor se puede actualizar en cualquier momento invocando el método de nuevo y una herramienta de recopilación de métricas mostrará el valor que se haya establecido más recientemente.

  • ObservableGauge (CreateObservableGauge): este instrumento permite al autor de la llamada proporcionar una devolución de llamada donde el valor medido se pasa directamente como métrica. Cada vez que se actualiza la herramienta de recopilación, se invoca el callback, y el valor que este devuelve se muestra en la herramienta.

  • Histograma (CreateHistogram) - Este instrumento monitorea la distribución de las mediciones. No hay una única manera canónica de describir un conjunto de medidas, pero se recomienda usar histogramas o percentiles calculados. Por ejemplo, supongamos que el autor de la llamada invoca Record para registrar estas medidas durante el intervalo de actualización de la herramienta de recopilación: 1,5,2,3,10,9,7,4,6,8. Una herramienta de recopilación podría informar de que los percentiles 50, 90 y 95 de estas medidas son 5, 9 y 9 respectivamente.

    Nota

    Para obtener más información sobre cómo establecer los límites de intervalo recomendados al crear un instrumento histograma, consulte: Uso de consejos para personalizar instrumentos de histograma.

Procedimientos recomendados al seleccionar un tipo de instrumento

  • Para contar cosas, o cualquier otro valor que aumente únicamente con el tiempo, use Counter o ObservableCounter. Elija entre Counter y ObservableCounter en función de cuál sea más fácil de agregar al código existente: una llamada a la API para cada operación de incremento o una devolución de llamada que lea el total actual de una variable que el código mantiene. En rutas de código extremadamente críticas donde el rendimiento resulta crucial y el uso de Add generaría más de un millón de llamadas por segundo por subproceso, emplear ObservableCounter puede ofrecer más oportunidades de optimización.

  • Para cosas relacionadas con el tiempo, se suele preferir Histogram. A menudo resulta útil comprender la cola de estas distribuciones (90, 95, percentil 99) en lugar de promedios o totales.

  • UpDownCounter o ObservableUpDownCounter suelen funcionar bien en otros casos comunes, como tasas de aciertos de caché o tamaños de cachés, colas y archivos. Elija entre ellos en función de cuál sea más fácil agregar al código existente: una llamada API para cada operación de incremento y decremento o una devolución de llamada que leerá el valor actual de una variable que mantiene el código.

Nota

Si usa una versión anterior de .NET o un paquete NuGet DiagnosticSource que no admite UpDownCounter y ObservableUpDownCounter (antes de la versión 7), ObservableGauge suele ser un buen sustituto.

Ejemplo de diferentes tipos de instrumento

Detenga el proceso de ejemplo iniciado anteriormente y reemplace el código de ejemplo en Program.cs por:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>("hatco.store.order_processing_time");
    static int s_coatsSold;
    static int s_ordersPending;

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_coatsSold);
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", () => s_ordersPending);

        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms that each sell 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);

            // Pretend we also sold 3 coats. For an ObservableCounter we track the value in our variable and report it
            // on demand in the callback
            s_coatsSold += 3;

            // Pretend we have some queue of orders that varies over time. The callback for the orders_pending gauge will report
            // this value on-demand.
            s_ordersPending = s_rand.Next(0, 20);

            // Last we pretend that we measured how long it took to do the transaction (for example we could time it with Stopwatch)
            s_orderProcessingTime.Record(s_rand.Next(5, 15)/1000.0);
        }
    }
}

Ejecute el nuevo proceso y use dotnet-counters como antes en una segunda ventana de terminal para ver las métricas.

> dotnet-counters monitor -n metric-demo.exe --counters HatCo.Store
Press p to pause, r to resume, q to quit.
    Status: Running

Name                                                  Current Value
[HatCo.Store]
    hatco.store.coats_sold (Count)                        8,181    
    hatco.store.hats_sold (Count)                           548    
    hatco.store.order_processing_time
        Percentile
        50                                                    0.012    
        95                                                    0.013   
        99                                                    0.013
    hatco.store.orders_pending                                9    

En este ejemplo se usan algunos números generados aleatoriamente para que los valores variarán un poco. Dotnet-counters representa los instrumentos de histograma como tres estadísticas de percentil (50, 95 y 99), pero otras herramientas pueden resumir la distribución de forma diferente o ofrecer más opciones de configuración.

Procedimientos recomendados

  • Los histogramas tienden a almacenar muchos más datos en memoria que otros tipos de métricas. Sin embargo, la herramienta de recopilación determina el uso exacto de la memoria. Si va a definir un gran número (>100) de métricas de histograma, es posible que tenga que proporcionar instrucciones a los usuarios para no habilitarlas al mismo tiempo o configurar sus herramientas para ahorrar memoria reduciendo la precisión. Algunas herramientas de recopilación pueden tener límites estrictos en el número de histogramas simultáneos que supervisarán para evitar un uso excesivo de memoria.

  • Las callbacks para todos los instrumentos observables se invocan en secuencia, por lo que cualquier callback que tarde mucho tiempo puede retrasar o impedir la recopilación de todas las métricas. Propicie una lectura rápida de un valor almacenado en caché, no devolver ninguna medida o iniciar una excepción al realizar cualquier operación de bloqueo o ejecución potencialmente prolongada.

  • Las devoluciones de llamada ObservableCounter, ObservableUpDownCounter y ObservableGauge se producen en un subproceso que normalmente no se sincroniza con el código que actualiza los valores. Es responsabilidad suya sincronizar el acceso a la memoria o aceptar los valores incoherentes que pueden resultar del uso del acceso no asincrónico. Los enfoques comunes para sincronizar el acceso son usar un bloqueo o hacer una llamada a Volatile.Read y Volatile.Write.

  • Las funciones CreateObservableGauge y CreateObservableCounter devuelven un objeto instrument, pero en la mayoría de los casos no es necesario guardarlo en una variable porque no se necesita ninguna interacción adicional con el objeto. Asignarlo a una variable estática como hicimos para los otros instrumentos es legal pero propenso a errores, ya que la inicialización estática de C# es diferida y la variable normalmente nunca se hace referencia a ella. Este es un ejemplo del problema:

    using System;
    using System.Diagnostics.Metrics;
    
    class Program
    {
        // BEWARE! Static initializers only run when code in a running method refers to a static variable.
        // These statics will never be initialized because none of them were referenced in Main().
        //
        static Meter s_meter = new Meter("HatCo.Store");
        static ObservableCounter<int> s_coatsSold = s_meter.CreateObservableCounter<int>("hatco.store.coats_sold", () => s_rand.Next(1,10));
        static Random s_rand = new Random();
    
        static void Main(string[] args)
        {
            Console.ReadLine();
        }
    }
    

Descripciones y unidades

Los instrumentos pueden especificar descripciones y unidades opcionales. Estos valores son opacos para todos los cálculos de métricas, pero se pueden mostrar en la interfaz de usuario de la herramienta de recopilación para ayudar a los ingenieros a comprender cómo interpretar los datos. Detenga el proceso de ejemplo que inició anteriormente y reemplace el código de ejemplo en Program.cs por:

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>(name: "hatco.store.hats_sold",
                                                                unit: "{hats}",
                                                                description: "The number of hats sold in our store");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction each 100ms that sells 4 hats
            Thread.Sleep(100);
            s_hatsSold.Add(4);
        }
    }
}

Ejecute el nuevo proceso y utilice dotnet-counters de nuevo en una segunda consola para ver las métricas.

Press p to pause, r to resume, q to quit.
    Status: Running

Name                                                       Current Value
[HatCo.Store]
    hatco.store.hats_sold ({hats})                                40

dotnet-counters no usa actualmente el texto de descripción en la interfaz de usuario, pero muestra la unidad cuando se proporciona. En este caso, verá que "Hats" ha reemplazado el término genérico "Count" visible en las descripciones anteriores.

Procedimientos recomendados

  • Las API de .NET permiten usar cualquier cadena como unidad, pero se recomienda usar UCUM, un estándar internacional para los nombres de unidad. Las llaves alrededor de "{hats}" forman parte del estándar UCUM, lo que indica que es una anotación descriptiva en lugar de un nombre de unidad con un significado estandarizado como segundos o bytes.

  • La unidad especificada en el constructor debe describir las unidades adecuadas para una medida individual. Esto a veces difiere de las unidades de la métrica notificada final. En este ejemplo, cada medida es un número de sombreros, por lo que "{hats}" es la unidad adecuada que pasar en el constructor. La herramienta de recopilación podría haber calculado la tasa de cambio y concluir por sí misma que la unidad adecuada para la métrica de velocidad calculada es {hats}/seg.

  • Cuando se graban medidas de tiempo, se prefieren unidades de segundos grabadas como un punto flotante o un valor doble.

Métricas multidimensionales

Las medidas también se pueden asociar a pares clave-valor denominados etiquetas que permiten clasificar los datos para su análisis. Por ejemplo, HatCo podría querer grabar no solo el número de sombreros vendidos, sino también el tamaño y el color que tenían. Al analizar los datos más adelante, los ingenieros de HatCo pueden dividir los totales por tamaño, color o cualquier combinación de ambos.

Las etiquetas Counter e Histogram se pueden especificar en sobrecargas de Add y Record que toman uno o varios argumentos KeyValuePair. Por ejemplo:

s_hatsSold.Add(2,
               new KeyValuePair<string, object?>("product.color", "red"),
               new KeyValuePair<string, object?>("product.size", 12));

Reemplace el código de Program.cs y vuelva a ejecutar la aplicación y dotnet-counters como antes:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while(!Console.KeyAvailable)
        {
            // Pretend our store has a transaction, every 100ms, that sells two size 12 red hats, and one size 19 blue hat.
            Thread.Sleep(100);
            s_hatsSold.Add(2,
                           new KeyValuePair<string,object?>("product.color", "red"),
                           new KeyValuePair<string,object?>("product.size", 12));
            s_hatsSold.Add(1,
                           new KeyValuePair<string,object?>("product.color", "blue"),
                           new KeyValuePair<string,object?>("product.size", 19));
        }
    }
}

Dotnet-counters ahora muestra una categorización básica:

Press p to pause, r to resume, q to quit.
    Status: Running

Name                                                  Current Value
[HatCo.Store]
    hatco.store.hats_sold (Count)
        product.color product.size
        blue          19                                     73
        red           12                                    146    

En ObservableCounter y ObservableGauge, se pueden proporcionar medidas etiquetadas en la devolución de llamada que se pasa al constructor:

using System;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");

    static void Main(string[] args)
    {
        s_meter.CreateObservableGauge<int>("hatco.store.orders_pending", GetOrdersPending);
        Console.WriteLine("Press any key to exit");
        Console.ReadLine();
    }

    static IEnumerable<Measurement<int>> GetOrdersPending()
    {
        return new Measurement<int>[]
        {
            // pretend these measurements were read from a real queue somewhere
            new Measurement<int>(6, new KeyValuePair<string,object?>("customer.country", "Italy")),
            new Measurement<int>(3, new KeyValuePair<string,object?>("customer.country", "Spain")),
            new Measurement<int>(1, new KeyValuePair<string,object?>("customer.country", "Mexico")),
        };
    }
}

Cuando se ejecuta con dotnet-counters como antes, el resultado es:

Press p to pause, r to resume, q to quit.
    Status: Running

Name                                                  Current Value
[HatCo.Store]
    hatco.store.orders_pending
        customer.country
        Italy                                                 6
        Mexico                                                1
        Spain                                                 3    

Procedimientos recomendados

  • Aunque la API permite usar cualquier objeto como valor de etiqueta, las herramientas de recopilación prevén tipos numéricos y cadenas. Otros tipos pueden o no ser compatibles con una herramienta de recopilación determinada.

  • Se recomienda seguir las instrucciones de nomenclatura de OpenTelemetry , que usan nombres jerárquicos de puntos en minúsculas con caracteres "_" para separar varias palabras en el mismo elemento. Si los nombres de etiqueta se reutilizan en diferentes métricas u otros registros de telemetría, deben tener el mismo significado y conjunto de valores legales en todas partes donde se usen.

    Nombres de etiqueta de ejemplo:

    • customer.country
    • store.payment_method
    • store.purchase_result
  • Tenga cuidado de registrar en la práctica combinaciones muy grandes o sin enlazar de valores de etiqueta. Aunque la implementación de la API de .NET puede controlarla, es probable que las herramientas de recopilación asignen almacenamiento para los datos de métricas asociados a cada combinación de etiquetas y esto podría llegar a ser muy grande. Por ejemplo, no pasa nada si HatCo tiene 10 colores y 25 tamaños de sombrero diferentes para realizar un seguimiento por un total de ventas máximo de 10x25=250. Pero si HatCo agregara una tercera etiqueta (por ejemplo, un identificador de cliente de la venta) y vende a 100 millones de clientes en todo el mundo, probablemente se estén registrando miles de millones de combinaciones de etiquetas diferentes. La mayoría de las herramientas de recopilación de métricas quitarán los datos para permanecer dentro de los límites técnicos o pueden haber grandes costos monetarios para cubrir el almacenamiento y el procesamiento de datos. La implementación de cada herramienta de recopilación determinará sus límites, pero probablemente menos de 1000 combinaciones para un instrumento sea seguro. Más de 1000 combinaciones requerirá que la herramienta de recopilación aplique el filtrado o deba diseñarse para funcionar a gran escala. Las implementaciones de histograma tienden a usar mucho más memoria que otras métricas, por lo que los límites seguros podrían ser de 10 a 100 veces más bajos. Si prevé un gran número de combinaciones de etiquetas únicas, los registros, las bases de datos transaccionales o los sistemas de procesamiento de macrodatos pueden ser soluciones más adecuadas para funcionar a la escala necesaria.

  • En el caso de los instrumentos que tendrán un gran número de combinaciones de etiquetas, prefiere usar un tipo de almacenamiento más pequeño para ayudar a reducir la sobrecarga de memoria. Por ejemplo, almacenar el short de un Counter<short> solo ocupa 2 bytes por combinación de etiquetas, mientras que un double para Counter<double> ocupa 8 bytes por combinación de etiquetas.

  • Se recomienda optimizar las herramientas de recopilación para el código que especifica el mismo conjunto de nombres de etiqueta en el mismo orden para cada llamada a registrar medidas en el mismo instrumento. En el caso de los códigos de alto rendimiento que necesitan llamar a Add y a Record con frecuencia, es preferible usar la misma secuencia de nombres de etiqueta en cada llamada.

  • La API de .NET está optimizada para seleccionar libremente la asignación de las llamadas Add y Record que tienen tres o menos etiquetas especificadas individualmente. Para evitar asignaciones con un mayor número de etiquetas, use TagList. En general, la sobrecarga de rendimiento de estas llamadas aumenta a medida que se usan más etiquetas.

Nota

OpenTelemetry hace referencia a etiquetas como "atributos". Estos son dos nombres diferentes para la misma funcionalidad.

Uso de consejos para personalizar instrumentos de histograma

Al usar Histogramas, es responsabilidad de la herramienta o biblioteca recopilar los datos para decidir cómo representar mejor la distribución de valores que se registraron. Una estrategia común (y el modo predeterminado de cuando se usa OpenTelemetry) es dividir el intervalo de valores posibles en sub rangos denominados cubos e informar del número de valores registrados en cada cubo. Por ejemplo, una herramienta podría dividir números en tres cubos, los menores que 1, los comprendidos entre 1 y 10 y los mayores que 10. Si la aplicación registró los valores 0,5, 6, 0,1, 12, habría dos puntos de datos en el primer cubo, uno en el segundo y otro en la tercera.

La herramienta o biblioteca que recopila los datos del histograma es responsable de definir los cubos que usará. La configuración de cubo predeterminada al usar OpenTelemetry es: [ 0, 5, 10, 25, 50, 75, 100, 250, 500, 750, 1000, 2500, 5000, 7500, 10000 ].

Es posible que los valores predeterminados no lleven a la mejor granularidad para cada histograma. Por ejemplo, las duraciones de solicitudes inferiores a un segundo caerían en la categoría de 0.

La herramienta o biblioteca que recopila los datos de histogramas puede ofrecer mecanismos para permitir a los usuarios personalizar la configuración del cubo. Por ejemplo, OpenTelemetry define un View API. Sin embargo, esto requiere la acción del usuario final y pone en el usuario la responsabilidad de comprender suficientemente bien la distribución de los datos para poder elegir los cubos correctos.

Para mejorar la experiencia, la versión 9.0.0 del paquete de System.Diagnostics.DiagnosticSource introdujo la API (InstrumentAdvice<T>).

Los autores de instrumentación pueden usar la API de InstrumentAdvice para especificar el conjunto de límites de cubo predeterminados recomendados para un histograma determinado. La herramienta o biblioteca que recopila los datos de histogramas puede optar por usar esos valores al configurar la agregación, lo que conduce a una experiencia de incorporación más fluida para los usuarios. Esto se admite a partir de la versión 1.10.0en el SDK de OpenTelemetry .NET .

Importante

En general, más cubos darán lugar a datos más precisos para un histograma determinado, pero cada cubo requiere memoria para almacenar los detalles agregados y hay un costo de CPU para encontrar el cubo correcto al procesar una medida. Es importante comprender los inconvenientes entre precisión y consumo de CPU/memoria al elegir el número de intervalos que se recomiendan a través de la API de InstrumentAdvice.

El siguiente código muestra un ejemplo de uso de la API InstrumentAdvice para establecer los intervalos predeterminados recomendados.

using System;
using System.Diagnostics.Metrics;
using System.Threading;

class Program
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Histogram<double> s_orderProcessingTime = s_meter.CreateHistogram<double>(
        name: "hatco.store.order_processing_time",
        unit: "s",
        description: "Order processing duration",
        advice: new InstrumentAdvice<double> { HistogramBucketBoundaries = [0.01, 0.05, 0.1, 0.5, 1, 5] });

    static Random s_rand = new Random();

    static void Main(string[] args)
    {
        Console.WriteLine("Press any key to exit");
        while (!Console.KeyAvailable)
        {
            // Pretend our store has one transaction each 100ms
            Thread.Sleep(100);

            // Pretend that we measured how long it took to do the transaction (for example we could time it with Stopwatch)
            s_orderProcessingTime.Record(s_rand.Next(5, 15) / 1000.0);
        }
    }
}

Información adicional

Para obtener más información sobre histogramas de cubo explícitos en OpenTelemetry, consulte:

Prueba de métricas personalizadas

Es posible probar las métricas personalizadas que agregue mediante MetricCollector<T>. Este tipo facilita el registro de las medidas de instrumentos específicos y la aserción de que los valores eran correctos.

Prueba con inserción de dependencias

En el código siguiente se muestra un caso de prueba de ejemplo para los componentes de código que usan la inserción de dependencias e IMeterFactory.

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var services = CreateServiceProvider();
        var metrics = services.GetRequiredService<HatCoMetrics>();
        var meterFactory = services.GetRequiredService<IMeterFactory>();
        var collector = new MetricCollector<int>(meterFactory, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }

    // Setup a new service provider. This example creates the collection explicitly but you might leverage
    // a host or some other application setup code to do this as well.
    private static IServiceProvider CreateServiceProvider()
    {
        var serviceCollection = new ServiceCollection();
        serviceCollection.AddMetrics();
        serviceCollection.AddSingleton<HatCoMetrics>();
        return serviceCollection.BuildServiceProvider();
    }
}

Cada objeto MetricCollector registra todas las medidas de un instrumento. Si necesita comprobar las medidas de varios instrumentos, cree un MetricCollector para cada uno.

Prueba sin inserción de dependencias

También es posible probar el código que usa un objeto Meter global compartido en un campo estático, pero asegúrese de que estas pruebas estén configuradas para no ejecutarse en paralelo. Dado que el objeto Meter se comparte, MetricCollector en una prueba observará las medidas creadas a partir de cualquier otra prueba que se ejecute en paralelo.

class HatCoMetricsWithGlobalMeter
{
    static Meter s_meter = new Meter("HatCo.Store");
    static Counter<int> s_hatsSold = s_meter.CreateCounter<int>("hatco.store.hats_sold");

    public void HatsSold(int quantity)
    {
        s_hatsSold.Add(quantity);
    }
}

public class MetricTests
{
    [Fact]
    public void SaleIncrementsHatsSoldCounter()
    {
        // Arrange
        var metrics = new HatCoMetricsWithGlobalMeter();
        // Be careful specifying scope=null. This binds the collector to a global Meter and tests
        // that use global state should not be configured to run in parallel.
        var collector = new MetricCollector<int>(null, "HatCo.Store", "hatco.store.hats_sold");

        // Act
        metrics.HatsSold(15);

        // Assert
        var measurements = collector.GetMeasurementSnapshot();
        Assert.Equal(1, measurements.Count);
        Assert.Equal(15, measurements[0].Value);
    }
}