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 подключает встроенное хранилище, которое позже будет использовать аналитический сервис.
Код: логирование¶
Логирование добавляется в момент создания события, потому что это ключевая операция журнала активности. Такая запись помогает понять, какой тип события был создан и от имени какого пользователя.
Настройки логирования в 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 через конструктор.
Результат¶
Администратор может создать тестовую нагрузку, а аналитические запросы не пересчитываются при каждом обращении.
Проверка этапа:
- Войти под
admin. - Выполнить
POST /api/generate/events?count=500. - Запросить
GET /api/events?page=1&pageSize=20. - Убедиться, что список содержит разные типы событий.
- Посмотреть консоль приложения и найти информационные логи.
Если после генерации список пустой, проверьте, что генератор использует тот же AppDbContext, что и API, и что база содержит пользователей.