При использовании выборки Application Insights можно сохранить связанные записи вместе с исключенными элементами

Я экспериментировал с выборкой данных телеметрии, чтобы не превысить лимит ежедневных журналов App Insights. В идеале я хочу применять выборку ко всему, но исключать исключения и сохранять связанные трассировки (тот же идентификатор операции) для исключений.

Я создал образец консольного приложения для тестирования и до сих пор могу успешно выбирать и сохранять исключения. Но связанные трассировки также отбираются.

Я рассмотрел реализацию пользовательского ITelemetryProcessor, но он обрабатывает одну запись за раз. Поэтому я не уверен, возможно ли это вообще с помощью специального процессора. Возможно, есть что-то, что поможет добиться желаемого поведения.

Код Program.cs ниже

using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

const string appInsightConnString = "<connection string>";
const double samplingPercentage = 50;

var services = new ServiceCollection();

// add context
var context = new Context();
services.AddSingleton(context);

// configure application insights
services.AddApplicationInsightsTelemetryWorkerService(
    (options) =>
    {
        // disable adaptive sampling
        options.EnableAdaptiveSampling = false;
        options.ConnectionString = appInsightConnString;
    });

// configure logging
services.AddLogging(loggingBuilder =>
{
    loggingBuilder.ClearProviders();
    loggingBuilder.Services.AddSingleton<ILoggerProvider, ContextApplicationInsightsLoggerProvider>();
    loggingBuilder.AddConsole();
});

var serviceProvider = services.BuildServiceProvider();

// setup sampling
var telemetryConfiguration = serviceProvider.GetRequiredService<TelemetryConfiguration>();
var telemetryBuilder = telemetryConfiguration.DefaultTelemetrySink.TelemetryProcessorChainBuilder;
telemetryBuilder.UseSampling(samplingPercentage, excludedTypes: "Exception");
telemetryBuilder.Build();

// get logger
var logger = serviceProvider.GetRequiredService<ILogger<Program>>();

// do something important
DoWork(context, logger);

// explicitly call Flush() followed by sleep is required in console apps.
// this is to ensure that even if application terminates, telemetry is sent to the back-end.
var telemetryClient = serviceProvider.GetRequiredService<TelemetryClient>();
telemetryClient.Flush();
Task.Delay(10000).Wait();

Console.WriteLine("Flushed. Press any key to exit");
Console.ReadKey();


static void DoWork(Context context, ILogger logger)
{
    const int iterations = 50;
    const int errors = 15;

    // session Id to filter logs
    var sessionId = Guid.NewGuid().ToString();
    Console.WriteLine($"Session Id: {sessionId}");

    // randomize errors
    var random = new Random();
    var errorsHash = new HashSet<int>();
    while (errorsHash.Count < errors)
    {
        errorsHash.Add(random.Next(0, iterations));
    }

    // log
    for (var i = 0; i < iterations; i++)
    {
        context.CorrelationId = Guid.NewGuid().ToString();
        logger.LogInformation($"Begin operation: {context.CorrelationId}. Session Id: {sessionId}");
        if (errorsHash.Contains(i))
            logger.LogError(new Exception("test ex"), $"Error operation: {context.CorrelationId}. Session Id: {sessionId}");
        logger.LogInformation($"End operation: {context.CorrelationId}. Session Id: {sessionId}");
    }
}
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
0
191
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

В приведенном ниже коде используется Microsoft Application Insights для мониторинга и регистрации в консольном приложении с использованием интерфейса ITelemetryProcessor из Application Insights. Класс RelatedTelemetryProcessor реализует ITelemetryProcessor и сохраняет связанные записи вместе с исключенными элементами при использовании Application Insights.

Я ссылался на этот документ для фильтрации и предварительной обработки в Azure Monitor SDK Application Insights.

  • Используется специальный процессор телеметрии (RelatedTelemetryProcessor), который сохраняет все элементы телеметрии, связанные с исключениями, на основе их идентификатора операции. Выборка также используется при исключении исключений (excludedTypes: "Exception") с цепочкой процессора телеметрии по умолчанию.
   private const string AppInsightConnectionString = "<connection string>";
    private const double SamplingPercentage = 50;

    public static async Task Main(string[] args)
    {
        // Create a ServiceCollection
        var services = new ServiceCollection();

        // Configure Application Insights telemetry
        services.AddApplicationInsightsTelemetryWorkerService(options =>
        {
            options.EnableAdaptiveSampling = false;
            options.ConnectionString = AppInsightConnectionString;
        });

        // Configure logging
        services.AddLogging(loggingBuilder =>
        {
            loggingBuilder.AddConsole();
            loggingBuilder.AddApplicationInsights(AppInsightConnectionString);
        });

        // Build the service provider
        var serviceProvider = services.BuildServiceProvider();

        // Configure telemetry pipeline with custom processor and sampling
        var telemetryConfiguration = serviceProvider.GetRequiredService<TelemetryConfiguration>();
        var telemetryProcessorChainBuilder = telemetryConfiguration.DefaultTelemetrySink.TelemetryProcessorChainBuilder;

        // Use custom processor and sampling
        telemetryProcessorChainBuilder.Use((next) => new CustomTelemetryProcessor(next));
        telemetryProcessorChainBuilder.UseSampling(SamplingPercentage, excludedTypes: "Exception");
        telemetryProcessorChainBuilder.Build();

        // Get logger
        var logger = serviceProvider.GetRequiredService<ILogger<Program>>();

        // Perform work
        DoWork(logger);

        // Flush telemetry
        var telemetryClient = serviceProvider.GetRequiredService<TelemetryClient>();
        telemetryClient.Flush();
        await Task.Delay(5000); // Wait for telemetry to be sent

        Console.WriteLine("Done. Press any key to exit.");
        Console.ReadKey();
    }

    static void DoWork(ILogger logger)
    {
        const int iterations = 50;
        const int errors = 10;

        var random = new Random();
        var errorsSet = new HashSet<int>();
        while (errorsSet.Count < errors)
        {
            errorsSet.Add(random.Next(0, iterations));
        }

        // Perform operations with logging
        for (int i = 0; i < iterations; i++)
        {
            string operationId = Guid.NewGuid().ToString();
            logger.LogInformation($"Begin operation: {operationId}");

            if (errorsSet.Contains(i))
            {
                logger.LogError(new Exception("Sample exception"), $"Error in operation: {operationId}");
            }

            logger.LogInformation($"End operation: {operationId}");
        }
    }
}

public class CustomTelemetryProcessor : ITelemetryProcessor
{
    private readonly ITelemetryProcessor _next;
    private readonly HashSet<string> _preservedOperationIds = new HashSet<string>();

    public CustomTelemetryProcessor(ITelemetryProcessor next)
    {
        _next = next;
    }

    public void Process(ITelemetry item)
    {
        // Get the operation ID of the telemetry item
        string operationId = item.Context.Operation.Id;

        // Check if the telemetry item is an exception
        if (item is ExceptionTelemetry exceptionTelemetry)
        {
            // Add the operation ID to the set of preserved operation IDs
            _preservedOperationIds.Add(operationId);
        }

        // Check if the operation ID is in the set of preserved operation IDs
        if (_preservedOperationIds.Contains(operationId))
        {
            // Pass the item through without sampling
            _next.Process(item);
        }
        else
        {
            // Apply your desired sampling here
            // If you decide to keep the item, pass it to the next processor
            _next.Process(item);
        }
    }

Чтобы обеспечить сохранение связанных трассировок в случае возникновения исключения при применении выборки к другим элементам телеметрии, вы можете настроить собственный процессор телеметрии для правильной обработки обоих случаев.

public class CustomTelemetryProcessor : ITelemetryProcessor
{
    private ITelemetryProcessor _next;
    private HashSet<string> _preservedOperationIds = new HashSet<string>();

    public CustomTelemetryProcessor(ITelemetryProcessor next)
    {
        _next = next;
    }

    public void Process(ITelemetry item)
    {
        if (item is ExceptionTelemetry exceptionTelemetry)
        {
            // Add the operation ID to the set of preserved IDs
            _preservedOperationIds.Add(exceptionTelemetry.Context.Operation.Id);
        }

        // Check if the operation ID is in the preserved set
        if (_preservedOperationIds.Contains(item.Context.Operation.Id))
        {
            // Pass the item through without sampling
            _next.Process(item);
        }
        else
        {
            // Apply sampling or other processing as usual
            _next.Process(item);
        }
    }
}

В вашем методе Main:

// Configure telemetry pipeline with custom processor and sampling
var telemetryConfiguration = serviceProvider.GetRequiredService<TelemetryConfiguration>();
var telemetryProcessorChainBuilder = telemetryConfiguration.DefaultTelemetrySink.TelemetryProcessorChainBuilder;

// Use custom processor and sampling
telemetryProcessorChainBuilder.Use((next) => new CustomTelemetryProcessor(next));
telemetryProcessorChainBuilder.UseSampling(SamplingPercentage, excludedTypes: "Exception");
telemetryProcessorChainBuilder.Build();

Пользовательский ITelemetryProcessor обрабатывает по одному элементу телеметрии за раз, что может затруднить обеспечение сохранения связанных трассировок при возникновении исключений.

Спасибо, что нашли время разобраться в этом! Я понял идею, но когда я пытаюсь реализовать что-то подобное, я вижу следы только после исключения, а не до него. Давайте будем честными: исключение, вероятно, будет последним в цепочке. Что меня сбивает с толку, так это то, как стандартная реализация включает в себя все заранее. Вот что говорят документы: «Он также синхронизирует выборку клиента и сервера, чтобы сохранить связанные элементы». (Learn.microsoft.com/en-us/azure/azure-monitor/app/… не совсем понимаю, есть ли что-то на стороне сервера

Alex K. 17.04.2024 04:17

Я думаю, что волшебство происходит в SamplingScoreGenerator github.com/microsoft/ApplicationInsights-dotnet/blob/…, если вы посмотрите на него, вы увидите, что записи с одинаковым идентификатором операции будут иметь одинаковую оценку выборки. Одним из возможных решений будет объединение записей с одинаковым идентификатором операции, а затем через некоторое время прохождение их через процессор выборки или передача дальше, если возникнет исключение. Я не думаю, что это идеально, не знаю, как это будет работать с функциями Azure.

Alex K. 17.04.2024 04:38

Рад узнать, что вы опубликовали ответ с помощью специального процессора телеметрии.

Sampath 22.04.2024 04:57
Ответ принят как подходящий

Просто хотел опубликовать то, что я придумал после более подробного изучения реализации специального процессора телеметрии. Идея продолжать накапливать сегменты телеметрии в течение некоторого времени, прежде чем принимать решение о выборке. Это создает небольшую задержку, но это лучшее, что у меня есть на данный момент.

using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Primitives;
using System.Collections.Concurrent;
using System.Diagnostics;

/// <summary>
/// Sampes only successful telemetry chains
/// </summary>
public class SuccessfulSamplingTelemetryProcessor : ITelemetryProcessor
{
    private readonly TimeSpan _bufferedTime;
    private readonly ITelemetryProcessor _next;
    private readonly SamplingTelemetryProcessor _samplingTelemetryProcessor;
    private readonly MemoryCache _cache;

    public SuccessfulSamplingTelemetryProcessor(double successfulSamplingPercentage, TimeSpan bufferedTime, ITelemetryProcessor next)
    {
        _bufferedTime = bufferedTime;
        _next = next;
        _samplingTelemetryProcessor = new SamplingTelemetryProcessor(next)
        {
            SamplingPercentage = successfulSamplingPercentage
        };
        _cache = new MemoryCache(new MemoryCacheOptions());
    }

    public void Process(ITelemetry item)
    {
        // get operation id 
        var operationId = item.Context.Operation.Id;
        if (string.IsNullOrEmpty(operationId))
        {
            // sample by default without correlation id
            _samplingTelemetryProcessor.Process(item);
            return;
        }

        var queue = _cache.GetOrCreate(operationId, entry =>
        {
            // expiration doesnt work as I'd expected, so use cancellation token instead
            // see https://stackoverflow.com/questions/42535408/net-core-memorycache-postevictioncallback-not-working-properly
            var expirationToken = new CancellationChangeToken(new CancellationTokenSource(_bufferedTime).Token);
            entry.AbsoluteExpirationRelativeToNow = _bufferedTime;
            entry.Priority = CacheItemPriority.NeverRemove;
            entry.AddExpirationToken(expirationToken);
            entry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration
            {
                EvictionCallback = OnPostEviction
            });
            return new ConcurrentQueue<ITelemetry>();
        });

        // just add to the queue for now
        queue.Enqueue(item);
    }

    private void OnPostEviction(object key, object? value, EvictionReason reason, object? state)
    {
        var queue = (ConcurrentQueue<ITelemetry>)value;

        // check if there is exception in the chain 
        var hasException = queue.Any(t => t is ExceptionTelemetry);
        while (queue.TryDequeue(out var telemetry))
        {
            if (hasException)
            {
                // pass through
                _next.Process(telemetry);
            }
            else
            {
                // apply sampling
                _samplingTelemetryProcessor.Process(telemetry);
            }
        }   
    }
}

Другие вопросы по теме