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


Цель кратко


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

Комментарии (12)


  1. alexs0ff
    15.05.2018 08:24

    в том числе и для каждого разработчика — своя личная

    ИМХО — большой оверхед, гораздо лучше для Develop контура использовать переменные окружения.


    1. breezemaster Автор
      15.05.2018 11:39

      Где вы рекомендуете задавать эти переменные окружения? Нужно, чтобы у каждого разработчика, и у каждого контура были свои значения.


      1. alexs0ff
        15.05.2018 11:47

        Смотря какие переменные, если тупо email тогда переменная среды пользователя. А вот для контуров (тестовый, стейдж, прод) — да, тут можно и трансформацию настроить.


        1. breezemaster Автор
          15.05.2018 11:53

          Так и все же, где и как вы рекомендуете задавать эти переменные окружения?


          1. alexs0ff
            15.05.2018 12:36

            Я же говорю — зависит от структуры ваших сред.
            Например, если есть ActiveDirectory, то каждому разработчику через групповые политики можно централизованно выставлять значения переменных окружений.
            Если же AD нет, ну тогда один раз можно и занести в ручную или через скрипты задать в целевой системе разработчика.


    1. eugenebb
      15.05.2018 17:10

      Поддержу предыдущего оратора.

      Когда количество разработчиков больше определенного числа N, то любые нестандартные решения начинают приносить больше головной боли чем пользы.

      То Шринивас из Брахмапутры забудет поменять конфигурацию и три дня подпрыгивает пытаясь понять что делать и кто виноват.

      Или новый разработчик Вася, не знает и знать не желает о трансформациях и т.п.

      А потом окажется что было бы неплохо чтобы логирование и нотификации при проблемах шли из базы единообразно с приложением. И компонент написанный на жабе тоже не прочь чтобы не требовалось дублировать поддержку подобного хозяйства.

      А в конце концов, кто-то скажет, так у нас не только логирование environment-specific, но и некоторые конфигурационные параметры (пути к файлам, url к серверам, емайлы и т.п.), давайте сделаем всё единообразно.

      Так что при достижении определенного масштаба, надо автоматизировать как можно больше и как можно меньше зависить от «волшебных» шагов при настройке и исполнении.


      1. breezemaster Автор
        15.05.2018 18:33

        Больше какого числа N?
        Для команды из 5 разработчиков никакого оверхеда.


        1. whirl
          15.05.2018 19:49

          Эммм оверхед приличный, особенно когда у всех разработчиков конфиги начинают разъезжаться


          1. breezemaster Автор
            16.05.2018 13:26

            Я вот не замечаю ну никакого оверхеда.
            А вот проблему, когда у одного разраба БД зовется по-одному, а на других контурах по-другому, видно очень хорошо.


        1. eugenebb
          15.05.2018 19:57

          N зависит от команды, распределённая или нет. Работаете одной командой или несколько. Уровня разработчиков. Количества слабо связанных компонентов в системе и т.п.

          Думаю начинается от 5-ти и где-то в районе 25-ти уже мало что спасёт, кроме жёсткой диктатуры.


  1. whirl
    15.05.2018 19:48

    А как вы все это собираете? Вы добавили в проект несколько конфигураций сборки. В данном случае у вас конкретная трансформация применяется в момент сборки проекта, но это же очевидно плохое решение. Конечный вид конфигурации должен зависеть от того в каком окружении запускается приложение, либо вобще от него не зависеть.
    В вашем случае имхо трансформация конфига должна применяться не в момент сборки, а в момент публикации.
    Так же непонятно как вы в своем решении прячете продакшн конфиг, в котором могут содержаться боевые логины/пароли и т.д. если все это зашито в релизную трансформацию, то это тоже решение не очень.

    Как выше заметили для решения таких проблем используются либо переменные окружением, либо сервисы которые в зависимости от 1 переменной окружения (dev|stage|prod) выдаст вам актуальную конфигурацию для приложения

    по поводу LogEvent недавно прикрутили в нлог structured logging, хотя я все равно поклонник Serilogа


    1. breezemaster Автор
      16.05.2018 13:21

      Обычный publish Visual Stuidio применяет трансформации.
      Поэтому для применения достаточно выбрать нужную solution configuration — либо как указано в статье, либо при создании publish-профиля.

      Вопрос сокрытия секретных данных из web.config'а темы трансформаций не касается ну вообще никак. Замечу только, что есть техники, позволяющие шифровать куски web.config'а.

      Ни для чего из этого переменные окружения не нужны.