Концептуально


Цель кратко


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


databaseLog
<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>

mailtargetError
<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" />

Console
<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) их под файл — на скриншоте они уже добавлены.

При этом добавляется в файл проекта (.csproj) нужные теги.

Пример — 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);
}