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 usando las API System.Diagnostics.Metrics para llevar un seguimiento de las métricas importantes. Algunas métricas se incluyen en bibliotecas .NET estándar, pero es posible que quiera agregar nuevas métricas personalizadas que sean pertinentes para sus aplicaciones y bibliotecas. En este tutorial, agregará nuevas métricas y sabrá qué tipos de métricas hay disponibles.

Nota

.NET tiene algunas API de métricas anteriores, a saber, EventCounters y System.Diagnostics.PerformanceCounter, que no abordaremos aquí. Para obtener más información sobre estas alternativas, vea 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 a la versión 8 o posterior del paquete System.Diagnostics.DiagnosticSource NuGet. Las aplicaciones que tienen como destino .NET 8+ incluyen esta referencia de forma predeterminada. Luego, actualice el código de Program.cs para que coincida:

> 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 de simulación, el código llama a Add para registrar la medida de los sombreros que se han vendido (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 los valores byte, short, int, long, float, double o decimal, dependiendo del tamaño de los datos que necesite almacenar y de si se necesitan valores fraccionales.

Ejecute la aplicación y déjela ejecutándose por ahora. Veremos las métricas luego.

> 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 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 invocan en el mismo objeto desde varios subprocesos, pero esto no es necesario en este caso.

  • Las API de instrumento para registrar medidas (Add en este ejemplo) normalmente se ejecutan en <10 ns cuando no se recopilan datos, o bien en decenas o cientos de nanosegundos cuando las medidas las recopila una herramienta o biblioteca de recopilación de alto rendimiento. Esto permite que estas API se utilicen libremente en la mayoría de los casos, pero tenga cuidado con el código que sea extremadamente sensible al rendimiento.

Visualización de la nueva métrica

Hay muchas opciones para almacenar y ver 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 ello:

> 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, se puede ver que la tienda HatCo vende 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. El uso de estáticos de esta manera no es un buen enfoque al usar la inserció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 registre los valores donde 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 Meter, y no tendrá ningún efecto.

Tipos de instrumentos

Hasta ahora solo hemos demostrado un instrumento 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 que se agreguen datos procedentes de muchas medidas. Una opción es que el autor de la llamada proporcione medidas individuales de manera arbitraria y que la herramienta de recopilación administre la agregación. Otra, que el autor de la llamada administre las medidas agregadas y las proporcione a petición en una devolución de llamada.

Estos son los 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, imaginemos que el autor de la llamada invoca Add() una vez cada segundo, con los valores sucesivos 1, 2, 4, 5, 4, 3. Si la herramienta de recopilación se actualiza cada tres segundos, el total después de transcurridos tres segundos es 1+2+4=7, y el total después de seis segundos, 1+2+4+5+4+3=19. La tasa de cambio es (current_total-previous_total), por lo que en tres segundos la herramienta comunica 7-0=7 y, después de seis segundos, 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 reducciones mediante Add. Por ejemplo, imaginemos que el autor de la llamada invoca Add() una vez cada segundo, con los valores sucesivos 1, 5, -2, 3, -1, -3. Si la herramienta de recopilación se actualiza cada tres segundos, el total después de transcurridos tres segundos es 1+5-2=4, y el total después de seis segundos, 1+5-2+3-1-3=3.

  • ObservableCounter (CreateObservableCounter): este instrumento es similar a Counter, 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 mostrarán tanto el total como la tasa de cambio en el total disponible. Si solo se puede mostrar una, se recomienda que sea la tasa de cambio. Si la devolución de llamada devuelve 0 en la llamada inicial, 7 cuando se la llama de nuevo después de tres segundos y 19 cuando se la llama después de seis segundos, la herramienta notificará esos valores sin modificar como los totales. Respecto a la tasa de cambio, la herramienta mostrará 7-0=7 después de transcurridos tres segundos y 19-7=12 después de transcurridos 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. Cualquier valor devuelto por la devolución de llamada se mostrará en la herramienta de recopilación sin cambios como el total.

  • 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 la devolución de llamada y cualquier valor devuelto por esta se muestra en la herramienta.

  • Histogram (CreateHistogram): este instrumento realiza un seguimiento de la distribución de las medidas. No hay una única manera canónica de describir un conjunto de medidas, pero se recomienda usar histogramas o percentiles calculados. Por ejemplo, imaginemos que el autor de la llamada invocó 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 comunicar que los percentiles 50, 90 y 95 de estas medidas son 5, 9 y 9 respectivamente.

Procedimientos recomendados al seleccionar un tipo de instrumento

  • Use Counter u ObservableCounter para contar cosas o cualquier otro valor que aumente únicamente con el paso del tiempo. Elija entre Counter y ObservableCounter en función de qué es más fácil agregar al código existente: una llamada API para cada operación de incremento, o bien una devolución de llamada que leerá el total actual de una variable que mantiene el código. En las rutas de acceso de código extremadamente activas, donde el rendimiento es importante y el uso de Add crearía más de un millón de llamadas por segundo y subproceso, el uso de ObservableCounter puede ofrecer más oportunidades de optimización.

  • Para cosas relacionadas con el tiempo, se suele preferir Histogram. A menudo, resulta útil comprender el final de estas distribuciones (percentil 90, 95, 99) en lugar de los 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 el más adecuado en función de qué es más fácil agregar al código existente: una llamada API para cada operación de incremento y reducción, o bien 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 distintos tipos de instrumentos

Detenga el proceso de ejemplo iniciado anteriormente y reemplace el código de ejemplo de 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(0.005, 0.015));
        }
    }
}

Ejecute el nuevo proceso y use dotnet-counters igual que antes en un segundo shell 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

[HatCo.Store]
    hatco.store.coats_sold (Count / 1 sec)                                27
    hatco.store.hats_sold (Count / 1 sec)                                 36
    hatco.store.order_processing_time
        Percentile=50                                                      0.012
        Percentile=95                                                      0.014
        Percentile=99                                                      0.014
    hatco.store.orders_pending                                             5

En este ejemplo se usan algunos números generados aleatoriamente, por lo que los valores variarán un poco. Puede ver que hatco.store.hats_sold (el elemento Counter) y hatco.store.coats_sold (el elemento ObservableCounter) se muestran como una tasa. El elemento ObservableGauge, hatco.store.orders_pending, aparece como un valor absoluto. dotnet-counters representa los instrumentos Histogram como estadísticas de tres percentiles (50, 95 y 99), pero otras herramientas pueden resumir la distribución de forma diferente u 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 que no las habiliten todas al mismo tiempo o para que configuren sus herramientas para ahorrar memoria mediante una reducción de la precisión. Algunas herramientas de recopilación pueden tener límites máximos en el número de histogramas simultáneos que supervisarán para evitar un uso excesivo de memoria.

  • Las devoluciones de llamada de todos los instrumentos observables se invocan en secuencia, por lo que cualquier devolución de llamada que tarde mucho tiempo puede retrasar o impedir que se recopilen 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 sincronizado. Los enfoques comunes para sincronizar el acceso son usar un bloqueo o una llamada a Volatile.Read y Volatile.Write.

  • Las funciones CreateObservableGauge y CreateObservableCounter sí que devuelven un objeto de instrumento, pero en la mayoría de los casos no es necesario guardarlo en una variable porque no se necesita ninguna interacción más con el objeto. Asignarlo a una variable estática como hicimos con los otros instrumentos es legal, si bien propenso a errores, porque la inicialización estática de C# es diferida y normalmente nunca se hace referencia a la variable. 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 a 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 saber cómo interpretar los datos. Detenga el proceso de ejemplo iniciado anteriormente y reemplace el código de ejemplo de 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 use dotnet-counters igual que antes en un segundo shell para ver las métricas:

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

[HatCo.Store]
    hatco.store.hats_sold ({hats} / 1 sec)                                40

dotnet-counters no usa actualmente el texto de descripción en la interfaz de usuario, pero sí 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 será diferente de las unidades de la métrica 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 colección ha calculado una tasa y ha deducido por sí misma que la unidad apropiada para la métrica 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 analizarlos. Por ejemplo, HatCo podría querer registrar 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 desglosar 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 igual que 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));
        }
    }
}

Ahora dotnet-counters muestra una categorización básica:

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

[HatCo.Store]
    hatco.store.hats_sold (Count / 1 sec)
        product.color=blue,product.size=19                                 9
        product.color=red,product.size=12                                 18

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

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

Procedimientos recomendados

  • Aunque la API permite usar cualquier objeto como valor de etiqueta, las herramientas de colección prevén que sean tipos numéricos y cadenas. Una herramienta de recopilación determinada puede o no admitir otros tipos.

  • Se recomienda seguir las directrices 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 controlar esto, es probable que las herramientas de recopilación asignen almacenamiento para los datos de métricas asociados a cada combinación de etiquetas, lo que 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 eliminarán datos para mantenerse dentro de los límites técnicos, o bien puede derivarse un coste económico elevado para cubrir el almacenamiento y el procesamiento de datos. La implementación de cada herramienta de recopilación determinará sus límites, aunque probablemente menos de 1000 combinaciones para un instrumento sea seguro. Cualquier cosa superior a 1000 combinaciones requerirá la herramienta de recopilación para aplicar el filtrado o diseñarse para funcionar a gran escala. Las implementaciones de Histogram tienden a usar mucho más memoria que otras métricas, por lo que los límites seguros podrían ser entre 10 y 100 veces inferiores. 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.

  • Respecto a os instrumentos que van a tener un gran número de combinaciones de etiquetas, use preferiblemente un tipo de almacenamiento más pequeño que ayude a reducir la sobrecarga de memoria. Por ejemplo, almacenar short de Counter<short> solamente ocupa 2 bytes por combinación de etiquetas, mientras que double de 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 de cada llamada para 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 número mayor de etiquetas, use TagList. En general, la sobrecarga de rendimiento de estas llamadas aumenta a medida que se usan más etiquetas.

Nota

En OpenTelemetry, las etiquetas se denominan "atributos". Estos son dos nombres diferentes para la misma funcionalidad.

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);
    }
}