Pull to refresh

Настройка Azure Application Insights для диагностики ПО с микросервисной архитектурой

Reading time8 min
Views5.7K

Application Insights — клёвая штука, позволяющяя проводить диагностику, профилирование и анализ использования развернутых систем (в т.ч. в продакшен режиме), и при этом не требующая от разработчика вообще никаких усилий. Конечно, всё это становится возможным ценой мучительной первоначальной настройки.


В рекламных видео конечно никакой особой настройки нет, но жизнь — она сложнее, особенно если ваше ПО микросервисное. Почему? А всё очень просто.


Что в первую очередь должна делать система диагностики в микросервисной архитектуре?
Правильно, коррелировать диагностику от различных микросервисов в рамках одной операции.
Тыркнул пользователь в UI кнопочку — надо увидеть диагностику от всех N микросервисов, которые так или иначе обрабатывали этот тырк. Случился где-нибудь exception — надо увидеть не только в каком микросервисе он произошёл, но и в рамках какой операции это случилось.
Только вот Application Insights с точки зрения конкретного микросервиса — это в первую очередь SDK. И SDK таких есть несколько — есть для JS, есть для .NET Core, .NET (со своими особенностями настройки для MVC, WebAPI, WCF), есть для Java и т.д.


Какие-то из этих SDK — opensource, какие-то — внутренняя разработка MS. И чтобы всё завелось — их надо подружить.


В этом и состоит основная сложность.


Не скажу, что я достиг 100% просветления в этом вопросе.
Но по крайней мере, я уже собрал несколько граблей и у меня есть рабочий семпл с UI на ASP.NET MVC (не Core) + JS и двумя микросервисами (Asp.Net WebApi, WCF)


Кому интересно — прошу под кат.


Немного про Application Insights


Если в двух словах — то работает он так:


  1. С помощью SDK ручками или автоматично генерируются объекты из дата модели Application Insights по событиям в ПО
  2. Сгенерированные объекты засылаются в Azure через REST API
  3. В Azure есть инструменты (очень богатые) для анализа сгенерированной инфы

Как дата модель Application Insights маппится на микросервисную архитектуру?



Как видно из картинки, каждый пинок в микросервис\UI\API порождает Request на той стороне, куда пнули и Dependency на той стороне, где пинали.
В процессе работы могут рождаться также трассировочные события, сообщения об ошибках, кастомные евенты, а также такие специализированные объекты как pageView


Как Application Insights коррелирует между собой объекты



На картинке синими стрелочками отмечены связи между объктами. Как видно,


  1. Все объекты связаны с определенной операцией (это не отдельная сущность датамодели AI, они просто имеют одинаковое свойство. Операция инициализируется при первом request не в рамках операции)


  2. Трассировочные сообщения/сообщения об ошибках/кастомные события/депенды микросервиса связаны дополнительно к объекту request (также через свойство)


  3. Объекты Request связаны дополнительно с объектом Dependency (если только это не самый первый Request) также через свойство.

Подробности можно почитать в официальной документации по Application Insights


Что дальше?


А дальше будет описание двух сценариев взаимодействия между микросервисами с подробным описанием способа реализации.


Сценарий 1 — AJAX, MVC, WebApi



Или то же самое, но словами:


  1. Для отображения странички требуется в MVC контроллере получить данные от WebAPI микросервиса (через HttpClient)


  2. После отображения странички она сама с помощью jQuery лезет на сервер в MVC контроллер и получает ещё данные, которые в свою очередь также берутся от WebAPI микросервиса.

Что мы хотим получить


Хотим иметь возможность


  1. Увидеть факт просмотра страницы
  2. Увидеть, что для генерации страницы сервер ходил в микросервис
  3. Увидеть, что сама страница выполняла ajax запрос на сервер
  4. Увидеть, что сервер для удовлетворения этого запроса также ходил в микросервис
  5. В случае возникновения где-нибудь ошибки — хотим сразу увидеть контекст в рамках которого она произошла

В идеале, мы должны всю информацию увидеть на одном экране как последовательность действий
Конечно, все эти цели в конечном счёте мы достигнем.
Чтобы не томить —


Сначала результат:


Переход от страницы к диагностической информации по операции


Сама диагностическая информация


А теперь с ошибкой в микросервисе:


Детали реализации


В Web API микросервисе:


  • Устанавливаем NuGets Microsoft.ApplicationInsights, Microsoft.ApplicationInsights.Web, Microsoft.AspNet.WebApi.Tracing
  • С пакетами выше приезжает TraceListener для ApplicationInsights, прописывается в web.config сам.
  • В WebApiConfig указываем config.EnableSystemDiagnosticsTracing()
  • Реализуем IExceptionFilter, регистрирующий все непойманные ошибки в ApplicationInsights, прописываем его в FilterConfig.cs

Код
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class WebApiAITrackingAttribute : FilterAttribute, IExceptionFilter
{
    public void OnException(ExceptionContext filterContext)
    {
        if (filterContext != null 
&& filterContext.HttpContext != null && filterContext.Exception != null)
        {
            ApplicationInsightsSettings.TelemetryClient.TrackException(filterContext.Exception);
        }
    }
}

TelemetryClient — объект из SDK ApplicationInsights. Его не рекомендуется создавать каждый раз, поэтому он тут singleton через ApplicationInsightsSettings


В ASP.Net MVC UI:


  • Устанавливаем NuGet'ы Microsoft.ApplicationInsights, Microsoft.ApplicationInsights.Web
  • Реализуем IExceptionFilter, регистрирующий все непойманные ошибки в ApplicationInsights, прописываем его в FilterConfig.cs (такой же как и для Web API микросервиса)
  • Реализуем HttpClientHandler для работы с HttpClient, создающий Dependency.
    Тут чуть поподробнее: вообще, документация декларирует, что System.Net.HttpClient начиная хз с какой версии умеет сам создавать Dependency.
    И это даже действительно так. Но только делает он это не напрямую (он напрямую с AI SDK не работает), а слегка через одно место, поэтому этот Dependency не всегда привязывается правильно к Request. Поэтому когда мне надоело ловить глюки с этим — я написал свой HttpClientHandler

Код
public class DependencyTrackingHandler : HttpClientHandler
{
    public DependencyTrackingHandler()
    {
    }

    private static int _dependencyCounter = 0;

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        using (var op = ApplicationInsightsSettings.TelemetryClient.StartOperation<DependencyTelemetry>(request.Method.Method + " " + request.RequestUri.AbsolutePath))
        {
            op.Telemetry.Target = request.RequestUri.Authority;
            op.Telemetry.Type = request.Method.Method;
            op.Telemetry.Data = request.Method.Method + " " + request.RequestUri.ToString();
            op.Telemetry.Id += "." + Interlocked.Increment(ref _dependencyCounter).ToString();

            request.Headers.Add("Request-Id", op.Telemetry.Id);

            var result = base.SendAsync(request, cancellationToken).Result;
            op.Telemetry.ResultCode = result.StatusCode.ToString();
            op.Telemetry.Success = result.IsSuccessStatusCode;
            return result;
        }
    }
}

Как видно, смысл этого хендлера — обернуть вызов HttpClient'а в DependencyTelemetry, а также (и это очень важно), установить правильный Request_Id хедер.
Про хедеры также чуть поподробнее. Через них инфраструктура AI передает информацию о id операции, а также информацию о id dependency при вызовах через HttpClient.
Хедеры для этих целей используются такие: "Request-Id", "x-ms-request-id", "x-ms-request-root-id". Для того чтобы корреляция правильно инициализировалась достаточно первого или (второго И третьего).
Подробнее про корреляции и хедеры можно почитать в документации (хотя она довольно сумбурна)


  • Вспоминаем, что тут мы имеем дело с двумя SDK — для JS и для .NET, поэтому где-нибудь в вьюхах MVC ищем код, инициализирующий appInsight на стороне браузера, а затем
  • Прописываем туда InstrumentationKey

Код
var appInsights=window.appInsights||function(config)
{
    function r(config){ t[config] = function(){ var i = arguments; t.queue.push(function(){ t[config].apply(t, i)})} }
    var t = { config:config},u=document,e=window,o='script',s=u.createElement(o),i,f;for(s.src=config.url||'//az416426.vo.msecnd.net/scripts/a/ai.0.js',u.getElementsByTagName(o)[0].parentNode.appendChild(s),t.cookie=u.cookie,t.queue=[],i=['Event','Exception','Metric','PageView','Trace','Ajax'];i.length;)r('track'+i.pop());return r('setAuthenticatedUserContext'),r('clearAuthenticatedUserContext'),config.disableExceptionTracking||(i='onerror',r('_'+i),f=e[i],e[i]=function(config, r, u, e, o) { var s = f && f(config, r, u, e, o); return s !== !0 && t['_' + i](config, r, u, e, o),s}),t
}({
    instrumentationKey: "@Microsoft.ApplicationInsights.Extensibility.TelemetryConfiguration.Active.InstrumentationKey"
});

  • Синхронизируем глобальную операцию между сервером и браузером (чтобы у нас в одной глобальной операции отображалась и серверная диагностика и диагностика с браузера)

Код
window.appInsights.queue.push(function () {
    var serverId ="@Context.GetRequestTelemetry().Context.Operation.Id";
    appInsights.context.operation.id = serverId;
});

Большое спасибо за этот WA **Sergey Kanzhelev** и его блогу [http://apmtips.com](http://apmtips.com)

Сценарий 2 — AJAX, WCF, WebApi



Или то же самое, но словами:


  • Для отображения странички не требуется данных от микросервиса
  • После отображения странички она сама с помощью jQuery лезет в WCF микросервис и получает ещё данные, которые в свою очеред также берутся от WebAPI микросервиса.

Цели те же, как и в предыдущем сценарии


Сначала результат:



С ошибкой в микросервисе:


Детали реализации


В Web API микросервисе — ничего нового относительно предыдущего сценария


В ASP.Net MVC UI:


  • Где-нибудь в вьюхах MVC ищем код, инициализирующий appInsight на стороне браузера, а затем перепределяем функцию appInsights._ajaxMonitor.sendHandler

Код
window.appInsights.queue.push(function () {
    appInsights._ajaxMonitor.sendHandler = function (e, n) {
        e.ajaxData.requestSentTime = Date.now();
        if (!this.appInsights.config.disableCorrelationHeaders) {
            var i = this.appInsights.context.operation.id;
            e.setRequestHeader("x-ms-request-root-id", i);
            e.setRequestHeader("x-ms-request-id", e.ajaxData.id);
        }
        e.ajaxData.xhrMonitoringState.sendDone = !0;
    };
});

С этим чуть поподробнее. Как известно, запросы через XmlHttpRequest на хосты, отличные от текущего, подвержены дополнительной секьюрити, т.н. CORS
Выражается это в том, что в http API могут прилетать т.н. preflight запросы перед основным для апрува хедеров, метода и хоста основного запроса.
Так вот, почему-то Application Insights SDK для JS видимо очень боится отправить эти preflight запросы и поэтому никогда не пересылает корреляционные хедеры на хосты, отличные от текущего (с учётом порта).
На команду Application Insights SDK для JS уже заведён FR по этому поводу.
В коде выше в качестве WA просто убирается проверка на соответствие хостов и таким образом, хедеры отсылаются в любом случае.


В WcfApi:


  • Устанавливаем NuGet'ы Microsoft.ApplicationInsights, Microsoft.ApplicationInsights.Web, Microsoft.ApplicationInsights.Wcf (из https://www.myget.org/F/applicationinsights-sdk-labs/)
  • Удаляем из ApplicationInsights.config
    <Add Type="Microsoft.ApplicationInsights.Web.OperationNameTelemetryInitializer, Microsoft.AI.Web"/>
  • Помечаем атрибутом [ServiceTelemetry] класс сервиса
  • Не забываем установить Method = "*" в атрибуте WebInvoke для методов интерфейса сервиса (т.к. в него будут прилетать preflight запросы с методом OPTIONS)
  • Реализуем ответ на preflight запрос в методах сервиса

Код
private bool CheckCorsPreFlight()
{
    var cors = false;
    if (WebOperationContext.Current != null)
    {
        var request = WebOperationContext.Current.IncomingRequest;
        var response = WebOperationContext.Current.OutgoingResponse;

        if (request.Method == "OPTIONS")
        {
            response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
            response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, X-Requested-With, x-ms-request-root-id, x-ms-request-id");
            response.Headers.Add("Access-Control-Allow-Credentials", "true");
            cors = true;
        }
        var origin = request.Headers.GetValues("Origin");
        if (origin.Length > 0)
            response.Headers.Add("Access-Control-Allow-Origin", origin[0]);
        else
            response.Headers.Add("Access-Control-Allow-Origin", "*");
    }
    return cors;
}

В качестве бонуса — запросы для Application Insights Analytics


Получение id глобальной операции по факту просмотра страницы


pageViews
| order by timestamp desc
| project timestamp, operation_Id, name


Получение id глобальной операции по факту возникновения ошибки


exceptions
| order by timestamp desc
| project timestamp, operation_Id, problemId, assembly  

Также для этих целей удобно использовать новый интерфейс Failures


Получение диагностических данных по глобальной операции


requests
| union dependencies
| union pageViews
| union exceptions
| where operation_Id == "<place operation id here>"
| order by timestamp desc
| project timestamp, itemType, data = iif(itemType == "exception", problemId, name) 


Всем спасибо
Тестовый проект на github


Что почитать:
Мега блог amptips.com
Официальная документация


Буду благодарен за любую конструктивную критику!

Tags:
Hubs:
+5
Comments1

Articles