Концептуально
Цель кратко
Нужно добиться оперативного (моментального) оповещения ответственных людей об ошибках во всех 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);
}