Регистрация заданий Hangfire в Application Insights и сопоставление активности с идентификатором операции

#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 запроса. Кто-нибудь знает?