Всем привет, с вами Юрий Ковальчук, backend разработчик в ВебРайз. В этой статье разберем процесс вывода логов из приложения c автотестами на .NET в ELK с последующей визуализаций в Kibana.

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

Отправка логов из .NET-теста

Ниже — обезличенный пример e2e-/UI-теста на .NET (xUnit + Playwright), который:

  1. Запускает браузер

  2. Выполняет действия на странице

  3. Формирует объект с результатом теста

  4. Пишет лог в Serilog, откуда его потом заберёт Filebeat

[Fact]
public async Task SubmitPaymentForm_EmptyForm_ShouldShowValidationErrors()
{
    string baseUrl = $"{_baseUrl}payment";
    var testName = "SubmitPaymentForm_EmptyForm_ShouldShowValidationErrors";

    var context = await _browser.NewContextAsync(new() { IgnoreHTTPSErrors = true });
    var page = await context.NewPageAsync();

    var result = new TestResult
    {
        Test = testName,
        Url = baseUrl,
        Timestamp = DateTime.UtcNow,
        ErrorText = "",
        ErrorStyle = "",
        Success = false
    };

    try
    {
        if (string.IsNullOrEmpty(baseUrl))
            throw new Exception("BaseUrl not configured");

        await WaitForSiteReady(page, baseUrl, 60);
        await page.GotoAsync(baseUrl);

        await page.ClickAsync("button.nf-button--primary");
        await page.ClickAsync("button.js-pay-button-submit");

        var error = await page.WaitForSelectorAsync(".cell__error-message", new() { Timeout = 30000 });
        var text = await error.InnerTextAsync();
        var style = await error.EvaluateAsync<string>("el => el.getAttribute('style')");

        Assert.Contains("Заполните", text);
        Assert.Contains("display: block", style);

        result.Success = true;
    }
    catch (Exception ex)
    {
        Log.Error(ex, $"Test {testName} failed");
        result.ErrorText = ex.Message;
        result.ErrorStyle = "danger";
    }
    finally
    {
        await context.CloseAsync();
        LogTestResult(result);
    }
}

Пояснения к ключевым строкам:

  • baseUrl — конечная точка тестируемой страницы

  • testName — удобное имя теста, которое попадёт в логи

  • result — объект, который вы будете сериализовать/логировать (его потом легко разобрать в Filebeat)

  • try/catch/finally — в catch пишем ошибку, в finally — пишем структурированный результат

  • Log.Error(...) — классический Serilog-вызов, который попадёт в файл

Filebeat: сбор логов тестов и отправка в Logstash

Пример filebeat.yml:

filebeat.inputs:
  - type: log
    enabled: true
    paths:
      - /var/www/project/tests/bin/Debug/net6.0/Logs/e2e*.log
    fields:
      type: e2e_tests
    fields_under_root: true
    scan_frequency: 5s
    processors:
      - decode_json_fields:
          fields: ["message"]
          process_array: false
          max_depth: 3
          target: ""
          overwrite_keys: true
      - rename:
          fields:
            - from: "Properties.TestResult.Timestamp"
              to: "test_timestamp"
            - from: "Properties.TestResult.Test"
              to: "test_name"
            - from: "Properties.TestResult.Success"
              to: "test_success"
          ignore_missing: true
      - drop_fields:
          fields: ["Properties", "MessageTemplate", "Level"]

output.logstash:
  hosts: ["111.18.100.38:5044"]

Что важно:

  • paths — путь до логов приложения/тестов

  • decode_json_fields — разбираем Serilog JSON, чтобы получить плоские поля

  • rename — переименовываем вложенные поля Serilog в удобные (test_name, test_success и т.д.)

  • drop_fields — удаляем лишний технический шум

  • output.logstash — указываем, куда отправлять (Logstash)

Logstash: приём и отправка в Elasticsearch

Минимальный pipeline Logstash (пример logstash.conf):

input {
  beats {
    port => 5044
  }
}

filter {
  if [type] == "e2e_tests" {
    mutate {
      add_field => { "[@metadata][index]" => "e2e-tests-%{+YYYY.MM.dd}" }
    }
  }
}

output {
  elasticsearch {
    hosts => ["http://localhost:9200"]
    index => "%{[@metadata][index]}"
  }
  stdout { codec => rubydebug }
}
  • input.beats — то, что получает от Filebeat

  • filter — можно добавлять/нормализовывать поля

  • output.elasticsearch — конечная точка, индекс называется e2e-tests-YYYY.MM.dd

Поиск логов в Kibana/Dev Tools

Стартуем с простого запроса:


GET _cat/indices?v

Так проверяем, что индекс e2e-tests-* вообще есть. Дальше — простой поиск:


GET e2e-tests-*/_search
{
  "size": 10
}

Чтобы находить именно тестовые логи Serilog со вложенным TestResult, используем match_phrase по message:


GET e2e-tests-*/_search
{
  "size": 10,
  "query": {
    "match_phrase": {
      "message": "\"Properties\":{\"TestResult\""
    }
  },
  "sort": [
    { "@timestamp": "desc" }
  ]
}
  • size — сколько документов вернуть

  • match_phrase — ищем конкретный фрагмент JSON, характерный для наших тестовых логов

  • sort — сортируем по времени прихода документа

Визуализация в Kibana

Ниже показано, как создать визуализации в Kibana для отображения результатов e2e-тестов .NET. Будем использовать Filebeat и Logstash для отправки логов в Elasticsearch, а Kibana — для построения графиков.

1. Список визуализаций

Список визулизаций
Список визулизаций

2. Создание новой визуализации

Создание новой визуализации
Создание новой визуализации

Нажимаем «Create visualization». Для простоты используем тип Lens — он подходит для построения базовых графиков.

3. Настройка Lens визуализации

Настройка Lens визуализации
Настройка Lens визуализации

В окне Lens выбираем индекс e2e-tests*. На оси X указываем @timestamp, на оси Y — уникальное количество test_success. Сверху можно добавить фильтр test_name, чтобы отображать результаты только конкретного теста.

4. Сохранение визуализации

Сохранение визуализации
Сохранение визуализации

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

5. Открытие Dashboard

Открытие Dashboard
Открытие Dashboard

В меню Kibana переходим в раздел Dashboard — здесь создаются панели мониторинга, состоящие из нескольких визуализаций.

6. Создание нового Dashboard

Создание нового Dashboard
Создание нового Dashboard

Можно создать новый дашборд («Create dashboard») или открыть существующий. В примере используется дашборд «Тесты_Site.ru».

7. Добавление визуализаций

Добавление визуализаций
Добавление визуализаций

Чтобы добавить визуализацию, нажмите Add → Lens Visualization и выберите нужный график из списка.

8. Итоговый Dashboard

Итоговый Dashboard
Итоговый Dashboard

После добавления всех графиков на панель вы получите наглядный дашборд, отображающий состояние e2e-тестов в реальном времени.

9. Пример визуализации теста

Пример визуализации теста
Пример визуализации теста

Этот график показывает результаты теста во времени: успешные и неуспешные прогоны.

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

По вопросам, телеграм @webrise1

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


  1. gotch
    13.11.2025 11:11

    input type: log исключен много релизов назад.
    Может эффективнее разобрать не через processors, а сразу в parsers?

    filebeat.inputs:
      - type: filestream    
        paths:
          - /var/www/project/tests/bin/Debug/net6.0/Logs/e2e*.log
        parsers:
         - ndjson:
           target: ""
           add_error_key: true

    Зачем переименовывать поля, если можно сразу в исходном JSON написать как надо?

          - rename:
              fields:
                - from: "Properties.TestResult.Timestamp"
                  to: "test_timestamp"

    Еще не понял, откуда берется @timestamp в документах Elastic. Было бы логично взять его из Properties.TestResult.Timestamp и не хранить два разных?


    1. webrise Автор
      13.11.2025 11:11

      Да, согласен.
      У меня Filebeat уже использовался с type: log, поэтому добавлял блок для e2e-тестов в ту же конфигурацию и сделал всё однотипно.

      Про filestream и timestamp — да, вариант с parsers.ndjson и подтягиванием времени из логов в @timestamp выглядит логичнее, особенно если логи уже в JSON.


  1. withkittens
    13.11.2025 11:11

    var testName = "SubmitPaymentForm_EmptyForm_ShouldShowValidationErrors";
    
    ...
    
    var result = new TestResult
    {
        Test = testName,
        Url = baseUrl,
        Timestamp = DateTime.UtcNow,
        ErrorText = "",
        ErrorStyle = "",
        Success = false
    };
    
    ...
    
    LogTestResult(result);

    У меня есть стойкое ощущение, что все это - лишний шум, которого в тестах не должно быть. У вас xUnit - разве runner reporter (json или на худой конец - самописный) не решает проблему выгрузки результатов тестов в обрабатывабельном виде?