Концептуально
Цель кратко
Нужно добиться оперативного (моментального) оповещения ответственных людей об ошибках во всех instance'ах приложения. Причем для разных instance'ов должны быть разные способы доставки логов: для локального запуска программистом оповещать только его; с ПРОДа — лидов проекта, сразу их мобилизуя; с тестового сервера — ответственных за соответствующий контур.
Подробно
Использование NLog позволит настраивать способ доставки логов не в коде приложения (в C#-коде будет _logger.Info(message) или _logger.Error(exception) ), а в xml-файле конфигурации NLog.config. На уровне этого файла для разных уровней (и других нужных условий) возможно задать разные способы доставки. Основных способа четыре:
- в БД — вызывается sql-команда с заданными конфигом параметрами;
- в файл — в заданный файл дописывается строка, сформированная по заданному формату (смена файлов возможна — например, если имя файла задать датой, то каждый день будет новый файл; использование переменных возможно);
- по email — шлется письмо, сформированное по заданному шаблону;
- в консоль — актуально только для консольных приложений, строка вывода формируется также по заданному конфигом шаблону.
Есть и другие способы, в том числе можно создать собственный способ, присоединив его к приложению плагином.
Для оперативности доставки ошибок (exception'ов) в веб-приложении их лучше всего доставлять по email. Все остальные (информационные) логи можно сохранять либо в БД, либо в файл. А в случае запуска консольного приложения — все в консоль. Таким образом, при исполнении одного и того же C#-кода (C#-библиотеки), логи должны доставляться разными способами — что достигается выделением условий доставки в отдельный файл, NLog.config, который хранится в проекте запускаемого приложения.
Одна и та же запись логирования может быть доставлена несколькими способами одновременно — например, ошибки, кроме посылки по email, стоит сохранять и в основном хранилище логов.
Чтобы иметь разные настройки доставки логов для разных контуров (instance'ов приложения), нужно использовать config transformations. Нужно сделать так, и с их помощью это возможно, чтобы:
- при запуске локально (программистом на своем компьютере) письма об ошибках слались только ему (и так для каждого разработчика!), информационные логи писались в локальную БД;
- при запуске на ПРОДе письма слались другим smtp-сервером на специфическую группу рассылки, а информационные логи писались в хранилище production-логов;
- при запуске на тестовом сервере письма слались внутренним smtp-сервером, а информационные логи — в тестовое хранилище логов.
Для этого C#-исходники править не надо — достаточно иметь разные файлы NLog.config в разных запускаемых проектах и config transformations на этих файлах.
Как это сделать
Все это возможно благодаря выносу настроек логирования в config-файл (что дает нам NLog), и настройке config transformations на него.
NLog.config
Я определил target'ы:
<target name="databaseLog" dbProvider="mssql" xsi:type="Database" connectionString="Data Source=${sqlserver}; Initial Catalog=Logs;Persist Security Info=True;User ID=log_writer;Password=gfhjkm;Application Name=${src} Logger;" commandText="exec AddLog @MachineName=@machinename, @Source=@source, @SubSource=@subsource, @Level=@level, @ThreadName=@threadname, @ThreadId=@threadid, @ProcessName=@pn, @ProcessFullName=@pfn, @Msg=@message" > <parameter name="@machinename" layout="${machinename}" /> <parameter name="@source" layout="${src}" /> <parameter name="@subsource" layout="${logger}" /> <parameter name="@level" layout="${level}" /> <parameter name="@threadname" layout="${threadname}" /> <parameter name="@threadid" layout="${threadid}" /> <parameter name="@pn" layout="${processname:fullName=false}" /> <parameter name="@pfn" layout="${processname:fullName=true}" /> <parameter name="@message" layout="${message}. ${exception:format=ToString}" /> </target>
<target name="mailtargetError" xsi:type="Mail" html="false" addNewLines="true" encoding="UTF-8" subject="${src} error notification (server ${machinename}, iis ${iis-site-name})" header="Runtime error in project ${src} at server ${machinename}, iis site ${iis-site-name}. ${newline} ${newline} " body="${date:format=dd.MM.yyyy HH\:mm\:ss} Thread=${threadname}:${threadid} ${level:uppercase=true} in ${logger}: ${newline} ${newline} ${message}. ${exception:format=ToString} ${newline} ${newline} Process [${processname:fullName=true}] ${newline} (${processname:fullName=false})" to="${mails_error_reciever}" from="${mails_error_sender}" smtpAuthentication="None" smtpServer="${mails_error_smtpserver}" smtpPort="25" />
<target xsi:type="Console" name="Console" layout="Thread ${threadname}:${threadid} ${level:uppercase=true} ${logger}: ${message}. ${exception:format=ToString}" error="true" />
Правила доставки (rules) в NLog.config для веб-приложения выглядят так:
<rules> <logger name="*" minlevel="Trace" writeTo="databaseLog" /> <logger name="*" minlevel="Error" writeTo="mailtargetError"/> </rules>
Такая запись означает, что ошибки будут слаться по email и писаться в БД, а все логи ниже уровнем — только писаться в БД.
Правило доставки для консоли выглядит так:
<rules> <logger name="*" minlevel="Trace" writeTo="Console" /> </rules>
Оно означает вывод всех логов в консоль.
Чтобы можно было переопределять параметры в config transformations раздельно, можно вынести nlog-переменные:
<variable name="fileLogDir" value="${basedir}/log"/> <variable name="fileLayout" value="${date:format=dd.MM.yyyy HH\:mm\:ss} Thread=${threadname}:${threadid} ${level:uppercase=true} in ${logger}: ${message}. ${exception:format=ToString}"/> <variable name="sqlserver" value="sqlserver_logs"/> <variable name="mails_error_smtpserver" value="mail.company.com"/> <variable name="mails_error_reciever" value="konstantin.chernyaev@company.com"/> <variable name="mails_error_sender" value="project@company.com"/>
Логировать дополнительные данные
NLog позволяет выводить, кроме непосредственно данной информационной строки или exception'а, довольно много данных (см. документацию). Если же нужно сохранять особенные данные, это возможно с помощью event-properties:
LogEventInfo e = new LogEventInfo(LogLevel.Info, _logger.Name, "message"); e.Properties["userId"] = user.Id; _logger.Log(e);
Тогда в NLog.config можно использовать переменную ${event-properties:item=domainId}:
<target name="databaseLogCustom" dbProvider="mssql" xsi:type="Database" commandText="exec AddLog @UserId = @userid, ..." ... > <parameter name="@userid" layout="${event-properties:item=domainId}" /> ...
Config Transformations
1) Config transformation'ы возможны только на solution configuration. Поэтому она нужна на каждый контур (instance приложения) своя, в том числе и для каждого разработчика — своя личная. Пример личной solution configuration:

2) Выбрать ее как активную:

3) Далее нужно установить Visual Studio extension "Configuration Transform" (https://marketplace.visualstudio.com/items?itemName=GolanAvraham.ConfigurationTransform).
После установки у любого файла (!) в Solution Explorer появляются пункты "Add Config Transforms", "Preview Config Transforms":

4) Пункт "Add Config Transforms" означает добавить по файлу вида "name.configuration name.extension" для каждой конфигурации проекта этого файла (не солюшена!), и включить (nest) их под файл — ��а скриншоте они уже добавлены.
Пример — App.config, NLog.config:
<UsingTask TaskName="TransformXml" AssemblyFile="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\Web\Microsoft.Web.Publishing.Tasks.dll" /> <Target Name="App_config_AfterCompile" AfterTargets="AfterCompile" Condition="Exists('App.$(Configuration).config')"> <!--Generate transformed app config in the intermediate directory--> <TransformXml Source="App.config" Destination="$(IntermediateOutputPath)$(TargetFileName).config" Transform="App.$(Configuration).config" /> <!--Force build process to use the transformed configuration file from now on.--> <ItemGroup> <AppConfigWithTargetPath Remove="App.config" /> <AppConfigWithTargetPath Include="$(IntermediateOutputPath)$(TargetFileName).config"> <TargetPath>$(TargetFileName).config</TargetPath> </AppConfigWithTargetPath> </ItemGroup> </Target> <!--Override After Publish to support ClickOnce AfterPublish. Target replaces the untransformed config file copied to the deployment directory with the transformed one.--> <Target Name="App_config_AfterPublish" AfterTargets="AfterPublish" Condition="Exists('App.$(Configuration).config')"> <PropertyGroup> <DeployedConfig>$(_DeploymentApplicationDir)$(TargetName)$(TargetExt).config$(_DeploymentFileMappingExtension)</DeployedConfig> </PropertyGroup> <!--Publish copies the untransformed App.config to deployment directory so overwrite it--> <Copy Condition="Exists('$(DeployedConfig)')" SourceFiles="$(IntermediateOutputPath)$(TargetFileName).config" DestinationFiles="$(DeployedConfig)" /> </Target> <Target Name="NLog_config_AfterBuild" AfterTargets="AfterBuild" Condition="Exists('NLog.$(Configuration).config')"> <TransformXml Source="NLog.config" Destination="$(OutputPath)NLog.config" Transform="NLog.$(Configuration).config" /> </Target>
(TeamCity эти трансформации подхватывает, потому что они являются частью процесса билда)
При таком написании тегов не меняется исходный файл (Web.config, NLog.config), правится только результирующий файл, который кладется в папку сборки — это значит, что он не будет постоянно изменяться и каждый раз коммититься, если у разработчиков в личных solution configuration будут различные трансформации этого файла.
5 Далее в созданном файле NLog.username.config, соответствующем личной solution configuration, нужно подменить адрес получателя ошибок:
<variable name="mails_error_reciever" value="konstantin.chernyaev@company.com" xdt:Locator="Match(name)" xdt:Transform="SetAttributes"/>
А для production-контура (например):
<variable name="mails_error_reciever" value="developers@company.com" xdt:Locator="Match(name)" xdt:Transform="SetAttributes"/>
Хелп по написанию трансформаций
Исчерпывающий хелп по написанию трансформаций тут: https://msdn.microsoft.com/en-us/library/dd465326(v=vs.110).aspx
Кратко:
Чтобы заменить целый тег, нужно его пометить атрибутом xdt:Transform="Replace":
<rules xdt:Transform="Replace"> <logger name="*" minlevel="Trace" writeTo="databaseLog" /> <logger name="*" minlevel="Error" writeTo="mailtargetError"/> </rules>
Чтобы удалить целый тег, нужно его пометить атрибутом xdt:Transform="Remove":
<authorization> <deny xdt:Transform="Remove" /> </authorization>
Чтобы вставить тег, нужно его пометить атрибутом xdt:Transform="Insert":
<nlog> <targets> <target ... xdt:Transform="Insert" >
Чтобы добавить атрибуты тега, нужно его пометить атрибутом xdt:Transform="SetAttributes(список атрибутов через зпт)", добавляя и сами атрибуты:
<compilation debug="true" xdt:Transform="SetAttributes(debug)" />
Чтобы удалить атрибут тега, нужно его пометить атрибутом xdt:Transform="RemoveAttributes(список атрибутов через зпт)":
<compilation xdt:Transform="RemoveAttributes(debug)" />
Чтобы заменить значения атрибутов, нужно тег пометить атрибутами xdt:Locator="Match(name)" xdt:Transform="SetAttributes":
<connectionStrings> <add name="CS" connectionString="Data Source=dbserver;Initial Catalog=DB;Persist Security Info=True;User ID=usr;Password=psw" xdt:Transform="SetAttributes" xdt:Locator="Match(name)" />
Результат
При запуске разработчиком веб-проекта из Visual Studio письма об ошибках будут слаться только запускающему разработчику (если он сделает все, о чем написано выше), при запуске из консоли — только ему в консоль, при запуске на ПРОДе — как указано в NLog.Prod.config.
Пример
Global.asax.cs:
static readonly Logger _logger = LogManager.GetLogger("Global.asax"); protected void Application_Error(object sender, EventArgs e) { HttpApplication app = (HttpApplication)sender; Exception ex = Server.GetLastError(); _logger.Fatal(ex, $"Application_Error: {app.Context.Request.RawUrl}"); }
WebAPI-контроллер:
// поле класса: static readonly Logger _logger = LogManager.GetLogger("(имя класса)"); // в методах: try { // code } catch (SomeSoftException ex) { return BadRequest(ex.Message); } catch (Exception ex) { _logger.Error(ex); // послать email и записать в лог return base.InternalServerError(ex); }
