В последние годы все большую популярность у разработчиков завоевывает структурное логирование. Поэтому неудивительно, что Microsoft добавило его поддержку в пакет Microsoft.Extensions.Logging, являющийся составляющей частью .Net Core/.Net 5/.Net 6. В этой статье я хотел бы рассмотреть, как мы можем использовать структурное логирование с данным пакетом и показать идею, как мы можем расширить его с использованием новых возможностей C# 10. Как известно, Microsoft.Extensions.Logging представляет собой фасад, к которому мы можем подключать различные нижележащие провайдеры логирования. В данной статье я буду использовать Serilog в качестве такого провайдера и настрою его для вывода логов на консоль в формате JSON. В качестве примера мы будем рассматривать простое консольное приложение (ссылка на GitHub), в котором конфигурация логирования имеет следующий вид: Также, создадим две записи, которые будем использовать в дальнейших примерах: Для логирования мы будем использовать интерфейс ILogger, который предоставляет несколько перегрузок метода Log, позволяющего указать уровень логирования в качестве параметра, а также методов вида Log<уровень>, например LogInformation, LogError и т.п. Все эти методы принимают в качестве параметров строку шаблона и массив аргументов (params object[]).  Залогируем, для примера, результат вычисления длины отрезка: Строка формата имеет два аргумента, заключенных в фигурные скобки: segment и length. Значения аргументов передаются как последующие параметры метода. Символ @ перед аргументом segment является частью соглашения Message Template и означает, что Serilog должен развернуть структуру аргумента как объект. Без данного символа Serilog по умолчанию просто залогировал бы результат вызова метода segment.ToString().  В результате мы получим в логе следующую запись: Получившийся JSON-объект имеет свойство @mt, содержащее нашу строку шаблона, а также свойства segment и length, содержащие наши аргументы. Несмотря на то, что данный подход достаточно прост и прямолинеен, он имеет ряд существенных недостатков: Необходимость разбирать строку формата во время выполнения, что является достаточно затратным. Чтобы сгладить этот недостаток, инфраструктура логирования имеет кэш на 1024 строки. Необходимость выделять память под массив аргументов даже в том случае, если указанный уровень логирования отключен. Аргументы значимых типов упаковываются при помещении в массив объектов. Дублирование имен переменных в строке шаблона и в параметрах метода. Легко забыть один из параметров или перепутать их порядок. В качестве альтернативы, лишенной всех этих недостатков, .Net 6 предлагает использовать новый генератор исходного кода: мы можем объявить частичный (partial) метод, принимающий нужные нам параметры и пометить его атрибутом LoggerMessageAttribute, как показано в следующем примере: Данный подход позволяет обеспечить наивысшую производительность логирования, однако также имеет свои недостатки: Захламляет исходный код объявлениями частичных методов. Не позволяет использовать символ @ перед имена аргументов, из-за чего Serilog не разворачивает их значения в объекты JSON. Итак, нам бы хотелось найти выразительный способ писать структурные логи и по возможности избежать описанных выше недостатков. Удобным способом могло бы стать интерполирование строк. Однако для этого нам придется сначала проделать дополнительную работу. Если мы попробуем вызвать метод логирования с интерполированной строкой, например: то в логах мы получим следующую запись: Как и следовало ожидать, мы потеряли всю информацию об аргументах. Это произошло потому, что компилятор C# превратил нашу интерполированную строку в обычный вызов метода String.Format.  Вообще говоря, до C# 10, компилятор мог преобразовать интерполированную строку либо в объект типа FormattableString, либо в обычную строку. Объекты типа FormattableString уже больше похожи на то, что нам нужно для решения нашей задачи, так как содержат в себе строку форматирования и массив аргументов в неизменном виде, однако их возможностей все еще недостаточно, так как мы теряем имена аргументов. Кроме того, при выборе перегрузки метода, C# отдает предпочтение версии, принимающей строку, что также не очень удобно на практике. К счастью, в C# 10 появился новый механизм для интерполированных строк, которые позволит нам добиться того, чего мы хотим. Вкратце, мы можем создать структуру-обработчик интерполированных строк, предназначенную специально для нашей задачи: Далее мы можем использовать ее как аргумент метода. Создадим, для примера, метод расширения Log: Если теперь мы попробуем, передать интерполированную строку в наш метод Log: то компилятор превратит этот вызов в следующий код: Как мы видим, компилятор использует вызов метода AppendLiteral для добавления литеральной части строки и вызов метода AppendFormatted для аргументов, а значит мы можем написать наш обработчик так, чтобы он сформировал строку шаблона, которую мы сможем передать в оригинальный метод ILogger.Log, а также список аргументов. У вас может возникнуть вопрос, а откуда мы получим имена наших аргументов. В это нам поможет еще одна новая возможность C# 10 - CallerArgumentExpressionAttribute. Как и в случае с другими родственными Caller*-атрибутами, мы можем пометить им дополнительный строковый параметр метода AppendFormatted, и компилятор вставит в качестве значения C#-выражение, использованное при вызове метода для другого параметра этого же метода. Итак, давайте соберем все то, что мы узнали вместе и напишем код нашего обработчика. Для начала, объявим поля для хранения шаблона сообщения и его аргументов, а также свойство IsEnabled, в котором мы будем хранить значение, указывающее является ли включенным требуемый уровень логирования. Далее мы должны создать конструктор, принимающий длину строкового литерала, количество аргументов, экземпляр ILogger, уровень логирования для сообщения и выходной параметр IsEnabled. Обратите внимание, что мы создаем StringBuilder и List<object?> только в случае, если переданный уровень логирования включен, тем самым экономя ресурсы, если это не так. Также у вас мог возникнуть вопрос, откуда компилятор получает ILogger и уровень логирования. Если вы посмотрите немного выше на объявление метода Log, то увидите, что параметр обработчика помечен атрибутом  с двумя аргументами. Эти аргументы указывают имена параметров этого метода, которые содержат эти объекты. Далее, добавим метод AppendLiteral: В коде этого метода мы также экранируем фигурные скобки вызовами метода String.Replace. На самом деле мы могли бы провести экранирование в один проход, однако для простоты примера оставим этот код как есть. Наконец, добавим метод AppendFormatted: Как я и говорил ранее, он содержит параметр name со значением по-умолчанию и помеченный атрибутом . Вместо значения по умолчанию компилятор подставит C#-выражение использованное при вызове для параметра value (в простейшем случае - имя переменной). Обратите внимание, что я всегда добавляю символ @ перед именем аргумента. Насколько я вижу, Serilog нормально обрабатывает его в том числе для примитивных/встроенных типов. Впрочем, если бы это было не так, мы всегда могли бы сделать отдельные перегрузки метода AppendFormatted для таких типов. Кстати, в этом методе используется перегрузка StringBuilder.Append(ref System.Text.StringBuilder.AppendInterpolatedStringHandler handler) появившаяся в .Net 6, которая в свою очередь также использует новый механизм интерполирования строк. Наконец добавим метод, возвращающий построенный шаблон сообщения и массив аргументов: Наш обработчик готов! Полный его код вы можете найти под спойлером:  Наш метод расширения Log может использовать его следующим образом: Теперь все готово, чтобы использовать его для логирования: В результате мы получим правильное сообщение: Кстати, обратите внимание на то, что перегрузка метода, принимающая обработчик, имеет приоритет над перегрузкой, принимающей строку. В данном случае, по моему мнению, разработчики C# исправили ошибку с приоритетами, которую допустили с FormattableString в C# 6. Итак, теперь у нас есть метод расширения, позволяющий писать в лог с указанным уровнем. Нам также хотелось бы иметь методы специфичные для каждого уровня логирования, такие как LogError. Мы можем создать их по аналогии, однако наш оригинальный обработчик требует передачи уровня логирования в качестве аргумента, который отсутствует в наших методах. Чтобы решить эту проблему, нам придется создать отдельный обработчик для каждого уровня логирования. К счастью, мы можем воспользоваться композицией и переиспользовать наш StructuredLoggingInterpolatedStringHandler внутри этих обработчиков. Пример такого обработчика приведен под спойлером:   Полный код проекта (с небольшими улучшениями) можно найти на GitHub. UPD: По просьбе @vabka добавил бенчмарки. Для замера производительности воспользуемся BenchmarkDotNet. Будем использовать логирование в Serilog с пустым логгером. Результаты выглядят следующим образом: Method IsEnabled Mean Error StdDev Ratio Allocated Template and Args False 94.43 ns 0.215 ns 0.191 ns 1.00 64 B Interpolated Handler False 14.65 ns 0.059 ns 0.055 ns 0.16 - Logger Message Delegate False 14.97 ns 0.086 ns 0.080 ns 0.16 - Formattable String False 31.72 ns 0.314 ns 0.294 ns 0.34 96 B Formattable String NP False 40.53 ns 0.277 ns 0.246 ns 0.43 160 B Formattable String Anonymous False 34.67 ns 0.286 ns 0.268 ns 0.37 120 B        Template and Args True 4,048.28 ns 11.917 ns 11.147 ns 1.00 3,024 B Interpolated Handler True 4,316.35 ns 16.610 ns 15.537 ns 1.07 3,400 B Logger Message Delegate True 3,977.99 ns 11.557 ns 10.811 ns 0.98 2,984 B Formattable String True 7,239.94 ns 19.100 ns 17.866 ns 1.79 7,673 B Formattable String NP True 5,828.89 ns 23.061 ns 20.443 ns 1.44 5,672 B Formattable String Anonymous True 6,402.80 ns 22.911 ns 21.431 ns 1.58 5,832 B В этой таблице: Template and Args - обычный сырой вызов LogInformation(string, object[]). Interpolated Handler - наш код с интерполяцией. Logger Message Delegate - закэшированный делегат, созданный при помощи LoggerMessage.Define. Этот способ использует и генератор исходного кода (source generator). Formattable String * - различные способы из библиотеки https://github.com/Drizin/InterpolatedLogging, на которую любезно указал в комментариях @vabka Как мы видим, при отключенном уровне логирования, наш метод оказывается быстрее и не выделяет ненужной памяти. При включенном уровне логирования, наш метод примерно на 7-10% медленне, чем использование сырого шаблона либо закэшированного делегата, но все равно существенно оптимальнее нежели https://github.com/Drizin/InterpolatedLogging. В результате у нас получилось с помощью C# 10 добавить поддержку структурного логирования с использованием интерполированных строк и избавиться от проблем с дублированием аргументов, передачей неверного количества аргументов, а также от выделения памяти в случае, когда данный уровень логирования выключен. В коде все еще присутствуют некоторые проблемы, такие как отсутствие экранирования имен (так, Serilog требует, чтобы имена были правильными C#-идентификаторами) или неоптимальная реализация экранирования фигурных скобок. Если вы ищете готовое для использования решение, то можете обратить внимание на мою библиотеку ISLE, в которой решены все эти проблемы. https://github.com/fedarovich/isle https://habr.com/ru/post/266299/ https://andrewlock.net/exploring-dotnet-6-part-8-improving-logging-performance-with-source-generators/ https://docs.microsoft.com/en-us/dotnet/core/extensions/logger-message-generator https://docs.microsoft.com/ru-ru/dotnet/csharp/whats-new/tutorials/interpolated-string-handler https://docs.microsoft.com/ru-ru/dotnet/api/system.runtime.compilerservices.callerargumentexpressionattribute?view=net-6.0  Software Development Engineer