Pull to refresh

Оповещение об ошибках: с каждого контура по-своему

Reading time8 min
Views5.1K

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


Цель кратко


Нужно добиться оперативного (моментального) оповещения ответственных людей об ошибках во всех 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);
} 
Tags:
Hubs:
Total votes 5: ↑5 and ↓0+5
Comments17

Articles