Ползунок windows forms c

File Logging using NLog in Windows Form Application

Today in this article, we will see how to achieve File-based logging in Windows Forms Application using .NET Core application.

As we know .NET Core has introduced ILogger as a generic interface for logging purposes. This framework supported interface ILogger can be used across different types of applications like ASP.NET Core API logging, Console App logging or Windows Form app logging using built-in providers.

Today in this article, we will cover below aspects,

  • Implement DI in Windows Forms
  • Getting started
  • DI Container
  • NLog Configuration
  • Summary

We also discovered that the File-based logging provider is still not available through the .NET Core framework and we need to rely on external solutions.

Microsoft recommends using a third-party logger framework like a Serilog or NLog for other high-end logging requirements like Database or File/Rolling File logging.

Implement DI in Windows Forms

Before we start adding NLog logging in the Windows forms app, we need to understand how to add DI framework in Windows Forms.

As we know , unlike ASP.NET Core the Windows Forms app doesn’t have dependency injection built into the framework but using few approaches like as discussed in the article Using Service provider for DI in Windows Forms App or using Generic HostBuilder for DI in Windows Forms App we can very much achieve the same.

Getting started

Here I am using a Forms/Windows .NET Core 3.1 application.

File Logging using NLog in Windows Form Application

Please add below Nuget Packages.

PM> Install-Package NLog.Extensions.Logging

PM> Install-Package Microsoft.Extensions.Hosting

DI Container

Please create Generic HosBuilder and register the dependencies that need to injected. These changes can be done in the Main() method.

Class HosBuilder is available through a namespace Microsoft.Extensions.Hosting

I have added my custom BusinessLayer and DataAccessLayer objects below and registered in the container.

var builder = new HostBuilder()
               .ConfigureServices((hostContext, services) =>
               {
                   services.AddScoped<Form1>();
                   services.AddScoped<IBusinessLayer, BusinessLayer>();
                   services.AddSingleton<IDataAccessLayer, CDataAccessLayer>();
               });

NLog Configuration

NLog configuration can be enabled as below,

 services.AddLogging(option =>
                   {
                       option.SetMinimumLevel(LogLevel.Information);
                       option.AddNLog("nlog.config");
                   });

Here nlog.cofig file contains the log properties and configuration for the logging file.

Sample nlog.config as below,

<?xml version="1.0" encoding="utf-8"?>
<configuration>

  <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        autoReload="true"
        internalLogLevel="Info">

    <!-- enable asp.net core layout renderers -->
    <extensions>
      <add assembly="NLog.Web.AspNetCore"/>
    </extensions>

    <!-- the targets to write to -->
    <targets>
      <!-- write logs to file  -->
      <target xsi:type="File" name="allfile" fileName="thecodebuzz-${shortdate}.log"
              layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
    </targets>

    <!-- rules to map from logger name to target -->
    <rules>
      <!--All logs, including from Microsoft-->
      <logger name="*" minlevel="Trace" writeTo="allfile" />
    </rules>
  </nlog>

</configuration>

Here code can be added in the Main() methods as below ,

   
            ///Generate Host Builder and Register the Services for DI
            
            var builder = new HostBuilder()
               .ConfigureServices((hostContext, services) =>
               {
                   services.AddScoped<Form1>();
                   services.AddScoped<IBusinessLayer, BusinessLayer>();
                   services.AddSingleton<IDataAccessLayer, CDataAccessLayer>();
                   services.AddLogging(option =>
                   {
                       option.SetMinimumLevel(LogLevel.Information);
                       option.AddNLog("nlog.config");
                   });
               });

            var host = builder.Build();

            using (var serviceScope = host.Services.CreateScope())
            {
                var services = serviceScope.ServiceProvider;
                try
                {
                    var form1 = services.GetRequiredService<Form1>();
                    Application.Run(form1);

                    Console.WriteLine("Success");
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error Occured");
                }
            }
       

Implementation for class ‘Form1’ with DI of Logger and business objects as below.

File Logging using NLog in Windows Form Application

Let’s run the application and check the log details captured in a file.

File Logging using NLog in Windows Form Application

That’s all! Happy Coding.

Summary

File logging provider is not yet available through the .NET Core framework. However, NLog helps us enabling logging in a few simple steps and addresses the file-logging requirement easily in .NET Core based Desktop or Windows Forms application.


Please bookmark this page and share it with your friends. Please Subscribe to the blog to receive notifications on freshly published(2024) best practices and guidelines for software design and development.


Please share this article with your friends and subscribe to the blog to get a notification on freshly published best practices of software development.

What’s more annoying than a bug in your code? Not knowing why there’s a bug in your code! I’ve worked in code bases before that have little to no logging, and it’s awful. When an exception is thrown, .NET tells us what and where, including the long chain of method calls (stack trace) all the way back to the origin. To not make a note of that somewhere is a shame.. and a waste of everyone’s time! Some people love debugging. I’m not one of them.

Even when your code is running perfectly, sometimes it’s handy to be able to log informational messages, especially during testing. Or maybe there’s no exception but something still seems «off». How convenient it is to write logs, and see what unexpected paths the system is going down!

Even in a monolithic WinForms app that has no logging, it’s possible to add it — a little at a time. Now if you’re on a team, don’t try to fix the whole app and issue the Guinness book of world records sized PR. It’s not going to garner the praise and admiration of your peers.. or management…. or customers. Just keep telling yourself things like «Rome wasn’t built in a day» and «the road to hell is paved with good intentions». 😂

Configure a tool like NLog, use it in whatever code you’re touching at the moment, and go from there!

Install NLog

There’s different kinds of tools to do this, and you could even roll your own if you’re a glutton for punishment, but NLog is a tried and true library, so we’ll just use that.

Open your solution, go to «Manage NuGet Packages», and search for NLog. You should see a few items. You could just install the first one, but I’d recommend the fourth instead (NLog.Config), which installs NLog, and a sample NLog.config file to start with, and some helpful intellisense for the config file (that’s the NLog.Schema one).

What’s the NLog.Extensions.Logging one that I glossed over? Something to do with new features in .NET Core and .NET Standard, and probably not something you’re worried about in a WinForms app.

Configure NLog

Everything’s driven off the NLog.config file, so open that up and check it out. You should see some boilerplate stuff, a few links, and intellisense if you hover over the different elements. Pretty exciting. (But then, I spent Friday evening writing this, so my judgement’s probably off.)

There are so many configuration options you can choose from, and you can see them all by visiting those links! And get overwhelmed to your heart’s content. For now though, let’s just focus on writing to a file, which is documented here.

Skip down to the section on «simple logging» and replace everything in the NLog.config file with the contents of that section. That’ll get you up and running quickly, and it’ll write log files to wherever your app is running. Maybe not ideal in production, but you can adjust that later.

  • Remove «fileName» and «keepFileOpen».
  • Replace «Debug» with «Trace» in «minLevel».
  • Skip further down in the docs to «archive old log files» and copy «fileName» and the two lines below it. Paste them into the «target» section.

You should end up with something like this:

<?xml version="1.0" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 
    <targets>
        <target name="file" xsi:type="File"
            layout="${longdate} ${logger} ${message}${exception:format=ToString}" 
            fileName="${basedir}/logs/AppLog.${shortdate}.txt" 
            maxArchiveFiles="4"
            archiveAboveSize="10240"
            encoding="utf-8" />
    </targets>
 
    <rules>
        <logger name="*" minlevel="Trace" writeTo="file" />
    </rules>
</nlog>

Those lines about archiving aren’t strictly needed for this basic demo, but I think they’re important to call out. You don’t want a log file that grows too large, so this will archive old files. It also hangs on to 4, although maybe you want to hang on to a month, or even several months. But log files from several years ago just aren’t necessary.

Use NLog

To use it, just create a new instance of the logger. Since we didn’t give it a name in the config file, it doesn’t matter what name you specify here — even an empty string works. Then write whatever you want, and check for the file in the «bin» folder where ${basedir} points to.

That’s it!

A more interesting example

NLog can do a lot more, way more than I could cover here. Before wrapping things up though, let’s try a slightly more interesting example. It’ll probably be easier if you just grab the code, but I made a couple changes in the config file — it logs everything (including trace messages), and the message will include the «level» (info, warning, etc).

<targets>
    <target name="file" xsi:type="File"
        layout="${longdate}|${level:uppercase=true}|${message} ${exception:format=ToString}${newline}" 
        fileName="${basedir}/logs/AppLog.txt" 
        maxArchiveFiles="10"
        archiveAboveSize="10240"
        archiveFileName="${basedir}/logs/archive/AppLog.{####}.txt"
        archiveNumbering="Sequence"
        encoding="utf-8" />
</targets>
 
<rules>
    <logger name="app_logger" minlevel="Trace" writeTo="file" />
</rules>

I wrote a little UI that logs a few different types of messages, and you can see the result here. If this is new to you, get the code, play around with it, change it, break it, revert the code.. lol.

The NLog wiki on GitHub is really comprehensive too — hundreds of pages on there covering everything imaginable. And if you’re interested in logging to several targets at once, check this out.

Good luck! Feel free to reach out and let me know how it goes…

Последнее обновление: 31.10.2015

TrackBar

TrackBar представляет собой элемент, который с помощью перемещения ползунка позволяет вводить числовые значения.

Некоторые важные свойства TrackBar:

  • Orientation: задает ориентацию ползунка — расположение по горизонтали или по вертикали

  • TickStyle: задает расположение делений на ползунке

  • TickFrequency: задает частоту делений на ползунке

  • Minimum: минимальное возможное значение на ползунке (по умолчанию 0)

  • Maximum: максимальное возможное значение на ползунке (по умолчанию 10)

  • Value: текущее значение ползунка. Должно находиться между Minimum и Maximum

Свойство TickStyle может принимать ряд значений:

  • None: деления отсутствуют

  • Both: деления расположены по обеим сторонам ползунка

  • BottomRight: у вертикального ползунка деления находятся справа, а у горизонтального — снизу

  • TopLeft: у вертикального ползунка деления находятся слева, а у горизонтального — сверху (применяется по умолчанию)

К наиболее важным событиям элемента следует отнести событие Scroll, которое позволяет обработать перемещение
ползунка от одного деления к другому. Что может быть полезно, если нам надо, например, устанавливать соответствующую громкость звука в
зависимости от значения ползунка, либо какике-нибудь другие настройки:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
		// установка обработчика события Scroll
        trackBar1.Scroll+=trackBar1_Scroll;
    }

    private void trackBar1_Scroll(object sender, EventArgs e)
    {
        label1.Text = String.Format("Текущее значение: {0}", trackBar1.Value);
    }
}

Timer

Timer является компонентом для запуска действий, повторяющихся через определенный промежуток времени. Хотя он не является
визуальным элементом, но его аткже можно перетащить с Панели Инструментов на форму:

Наиболее важные свойства и методы таймера:

  • Свойство Enabled: при значении true указывает, что таймер будет запускаться вместе с запуском формы

  • Свойство Interval: указывает интервал в миллисекундах, через который будет срабатывать обработчик события Tick, которое есть у таймера

  • Метод Start(): запускает таймер

  • Метод Stop(): останавливает таймер

Для примера определим простую форму, на которую добавим кнопку и таймер. В файле кода формы определим следующий код:

public partial class Form1 : Form
{
    int koef = 1;
    public Form1()
    {
        InitializeComponent();

        this.Width = 400;
        button1.Width = 40;
        button1.Left = 40;
        button1.Text = "";
        button1.BackColor = Color.Aqua;

        timer1.Interval = 500; // 500 миллисекунд
        timer1.Enabled = true;
        button1.Click += button1_Click;
        timer1.Tick += timer1_Tick;
    }
    // обработчик события Tick таймера
    void timer1_Tick(object sender, EventArgs e)
    {
        if (button1.Left == (this.Width-button1.Width-10))
        {
            koef=-1;
        }
        else if (button1.Left == 0)
        {
            koef = 1;
        }
        button1.Left += 10 *koef;
    }
    // обработчик нажатия на кнопку
    void button1_Click(object sender, EventArgs e)
    {
        if(timer1.Enabled==true)
        {
            timer1.Stop();
        }
        else
        {
            timer1.Start();
        }
    }
}

Здесь в конструкторе формы устанавливаются начальные значения для таймера, кнопки и формы.

Через каждый интервал таймера будет срабатывать обработчик timer1_Tick, в котором изменяется положение
кнопки по горизонтали с помощью свойства button1.Left. А с помощью дополнительной переменной koef
можно управлять направлением движения.

Кроме того, с помощью обраотчика нажатия кнопки button1_Click можно либо остановить таймер (и вместе с ним движение
кнопки), либо опять его запустить.

Индикатор прогресса ProgressBar

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

Наиболее важые свойства ProgressBar:

  • Minimum: минимальное возможное значение

  • Maximum: максимальное возможное значение

  • Value: текущее значение элемента

  • Step: шаг, на который изменится значение Value при вызове метода PerformStep

Для имитации работы прогрессбара поместим на форму таймер и в коде формы определим следующий код:

public partial class Form1 : Form
{        
    public Form1()
    {
        InitializeComponent();

        timer1.Interval = 500; // 500 миллисекунд
        timer1.Enabled = true;
        timer1.Tick += timer1_Tick;   
    }
    // обработчик события Tick таймера
    void timer1_Tick(object sender, EventArgs e)
    {
        progressBar1.PerformStep();
    }
}

Логирование в современных .NET-приложениях часто недооценивают, хотя именно оно спасает нас, когда всё идет наперекосяк. Помню случай, когда мы неделю искали причину странного поведения микросервиса в продакшне — и только благодаря грамотно настроенным логам удалось поймать редкую гонку условий, возникавшую раз в несколько тысяч запросов.

Основные принципы современного логирования

Эффективное логирование строится на нескольких принципах:
Контекстуальность — логи должны содержать достаточный контекст для понимания ситуации. Запись «Ошибка подключения к базе данных» малоинформативна. Гораздо полезнее знать какое именно подключение, с какими параметрами, при выполнении какой операции и с каким пользователем произошла ошибка.
Структурированность — современный подход к логированию не просто записывает текстовые сообщения, а формирует структурированные данные с метаинформацией, которые потом можно фильтровать, группировать и анализировать.
Избирательность — нет смысла логировать абсолютно все, это создает информационный шум и снижает производительность. Следует четко определять, какая информация действительно важна.
Уровни критичности — разделение логов по уровням позволяет гибко настраивать вывод в зависимости от среды (dev, staging, production) и быстро находить важные сообщения.
Централизация — в распределенных системах логи должны быть собраны в одном месте с возможностью корреляции событий между разными компонентами.

Вывод диалога исключения в лог
Привет! у меня следующий вопрос — возможно ли вывести текст из диалога исключения например в лог? Если можно то как?

Как программно записывать лог в RichTextBox
Как программно записывать совершенные программой действия (лог) в RichTextBox. что-то типа того, как на рисунке: . Посоветуйте, как лучше всего это…

Лог вызовов для unity3d
Есть желание иметь в своем проекте лог вызовов для отладки. Пробовал использовать log4net с patternLayout = &quot;… %M&quot;, но скорость подобного…

Regex для лог файла веб-сервера
Есть две строки (которые повторяются 10 000 раз) но дело не в том
//string string1 = @&quot;2010-06-25 20:58:11 fe80::a4a1:7534:68bd:251%10 POST…

Уровни логирования в контексте .NET

Фреймворк .NET предлагает стандартные уровни логирования, которыми стоит руководствоваться. Правильное использование этих уровней значительно облегчает последующий анализ:

Trace — предельно подробные сообщения для диагностики, используемые только в процессе отладки и практически никогда не включаемые в продакшн.

C#
1
_logger.LogTrace("Начинаю обработку запроса платежа: {Amount} {Currency}", amount, currency);

Debug — диагностическая информация, полезная разработчикам. Также обычно отключается в производственной среде.

C#
1
_logger.LogDebug("Пользователь {UserId} запросил {ItemCount} товаров из БД", userId, itemCount);

Information — штатные события приложения, которые стоит отслеживать даже в продакшне: запуск сервисов, успешное выполнение важных бизнес-операций, плановые действия.

C#
1
_logger.LogInformation("Приложение успешно запущено в {Environment} среде", environment);

Warning — потенциально проблемные ситуации, которые не нарушают работу, но могут привести к ошибкам: приближение к лимитам ресурсов, необработанные ветки кода, устаревшие API.

C#
1
_logger.LogWarning("Заканчивается место на диске: осталось {AvailableSpace}MB", availableSpace);

Error — ошибки, которые нарушают текущую операцию, но не весь процесс работы.

C#
1
2
3
4
5
6
7
8
try
{
    var result = ProcessOrder(orderId);
}
catch (Exception ex)
{
    _logger.LogError(ex, "Не удалось обработать заказ {OrderId}", orderId);
}

Critical — критические ошибки, которые могут привести к аварийному завершению работы приложения.

C#
1
2
3
4
5
6
7
8
9
try
{
    var connection = ConnectToPaymentGateway();
}
catch (Exception ex)
{
    _logger.LogCritical(ex, "Критическая ошибка подключения к платежному шлюзу. Завершаю работу сервиса");
    Environment.Exit(1);
}

Многие команды используют уровни неправильно, злоупотребляя Warning (когда на самом деле ситуация требует Error) или, наоборот, помечая все ошибки как Critical, что вызывает «усталость от алертов» и в конечном итоге снижает эффективность мониторинга.

Встроенные возможности .NET для логирования

Начиная с .NET Core, экосистема получила мощный встроенный механизм логирования — Microsoft.Extensions.Logging. Эта абстракция позволяет единообразно настраивать разные провайдеры логов (консоль, файлы, базы данных) и интегрировать их с DI-контейнером. Базовая настройка выглядит так:

C#
1
2
3
4
5
6
7
8
9
10
11
var builder = WebApplication.CreateBuilder(args);
 
// Настройка логирования
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddDebug();
 
// Настройка минимального уровня логов для разных категорий
builder.Logging.SetMinimumLevel(LogLevel.Information);
builder.Logging.AddFilter("Microsoft", LogLevel.Warning);
builder.Logging.AddFilter("System", LogLevel.Warning);

Интерфейс ILogger<T> из этого пространства имен стал стандартом для логирования в .NET. Пример использования в контроллере:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class ProductsController : ControllerBase
{
    private readonly ILogger<ProductsController> _logger;
    
    public ProductsController(ILogger<ProductsController> logger)
    {
        _logger = logger;
    }
    
    [HttpGet("{id}")]
    public async Task<IActionResult> GetProduct(int id)
    {
        _logger.LogInformation("Получен запрос продукта с ID: {ProductId}", id);
        
        try
        {
            var product = await _productService.GetProductAsync(id);
            if (product == null)
            {
                _logger.LogWarning("Продукт {ProductId} не найден", id);
                return NotFound();
            }
            
            return Ok(product);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при получении продукта {ProductId}", id);
            return StatusCode(500);
        }
    }
}

Важным преимуществом встроенного механизма логирования является его масштабируемость и расширяемость. Вы можете начать с простой консольной конфигурации, а затем, когда приложение вырастет, добавить более сложные провайдеры без изменения кода логирования.
Ещё одна полезная концепция — категории логов. Когда вы используете ILogger<T>, имя типа T автоматически становится категорией. Это позволяет фильтровать логи по компонентам системы и устанавливать для них разные уровни детализации.

C#
1
2
3
4
// Устанавливаем разные уровни для разных компонентов
builder.Logging.AddFilter("MyApp.Controllers", LogLevel.Information);
builder.Logging.AddFilter("MyApp.Services", LogLevel.Debug);
builder.Logging.AddFilter("MyApp.Data", LogLevel.Warning);

Особенно удобно, что этим можно управлять через конфигурацию в appsettings.json:

JSON
1
2
3
4
5
6
7
8
9
10
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information",
      "MyApp.Controllers.PaymentsController": "Debug"
    }
  }
}

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

Особенности логирования в асинхронных операциях

Асинхронное программирование стало стандартом в .NET, особенно в веб-приложениях и микросервисах. Однако оно создаёт определённые сложности для системы логирования. Основная проблема — сохранение контекста между асинхронными операциями. В многопоточной среде вызовы часто переключаются между разными потоками, что может привести к потере связи между логами, относящимися к одному бизнес-процессу. Представьте: пользователь нажал кнопку, запустив цепочку из десятка асинхронных операций. Как отследить весь этот путь в логах? Решением стал механизм AsyncLocal<T> и контексты логирования. Вот пример применения этой концепции:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class LoggingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger<LoggingMiddleware> _logger;
 
    public LoggingMiddleware(RequestDelegate next, ILogger<LoggingMiddleware> logger)
    {
        _next = next;
        _logger = logger;
    }
 
    public async Task InvokeAsync(HttpContext context)
    {
        // Создаем уникальный идентификатор запроса
        var correlationId = context.Request.Headers["X-Correlation-ID"].FirstOrDefault() 
            ?? Guid.NewGuid().ToString();
            
        // Добавляем его в контекст логирования
        using (LogContext.PushProperty("CorrelationId", correlationId))
        {
            // Записываем начало запроса
            _logger.LogInformation("HTTP {Method} {Path} началась обработка", 
                context.Request.Method, context.Request.Path);
            
            // Сохраняем время начала для измерения длительности
            var sw = Stopwatch.StartNew();
            
            try
            {
                // Вызываем следующий middleware в цепочке
                await _next(context);
                
                // Логируем успешное завершение
                _logger.LogInformation("HTTP {StatusCode} {Method} {Path} обработан за {ElapsedMs}ms", 
                    context.Response.StatusCode,
                    context.Request.Method,
                    context.Request.Path,
                    sw.ElapsedMilliseconds);
            }
            catch (Exception ex)
            {
                // Логируем исключение сохраняя тот же correlationId
                _logger.LogError(ex, "HTTP {Method} {Path} вызвал необработанное исключение", 
                    context.Request.Method, context.Request.Path);
                throw;
            }
        }
    }
}

В этом примере мы создаём идентификатор корреляции и добавляем его в контекст логирования. Теперь все логи, созданные внутри этого контекста (даже из других потоков), будут содержать один и тот же correlationId, что позволит легко отследить путь обработки запроса.
Для многопоточных приложений также важно убедиться, что ваш логгер поддерживает потокобезопасность. Большинство современных фреймворков логирования в .NET (Serilog, NLog) имеют встроенную поддержку многопоточности, но при создании собственных решений это нужно учитывать.

Контекстное логирование: когда и какую информацию фиксировать

Контекстное логирование — это подход, при котором логи обогащаются дополнительной информацией о контексте выполнения операций. Это позволяет отвечать не только на вопрос «что произошло?», но и «при каких условиях?», «с какими данными?» и «кто инициировал?».

В .NET можно выделить несколько типов контекстной информации:
1. Информация о запросе — HTTP-метод, путь, IP-адрес, User-Agent
2. Информация о пользователе — идентификатор, роль, иногда email или имя
3. Бизнес-контекст — идентификаторы бизнес-сущностей (заказ, клиент, продукт)
4. Технический контекст — версия приложения, имя сервера, окружение
5. Временные характеристики — длительность операции, временные метки

Пример обогащения логов контекстом с помощью Serilog:

C#
1
2
3
4
5
6
7
8
9
// Глобальное обогащение всех логов техническим контекстом
Log.Logger = new LoggerConfiguration()
    .Enrich.WithMachineName()
    .Enrich.WithEnvironmentName()
    .Enrich.WithProperty("Application", "OrderService")
    .Enrich.WithProperty("Version", typeof(Program).Assembly.GetName().Version.ToString())
    .WriteTo.Console(outputTemplate: 
        "{Timestamp:HH:mm:ss} [{Level:u3}] [{Application}/{Version}] {Message}{NewLine}{Exception}")
    .CreateLogger();

А вот пример обогащения логов бизнес-контекстом в рамках конкретного метода:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public async Task<OrderResult> ProcessOrderAsync(Order order, CancellationToken cancellationToken)
{
    // Начинаем логический блок с обогащением всех логов внутри него
    using (LogContext.PushProperty("OrderId", order.Id))
    using (LogContext.PushProperty("CustomerId", order.CustomerId))
    using (LogContext.PushProperty("TotalAmount", order.TotalAmount))
    {
        _logger.LogInformation("Начата обработка заказа");
        
        try
        {
            // Проверка наличия товаров
            var stockResult = await _stockService.CheckAvailabilityAsync(order.Items);
            if (!stockResult.AllAvailable)
            {
                _logger.LogWarning("Недостаточно товара на складе: {MissingItems}", 
                    stockResult.MissingItems);
                return OrderResult.Failed("Недостаточно товара");
            }
            
            // Резервирование товаров
            await _stockService.ReserveItemsAsync(order.Items);
            
            // Обработка платежа
            var paymentResult = await _paymentService.ProcessPaymentAsync(
                order.CustomerId, order.TotalAmount, cancellationToken);
                
            if (!paymentResult.Success)
            {
                _logger.LogWarning("Ошибка платежа: {PaymentErrorCode} {PaymentErrorMessage}", 
                    paymentResult.ErrorCode, paymentResult.ErrorMessage);
                return OrderResult.Failed(paymentResult.ErrorMessage);
            }
            
            // Финализация заказа
            await _orderRepository.UpdateStatusAsync(order.Id, OrderStatus.Paid);
            
            _logger.LogInformation("Заказ успешно обработан");
            return OrderResult.Success();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Ошибка при обработке заказа");
            return OrderResult.Failed("Внутренняя ошибка системы");
        }
    }
}

Один из интересных приемов — использование логических областей (scopes) для группировки логов. В Microsoft.Extensions.Logging это выглядит так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public async Task ProcessBatchAsync(string batchId, IEnumerable<Item> items)
{
    // Создаем логическую область для всех логов внутри этого метода
    using (_logger.BeginScope("Batch {BatchId} processing", batchId))
    {
        _logger.LogInformation("Начата обработка пакета с {ItemCount} элементами", items.Count());
        
        foreach (var item in items)
        {
            // Создаем вложенную область для конкретного элемента
            using (_logger.BeginScope("Item {ItemId}", item.Id))
            {
                _logger.LogDebug("Обработка элемента");
                // ...обработка...
                _logger.LogDebug("Элемент обработан");
            }
        }
        
        _logger.LogInformation("Пакет обработан");
    }
}

Этот подход особенно полезен при отладке сложных бизнес-процессов, так как позволяет быстро визуализировать иерархию вызовов и отношения между разными операциями.

Популярные библиотеки и фреймворки

В экосистеме .NET существует несколько зрелых решений для логирования, каждое со своими особенностями и преимуществами. Выбор конкретного инструмента может существенно повлиять на удобство разработки, производительность и возможности мониторинга приложения.

Serilog: мощь структурированного логирования

Serilog стал одним из самых популярных решений благодаря проработанному API и мощной поддержке структурированного логирования. Его главное преимущество — это «sinks» (приемники), позволяющие направлять логи в различные хранилища: от обычных файлов до специализированных систем анализа логов.
Настройка Serilog обычно выглядит так:

C#
1
2
3
4
5
6
7
8
9
10
11
var logger = new LoggerConfiguration()
    .MinimumLevel.Information()
    .WriteTo.Console(new JsonFormatter())
    .WriteTo.File("logs/app-.log", rollingInterval: RollingInterval.Day)
    .WriteTo.Seq("http://seq-server:5341")
    .Enrich.WithProperty("Application", "MyService")
    .Enrich.WithMachineName()
    .CreateLogger();
 
// Регистрация в контейнере .NET
builder.Logging.AddSerilog(logger);

Главная особенность Serilog — шаблоны сообщений с именованными плейсхолдерами:

C#
1
2
logger.Information("Пользователь {UserId} совершил платеж на сумму {Amount} {Currency}", 
    userId, amount, "USD");

Эти значения не просто подставляются в строку, но и сохраняются как отдельные свойства в структурированном логе. Позже это позволяет делать поиск по конкретным полям, например, найти все платежи с суммой больше определенного значения. Serilog также предлагает много дополнительных пакетов: десятки разных приемников (от Amazon CloudWatch до Elasticsearch), обогатителей контекста и настраиваемые форматировщики.

NLog: гибкость и производительность

NLog — другой популярный фреймворк, существующий в экосистеме .NET намного дольше других. Его главные преимущества — детальная настройка через конфигурационные файлы и высокая производительность. Типичная настройка NLog через nlog.config:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  
  <targets>
    <target name="file" xsi:type="File" 
            fileName="${basedir}/logs/${shortdate}.log"
            layout="${longdate} | ${level:uppercase=true} | ${logger} | ${message} ${exception:format=ToString}" />
    
    <target name="database" xsi:type="Database"
            connectionString="${appsettings:name=ConnectionStrings.LogDb}"
            commandText="INSERT INTO Logs(Date,Level,Logger,Message,Exception) VALUES(@date,@level,@logger,@message,@exception)">
      <parameter name="@date" layout="${date}" />
      <parameter name="@level" layout="${level}" />
      <parameter name="@logger" layout="${logger}" />
      <parameter name="@message" layout="${message}" />
      <parameter name="@exception" layout="${exception:format=ToString}" />
    </target>
  </targets>
  
  <rules>
    <logger name="*" minlevel="Info" writeTo="file" />
    <logger name="*" minlevel="Error" writeTo="database" />
  </rules>
</nlog>

Особенно примечательна возможность настройки сложной маршрутизации логов в NLog. Вы можете создавать правила, отправляющие определённые типы сообщений в разные приёмники в зависимости от категории, уровня или даже содержимого сообщения.

В коде интеграция c ASP.NET Core выглядит следующим образом:

C#
1
2
builder.Logging.ClearProviders();
builder.Host.UseNLog();

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

Интересный паттерн использования — комбинация NLog с другими инструментами мониторинга:

C#
1
2
3
4
5
6
7
8
9
10
// Специальный маркер для начала важной операции
logger.Info("BEGIN_OPERATION: ProcessPayment");
 
try {
    // Бизнес-логика...
}
finally {
    // Маркер окончания операции с метрикой времени
    logger.Info("END_OPERATION: ProcessPayment | Duration: {0}ms", sw.ElapsedMilliseconds);
}

Парсеры логов могут считывать такие маркеры и автоматически строить метрики производительности даже без специальных APM-решений.

Log4net: проверенный временем стандарт

Log4net — порт классического Java-фреймворка, который всё ещё используется во многих .NET-проектах, особенно legacy-системах. Он был стандартом до появления более современных альтернатив. Несмотря на возраст, log4net предлагает гибкую систему аппендеров (аналог sinks в Serilog), иерархическую систему логгеров и возможность перезагрузки конфигурации во время работы приложения.
Настройка log4net через XML-конфигурацию:

XML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<log4net>
  <appender name="RollingFile" type="log4net.Appender.RollingFileAppender">
    <file value="logs/application.log" />
    <appendToFile value="true" />
    <rollingStyle value="Composite" />
    <datePattern value="yyyyMMdd" />
    <maxSizeRollBackups value="10" />
    <maximumFileSize value="10MB" />
    <layout type="log4net.Layout.PatternLayout">
      <conversionPattern value="%date [%thread] %-5level %logger - %message%newline" />
    </layout>
  </appender>
  <root>
    <level value="INFO" />
    <appender-ref ref="RollingFile" />
  </root>
</log4net>

Использование log4net в коде:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
[assembly: log4net.Config.XmlConfigurator(ConfigFile = "log4net.config", Watch = true)]
 
public class PaymentService
{
    private static readonly ILog Logger = LogManager.GetLogger(typeof(PaymentService));
    
    public void ProcessPayment(decimal amount)
    {
        Logger.Info($"Обработка платежа на сумму {amount}");
        
        try
        {
            // Бизнес-логика...
        }
        catch (Exception ex)
        {
            Logger.Error("Ошибка обработки платежа", ex);
            throw;
        }
    }
}

Log4net сегодня не лучший выбор для нового проекта, но для поддержки существующих систем он вполне адекватен. Многие команды успешно мигрируют с log4net на Serilog или NLog, часто это можно сделать с минимальными изменениями в коде благодаря совместимости с Microsoft.Extensions.Logging.

Microsoft.Extensions.Logging: встроенное решение

Как уже упоминалось ранее, в .NET Core и более новых версиях фреймворка Microsoft представила свою абстракцию для логирования. Она не является полноценным фреймворком, скорее это интерфейс, под который можно подключать разные провайдеры, включая Serilog, NLog и другие. Ценность этого подхода в стандартизации — все библиотеки и фреймворки .NET используют этот интерфейс, что упрощает их интеграцию в ваш проект:

C#
1
2
3
4
5
6
// Настройка встроенного логирования для файлов
builder.Logging.AddFile("Logs/app-{Date}.log");
 
// А также можно подключить любой другой провайдер
builder.Logging.AddEventLog(); // Для Windows Event Log
builder.Logging.AddApplicationInsights(); // Для Azure Application Insights

Интересно, что Microsoft.Extensions.Logging не имеет нативной поддержки структурированного логирования, но может использовать его через провайдеры вроде Serilog:

C#
1
2
3
4
5
6
7
8
// Интеграция с Serilog через встроенное логирование
builder.Host.UseSerilog((context, services, logConfig) => logConfig
    .ReadFrom.Configuration(context.Configuration)
    .ReadFrom.Services(services)
    .Enrich.FromLogContext()
    .WriteTo.Console()
    .WriteTo.Seq("http://seq-server:5341")
);

Преимущество Microsoft.Extensions.Logging в том, что вы можете начать с базовой функциональности, а по мере роста проекта подключать более продвинутые провайдеры без изменения кода логирования.

Производительность и бенчмарки

Производительность систем логирования часто недооценивают, но в высоконагруженных приложениях это может стать узким местом. Особенно критичны сценарии, когда приложение генерирует тысячи логов в секунду. Согласно независимым бенчмаркам, NLog обычно показывает наилучшую производительность при синхронном логировании, а Serilog лидирует при асинхронном подходе. Вот примерные показатели из одного из недавних тестов:

NLog (синхронно): ~1.2 млн операций/сек
Serilog (синхронно): ~0.9 млн операций/сек
Log4net: ~0.6 млн операций/сек

Однако эти цифры сильно зависят от конфигурации, типа приёмников и формата логов. Структурированное логирование обычно требует больше ресурсов, чем простое текстовое. При включении асинхронной записи все эти фреймворки могут обрабатывать десятки миллионов сообщений в секунду, перемещая запись на диск в фоновый поток:

C#
1
2
3
4
5
6
7
8
9
// Асинхронная запись в Serilog
var logger = new LoggerConfiguration()
    .WriteTo.Async(a => a.File("logs/app.log"))
    .CreateLogger();
 
// Асинхронная запись в NLog
<target name="asyncFile" xsi:type="AsyncWrapper">
    <target name="file" xsi:type="File" fileName="logs/app.log" />
</target>

Для оптимизации производительности логирования существуют дополнительные техники. Например, фильтрация сообщений на ранних этапах:

C#
1
2
3
4
5
6
// Проверка уровня перед созданием сложных сообщений
if (logger.IsEnabled(LogLevel.Debug))
{
    logger.LogDebug("Детальная информация о пользователе {User}: {Details}", 
        username, JsonSerializer.Serialize(complexUserObject));
}

Такой подход помогает избежать лишних операций, когда данный уровень логирования отключен.

Интеграция с облачными сервисами мониторинга

Современные приложения редко существуют в вакууме. Большинство .NET-систем интегрируются с облачными платформами мониторинга для улучшения наблюдаемости. Популярные варианты включают:

  • Azure Application Insights
  • AWS CloudWatch
  • DataDog
  • New Relic
  • Elasticsearch + Kibana (ELK stack)
  • Grafana + Loki

Serilog особенно хорошо подходит для интеграции с облачными сервисами благодаря структурированности логов и наличию готовых приёмников. Например, интеграция с Azure Application Insights:

C#
1
2
3
4
5
6
// Настройка Serilog для отправки логов в Application Insights
var logger = new LoggerConfiguration()
    .WriteTo.ApplicationInsights(
        telemetryConfiguration,
        TelemetryConverter.Traces)
    .CreateLogger();

Для ELK-стека, который часто используется в контейнеризированных средах:

C#
1
2
3
4
5
6
7
// Отправка логов напрямую в Elasticsearch
logger.WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://elasticsearch:9200"))
{
    IndexFormat = "myapp-logs-{0:yyyy.MM}",
    AutoRegisterTemplate = true,
    FailureCallback = e => Console.Error.WriteLine($"Не удалось отправить событие в Elasticsearch: {e.Exception}")
});

При отправке логов в облачные сервисы стоит учитывать дополнительные факторы:
1. Буферизация — накапливание логов перед отправкой для уменьшения числа запросов
2. Политика повторных попыток — как поступать при недоступности сервиса
3. Локальный резервный вывод — куда писать логи, если облако недоступно
4. Регулирование трафика — предотвращение чрезмерной нагрузки на сеть

Пример реализации с буферизацией и локальным резервным выводом:

C#
1
2
3
4
5
6
7
8
var logger = new LoggerConfiguration()
    .WriteTo.Logger(lc => lc
        .WriteTo.Async(a => a
            .Elasticsearch(elasticsearchOptions)
            .WriteTo.File("logs/failed-to-send.log", restrictedToMinimumLevel: LogEventLevel.Error))
        .WriteTo.BufferedSink(buffer)
    )
    .CreateLogger();

Выбор оптимального решения для логирования

Выбор между Serilog, NLog, log4net и другими решениями зависит от множества факторов:

Тип приложения:
Микросервисы и контейнеризированные приложения — Serilog
Монолитные enterprise-приложения — NLog или Serilog
Legacy-системы — log4net или переход на NLog

Требования к производительности:
Критическая производительность — NLog с асинхронными обертками
Высоконагруженные системы с аналитикой логов — Serilog
Стандартные веб-приложения — подходит любой, включая Microsoft.Extensions.Logging

Аналитические возможности:
Продвинутая аналитика и поиск — Serilog с Elasticsearch или Seq
Базовый мониторинг — NLog с построением графиков
Интеграция с APM — любой через Microsoft.Extensions.Logging

Экосистема:
Azure — Microsoft.Extensions.Logging + ApplicationInsights
AWS — Serilog с CloudWatch sink
Kubernetes — Serilog с Elasticsearch или Grafana Loki

Из своей практики могу отметить, что Serilog стал фактическим стандартом в новых .NET-проектах благодаря отличной поддержке структурированного логирования и богатой экосистеме. NLog остается хорошим выбором для высоконагруженных систем с простыми требованиями. Microsoft.Extensions.Logging же лучше всего подходит для стандартных ASP.NET Core приложений без специфических требований.

Критерий, который часто упускают из виду — удобство для команды разработки. Простота конфигурации и понятное API могут существенно уменьшить количество ошибок логирования, особенно в больших командах.
Практический совет: начинайте с Microsoft.Extensions.Logging со стандартными провайдерами, а по мере роста проекта подключайте специализированные решения через соответствующие адаптеры:

C#
1
2
3
4
5
6
7
8
// Базовый подход
builder.Logging.AddConsole();
builder.Logging.AddDebug();
 
// При росте требований — добавление Serilog
builder.Host.UseSerilog((ctx, lc) => lc
    .ReadFrom.Configuration(ctx.Configuration)
    .Enrich.FromLogContext());

Такой эволюционный подход позволяет гибко адаптироваться к изменяющимся требованиям без необходимости переписывать код логирования.
Ещё одна рекомендация — использовать абстракцию Microsoft.Extensions.Logging в публичном API вашей библиотеки, даже если внутренне вы применяете другое решение:

C#
1
2
3
4
5
6
7
8
9
10
11
public class PaymentProcessor
{
    private readonly ILogger<PaymentProcessor> _logger;
    
    public PaymentProcessor(ILogger<PaymentProcessor> logger)
    {
        _logger = logger;
    }
    
    // Методы с использованием _logger
}

Это обеспечит совместимость с разными системами логирования у потребителей вашего API.
Существует интересное гибридное решение — использование разных систем логирования для разных целей в одном приложении:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Быстрые диагностические логи с NLog
var diagnosticLogger = LogManager.GetLogger("Diagnostics");
 
// Структурированные бизнес-логи с Serilog
var businessLogger = Log.ForContext<OrderService>();
 
public void ProcessOrder(Order order)
{
    diagnosticLogger.Trace("Начало ProcessOrder");
    
    businessLogger.Information("Обработка заказа {OrderId} от клиента {CustomerId}", 
        order.Id, order.CustomerId);
    
    // Бизнес-логика...
    
    diagnosticLogger.Trace("Конец ProcessOrder");
}

Я видел такой подход в высоконагруженных системах, где технические логи требуют максимальной производительности, а бизнес-логи — лучшей структурированности и анализируемости.
Если ваше приложение работает в контейнерной среде вроде Kubernetes, есть дополнительные нюансы — например, все контейнеры в кластере обычно настроены на запись логов в stdout/stderr вместо файлов, а специальные агенты собирают эти логи и отправляют в центральное хранилище:

C#
1
2
3
4
// Настройка для контейнерных сред
var logger = new LoggerConfiguration()
    .WriteTo.Console(new CompactJsonFormatter())
    .CreateLogger();

В таком случае важно, чтобы логи были в формате JSON для последующего парсинга и индексации. Популярная инфраструктура логирования в Kubernetes включает Fluentd или Fluent Bit для сбора логов, Elasticsearch для хранения и Kibana для визуализации.

Наконец, не забывайте о безопасности логов — они могут содержать чувствительную информацию. Все современные библиотеки логирования предлагают механизмы маскирования или исключения конфиденциальных данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Маскирование чувствительных данных в Serilog
Log.Logger = new LoggerConfiguration()
    .Destructure.ByTransforming<CreditCard>(card => 
        new { MaskedNumber = $"XXXX-XXXX-XXXX-{card.Number.Substring(card.Number.Length - 4)}" })
    .CreateLogger();
 
// Маскирование в NLog
<targets>
  <target name="file" xsi:type="File">
    <layout xsi:type="JsonLayout">
      <attribute name="password" layout="${event-properties:password}" encode="false">
        <layout type="System.String" value="[MASKED]" />
      </attribute>
    </layout>
  </target>
</targets>

Структурированное логирование

Традиционный подход к логированию, основанный на текстовых сообщениях, постепенно уступает место структурированному логированию. Это существенный сдвиг парадигмы — от простых строк текста к набору структурированных данных с богатым контекстом.

Концепция и преимущества структурированного подхода

Структурированное логирование рассматривает каждую запись лога не как простой текст, а как объект с набором свойств. В отличие от обычного текстового сообщения:

C#
1
2024-02-18 15:30:45.123 ERROR - Ошибка при обработке заказа: Нехватка товара на складе

Структурированный лог представляет собой набор именованных полей:

JSON
1
2
3
4
5
6
7
8
9
10
11
{
  "Timestamp": "2024-02-18T15:30:45.123Z",
  "Level": "Error",
  "Message": "Ошибка при обработке заказа",
  "OrderId": "ORD-12345",
  "CustomerId": "CUST-789",
  "ErrorCode": "STOCK_SHORTAGE",
  "AvailableQuantity": 5,
  "RequestedQuantity": 10,
  "ProductId": "PROD-456" 
}

Такой подход даёт ряд критических преимуществ:
1. Улучшенная аналитика — возможность фильтровать, группировать и агрегировать логи по конкретным полям.
2. Контекстуальность — каждая запись содержит исчерпывающую информацию о событии.
3. Машинная обработка — логи можно эффективно анализировать автоматическими системами.
4. Гибкость представления — один и тот же лог может быть отображен по-разному в зависимости от контекста (как JSON, XML, таблица).

На практике это означает возможность делать запросы типа «покажи все ошибки для клиента CUST-789» или «найди все заказы с ошибкой нехватки товара за последний час» без парсинга текстовых строк. Пример структурированного логирования с Serilog:

C#
1
2
3
4
5
logger.Error(
    "Ошибка при обработке заказа {@Order} из-за нехватки товара {ProductId}",
    order,
    productId
);

Обратите внимание на использование {@Order} вместо {Order}. Символ @ указывает Serilog, что нужно сериализовать объект order как структурированное свойство, а не просто вызвать его метод ToString().

Реализация с использованием современных инструментов

В .NET экосистеме Serilog является флагманом структурированного логирования, но и другие библиотеки тоже поддерживают этот подход:

Serilog:

C#
1
2
3
4
5
6
7
Log.Logger = new LoggerConfiguration()
  .WriteTo.Console(new JsonFormatter())
  .WriteTo.Seq("http://seq-server:5341")
  .CreateLogger();
 
Log.Information("Пользователь {UserId} приобрел {ProductName} за {Amount:N2} {Currency}", 
  "user123", "Смартфон XYZ", 599.99, "USD");

NLog:

C#
1
2
3
var logger = LogManager.GetCurrentClassLogger();
logger.Info("Пользователь {UserId} приобрел {ProductName} за {Amount:N2} {Currency}",
  "user123", "Смартфон XYZ", 599.99, "USD");

Microsoft.Extensions.Logging с Serilog:

C#
1
2
3
var logger = factory.CreateLogger<Program>();
logger.LogInformation("Пользователь {UserId} приобрел {ProductName} за {Amount:N2} {Currency}", 
  "user123", "Смартфон XYZ", 599.99, "USD");

Важно отметить, что не все приёмники логов одинаково поддерживают структурированный формат. Например, консольный вывод по умолчанию может отображать только текстовое сообщение, игнорируя структуру. Для полноценного использования преимуществ структурированного логирования необходимо использовать специализированные хранилища и инструменты анализа, такие как:
Seq — специализированный сервер для хранения и анализа структурированных логов
Elasticsearch + Kibana — мощный стек для индексации и визуализации
Splunk — enterprise-решение для анализа логов и мониторинга
Application Insights — облачное решение от Microsoft

При этом в промежуточном хранилище (например, файлы) структурированные логи обычно хранятся в JSON-формате:

C#
1
2
3
var logger = new LoggerConfiguration()
  .WriteTo.File(new JsonFormatter(), "logs/app.json")
  .CreateLogger();

Семантическое логирование: возможности и ограничения

Семантическое логирование — эволюция структурированного подхода, где акцент делается на значении события, а не просто на его структуре. Термин был популяризирован Microsoft с их библиотекой Semantic Logging Application Block (SLAB), хотя сама концепция шире.
Основное отличие семантического подхода — это определение конкретных типов событий, а не просто сообщений с контекстом:

C#
1
2
3
4
5
// Структурированное логирование
logger.Information("Пользователь {UserId} вошел в систему с IP {IpAddress}", userId, ipAddress);
 
// Семантический подход
logger.LogUserLoggedIn(userId, ipAddress);

Где LogUserLoggedIn — специализированный метод для конкретного типа события:

C#
1
2
3
4
5
6
7
8
9
10
public static class LoggerExtensions
{
  private static readonly EventId UserLoggedInEventId = new EventId(1000, "UserLoggedIn");
  
  public static void LogUserLoggedIn(this ILogger logger, string userId, string ipAddress)
  {
      logger.LogInformation(UserLoggedInEventId, 
          "Пользователь {UserId} вошел в систему с IP {IpAddress}", userId, ipAddress);
  }
}

Преимущество такого подхода в том, что типы событий становятся частью вашего API. Это обеспечивает лучшую расширяемость, консистентность и позволяет инструментам строить более точные метрики. Однако у семантического подхода есть и ограничения:

  • Требуется больше начального кодирования для определения всех типов событий.
  • Может усложнить поддержку при частых изменениях в требованиях к логированию.
  • Нужна дисциплина со стороны команды, чтобы использовать специализированные методы, а не просто вызывать logger.Info().

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

Примеры эффективных паттернов структурированного логирования

В процессе работы с многими проектами сформировались несколько эффективных паттернов структурированного логирования, которые стоит применять в своих системах:

1. Контексты выполнения для группировки связанных логов:

C#
1
2
3
4
5
6
7
8
9
10
11
// При обработке запроса
using (LogContext.PushProperty("RequestId", requestId))
using (LogContext.PushProperty("UserId", userId))
{
    // Все логи внутри этого блока автоматически получат эти свойства
    logger.Information("Начало обработки запроса");
    
    // Подробная логика...
    
    logger.Information("Завершение обработки запроса");
}

2. Выделенные объекты метрик для стандартизации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PerformanceMetrics
{
  public string Operation { get; set; }
  public long DurationMs { get; set; }
  public int ItemsProcessed { get; set; }
  public string Status { get; set; }
}
 
// Использование
var metrics = new PerformanceMetrics 
{
  Operation = "ImportCustomers",
  DurationMs = sw.ElapsedMilliseconds,
  ItemsProcessed = customers.Count,
  Status = success ? "Success" : "Failure"
};
 
logger.Information("Операция завершена {@PerformanceMetrics}", metrics);

3. Шаблоны нестандартных операций с атрибутами:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Помечаем класс атрибутом
[LogCategory("PaymentProcessing")]
public class PaymentService
{
  private readonly ILogger _logger;
  
  // Конструктор...
  
  [LogOperation("RefundPayment")]
  public async Task<RefundResult> RefundPaymentAsync(string paymentId, decimal amount)
  {
      // Логика метода с автоматическим логированием, учитывающим атрибуты
  }
}

Реализация такого логирования через middleware:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LoggingMiddleware : IMiddleware
{
  // ... другие детали ...
  
  public async Task InvokeAsync(HttpContext context, RequestDelegate next)
  {
      var endpoint = context.GetEndpoint();
      var operation = endpoint?.Metadata.GetMetadata<LogOperationAttribute>()?.Name;
      var category = endpoint?.Metadata.GetMetadata<LogCategoryAttribute>()?.Name;
      
      using (_logger.BeginScope(new Dictionary<string, object>
      {
          ["Operation"] = operation ?? "Unknown",
          ["Category"] = category ?? "General"
      }))
      {
          await next(context);
      }
  }
}

4. Разметка потенциально чувствительных данных:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
class UserDetails
{
  public string UserId { get; set; }
  
  [SensitiveData]
  public string Email { get; set; }
  
  [SensitiveData(MaskingStrategy = MaskingStrategy.Last4Visible)]
  public string CreditCard { get; set; }
}
 
// Регистрация обработчика чувствительных данных
loggerConfig.Destructure.With<SensitiveDataDestructuringPolicy>();

5. Вложенные объекты с контролем глубины:

C#
1
2
3
4
5
6
// Настройка ограничения глубины для предотвращения циклических ссылок
loggerConfig.Destructure.ToMaximumDepth(3);
 
// Использование
logger.Information("Обработка заказа со сложной структурой данных: {@OrderDetails}", 
    complexOrderWithNestedObjects);

Самым важным аспектом структурированного логирования является последовательность и договорённость в команде. Целесообразно создать набор соглашений или даже внутреннюю библиотеку которая стандартизирует подход к логированию во всех компонентах системы:

C#
1
2
3
4
5
6
7
8
9
10
11
// Пример корпоративного стандарта логирования
logger.LogBusinessOperation(
    operation: "CustomerRegistration", 
    status: OperationStatus.Success, 
    durationMs: timer.ElapsedMilliseconds, 
    metadata: new { 
        CustomerId = customer.Id, 
        MarketingConsent = customer.MarketingConsent,
        RegistrationType = "SelfRegistration"
    }
);

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

Практическая реализация и лучшие практики

Здесь мы рассмотрим примеры настройки и распространённые антипаттерны, которых следует избегать.

Конфигурирование многоуровневого логирования

Правильное конфигурирование логирования может существенно упростить как разработку, так и последующую эксплуатацию приложения. Рассмотрим подробно, как создать гибкую и масштабируемую конфигурацию для разных окружений.
Один из самых эффективных подходов — использовать комбинацию нескольких приёмников с разными уровнями детализации:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var logger = new LoggerConfiguration()
    // Консоль для быстрой обратной связи при разработке
    .WriteTo.Console(restrictedToMinimumLevel: LogEventLevel.Information)
    // Файлы для основной диагностики (с ротацией)
    .WriteTo.File("logs/app-.log", 
        rollingInterval: RollingInterval.Day,
        restrictedToMinimumLevel: LogEventLevel.Debug)
    // Специализированное хранилище для аналитики и мониторинга
    .WriteTo.Seq("http://seq-server:5341")
    // Отдельный файл только для критических ошибок
    .WriteTo.File("logs/critical-.log", 
        rollingInterval: RollingInterval.Day,
        restrictedToMinimumLevel: LogEventLevel.Error)
    .CreateLogger();

Важно различать конфигурацию для разных окружений. В ASP.NET Core это можно реализовать так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup<Startup>(); })
        .UseSerilog((hostingContext, loggerConfiguration) =>
        {
            // Базовая конфигурация из appsettings.json
            loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration);
            
            // Специфичные настройки для окружений
            if (hostingContext.HostingEnvironment.IsDevelopment())
            {
                loggerConfiguration
                    .MinimumLevel.Debug()
                    .WriteTo.Console(
                        outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}")
                    .WriteTo.Debug();
            }
            else if (hostingContext.HostingEnvironment.IsProduction())
            {
                loggerConfiguration
                    .MinimumLevel.Information()
                    .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
                    .Enrich.WithProperty("Environment", "Production")
                    .WriteTo.Async(a => a.File("logs/app-.log", rollingInterval: RollingInterval.Hour))
                    .WriteTo.ApplicationInsights(
                        hostingContext.Configuration["ApplicationInsights:InstrumentationKey"],
                        TelemetryConverter.Traces);
            }
        });

Конфигурация через appsettings.json позволяет менять настройки логирования без перекомпиляции:

JSON
1
2
3
4
5
6
7
8
9
10
11
12
13
{
  "Serilog": {
    "MinimumLevel": {
      "Default": "Information",
      "Override": {
        "Microsoft": "Warning",
        "System": "Warning",
        "Microsoft.Hosting.Lifetime": "Information",
        "MyApp.CriticalServices": "Debug"
      }
    }
  }
}

Интересный приём, который я часто использую в продакшн-системах — динамическое изменение уровня логирования через API:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[ApiController]
[Route("api/diagnostics")]
[Authorize(Roles = "Admin")]
public class DiagnosticsController : ControllerBase
{
    private static readonly LoggingLevelSwitch LevelSwitch = new LoggingLevelSwitch();
    
    static DiagnosticsController()
    {
        // Глобальная настройка Serilog для использования LevelSwitch
        Log.Logger = new LoggerConfiguration()
            .MinimumLevel.ControlledBy(LevelSwitch)
            // другие настройки...
            .CreateLogger();
    }
    
    [HttpPost("logging-level")]
    public IActionResult SetLoggingLevel([FromBody] LogLevelRequest request)
    {
        if (!Enum.TryParse<LogEventLevel>(request.Level, out var level))
        {
            return BadRequest("Invalid log level");
        }
        
        LevelSwitch.MinimumLevel = level;
        return Ok($"Logging level changed to {level}");
    }
    
    public class LogLevelRequest
    {
        public string Level { get; set; }
    }
}

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

Обработка исключений и контекст ошибок

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

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try
{
    await _paymentService.ProcessPaymentAsync(paymentRequest);
}
catch (Exception ex) when (ex is PaymentGatewayException or CardValidationException)
{
    // Логирование с бизнес-контекстом и полной информацией об исключении
    _logger.LogError(ex, "Ошибка обработки платежа {PaymentId} для клиента {CustomerId}. " +
                         "Сумма: {Amount} {Currency}, Метод: {PaymentMethod}", 
        paymentRequest.PaymentId,
        paymentRequest.CustomerId,
        paymentRequest.Amount,
        paymentRequest.Currency,
        paymentRequest.PaymentMethod);
    
    // Продолжаем выбрасывать исключение после логирования
    throw;
}

Заметьте использование паттерна when для избирательного логирования определённых типов исключений. Это может быть особенно полезно для игнорирования ожидаемых исключений:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
try
{
    return await _cacheService.GetAsync<CustomerData>(customerId);
}
catch (Exception ex) when (ex is CacheMissException)
{
    // Кэш-промах — это не ошибка, а ожидаемое поведение
    _logger.LogDebug("Кэш-промах для клиента {CustomerId}, загружаю из базы", customerId);
    var data = await _customerRepository.GetByIdAsync(customerId);
    await _cacheService.SetAsync(customerId, data);
    return data;
}
catch (Exception ex)
{
    // Другие исключения — это реальные ошибки
    _logger.LogError(ex, "Ошибка при получении данных клиента {CustomerId}", customerId);
    throw;
}

Один из наиболее полезных паттернов — глобальная обработка необработанных исключений для аварийного логирования:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// В Program.cs или аналогичной точке входа
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
{
    var exception = (Exception)eventArgs.ExceptionObject;
    Log.Fatal(exception, "Необработанное исключение привело к аварийному завершению приложения");
    
    // Важно: убедиться, что все логи сбросились на диск
    Log.CloseAndFlush();
};
 
// Для веб-приложений ASP.NET Core
app.UseExceptionHandler(errorApp =>
{
    errorApp.Run(async context =>
    {
        var exceptionHandlerPathFeature = 
            context.Features.Get<IExceptionHandlerPathFeature>();
        var exception = exceptionHandlerPathFeature?.Error;
        
        var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
        logger.LogError(exception, "Необработанное исключение в запросе {Path}", 
            context.Request.Path);
        
        context.Response.StatusCode = 500;
        await context.Response.WriteAsJsonAsync(new { error = "Внутренняя ошибка сервера" });
    });
});

Оптимизация производительности логирования

При высокой нагрузке логирование может стать узким местом. Вот несколько приёмов оптимизации:

1. Асинхронная запись логов — не блокирует основной поток выполнения:

C#
1
2
3
var logger = new LoggerConfiguration()
    .WriteTo.Async(a => a.File("logs/app-.log"))
    .CreateLogger();

2. Буферизация — накапливает сообщения и записывает их батчами:

C#
1
2
3
var logger = new LoggerConfiguration()
    .WriteTo.BufferedFile("logs/app-.log", bufferSize: 1000)
    .CreateLogger();

3. Проверка уровня перед формированием сообщений — экономит на создании строк:

C#
1
2
3
4
5
if (_logger.IsEnabled(LogLevel.Debug))
{
    var expensiveData = CalculateExpensiveLogData();
    _logger.LogDebug("Детальная информация: {ExpensiveData}", expensiveData);
}

4. Оптимизация параметров в сообщениях — правильное использование интерполяции:

C#
1
2
3
4
5
// Неправильно — всегда выполняется Serialize
_logger.LogDebug($"Получены данные: {JsonSerializer.Serialize(complexObject)}");
 
// Правильно — выполняется только при нужном уровне
_logger.LogDebug("Получены данные: {@ComplexObject}", complexObject);

Также важно соблюдать баланс между информативностью и объёмом данных:

C#
1
2
3
4
5
6
7
8
9
// Избыточное логирование
_logger.LogInformation("Пользователь {User} запросил {Data}", 
    JsonSerializer.Serialize(user), // всё, включая личные данные
    JsonSerializer.Serialize(hugeDataset)); // весь набор данных
 
// Оптимизированное логирование
_logger.LogInformation("Пользователь {UserId} запросил данные. Размер: {ItemCount} записей", 
    user.Id, 
    hugeDataset.Items.Count);

Для оценки производительности логирования можно использовать бенчмарки:

C#
1
2
3
4
5
6
7
8
9
// С Benchmark.NET
[Benchmark]
public void LogWithSerilogAsync()
{
    for (int i = 0; i < 10000; i++)
    {
        _serilogLogger.Information("Обработка элемента {Index}", i);
    }
}

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

Типичные ошибки при настройке логирования

За годы работы с .NET-проектами я наблюдал несколько типичных ошибок, которые регулярно повторяются в разных командах:

1. Логирование чувствительных данных — персональной информации, паролей, токенов:

C#
1
2
// Неправильно — утечка пароля в логи
_logger.LogDebug("Попытка входа с логином: {Login}, паролем: {Password}", login, password);

2. Игнорирование параллельного и асинхронного контекста — потеря следа выполнения:

C#
1
2
3
4
5
6
// Без async/await паттерна или контекста
Task.Run(() => 
{ 
    // Эти логи могут потерять связь с вызывающим контекстом
    _logger.LogInformation("Фоновая задача запущена");
});

3. Несистематические уровни логирования — всё становится Critical или Info:

C#
1
2
3
// Неправильное использование уровней
_logger.LogCritical("Файл не найден"); // но это не критическая ошибка!
_logger.LogInformation("Обнаружена утечка памяти"); // это должно быть Warning или Error!

4. Отсутствие ротации логов — диск заполняется, приложение падает:

C#
1
2
3
4
// Без ротации файл может вырасти неограниченно
var logger = new LoggerConfiguration()
    .WriteTo.File("app.log")
    .CreateLogger();

5. Избыточное логирование — проблема «информационного шума»:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public async Task<User> GetUserAsync(int userId)
{
    _logger.LogDebug("Метод GetUserAsync начал выполнение"); // избыточно
    _logger.LogDebug($"Параметр userId: {userId}"); // избыточно
    
    var user = await _userRepository.GetByIdAsync(userId);
    
    _logger.LogDebug($"Получен пользователь: {user}"); // может быть полезно
    _logger.LogDebug("Метод GetUserAsync завершил выполнение"); // избыточно
    
    return user;
}

6. Текстовые шаблоны вместо структурированного логирования:

C#
1
2
3
4
5
6
// Неправильно - сложно парсить и анализировать
_logger.LogInformation($"Ошибка: код={errorCode}, сообщение={errorMessage}");
 
// Правильно
_logger.LogInformation("Операция завершилась с ошибкой. Код: {ErrorCode}, сообщение: {ErrorMessage}", 
    errorCode, errorMessage);

7. Отсутствие управления размером JSON:

C#
1
2
3
4
5
6
7
8
9
10
11
// Опасно - огромный JSON может замедлить систему
_logger.LogInformation("Получены данные {@HugeObject}", hugeObjectWithNestedCollections);
 
// Лучше - контроль глубины и избирательность
_logger.LogInformation("Получен запрос {RequestSummary}", new 
{
    hugeObject.Id,
    hugeObject.Type,
    ItemCount = hugeObject.Items?.Count ?? 0
    // только нужные поля
});

8. Забывание о транзакционности — когда часть логов не попадает на диск при сбое:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try 
{
    _logger.LogInformation("Начата важная транзакция");
    // Критическая операция...
    // Если здесь произойдёт сбой, следующий лог не запишется
    _logger.LogInformation("Транзакция успешно завершена"); 
}
catch 
{
    // Этот блок логов может не выполниться
}
finally
{
    // Важно форсировать запись на диск критически важных логов
    Log.CloseAndFlush();
}

9. Использование логирования на уровне метаданных без соответствующего контекста:

C#
1
2
3
4
5
6
// Непонятный контекст - что такое "Статус 3"?
_logger.LogWarning("Обработка заказа, Статус: 3");
 
// Понятный контекст через перечисления и константы
_logger.LogWarning("Обработка заказа, Статус: {Status}", 
    Enum.GetName(typeof(OrderStatus), OrderStatus.PaymentDeclined));

Продвинутые техники и нестандартные решения

Помимо стандартных подходов существует ряд продвинутых техник, которые могут значительно улучшить экосистему логирования в сложных приложениях.

Профилирование участков кода через аспекты логирования

Один из моих любимых подходов — создание атрибутов для автоматического логирования производительности методов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// Атрибут для декорирования методов, которые нужно профилировать
[AttributeUsage(AttributeTargets.Method)]
public class LogProfileAttribute : Attribute
{
    public string Category { get; }
    
    public LogProfileAttribute(string category = null)
    {
        Category = category;
    }
}
 
// Аспект для внедрения логики профилирования
public class LogProfileAspect : OnMethodBoundaryAspect
{
    private static readonly ILogger Logger = 
        LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger<LogProfileAspect>();
 
    public override void OnEntry(MethodExecutionArgs args)
    {
        args.MethodExecutionTag = Stopwatch.StartNew();
    }
 
    public override void OnExit(MethodExecutionArgs args)
    {
        var sw = (Stopwatch)args.MethodExecutionTag;
        sw.Stop();
        
        var attribute = (LogProfileAttribute)args.Method.GetCustomAttributes(
            typeof(LogProfileAttribute), true).FirstOrDefault();
            
        var category = attribute?.Category ?? "Default";
        
        Logger.LogInformation(
            "Метод {MethodName} из категории {Category} выполнен за {ElapsedMs}ms",
            args.Method.Name, category, sw.ElapsedMilliseconds);
    }
}

Применение:

C#
1
2
3
4
5
6
7
8
public class OrderService
{
    [LogProfile("BusinessOperation")]
    public async Task<OrderResult> ProcessOrderAsync(Order order)
    {
        // Код будет автоматически профилирован
    }
}

Создание собственного middleware для обогащения логов

Ещё одна техника — создание специализированных middleware для автоматического обогащения всех логов в приложении:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class EnrichmentMiddleware
{
    private readonly RequestDelegate _next;
    
    public EnrichmentMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    
    public async Task InvokeAsync(HttpContext context)
    {
        // Добавляем пользовательский контекст, если пользователь аутентифицирован
        if (context.User.Identity.IsAuthenticated)
        {
            var userId = context.User.FindFirst("sub")?.Value;
            var userEmail = context.User.FindFirst("email")?.Value;
            
            using (LogContext.PushProperty("AuthenticatedUserId", userId))
            using (LogContext.PushProperty("AuthenticatedUserEmail", userEmail))
            {
                await _next(context);
                return;
            }
        }
        
        // Добавляем IP, UserAgent и базовый request ID в любом случае
        using (LogContext.PushProperty("ClientIp", context.Connection.RemoteIpAddress))
        using (LogContext.PushProperty("UserAgent", context.Request.Headers["User-Agent"].ToString()))
        {
            await _next(context);
        }
    }
}

Условное логирование с помощью фильтров

Вместо статических уровней логирования иногда полезно использовать динамические фильтры:

C#
1
2
3
4
5
6
// Настраиваем фильтр, который включает Debug-логи только для определённых типов запросов
logger.Filter.ByIncludingOnly(logEvent => 
    (logEvent.Level >= LogEventLevel.Information) ||
    (logEvent.Level == LogEventLevel.Debug && 
     logEvent.Properties.ContainsKey("RequestType") && 
     logEvent.Properties["RequestType"].ToString().Contains("Payment")));

Такой подход позволяет выборочно увеличивать детализацию для определённых компонентов системы, не влияя на остальные.

Логирование в микросервисной архитектуре с распределенным трейсингом

В распределенных системах особенно важно трассировать запросы через множество микросервисов. Для этого можно комбинировать логирование с OpenTelemetry:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// В Startup.cs
services.AddOpenTelemetryTracing(builder =>
{
    builder
        .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("PaymentService"))
        .AddAspNetCoreInstrumentation()
        .AddHttpClientInstrumentation()
        .AddSqlClientInstrumentation()
        .AddOtlpExporter(options => 
        {
            options.Endpoint = new Uri("http://jaeger:4317");
        });
});
 
// В коде сервиса
public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
{
    // Текущее активное span автоматически добавляется в логи
    var activity = Activity.Current;
    
    // Добавляем бизнес-контекст в трассировку
    activity?.AddTag("payment.id", request.PaymentId);
    activity?.AddTag("payment.amount", request.Amount);
    
    _logger.LogInformation("Начало обработки платежа {PaymentId}", request.PaymentId);
    return await _paymentProcessor.ProcessAsync(request);
}

Генерация документации на основе логов

Необычное, но полезное применение логирования — автоматическая генерация документации на основе структурированных логов:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Специальный маркер для журнала API
[LogApiDocumentation("Создание заказа")]
[HttpPost]
public async Task<ActionResult<Order>> CreateOrder([FromBody] OrderRequest request)
{
    _logger.LogDocumentedEndpoint(new ApiEndpointInfo
    {
        Name = "Создание заказа",
        Description = "Создаёт новый заказ в системе",
        RequestExample = request,
        AuthenticationRequired = true,
        RequiredPermissions = new[] { "orders:create" }
    });
    
    // Реализация...
}

Специальное расширение для генерации документации парсит логи и строит API-документацию, включая примеры запросов и ответов.

Логирование состояния системы при завершении работы

Полезная техника — специальное логирование при остановке приложения:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public static class Program
{
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .UseConsoleLifetime()
            .ConfigureServices((hostContext, services) =>
            {
                // Добавляем специальный сервис для логирования при завершении
                services.AddSingleton<IHostedService, ShutdownLoggerService>();
            });
}
 
public class ShutdownLoggerService : IHostedService
{
    private readonly ILogger<ShutdownLoggerService> _logger;
    private readonly IHostApplicationLifetime _appLifetime;
    
    public ShutdownLoggerService(
        ILogger<ShutdownLoggerService> logger, 
        IHostApplicationLifetime appLifetime)
    {
        _logger = logger;
        _appLifetime = appLifetime;
    }
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _appLifetime.ApplicationStopping.Register(OnStopping);
        _appLifetime.ApplicationStopped.Register(OnStopped);
        return Task.CompletedTask;
    }
    
    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
    
    private void OnStopping()
    {
        _logger.LogWarning("Приложение останавливается. Состояние: {@SystemState}", 
            GetCurrentSystemState());
    }
    
    private void OnStopped()
    {
        _logger.LogWarning("Приложение остановлено.");
        Log.CloseAndFlush(); // Гарантируем запись всех логов
    }
    
    private SystemState GetCurrentSystemState()
    {
        // Собираем информацию о текущих активных задачах, соединениях, etc.
    }
}

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

Проверять, какие программы запускаются, вести лог
Здравствуйте, нужно сделать программу которая проверяет какие программы запускаются и записывать сколько раз.
Допустим запускается program.exe и…

При изменениях в DGW нужно ведение лог-файла
надо чтобы вёлся лог файл.На форме datagridview изменяющийся каждые 5 сек.надо чтобы при изменении записывалось в лог файл.Я пытаюсь так.но…

Система лог — пас)
Всем привет)
Пишу прогу для учительницы по английскому языку) С# изучала на курсах, так что сейчас уже пытаюсь что-то самопознать)
В общем, нужна…

Чтение из лог-файла
Здравствуйте! Есть задача — отследить запись в лог файл и проверить, какой именно текст записался. Для отслеживания изменения лога использовала…

Лог приложения.
Подскажите пожалуйста, как можно организовать создание лога приложения? Желательно с наименьшим падением производительности :).

Чтение лог-файла, когда он занят
Здравствуйте.
Есть лог файл, который постоянно пишеться (может быть по 5-6 записей за 1 мс.) Задача прочитать файл.
Но вот он занят. Уже…

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

Создание лог файла в ASP чате.
Вобщем проблемса в следуещем, есть некий приметивный чатик, хотелось бы в нем вести лог файл. Тобиш, есть апликашка с описаными событиями…

Как организовать лог файл на ASP
У меня почему то файл создается каждый раз заново, а мне нужно открывать лог файл и записат в конец, как это сделать?

Класс лог-файла, как лучше осуществить запись в файл
Здравствуйте!
Хочу создать класс лог-файла с конструктором, в котором передаваемое значение — путь к файлу. Связь с файлом хотел сделать через…

Можно ли вести подробный лог ошибок в приложении
Всем привет!
Возник такой вопрос. Вот мы пишем какой то проект, во время его отладки и т.д. сообщения о ошибках выдает нам среда к примеру. Когда…

Проверка на существование строки, запись в лог-файл
Здравствуйте!
Имеется файл excel и база данных, перебрасываю из файла в таблицу базы.
Делаю запись в лог-файл строк, которые были добавлены.

LogParser is a free log analysis software for Microsoft. ) Files, and event logs, registry, file system, and Active Directory. It can query these data like using SQL statements, and even display the results in various charts.


download link:
http://www.microsoft.com/en-us/download/details.aspx?id=24659

Log Parser

  • C # + logparser implementation of Windows Log Analysis
  • Write your own log analysis software
    • C # code
      • system.cs
      • security.cs
    • Visual output
    • Write query statements in our C # program
  • Reference article

Write your own log analysis software

Use C # to call LogParser.dll to implement the GUI interface of the LogParser tool, accumulate safety scenarios during log analysis, and improve efficiency for our emergency response.

C # code

system.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using LogQuery = MSUtil.LogQueryClassClass;
using LogRecordSet = MSUtil.ILogRecordset;
using tsvinputformat = MSUtil.COMTSVInputContextClassClass;
using EventLogInputFormat = MSUtil.COMEventLogInputContextClassClass;
using DataGridOutput = MSUtil.COMDataGridOutputContextClassClass;
using MSUtil;

namespace logparserGUI
{
    public partial class system : Form
    {
        public system()
        {
            InitializeComponent();
        }

        private void system_Load(object sender, EventArgs e)
        {
            comboBox1.Items.Add("Boot / shutdown / restart");
        }

        private void button1_Click(object sender, EventArgs e)
        {
            if (comboBox1.Text.Length == 0)
            {
                MessageBox.Show("Please select the log type");
            }
            else
            {
                switch (comboBox1.Text)
                {
                    case "Boot / shutdown / restart":
                        security sec = new security();
                        this.Hide();// Hide now this window
                        sec.ShowDialog();
                        break;
                    case "application":
                        application app = new application();
                        this.Hide();// Hide now this window
                        app.ShowDialog();
                        break;
                    case "system":
                        system sys = new system();
                        this.Hide();// Hide now this window
                        sys.ShowDialog();
                        break;

                }
                // String SQL = @ "SELECT EVENTID, TIMEGENERATED, SOURCENAME, Message from E: \ TOPSEC \ YJ \ Galaxy \ Yinghang \ 155system.evtx"; // This is the system log file path
                 string sql = @ "Select EventID, TimeGenerated, SourceName, Message from E: \ TOPSEC \ YJ \ Galaxy \ Yinghang \ 155System.evtx";// This is the system log file path
                DataTable dt = ReadFromEvt(sql);
                //writeToDataBase(dt);
                dataGridView1.DataSource = dt;
                MessageBox.Show("Read it!");
            }
        }
        public DataTable ReadFromEvt(string sql)
        {
            try
            {
                DataTable datat = new DataTable();
                datat.Columns.Add("Event ID", typeof(string));
                datat.Columns.Add("date", typeof(string));
                datat.Columns.Add("source", typeof(string));
                datat.Columns.Add("description", typeof(string));
                // Instantiate the LogQuery object  
                LogQuery oLogQuery = new LogQuery();
                // Instantiate the Event Log Input Format object  
                EventLogInputFormat oEvtInputFormat = new EventLogInputFormat();
                // Set its "direction" parameter to "BW"  
                oEvtInputFormat.direction = "BW";
                // Create the query  
                string query = sql;
                // Execute the query  
                LogRecordSet oRecordSet = oLogQuery.Execute(query, oEvtInputFormat);
                while (!oRecordSet.atEnd())
                {
                    var itemData = oRecordSet.getRecord();
                    DataRow dr = datat.NewRow();
                    dr["Event ID"] = itemData.getValue("EventID").ToString();
                    dr["date"] = itemData.getValue("TimeGenerated").ToString();
                    dr["source"] = itemData.getValue("SourceName").ToString();
                    dr["description"] = itemData.getValue("Message").ToString();
                    datat.Rows.Add(dr);
                    oRecordSet.moveNext();
                }

                // Close the recordset  
                oRecordSet.close();
                return datat;
            }
            catch (System.Runtime.InteropServices.COMException exc)
            {
                MessageBox.Show("Unexpected error: " + exc.Message);
                return null;
            }
        }

        private void comboBox1_SelectedIndexChanged(object sender, EventArgs e)
        {

        }

        private void dataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
        {

        }
    }
}

security.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using LogQuery = MSUtil.LogQueryClassClass;
using LogRecordSet = MSUtil.ILogRecordset;
using tsvinputformat = MSUtil.COMTSVInputContextClassClass;
using EventLogInputFormat = MSUtil.COMEventLogInputContextClassClass;
using DataGridOutput = MSUtil.COMDataGridOutputContextClassClass;
using MSUtil;

namespace logparserGUI
{
    public partial class security : Form
    {
        public security()
        {
            InitializeComponent();
        }
        public void eventlog()
        {
            try
            {
                LogQuery oLogQuery = new LogQuery();
                
                EventLogInputFormat oEVTInputFormat = new EventLogInputFormat();
                oEVTInputFormat.direction = "BW";
                // string query = @ "SELECT TimeGenerated as LoginTime, EXTRACT_TOKEN (Strings, 5, '|') as username, EXTRACT_TOKEN (Strings, 8, '|') as LogonType, EXTRACT_TOKEN (Strings, 17, '|') AS ProcessName , Extract_token (Strings, 18, '|') AS Sourceip from D: \ Users \ Tenon \ Desktop \ Taihu Bureau \ 11.135 \ 11.135safe.evtx where eventid = 4624 "
                // String query = @ "SELECT * from D: \ users \ Tenon \ Desktop \ Taihu Bureau \ Log \ 11.99app.evtx";
                string query = @"SELECT * FROM E:\hack\anfu\yj\security.evtx";
                //string query = @"SELECT TimeGenerated as LoginTime,EXTRACT_TOKEN(Strings,5,'|') as username,EXTRACT_TOKEN(Strings,6,'|') as AAA,EXTRACT_TOKEN(Strings,7,'|') as BBB,EXTRACT_TOKEN(Strings,8, '|') as LogonType,EXTRACT_TOKEN(Strings,9, '|') as CCC,EXTRACT_TOKEN(Strings, 10, '|') as DDD,EXTRACT_TOKEN(Strings, 11, '|') as EEE,EXTRACT_TOKEN(Strings, 12, '|') as FFF,EXTRACT_TOKEN(Strings, 13, '|') as GGG,EXTRACT_TOKEN(Strings, 14, '|') as HHH,EXTRACT_TOKEN(Strings, 15, '|') as III,EXTRACT_TOKEN(Strings, 16, '|') as JJJ,EXTRACT_TOKEN(Strings, 17, '|') AS ProcessName,EXTRACT_TOKEN(Strings, 18, '|') AS SourceIP FROM D:\Users\TenOn\Desktop\log20190629\log\security.evtx  where EventID=4625";
                //string query = @"SELECT TimeGenerated as LoginTime,EXTRACT_TOKEN(Strings,5,'|') as username,EXTRACT_TOKEN(Strings,6,'|') as AAA,EXTRACT_TOKEN(Strings,7,'|') as BBB,EXTRACT_TOKEN(Strings,8, '|') as LogonType,EXTRACT_TOKEN(Strings,9, '|') as CCC,EXTRACT_TOKEN(Strings, 10, '|') as DDD,EXTRACT_TOKEN(Strings, 11, '|') as EEE,EXTRACT_TOKEN(Strings, 12, '|') as FFF,EXTRACT_TOKEN(Strings, 13, '|') as GGG,EXTRACT_TOKEN(Strings, 14, '|') as HHH,EXTRACT_TOKEN(Strings, 15, '|') as III,EXTRACT_TOKEN(Strings, 16, '|') as JJJ,EXTRACT_TOKEN(Strings, 17, '|') AS ProcessName,EXTRACT_TOKEN(Strings, 18, '|') AS SourceIP FROM D:\Users\TenOn\Desktop\log20190629\log\security.evt where EventTypeName='Warning event'";
                // String query = @ "SETRINGENERATED AS Logintime, Extract_token (Strings, 5, '|') AS UserName, Extract_token (Strings, 8, '|') As Logontype, Extract_token (Strings, 9, '|') AAA , Extract_token (Strings, 10, '|') AS BBBBBB, EXTRACT_TOKEN (Strings, 17, '|') As ProcessName, Extract_token (Strings, 18, '|') AS Sourceip from D: \ Users \ Tenon \ Desktop \ Taihu Lake Bureau \ Log \ 11.99.Evtx where EventId = 4624 ";
                //Logparser.exe –i:EVT –o:DATAGRID "SELECT * FROM E:\study\yj\security.evtx where EventID=4625"
                // String query = @ "SELECT * from D: \ Users \ Tenon \ Desktop \ Taihu Bureau \ 11.135 \ 11.135safe.evtx";
                LogRecordSet oRecordSet = oLogQuery.Execute(query, oEVTInputFormat);// Execute a statement
                DataGridOutput oEVTOutputFormat = new DataGridOutput();
                MessageBox.Show("oLogQuery.ToString");
                oLogQuery.ExecuteBatch(query, oEVTInputFormat, oEVTOutputFormat);
                if (oLogQuery.errorMessages != null)
                {
                    MessageBox.Show(oLogQuery.errorMessages.ToString());
                }

            }

            catch (System.Runtime.InteropServices.COMException exc)
            {
                Console.WriteLine("Unexpected error: " + exc.Message);
            }
        }
        private void security_Load(object sender, EventArgs e)
        {

        }

        private void button1_Click(object sender, EventArgs e)
        {
            string sql = @ "Select EventID, TimeGenerated, SourceName, Message from E: \ TOPSEC \ YJ \ Galaxy \ Yinghang \ 155System.evtx";// This is the system log file path
            DataTable dt = ReadFromEvt(sql);
            //writeToDataBase(dt);
            dataGridView1.DataSource = dt;
            MessageBox.Show("Read it!");
        }
        public DataTable ReadFromEvt(string sql)
        {
            try
            {
                DataTable datat = new DataTable();
                datat.Columns.Add("Event ID", typeof(string));
                datat.Columns.Add("date", typeof(string));
                datat.Columns.Add("source", typeof(string));
                datat.Columns.Add("description", typeof(string));
                // Instantiate the LogQuery object  
                LogQuery oLogQuery = new LogQuery();
                // Instantiate the Event Log Input Format object  
                EventLogInputFormat oEvtInputFormat = new EventLogInputFormat();
                // Set its "direction" parameter to "BW"  
                oEvtInputFormat.direction = "BW";
                // Create the query  
                string query = sql;
                // Execute the query  
                LogRecordSet oRecordSet = oLogQuery.Execute(query, oEvtInputFormat);
                while (!oRecordSet.atEnd())
                {
                    var itemData = oRecordSet.getRecord();
                    DataRow dr = datat.NewRow();
                    dr["Event ID"] = itemData.getValue("EventID").ToString();
                    dr["date"] = itemData.getValue("TimeGenerated").ToString();
                    dr["source"] = itemData.getValue("SourceName").ToString();
                    dr["description"] = itemData.getValue("Message").ToString();
                    datat.Rows.Add(dr);
                    oRecordSet.moveNext();
                }

                // Close the recordset  
                oRecordSet.close();
                return datat;
            }
            catch (System.Runtime.InteropServices.COMException exc)
            {
                MessageBox.Show("Unexpected error: " + exc.Message);
                return null;
            }
        }

        private void dataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
        {

        }
    }
}

Visual output

–o:DATAGRID

Windows log strings field extraction
Other relatively simple in the extracted project, but String is very complicated, but it is more important.
Use functions extract_token (strings, num, ‘|’)
Strings Use | Split extract_token (strings, 0, ‘|’) extract S-1-5-18, extract_token (strings, 1, ‘|’) extract Desktop-Kjihhdo $ with this class
(Here, use the next picture.
https://blog.csdn.net/qq_29647709)

For example: Strings field, according to the fifth value of the | segment, turn the head header UserName

EXTRACT_TOKEN(Strings,5,'|') as username

For example: Analysis Successful Login Event 4624 We filth our data you want to extract:
These include, the 5th user, the 8th login type, the 17th program path, and the 18th source IP address we care about.

LogParser.exe -i:EVT -o:DATAGRID "SELECT TimeGenerated as LoginTime,EXTRACT_TOKEN(Strings,5,'|') as username,EXTRACT_TOKEN(Strings, 8, '|') as LogonType,EXTRACT_TOKEN(Strings, 17, '|') AS ProcessName,EXTRACT_TOKEN(Strings, 18, '|') AS SourceIP FROM E:\study\yj\security.evtx where EventID=4624"

Write query statements in our C # program

Such as: Analysis Successful Login Event 4624
We filled out our data you want to extract:
These include, the 5th user, the 8th login type, the 17th program path, and the 18th source IP address we care about.

LogParser.exe -i:EVT -o:DATAGRID "SELECT TimeGenerated as LoginTime,EXTRACT_TOKEN(Strings,5,'|') as username,EXTRACT_TOKEN(Strings, 8, '|') as LogonType,EXTRACT_TOKEN(Strings, 17, '|') AS ProcessName,EXTRACT_TOKEN(Strings, 18, '|') AS SourceIP FROM

View and login event

Logparser -i:evt –o:DATAGRID “SELECT * FROM E:\study\yj\security.evtx WHERE Strings LIKE '%logon%'”

View all fields for login audit failed logs and visual output

Logparser.exe –i:EVT –o:DATAGRID "SELECT * FROM E:\study\yj\security.evtx where EventID=4625"

Reference article

https://www.jianshu.com/p/d325b4b1169c
http://blog.sina.com.cn/s/blog_54b976460100o8qt.html
https://blog.csdn.net/qq_29647709/article/details/85124105
https://www.t00ls.net/viewthread.php?tid=48067&highlight=%E5%BA%94%E6%80%A5%E5%93%8D%E5%BA%94

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Как установить pip на windows через командную строку
  • Huananzhi x99 tf bios прошивка через windows
  • Говорящие часы программа windows
  • Кнопка windows на клаве
  • Как активировать windows 10 через powershell