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
Если в двух словах — то работает он так:
- С помощью SDK ручками или автоматично генерируются объекты из дата модели Application Insights по событиям в ПО
- Сгенерированные объекты засылаются в Azure через REST API
- В Azure есть инструменты (очень богатые) для анализа сгенерированной инфы
Как дата модель Application Insights маппится на микросервисную архитектуру?
Как видно из картинки, каждый пинок в микросервис\UI\API порождает Request на той стороне, куда пнули и Dependency на той стороне, где пинали.
В процессе работы могут рождаться также трассировочные события, сообщения об ошибках, кастомные евенты, а также такие специализированные объекты как pageView
Как Application Insights коррелирует между собой объекты
На картинке синими стрелочками отмечены связи между объктами. Как видно,
Все объекты связаны с определенной операцией (это не отдельная сущность датамодели AI, они просто имеют одинаковое свойство. Операция инициализируется при первом request не в рамках операции)
Трассировочные сообщения/сообщения об ошибках/кастомные события/депенды микросервиса связаны дополнительно к объекту request (также через свойство)
- Объекты Request связаны дополнительно с объектом Dependency (если только это не самый первый Request) также через свойство.
Подробности можно почитать в официальной документации по Application Insights
Что дальше?
А дальше будет описание двух сценариев взаимодействия между микросервисами с подробным описанием способа реализации.
Сценарий 1 — AJAX, MVC, WebApi
Или то же самое, но словами:
Для отображения странички требуется в MVC контроллере получить данные от WebAPI микросервиса (через HttpClient)
- После отображения странички она сама с помощью jQuery лезет на сервер в MVC контроллер и получает ещё данные, которые в свою очередь также берутся от WebAPI микросервиса.
Что мы хотим получить
Хотим иметь возможность
- Увидеть факт просмотра страницы
- Увидеть, что для генерации страницы сервер ходил в микросервис
- Увидеть, что сама страница выполняла ajax запрос на сервер
- Увидеть, что сервер для удовлетворения этого запроса также ходил в микросервис
- В случае возникновения где-нибудь ошибки — хотим сразу увидеть контекст в рамках которого она произошла
В идеале, мы должны всю информацию увидеть на одном экране как последовательность действий
Конечно, все эти цели в конечном счёте мы достигнем.
Чтобы не томить —
Сначала результат:
Переход от страницы к диагностической информации по операции
Сама диагностическая информация
А теперь с ошибкой в микросервисе:
Детали реализации
В 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
Официальная документация
Буду благодарен за любую конструктивную критику!