Трасування з .NET додатків в Azure

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Усім привіт, на зв’язку Вова Вердиш, у цій статті хотів би поговорити про трасування з .NET додатків. Трасування — це один з трьох стовпів телеметрії. Звернемося до енциклопедичного значення цього терміну, тобто, до «Вікіпедії»:

Телеметрія (від грец. τῆλε «далеко» + μέτρεω — «вимірюю») — галузь науки і техніки, що займається питаннями розробки та експлуатації телеметричних систем — комплексу автоматизованих засобів, що забезпечують отримання, перетворення, передачу по каналу зв’язку, прийом, обробку та реєстрацію вимірювальної (телеметричної) інформації та інформації про різні події з метою контролю на відстані різних об’єктів та процесів.

З академічної точки зору, напевно, все чітко та зрозуміло. З погляду простої людини (мене, наприклад) — не дуже. Зрозуміло, що є якісь вимірювальні дані, зрозуміло, що є мета контролю за чимось, але, як і раніше, немає чіткого уявлення, що тут відбувається.

Що таке телеметрія у контексті розробки програмного забезпечення

Дуже влучний опис, на мій погляд, звучить так: телеметрія — це якісь дані, що виходять із системи, про її поведінку; до цих даних належать трасування, метрики, журнали. Взяв я цей опис звідси, в оригіналі він звучить так:

Telemetry refers to data emitted from a system, about its behavior. The data can come in the form of Traces, Metrics, and Logs.

До того, як я побачив Azure, я працював з додатками, які складали логи в Elasticsearch, які можна було дивитися через Kibana. Це класна штука. Коли я почав працювати з Azure, одне з перших питань в мене було: «А де тут логи?».

Application Insights

В Azure є спеціальний сервіс для збору та відображення телеметрії — Application Insights. Там зберігається все: і трасування, і метрики, і логи. Коли я вперше туди потрапив, я подумав: «Погань якась». Ця думка була в моїй голові якийсь час. А потім я побачив «це»:

Раніше я нічого такого не бачив. Я закохався у це. На цій картинці ми бачимо все, що в нас сталося в рамках «транзакції». До нас залетів вхідний http-запит, обробка цього вхідного запиту складалася зі звернення до БД, вихідного http-виклику до Google, розміщення повідомлення в Service Bus (черга в Azure), вичитування повідомлення з Service Bus.

Причому вичитування повідомлення з черги відбувається вже після того, як вхідний http-запит завершився. На цій картинці цього не видно, але я це знаю, тому що це мій код. В рамках «транзакції» не має значення, чи відбулось щось в рамках одного запита, чи ні.

Хоча вся ця штука і називається «end-to-end transaction», я беру слово «транзакція» в лапки, щоб ніхто раптом не подумав, що це має якесь відношення до БД. Мені здається, для цієї штуки могли б вигадати інший, більш відповідний термін. «Траса» (від слова трасування), «операція» або щось на зразок того, на мій погляд, підійшло б краще. Але «trace»-ом в Azure вирішили назвати логи, тож маємо, що маємо. Ну і грець з ним.

Подивімося детальніше на деякі елементи нашої «транзакції».

Всі елементи мають ряд загальних реквізитів, таких як, наприклад, час події, тривалість, ім’я, тип, службові ідентифікатори. Але кожен з них може мати щось своє. На зображенні вище — вхідний http-запит. Ми бачимо, що він був успішний (бачимо код відповіді 200), бачимо, куди саме був цей запит та якого типу він був.

На зображенні нижче — SQL-запит. І це при тому, що я використовую EntityFramework, а не «сирі запити».

Один з приємних моментів полягає в тому, що для того, щоб отримати це, нам не потрібно сильно напружуватися.

Як мені отримати трасування для свого додатка

Нагадаю свій сценарій: вхідний http-запит, звернення до БД, http-запит до Google, запис повідомлення в чергу, вичитування повідомлення з черги.

Є кілька способів, як можна почати писати трасування. Варіант перший — це використання Application Insights SDK. Підключаємо пакет Microsoft.ApplicationInsights.AspNetCore та конфігуруємо запис трасувань:

builder.Services.AddApplicationInsightsTelemetry();
builder.Services.ConfigureTelemetryModule<DependencyTrackingTelemetryModule>((module, o) =>
{
	module.EnableSqlCommandTextInstrumentation = true; // щоб SQL текст запитів було включено в трасування
});

Це все, що треба для того, щоб побачити таку красу:

Інший варіант — використовувати OpenTelemetry .NET. OpenTelemetry — це специфікація, розроблена Google ще з якоюсь компанією (здається, це прямо в них на сайті десь було написано; я читав колись, але зараз знайти не зміг). Є багато реалізацій цієї специфікації, розроблених під різні мови та платформи. OpenTelemetry .NET — це реалізація цієї специфікації на .NET.

Підключаємо ряд пакетів з набору OpenTelemetry:

OpenTelemetry.Extensions.Hosting, OpenTelemetry.Instrumentation.AspNetCore, OpenTelemetry.Instrumentation.Http, OpenTelemetry.Instrumentation.SqlClient, і оскільки ми хочемо все це побачити в Azure, підключаємо ще відповідний експортер Azure.Monitor.OpenTelemetry.Exporter.

Конфігуруємо це все:

builder.Services.AddOpenTelemetryTracing(tracingBuilder =>
{
	tracingBuilder.AddAzureMonitorTraceExporter(o =>
	{
		o.ConnectionString = configuration.GetValue<string>("ApplicationInsights:ConnectionString");
	});
	tracingBuilder.AddAspNetCoreInstrumentation();
	tracingBuilder.AddHttpClientInstrumentation();
	tracingBuilder.AddSqlClientInstrumentation(opt =>
	{
		opt.SetDbStatementForText = true; // якщо хочему SQL тексти додати в трасування
	});
});

У результаті бачимо дуже схожу красу:

Різниця в порівнянні з першим варіантом у тому, що в цьому випадку ми не маємо трасування для Service Bus. Формат рядка вхідного http-запиту я за різницю не вважаю. Наразі, коли я пишу ці літери, офіційного інструменту для запису трасувань ServiceBus немає. Можливо, є неофіційні, я не шукав. За бажання, звичайно, можна і самому написати.

То що мені обрати: Application Insights SDK або OpenTelemetry .NET?

OpenTelemetry — це майбутнє телеметрії, в тому числі для .NET додатків. Але зараз під .NET для Azure реалізовано не все те, що нам хотілося б. Якщо чесно, я не розумію, чому в Microsoft ще не реалізували все, що треба, при тому, що вони самі кажуть, що Application Insights SDK зараз не розвивається, що майбутнє за OpenTelemetry. На цю мить немає повноцінного експортера в Azure для OpenTelemetry. Ось що говорить Microsoft:

Carefully consider whether this preview is right for you. It enables distributed tracing only and excludes:

  • Metrics API (like custom metrics and pre-aggregated metrics).
  • Live Metrics.
  • Logging API (like console logs and logging libraries).
  • Autocapture of unhandled exceptions.
  • Profiler.
  • Snapshot Debugger.
  • Offline disk storage and retry logic.
  • Azure Active Directory authentication.
  • Sampling.
  • Autopopulation of Cloud Role Name and Cloud Role Instance in Azure environments.
  • Autopopulation of User ID and Authenticated User ID when you use the Application Insights JavaScript SDK.
  • Autopopulation of User IP (to determine location attributes).
  • Ability to override Operation Name.
  • Ability to manually set User ID or Authenticated User ID.
  • Propagating Operation Name to Dependency Telemetry.
  • Instrumentation libraries support on Azure Functions.

If you require a full-feature experience, use the existing Application Insights ASP.NET or ASP.NET Core SDK until the OpenTelemetry-based offering matures.

Якщо чесно, я сам не розумію, про що частина з описаних обмежень, або я це і так не використовую. Якщо продовжити говорити на чистоту, то я не до кінця розумію, що вже вміє OpenTelemetry, а що ще ні. Що у мене вийшло з OpenTelemetry в Azure:

  • запис трасувань; хоча ASP.NET Core SDK з коробки вміє писати більше трасувань, пов’язаних з Azure, але для OpenTelemetry, наприклад, вже є розширення, які вміють збирати метрики з компонентів, не пов’язаних з Azure, наприклад MassTransit, MySQL, Elasticsearch;
  • логи пишуться; не знаю, що означає 3 пункт зі списку обмежень вище, можливо, цей список застарів, а можливо він означає щось, що я не розумію;
  • метрики пишуться; правда, якщо говорити, наприклад, про метрики середовища виконання, то ASP.NET Core SDK, схоже, дозволяє з коробки писати їх більше, ніж те, що дозволяє пакет для OpenTelemetry; у випадку з ASP.NET Core SDK ми самостійно вказуємо метрики, які ми хочемо писати, а які ні; у випадку з OpenTelemetry воно пише набір метрик, який закодовано у бібліотеці, без можливості конфігурації.

На цю мить у мене до OpenTelemetry багато питань. «Питань» не в плані «А що це за херня у вас тут?», «А от так навіщо робити?», а в плані, що я не знаю як, використовуючи її, не втратити нічого з того, що зараз нам дає ASP.NET Core SDK. Але ж ви пам’ятаєте, що я використовую Azure? Якщо у вас не Azure, то, можливо, вам варто подивитися у бік OpenTelemetry.

«Навіщо нам вся ця маячня з телеметрією? Ти ж про трасування обіцяв»

Про трасування, отже про трасування. Швидше за все, вас може цікавити ще одне питання: а як мені щось своє додати в трасування? Оскільки я використовую ASP.NET Core SDK, то покажу на його прикладі.

Сценарій: у мене є якесь завдання, яке виконується кожні Х хвилин, воно вибирає якісь ID з бази даних, робить якісь http-запити та складає щось назад у БД. Якщо ми нічого спеціально робити не будемо, ми побачимо і запити до БД, і http-запити, але вони будуть самі по собі. А я хочу бачити, що все це відбувається в рамках чогось, в цьому випадку, в рамках цього завдання.

Ключовим елементом у трасуванні сучасних .NET додатків є Activity. Microsoft описує цей клас наступним чином: Represents an operation with context to be used for logging. Я б описав це так: Represents an operation with context to be used for tracing. Характерними ознаками операції є час початку та час закінчення. У елемента лога є тільки дата події, в той час як у трасування є час початку і час закінчення операції. Але Microsoft, напевно, видніше.

Теоретично, ми могли б не використовувати Activity у своєму додатку. Але якби ми розробляли бібліотеку і хотіли виставити якісь свої активності для користувача бібліотеки, то я сказав би, що це найбільш пристойний варіант для нас сьогодні. Це абстракція, яка дозволяє відв’язати запис трасувань від конкретного сервісу для їх зберігання та візуалізації. Зроблено це все розумно, і якщо, наприклад, ви виставили якусь активність у своєму коді, але ніхто не зацікавлений у ній, то накладних витрат при цьому практично ніяких не буде.

Насамперед нам потрібно у своєму завданні створити активність. Для цього зручно використовувати ActivitySource. Зазвичай ActivitySource оголошується статичним полем, йому в конструктор передається ім’я джерела активності. Джерело активності нам знадобиться далі, коли ми захочемо слухати ці активності.

private static ActivitySource _source = new ActivitySource(ActivitySourceName);
//…
var activity = _source.StartActivity(“The operation name of the activity”);

Activity є IDisposable. Тому якщо ви запускаєте та зупиняється активність усередині одного методу, ви можете використовувати using:

using(var activity = _source.StartActivity(“The operation name of the activity”))
{
	// …
}

Використання using зробить код читабельнішим, плюс ця конструкція виконає за вас перевірку на null і викличе Dispose тільки в тому випадку, якщо ваша activity не null. А activity буде null у тому випадку, коли немає жодного слухача.

Активність є, але сама собою вона нікуди не потрапить. Код вище дасть можливість якомусь іншому коду у вашому додатку підписатися та отримувати повідомлення, коли активність стартує і зупиняється. Тому тепер нам потрібно зробити слухача цієї активності, який при її завершенні писатиме її в Azure. Запис будь-яких елементів телеметрії Azure ми будемо робити за допомогою TelemetryClient.

За одним з посилань вище ви можете почитати, як записувати будь-які елементи телеметрії в термінах Azure. У моєму випадку я вирішив, що мої елементи телеметрії — це Dependency. Я зробив собі такий клас:

public class ApplicationInsightsActivityToDependencyExporter: IApplicationInsightsActivityExporter
{
	private readonly TelemetryClient _telemetryClient;
	private readonly string _activitySourceName;
	private readonly Func<ApplicationInsightsActivityToDependencyExporter, string> _dependencyTypeNameGetter;
	private readonly Func<ApplicationInsightsActivityToDependencyExporter, string>? _dataGetter;

	public ApplicationInsightsActivityToDependencyExporter(
		TelemetryConfiguration telemetryConfiguration,
		string activitySourceName,
		Func<ApplicationInsightsActivityToDependencyExporter, string> dependencyTypeNameGetter,
		Func<ApplicationInsightsActivityToDependencyExporter, string> dataGetter = null!
	)
	{
		_telemetryClient = new TelemetryClient(telemetryConfiguration);

		_activitySourceName = activitySourceName;
		_dependencyTypeNameGetter = dependencyTypeNameGetter;
		_dataGetter = dataGetter;

		var listener = new ActivityListener
		{
			ShouldListenTo = activitySource => activitySource.Name == _activitySourceName,
			Sample = (ref ActivityCreationOptions<ActivityContext> _) => ActivitySamplingResult.AllData,
			ActivityStarted = ActivityStarted,
			ActivityStopped = ActivityStopped,
		};

		ActivitySource.AddActivityListener(listener);
		
	}

	protected virtual void ActivityStarted(Activity activity)
	{

	}

	protected virtual void ActivityStopped(Activity activity)
	{
		var dependencyTypeName = _dependencyTypeNameGetter(this);
		var data = _dataGetter?.Invoke(this) ?? $"Start time {activity.StartTimeUtc} - Stop time {DateTime.UtcNow}";

		DependencyTelemetry dependencyTelemetry = new DependencyTelemetry
		(
			dependencyTypeName,
			activity.Source.Name,
			activity.OperationName,
			data
		);
		
		dependencyTelemetry.Success = activity.Status != ActivityStatusCode.Error;
		dependencyTelemetry.Duration = activity.Duration;

		if (!string.IsNullOrEmpty(activity.StatusDescription))
			dependencyTelemetry.Properties["status-description"] = activity.StatusDescription;

		if (activity.IdFormat == ActivityIdFormat.W3C)
		{
			dependencyTelemetry.Id = activity.SpanId.ToHexString();
			dependencyTelemetry.Context.Operation.Id = activity.TraceId.ToHexString();
		}
		else
		{
			dependencyTelemetry.Id = activity.Id;
			dependencyTelemetry.Context.Operation.Id = activity.RootId;
		}

		dependencyTelemetry.Context.Operation.ParentId = activity.Parent?.SpanId.ToHexString();

		foreach (var keyValuePair in activity.Tags)
		{
			if (!dependencyTelemetry.Properties.ContainsKey(keyValuePair.Key))
				dependencyTelemetry.Properties[keyValuePair.Key] = keyValuePair.Value;
		}

		foreach (var keyValuePair in activity.Baggage)
		{
			if (!dependencyTelemetry.Properties.ContainsKey(keyValuePair.Key))
				dependencyTelemetry.Properties[keyValuePair.Key] = keyValuePair.Value;
		}

		dependencyTelemetry.Context.Operation.Name = activity.OperationName;

		_telemetryClient.TrackDependency(dependencyTelemetry);
	}
}

Декілька коментарів:

  • для прослуховування активності використовується клас ActivityListener;
  • обробник ActivityStarted я не використовую; я навіть не можу вигадати, для чого його можна було б використати з користю; але, напевно, є якісь сценарії, коли при старті активності треба щось своє зробити;
  • інтерфейс IApplicationInsightsActivityExporter порожній, зроблений для зручної реєстрації в DI контейнері;
  • об’єкт TelemetryConfiguration присутній у DI контейнері, тому дістати його можна звідти.

Резюме

Телеметрія — тема дуже цікава та досить об’ємна. Спочатку я хотів написати про те, як телеметрія працює і як це влаштовано зсередини .NET. Ідея ця у мене з’явилася кілька місяців тому.

Але коли я почав думати про те, що написати, то в голові була каша, бо написати можна дуже багато. Тільки на опис трасувань у мене пішло більше як місяць часу, тому що інформації багато, її складно структурувати і водночас зробити такою, щоб читач не заснув за читанням 377 сторінок.

Тож початковий план на статтю провалився, але вийшов ось цей матеріал. Якщо тема цікава й приверне увагу, може, буде якесь продовження.

👍ПодобаєтьсяСподобалось6
До обраногоВ обраному5
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Одна з речей, яка відлякує при виборі Azure App Services — логи і трасування. Особливо, коли сайт падає при запуску важко зрозуміти причину.

Одна з речей, яка відлякує при виборі Azure App Services — логи і трасування.

А що саме відлякує? Мені, наприклад, навпаки воно дуже сподобалося, особливо те, що і логи і трасування знаходяться в одному місці.

Особливо, коли сайт падає при запуску важко зрозуміти причину.

Якщо це не Azure App Services, як ви досліджуєте таки проблеми? Мені здається, що незалежно від того, де/як розміщуються додатки, однаково важко досліджувати чому додаток не запускається.

Можливо недостатньо системно користувався, щоб досягти рівня коли дуже подобаться.
Якщо хостити на Windows в IIS, помилки при старті сайта зазвичай видно в Event viewer

Якщо хостити на Windows в IIS

Зрозумів. Можливо воно й так, але навряд чи у вас буде опція вибору IIS для хостингу додатків. Хіба що ви потрапили туди, де вже вкористовується IIS. Але навіть в цьому випадку перспективи використання IIS в майбутньому під великим питанням.

користуйся контейнерами і буде тобі щастя.
А про

хостити на Windows в IIS

забудь як про страшний сон.
До речі якшо аплікейшн в ажурі не стартує там зазвичай є лінк на логі, в яких все так же можна знайти все що треба як і в Event viewer

В общем-то самое важное на learn.microsoft.com/...​elemetry-enable?tabs=net

The Azure Monitor OpenTelemetry-based Offerings for .NET, Node.js, and Python applications are currently in preview

Использовать превью-фичи в разработке — очень так себе идея.

Підписатись на коментарі