#azure #azure-application-insights #asp.net-core-3.1 #hangfire
#azure #azure-application-insights #asp.net-core-3.1 #hangfire
Вопрос:
Я чувствую, что это должно быть намного проще, чем получается, или я просто слишком много об этом думаю.
У меня есть приложение .NET Core 3.1 Web API, которое использует HangFire для обработки некоторых заданий в фоновом режиме. Я также настроил Application Insights для регистрации телеметрии из .NET Core API.
Я вижу события регистрации и данные телеметрии зависимостей, зарегистрированные в Application Insights. Однако каждое событие / журнал / зависимость записывается с использованием уникального идентификатора операции и родительского идентификатора.
Я пытаюсь определить, как гарантировать, что любое регистрируемое действие или любые зависимости, которые используются в контексте фонового задания, регистрируются по идентификатору операции и / или родительскому идентификатору исходного запроса, который поставил фоновое задание в очередь.
Когда я ставлю задание в очередь, я могу получить текущий OperationId входящего HTTP-запроса и помещаю его в очередь HangFire вместе с заданием. После выполнения задания я могу вернуть этот идентификатор операции. Затем мне нужно сделать, это сделать этот OperationId доступным на протяжении всего контекста / времени выполнения задания, чтобы он был привязан к любой телеметрии, отправленной в Application Insightd.
Я подумал, что мог бы создать интерфейс IJobContext, который можно было бы внедрить в класс, выполняющий задание. В этом контексте я мог бы нажать OperationId. Затем я мог бы создать инициализатор ITelemetryInitializer, который также принимал бы IJobContext в качестве зависимости. Затем в инициализаторе ITelemetryInitializer я мог бы установить OperationId и ParentID телеметрии, отправляемой в Application Insights. Вот несколько простых кодов:
public class HangFirePanelMessageQueue : IMessageQueue
{
private readonly MessageProcessor _messageProcessor;
private readonly IHangFireJobContext _jobContext;
private readonly TelemetryClient _telemetryClient;
public HangFirePanelMessageQueue(MessageProcessor panelMessageProcessor,
IIoTMessageSerializer iotHubMessageSerialiser,
IHangFireJobContext jobContext, TelemetryClient telemetryClient)
{
_messageProcessor = panelMessageProcessor;
_jobContext = jobContext;
_telemetryClient = telemetryClient;
}
public async Task ProcessQueuedMessage(string message, string operationId)
{
var iotMessage = _iotHubMessageSerialiser.GetMessage(message);
_jobContext?.Set(iotMessage.CorrelationID, iotMessage.MessageID);
await _messageProcessor.ProcessMessage(iotMessage);
}
public Task QueueMessageForProcessing(string message)
{
var dummyTrace = new TraceTelemetry("Queuing message for processing", SeverityLevel.Information);
_telemetryClient.TrackTrace(dummyTrace);
string opId = dummyTrace.Context.Operation.Id;
BackgroundJob.Enqueue(() =>
ProcessQueuedMessage(message, opId));
return Task.CompletedTask;
}
}
IJobContext будет выглядеть примерно так:
public interface IHangFireJobContext
{
bool Initialised { get; }
string OperationId { get; }
string JobId { get; }
void Set(string operationId, string jobId);
}
И тогда у меня был бы инициализатор ITelemetryInitializer, который обогащает любую ителеметрию:
public class EnrichBackgroundJobTelemetry : ITelemetryInitializer
{
private readonly IHangFireJobContext jobContext;
public EnrichBackgroundJobTelemetry(IHangFireJobContext jobContext)
{
this.jobContext = jobContext;
}
public void Initialize(ITelemetry telemetry)
{
if (!jobContext.Initialised)
{
return;
}
telemetry.Context.Operation.Id = jobContext.OperationId;
}
}
Однако проблема, с которой я сталкиваюсь, заключается в том, что инициализатор ITelemetryInitializer является одноэлементным, и поэтому он будет создан один раз с помощью IHangFireJobContext, который затем никогда не будет обновляться для любого последующего задания HangFire.
Я нашел https://github.com/skwasjer/Hangfire .Сопоставить проект, который расширяет https://github.com/skwasjer/Correlate . Correlate создает контекст корреляции, доступ к которому можно получить через ICorrelationContextAccessor, аналогичный IHttpContextAccessor.
Однако в сносках для Correlate указано: «Пожалуйста, учтите, что в .NET Core 3 теперь есть встроенная поддержка W3C TraceContext (blog) и что существуют другие распределенные библиотеки трассировки с большей функциональностью, чем Correlate». в котором перечислены Application Insights как одна из альтернатив для более продвинутой распределенной трассировки.
Итак, кто-нибудь может помочь мне понять, как я могу обогатить любую телеметрию, поступающую в Application Insights, когда она создается в контексте задания HangFire? Я считаю, что правильный ответ — использовать инициализатор ITelemetryInitializer и заполнить OperationId для этого элемента ITelemetry, однако я не уверен, какую зависимость вводить в ITelemetryInitialzer, чтобы получить доступ к контексту задания HangFire.
Комментарии:
1. Я только что наткнулся на След. CorrelationManager, который задокументирован как «Получает менеджер корреляции для потока для этой трассировки».. Я собираюсь исследовать использование этого для запуска и остановки логической операции вокруг моего задания HangFire и извлечения OperationId из трассировки. CorrelationManager в инициализаторе ITelemetryInitializer.
Ответ №1:
Когда я ставлю задание в очередь, я могу получить текущий OperationId входящего HTTP-запроса и помещаю его в очередь HangFire вместе с заданием.
Итак, правильно ли я говорю, что у вас есть действие контроллера, которое отправляет работу в hangfire? Если да, то, что вы можете сделать, это внутри метода контроллера получить идентификатор операции и передать его заданию. Используйте этот идентификатор операции для запуска новой операции с использованием идентификатора операции. Эта операция вместе со всей телеметрией, сгенерированной во время этой операции, будет связана с исходным запросом.
У меня нет интеграции с hangfire, но приведенный ниже код показывает общую идею: некоторая работа ставится в очередь для выполнения в фоновом режиме и должна быть связана с запросом относительно телеметрии:
[HttpGet("/api/demo5")]
public ActionResult TrackWorker()
{
var requestTelemetry = HttpContext.Features.Get<RequestTelemetry>();
_taskQueue.QueueBackgroundWorkItem(async ct =>
{
using(var op = _telemetryClient.StartOperation<DependencyTelemetry>("QueuedWork", requestTelemetry.Context.Operation.Id))
{
_ = await new HttpClient().GetStringAsync("http://blank.org");
await Task.Delay(250);
op.Telemetry.ResultCode = "200";
op.Telemetry.Success = true;
}
});
return Accepted();
}
Полный пример можно найти здесь.
Ответ №2:
Работая с примером Питера Бонса, я сделал это следующим образом:
Код, первоначально запущенный из действия контроллера:
// Get the current ApplicationInsights Id. Could use .RootId if
// you only want the OperationId, but I want the ParentId too
var activityId = System.Diagnostics.Activity.Current?.Id;
_backgroundJobClient.Enqueue<JobDefinition>(x =>
x.MyMethod(queueName, otherMethodParams, activityId));
В моем JobDefinition
классе:
// I use different queues, but you don't need to.
// otherMethodParams is just an example. Have as many as you need, like normal.
[AutomaticRetry(OnAttemptsExceeded = AttemptsExceededAction.Delete, Attempts = 10)]
[QueueNameFromFirstParameter]
public async Task MyMethod(string queueName, string otherMethodParams,
string activityId)
{
var (operationId, parentId) = SplitCorrelationIdIntoOperationIdAndParentId(
activityId);
// Starting this new operation will initialise
// System.Diagnostics.Activity.Current.
using (var operation = _telemetryClient.StartOperation<DependencyTelemetry>(
"JobDefinition.MyMethod", operationId, parentId))
{
try
{
operation.Telemetry.Data = $"something useful here";
// If you have other state you'd like in App Insights logs,
// call AddBaggage and they show up as a customDimension,
// e.g. in any trace logs.
System.Diagnostics.Activity.Current.AddBaggage("QueueName", queueName);
// ... do the real background work here...
operation.Telemetry.Success = true;
}
catch (Exception)
{
operation.Telemetry.Success = false;
throw;
}
}
}
// Splits full value from System.Diagnostics.Current.Activity.Id
// like "00-12994526f1cb134bbddd0f256e8bc3f0-872b3bd78c345a46-00"
// into values ( "12994526f1cb134bbddd0f256e8bc3f0", "872b3bd78c345a46" )
private static (string, string) SplitCorrelationIdIntoOperationIdAndParentId(string activityId)
{
if (string.IsNullOrEmpty(activityId))
return (null, null);
var splits = activityId.Split('-');
// This is what should happen
if (splits.Length >= 3)
return (splits[1], splits[2]);
// Must be in a weird format. Try to return something useful.
if (splits.Length == 2)
return (splits[0], splits[1]);
return (activityId, null);
}
Я не уверен, что использование OperationId и ParentID здесь совершенно правильно, например, он привязывает фоновое задание к OperationId исходного запроса, но если исходный запрос имеет ParentID, то это фоновое задание действительно должно иметь свой ParentID, установленный в качестве запроса, а не в качестве ParentID запроса. Кто-нибудь знает?