Перейти к содержанию

6. Генератор событий, логи и кэш

Цель

Добавить генератор нагрузки, логирование операций и кэширование аналитики.

Объяснение

Аналитика без данных бесполезна. Генератор создаёт тестовые события, чтобы проверить фильтры, отчёты и производительность.

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

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

Код: генератор событий

Генератор нужен, чтобы быстро наполнить базу реалистичными учебными данными. Он работает напрямую с AppDbContext, потому что это служебный инструмент администратора, а не основной пользовательский сценарий создания одного события.

ActivityMonitoring.Infrastructure/EventGeneration/ActivityEventGenerator.cs:

using ActivityMonitoring.Domain.Entities;
using ActivityMonitoring.Infrastructure.Persistence;

namespace ActivityMonitoring.Infrastructure.EventGeneration;

public class ActivityEventGenerator
{
    private readonly AppDbContext _db;
    private readonly Random _random = new();

    private static readonly string[] Types = ["Login", "Logout", "PageView", "CreateData", "UpdateData", "Error"];
    private static readonly string[] Sources = ["web-client", "mobile-client", "admin-panel", "integration"];
    private static readonly string[] Severities = ["Info", "Warning", "Error"];

    public ActivityEventGenerator(AppDbContext db)
    {
        _db = db;
    }

    public async Task<int> GenerateAsync(int count, CancellationToken ct)
    {
        var userIds = _db.Users.Select(x => x.Id).ToList();

        for (var i = 0; i < count; i++)
        {
            var type = Types[_random.Next(Types.Length)];

            _db.ActivityEvents.Add(new ActivityEvent
            {
                UserId = userIds[_random.Next(userIds.Count)],
                Type = type,
                Source = Sources[_random.Next(Sources.Length)],
                Severity = type == "Error" ? "Error" : Severities[_random.Next(Severities.Length)],
                Description = $"Сгенерированное событие {type}",
                CreatedAt = DateTime.UtcNow.AddMinutes(-_random.Next(0, 10080))
            });
        }

        return await _db.SaveChangesAsync(ct);
    }
}

Контроллер генерации делает этот инструмент доступным через API. Доступ ограничен ролью Admin, а параметр count дополнительно ограничивается, чтобы случайно не создать слишком большую нагрузку.

ActivityMonitoring.Api/Controllers/GenerateController.cs:

using ActivityMonitoring.Infrastructure.EventGeneration;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace ActivityMonitoring.Api.Controllers;

[ApiController]
[Route("api/generate")]
public class GenerateController : ControllerBase
{
    private readonly ActivityEventGenerator _generator;

    public GenerateController(ActivityEventGenerator generator)
    {
        _generator = generator;
    }

    [Authorize(Roles = "Admin")]
    [HttpPost("events")]
    public async Task<ActionResult<object>> Generate(int count = 100, CancellationToken ct = default)
    {
        var safeCount = Math.Clamp(count, 1, 10000);
        var created = await _generator.GenerateAsync(safeCount, ct);
        return Ok(new { created });
    }
}

Зарегистрируйте генератор и кэш в DI-контейнере. Генератор создаётся на время запроса, а AddMemoryCache подключает встроенное хранилище, которое позже будет использовать аналитический сервис.

builder.Services.AddScoped<ActivityEventGenerator>();
builder.Services.AddMemoryCache();

Код: логирование

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

_logger.LogInformation("Event {EventType} created by user {UserId}", entity.Type, userId);

Настройки логирования в appsettings.Development.json делают вывод полезным для разработки: собственные информационные сообщения остаются видимыми, а слишком подробные SQL-команды EF Core не засоряют консоль.

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore.Database.Command": "Warning"
    }
  }
}

Код: кэш аналитики

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

ActivityMonitoring.Application/Analytics/AnalyticsDtos.cs:

namespace ActivityMonitoring.Application.Analytics;

public record AnalyticsSummaryDto(int TotalEvents, int UniqueUsers, int Errors);

Аналитический сервис считает сводку по базе и сохраняет результат в памяти на короткое время. Это показывает базовый принцип кэширования: одинаковый запрос за тот же период не должен каждый раз заново выполнять агрегирующие SQL-запросы.

ActivityMonitoring.Infrastructure/Analytics/AnalyticsService.cs:

using ActivityMonitoring.Application.Analytics;
using ActivityMonitoring.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;

namespace ActivityMonitoring.Infrastructure.Analytics;

public class AnalyticsService
{
    private readonly AppDbContext _db;
    private readonly IMemoryCache _cache;

    public AnalyticsService(AppDbContext db, IMemoryCache cache)
    {
        _db = db;
        _cache = cache;
    }

    public async Task<AnalyticsSummaryDto> GetSummaryAsync(DateTime from, DateTime to, CancellationToken ct)
    {
        var key = $"summary:{from:yyyyMMddHH}:{to:yyyyMMddHH}";

        return await _cache.GetOrCreateAsync(key, async entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(2);

            var query = _db.ActivityEvents.Where(x => x.CreatedAt >= from && x.CreatedAt <= to);

            return new AnalyticsSummaryDto(
                await query.CountAsync(ct),
                await query.Select(x => x.UserId).Distinct().CountAsync(ct),
                await query.CountAsync(x => x.Severity == "Error", ct));
        }) ?? new AnalyticsSummaryDto(0, 0, 0);
    }
}

После создания сервиса зарегистрируйте его в DI. Тогда контроллер аналитики на следующем этапе сможет получить AnalyticsService через конструктор.

builder.Services.AddScoped<AnalyticsService>();

Результат

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

Проверка этапа:

  1. Войти под admin.
  2. Выполнить POST /api/generate/events?count=500.
  3. Запросить GET /api/events?page=1&pageSize=20.
  4. Убедиться, что список содержит разные типы событий.
  5. Посмотреть консоль приложения и найти информационные логи.

Если после генерации список пустой, проверьте, что генератор использует тот же AppDbContext, что и API, и что база содержит пользователей.