Я экспериментировал с выборкой данных телеметрии, чтобы не превысить лимит ежедневных журналов 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}");
}
}
В приведенном ниже коде используется 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
обрабатывает по одному элементу телеметрии за раз, что может затруднить обеспечение сохранения связанных трассировок при возникновении исключений.
Я думаю, что волшебство происходит в SamplingScoreGenerator github.com/microsoft/ApplicationInsights-dotnet/blob/…, если вы посмотрите на него, вы увидите, что записи с одинаковым идентификатором операции будут иметь одинаковую оценку выборки. Одним из возможных решений будет объединение записей с одинаковым идентификатором операции, а затем через некоторое время прохождение их через процессор выборки или передача дальше, если возникнет исключение. Я не думаю, что это идеально, не знаю, как это будет работать с функциями Azure.
Рад узнать, что вы опубликовали ответ с помощью специального процессора телеметрии.
Просто хотел опубликовать то, что я придумал после более подробного изучения реализации специального процессора телеметрии. Идея продолжать накапливать сегменты телеметрии в течение некоторого времени, прежде чем принимать решение о выборке. Это создает небольшую задержку, но это лучшее, что у меня есть на данный момент.
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);
}
}
}
}
Спасибо, что нашли время разобраться в этом! Я понял идею, но когда я пытаюсь реализовать что-то подобное, я вижу следы только после исключения, а не до него. Давайте будем честными: исключение, вероятно, будет последним в цепочке. Что меня сбивает с толку, так это то, как стандартная реализация включает в себя все заранее. Вот что говорят документы: «Он также синхронизирует выборку клиента и сервера, чтобы сохранить связанные элементы». (Learn.microsoft.com/en-us/azure/azure-monitor/app/… не совсем понимаю, есть ли что-то на стороне сервера