Files
parr_api/PARR.Core/Services/RobotMetrics/RobotMetricsService.cs

261 lines
15 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using PARR.Core.Extensions;
using PARR.Core.Repositories.Interfaces;
using PARR.Core.Repositories.Interfaces.RobotRepositories;
using PARR.Domain.DTOs.RobotMetrics;
using PARR.Domain.Enums;
using PARR.Domain.Exceptions;
namespace PARR.Core.Services.RobotMetrics
{
internal class RobotMetricsService : IRobotMetricsService
{
private readonly IRobotConfigurationSnapshotRepository _snapshotRepository;
private readonly IRobotConfigurationRepository _configurationRepository;
private readonly ILogger<RobotMetricsService> _logger;
public RobotMetricsService(
IRobotConfigurationSnapshotRepository snapshotRepository,
IRobotConfigurationRepository configurationRepository,
ILogger<RobotMetricsService> logger
)
{
_snapshotRepository = snapshotRepository;
_configurationRepository = configurationRepository;
_logger = logger;
}
public async Task<List<FilteredChartPoint>> GetFilteredMetricsAsync(MetricFilter filter, CancellationToken cancellationToken)
{
// ------- Правильность расчетов этого метода доконца не проверена -------
if (filter == null)
throw new AppValidationException("Фильтр не может быть пустым.");
if (filter.IntervalMinutes < 1)
throw new AppValidationException("Интервал группировки не может быть меньше 1 минуты.");
var query = _snapshotRepository.Get().AsNoTracking();
if (filter.RobotCode.HasValue)
query = query.Where(t => t.RobotCode == (int)filter.RobotCode.Value);
if (filter.RobotStatusCode.HasValue)
query = query.Where(t => t.RobotStatusCode == (int)filter.RobotStatusCode.Value);
if (filter.TaskStatusCode.HasValue)
query = query.Where(t => t.TaskStatusCode == (int)filter.TaskStatusCode);
// Если даты не переданы, берем последние 24 часа по умолчанию
var dateFrom = filter.DateFrom ?? DateTimeOffset.UtcNow.AddDays(-1);
query = query.Where(s => s.DateCreated >= dateFrom);
if (filter.DateTo.HasValue)
query = query.Where(s => s.DateCreated <= filter.DateTo.Value);
var dbData = await query.ToListAsync(cancellationToken);
var dbGrouped = dbData
.GroupBy(s => RoundToInterval(s.DateCreated, filter.IntervalMinutes))
.ToDictionary(t => t.Key, t => t.ToList());
var dateTo = filter.DateTo ?? DateTimeOffset.UtcNow;
var result = GenerateTimeGrid(dateFrom, dateTo, filter.IntervalMinutes)
.Select(time => new FilteredChartPoint(
Timestamp: time,
//Count: dbGrouped.TryGetValue(time, out var points) ? points.Max(x => x.Count) : 0
Count: dbGrouped.TryGetValue(time, out var points)
? points.GroupBy(x => x.DateCreated) // Группируем по точной минуте снапшота
.Select(g => g.Sum(x => x.Count)) // Складываем всё, что подошли под фильтр в эту минуту
.MaxOrDefault() // Берем максимальный пик за весь интервал (например, за час)
: 0 // Если снапшотов не было — честный ноль
)).ToList();
return result;
}
public async Task<List<RobotStatusChartPoint>> GetRobotStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken)
{
ValidateRobot(robotCode);
CalculatePeriodDates(period, out var fromDate, out var intervalMinutes);
// История из снапшотов
var snapshots = await _snapshotRepository.Get()
.AsNoTracking()
.Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate)
.ToListAsync(cancellationToken);
var dbGrouped = snapshots
.GroupBy(t => RoundToInterval(t.DateCreated, intervalMinutes))
.ToDictionary(t => t.Key, t => t.ToList());
// Генерим сетку значений, если значений нет, вставляем нули
var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes)
.Select(time => dbGrouped.TryGetValue(time, out var points)
? new RobotStatusChartPoint(
Timestamp: time,
// группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал
Wait: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).MaxOrDefault(x => x.Count),
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait)
.GroupBy(x => x.DateCreated)
.Select(t => t.Sum(x => x.Count))
.MaxOrDefault(),
InProgress: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).MaxOrDefault(x => x.Count),
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress)
.GroupBy(x => x.DateCreated)
.Select(g => g.Sum(x => x.Count))
.MaxOrDefault(),
Error: //points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).MaxOrDefault(x => x.Count)
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error)
.GroupBy(x => x.DateCreated)
.Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота
.MaxOrDefault(),
Complete:
points.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete)
.GroupBy(x => x.DateCreated)
.Select(g => g.Sum(x => x.Count)) // Честная сумма всех ошибок в рамках одной минуты снапшота
.MaxOrDefault()
)
: new RobotStatusChartPoint(time, Wait: 0, InProgress: 0, Error: 0, Complete: 0)
).ToList();
// Последнее значение в конце графика из реальной таблицы
var liveRaw = await _configurationRepository
.Get()
.AsNoTracking()
.Where(t => t.RobotCode == (int)robotCode)
.GroupBy(t => t.RobotStatusCode)
.Select(g => new { RobotStatusCode = g.Key, Count = g.Count() })
.ToListAsync(cancellationToken);
history.Add(new RobotStatusChartPoint(
Timestamp: DateTimeOffset.UtcNow,
Wait: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Wait).Sum(x => x.Count),
InProgress: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.InProgress).Sum(x => x.Count),
Error: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Error).Sum(x => x.Count),
Complete: liveRaw.Where(x => x.RobotStatusCode == (int)RobotStatusEnum.Complete).Sum(x => x.Count)
));
return history;
}
public async Task<List<TaskStatusChartPoint>> GetTaskStatusMetricsAsync(RobotsEnum robotCode, ChartPeriod period, CancellationToken cancellationToken)
{
ValidateRobot(robotCode);
CalculatePeriodDates(period, out var fromDate, out var intervalMinutes);
// История из снапшотов
var snapshots = await _snapshotRepository.Get()
.AsNoTracking()
.Where(s => s.RobotCode == (int)robotCode && s.DateCreated >= fromDate)
.ToListAsync(cancellationToken);
var dbGrouped = snapshots
.GroupBy(s => RoundToInterval(s.DateCreated, intervalMinutes))
.ToDictionary(t => t.Key, t => t.ToList());
var history = GenerateTimeGrid(fromDate, DateTimeOffset.UtcNow, intervalMinutes)
.Select(time => dbGrouped.TryGetValue(time, out var points)
? new TaskStatusChartPoint(
Timestamp: time,
// группируем по точной минуте снапшота, складываем внутренности, а потом ищем пик (Max) за весь интервал
Creating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).MaxOrDefault(x => x.Count),
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating)
.GroupBy(x => x.DateCreated)
.Select(g => g.Sum(x => x.Count))
.MaxOrDefault(),
Updating: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).MaxOrDefault(x => x.Count),
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating)
.GroupBy(x => x.DateCreated)
.Select(g => g.Sum(x => x.Count))
.MaxOrDefault(),
Ok: //points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).MaxOrDefault(x => x.Count)
points.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok)
.GroupBy(x => x.DateCreated)
.Select(g => g.Sum(x => x.Count))
.MaxOrDefault()
)
: new TaskStatusChartPoint(time, Creating: 0, Updating: 0, Ok: 0)
).ToList();
// Живой текущий кадр в конец графика
var liveRaw = await _configurationRepository
.Get()
.AsNoTracking()
.Where(t => t.RobotCode == (int)robotCode)
.GroupBy(t => t.TaskStatusCode)
.Select(g => new { TaskStatusCode = g.Key, Count = g.Count() })
.ToListAsync(cancellationToken);
history.Add(new TaskStatusChartPoint(
Timestamp: DateTimeOffset.UtcNow,
Creating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Creating).Sum(x => x.Count),
Updating: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Updating).Sum(x => x.Count),
Ok: liveRaw.Where(x => x.TaskStatusCode == (int)TaskStatusEnum.Ok).Sum(x => x.Count)
));
return history;
}
private void ValidateRobot(RobotsEnum robotCode)
{
if (!Enum.IsDefined(typeof(RobotsEnum), robotCode))
throw new NotFoundException($"Робот с кодом {robotCode} не найден в системе.");
}
private void CalculatePeriodDates(ChartPeriod period, out DateTimeOffset fromDate, out int intervalMinutes)
{
switch (period)
{
case ChartPeriod.TwoHours:
fromDate = DateTimeOffset.UtcNow.AddHours(-2);
intervalMinutes = 2;
break;
case ChartPeriod.TwentyFourHours:
fromDate = DateTimeOffset.UtcNow.AddDays(-1);
intervalMinutes = 30;
break;
case ChartPeriod.SevenDays:
fromDate = DateTimeOffset.UtcNow.AddDays(-7);
intervalMinutes = 60;
break;
default:
throw new AppValidationException("Указан неподдерживаемый период времени.");
}
}
private DateTimeOffset RoundToInterval(DateTimeOffset dt, int intervalMinutes)
{
var minutes = (dt.Minute / intervalMinutes) * intervalMinutes;
return new DateTimeOffset(dt.Year, dt.Month, dt.Day, dt.Hour, minutes, 0, dt.Offset);
}
/// <summary>
/// Генератор сетки времени
/// </summary>
/// <param name="fromDate"></param>
/// <param name="toDate"></param>
/// <param name="intervalMinutes"></param>
/// <returns></returns>
private IEnumerable<DateTimeOffset> GenerateTimeGrid(DateTimeOffset fromDate, DateTimeOffset toDate, int intervalMinutes)
{
var startTime = RoundToInterval(fromDate, intervalMinutes);
var endTime = RoundToInterval(toDate, intervalMinutes);
for (var time = startTime; time <= endTime; time = time.AddMinutes(intervalMinutes))
{
yield return time;
}
}
}
}