feat(api,core,domain): RobotSnapshot - почасовой отчет о загрузке роботов
This commit is contained in:
@@ -50,6 +50,8 @@
|
|||||||
public const string GetNextRun = Base + "/tests/next-run";
|
public const string GetNextRun = Base + "/tests/next-run";
|
||||||
|
|
||||||
public const string CreateCache = Base + "/tests/cache/";
|
public const string CreateCache = Base + "/tests/cache/";
|
||||||
|
|
||||||
|
public const string TestHandler = Base + "/tests/test/";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Template
|
public static class Template
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ using PARR.Core.Common.Interfaces;
|
|||||||
using PARR.Core.Repositories.Interfaces;
|
using PARR.Core.Repositories.Interfaces;
|
||||||
using PARR.Core.Repositories.Interfaces.Unit;
|
using PARR.Core.Repositories.Interfaces.Unit;
|
||||||
using PARR.Core.Services.NextRunServices;
|
using PARR.Core.Services.NextRunServices;
|
||||||
|
using PARR.Core.Services.RobotSnapshotServices;
|
||||||
using PARR.Core.Services.UnitService.Interfaces;
|
using PARR.Core.Services.UnitService.Interfaces;
|
||||||
using PARR.Core.Services.Workload.Implementations;
|
using PARR.Core.Services.Workload.Implementations;
|
||||||
using PARR.Domain.Cache;
|
using PARR.Domain.Cache;
|
||||||
|
using PARR.Domain.DTOs.RobotSnapshotDTO;
|
||||||
|
|
||||||
namespace PARR.API.Controllers.V1
|
namespace PARR.API.Controllers.V1
|
||||||
{
|
{
|
||||||
@@ -27,6 +29,7 @@ namespace PARR.API.Controllers.V1
|
|||||||
private readonly ILogger<TestController> logger;
|
private readonly ILogger<TestController> logger;
|
||||||
private readonly IUnitService unitService;
|
private readonly IUnitService unitService;
|
||||||
private readonly IUnitRepository unitRepository;
|
private readonly IUnitRepository unitRepository;
|
||||||
|
private readonly IRobotSnapshotService _robotSnapshotService;
|
||||||
|
|
||||||
public TestController(
|
public TestController(
|
||||||
IClientService clientService,
|
IClientService clientService,
|
||||||
@@ -37,7 +40,8 @@ namespace PARR.API.Controllers.V1
|
|||||||
WorkloadCacheService workloadCacheService,
|
WorkloadCacheService workloadCacheService,
|
||||||
ILogger<TestController> logger,
|
ILogger<TestController> logger,
|
||||||
IUnitService unitService,
|
IUnitService unitService,
|
||||||
IUnitRepository unitRepository
|
IUnitRepository unitRepository,
|
||||||
|
IRobotSnapshotService robotSnapshotService
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
this.clientService = clientService;
|
this.clientService = clientService;
|
||||||
@@ -48,6 +52,7 @@ namespace PARR.API.Controllers.V1
|
|||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.unitService = unitService;
|
this.unitService = unitService;
|
||||||
this.unitRepository = unitRepository;
|
this.unitRepository = unitRepository;
|
||||||
|
_robotSnapshotService = robotSnapshotService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -161,6 +166,22 @@ namespace PARR.API.Controllers.V1
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Тестовый метод
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
[HttpGet(ApiRoutes.Test.TestHandler)]
|
||||||
|
public async Task<IActionResult> Test()
|
||||||
|
{
|
||||||
|
var data = await _robotSnapshotService.GetHourlyAnalyticsAsync(new RobotAnalyticsQuery
|
||||||
|
{
|
||||||
|
DateStart = DateTimeOffset.UtcNow.AddHours(-1),
|
||||||
|
DateEnd = DateTimeOffset.UtcNow,
|
||||||
|
Offset = TimeSpan.FromMinutes(600)
|
||||||
|
});
|
||||||
|
|
||||||
|
return Ok(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,25 @@ namespace PARR.Core.Services.RobotSnapshotServices
|
|||||||
{
|
{
|
||||||
public interface IRobotSnapshotService
|
public interface IRobotSnapshotService
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Получить статистику
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queryDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
Task<List<RobotSnapshotItemDto>> GetAsync(RobotSnapshotQuery queryDto);
|
Task<List<RobotSnapshotItemDto>> GetAsync(RobotSnapshotQuery queryDto);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Записать статистику
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="robotSnapshot"></param>
|
||||||
|
/// <returns></returns>
|
||||||
Task<RobotSnapshotItemDto> CreateAsync(CreateRobotSnapshot robotSnapshot);
|
Task<RobotSnapshotItemDto> CreateAsync(CreateRobotSnapshot robotSnapshot);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Получить аналитику за период
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="queryDto"></param>
|
||||||
|
/// <returns></returns>
|
||||||
|
Task<List<ServerHourlyAnalyticsDto>> GetHourlyAnalyticsAsync(RobotAnalyticsQuery queryDto);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,145 @@ namespace PARR.Core.Services.RobotSnapshotServices
|
|||||||
result.Add(new RobotSnapshotItemDto { Robot = userDto, Snapshots = snapshots });
|
result.Add(new RobotSnapshotItemDto { Robot = userDto, Snapshots = snapshots });
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.OrderBy(t => t.Robot.Ip).ThenBy(t => t.Robot.Name).ToList();
|
return result.OrderBy(t => t.Robot.Name).ThenBy(t => t.Robot.Ip).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async Task<List<ServerHourlyAnalyticsDto>> GetHourlyAnalyticsAsync(RobotAnalyticsQuery queryDto)
|
||||||
|
{
|
||||||
|
var snapshotsQuery = robotSnapshotRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => queryDto.DateStart <= t.DateCreated && t.DateCreated <= queryDto.DateEnd);
|
||||||
|
|
||||||
|
// Фильтруем по конкретному IP
|
||||||
|
if (!string.IsNullOrEmpty(queryDto.Ip))
|
||||||
|
snapshotsQuery = snapshotsQuery.Where(t => t.Ip == queryDto.Ip);
|
||||||
|
|
||||||
|
// Превращает TimeSpan в строку вида "+03:00" или "-05:00"
|
||||||
|
string formattedOffset = (queryDto.Offset >= TimeSpan.Zero ? "+" : "-") +
|
||||||
|
queryDto.Offset.ToString(@"hh\:mm");
|
||||||
|
|
||||||
|
// Группируем на стороне PostgreSQL с учетом часового пояса пользователя.
|
||||||
|
// Метод .Add(offset) сдвигает UTC-дату в Postgres на интервал времени пользователя,
|
||||||
|
// благодаря чему минуты объединяются в правильные локальные "часовые корзины".
|
||||||
|
var rawGroupedData = await snapshotsQuery
|
||||||
|
.GroupBy(t => new
|
||||||
|
{
|
||||||
|
t.Ip,
|
||||||
|
// Явно сдвигаем дату и достаем только нужные компоненты
|
||||||
|
Year = (t.DateCreated + queryDto.Offset).DateTime.Year,
|
||||||
|
Month = (t.DateCreated + queryDto.Offset).DateTime.Month,
|
||||||
|
Day = (t.DateCreated + queryDto.Offset).DateTime.Day,
|
||||||
|
Hour = (t.DateCreated + queryDto.Offset).DateTime.Hour
|
||||||
|
})
|
||||||
|
.Select(g => new
|
||||||
|
{
|
||||||
|
g.Key.Ip,
|
||||||
|
// Вытаскиваем компоненты даты из уже смещенного локального времени
|
||||||
|
LocalYear = g.Key.Year,
|
||||||
|
LocalMonth = g.Key.Month,
|
||||||
|
LocalDay = g.Key.Day,
|
||||||
|
LocalHour = g.Key.Hour,
|
||||||
|
|
||||||
|
MaxAllowed = g.Max(t => t.MaxRobots),
|
||||||
|
MaxTemplates = g.Max(t => t.TemplateRobotsCount),
|
||||||
|
MaxSchedules = g.Max(t => t.ScheduleRobotsCount),
|
||||||
|
SumTemplates = g.Sum(t => t.TemplateRobotsCount),
|
||||||
|
SumSchedules = g.Sum(t => t.ScheduleRobotsCount)
|
||||||
|
})
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
if (!rawGroupedData.Any())
|
||||||
|
return new List<ServerHourlyAnalyticsDto>();
|
||||||
|
|
||||||
|
// Получаем информацию о серверах
|
||||||
|
var serverIps = rawGroupedData.Select(g => g.Ip).ToHashSet();
|
||||||
|
var serverDictionary = await userRepository.Get()
|
||||||
|
.AsNoTracking()
|
||||||
|
.Where(t => serverIps.Contains(t.Ip))
|
||||||
|
.ToDictionaryAsync(t => t.Ip);
|
||||||
|
|
||||||
|
// Группируем полученные агрегаты по серверам уже в памяти веб-сервера
|
||||||
|
var dataByServers = rawGroupedData.GroupBy(g => g.Ip);
|
||||||
|
var result = new List<ServerHourlyAnalyticsDto>();
|
||||||
|
|
||||||
|
// Принудительно переводим UTC-границы запроса в локальное время пользователя для корректной генерации "дыр"
|
||||||
|
var localStart = queryDto.DateStart.ToOffset(queryDto.Offset).DateTime;
|
||||||
|
var localEnd = queryDto.DateEnd.ToOffset(queryDto.Offset).DateTime;
|
||||||
|
|
||||||
|
// Округляем начальный час до ровного значения (00 минут, 00 секунд)
|
||||||
|
var startHour = new DateTime(localStart.Year, localStart.Month, localStart.Day, localStart.Hour, 0, 0);
|
||||||
|
|
||||||
|
foreach (var serverGroup in dataByServers)
|
||||||
|
{
|
||||||
|
serverDictionary.TryGetValue(serverGroup.Key, out var robotInfo);
|
||||||
|
|
||||||
|
var userDto = new UserBaseDto
|
||||||
|
{
|
||||||
|
Ip = serverGroup.Key,
|
||||||
|
Description = robotInfo?.Description ?? string.Empty,
|
||||||
|
Name = robotInfo?.Name ?? string.Empty
|
||||||
|
};
|
||||||
|
|
||||||
|
// Формируем точки, которые удалось вытащить из базы данных
|
||||||
|
var hourlyPoints = serverGroup.Select(g => new RobotHourlyPointDto
|
||||||
|
{
|
||||||
|
// Возвращаем дату со смещением пользователя, чтобы Angular сразу отображал правильный час
|
||||||
|
Hour = new DateTimeOffset(g.LocalYear, g.LocalMonth, g.LocalDay, g.LocalHour, 0, 0, TimeSpan.Zero),
|
||||||
|
MaxAllowedRobots = g.MaxAllowed,
|
||||||
|
MaxTemplateRobots = g.MaxTemplates,
|
||||||
|
MaxScheduleRobots = g.MaxSchedules,
|
||||||
|
// Считаем среднее количество роботов, деля поминутную сумму строго на 60 минут часа
|
||||||
|
AvgTemplateRobots = Math.Round(g.SumTemplates / 60.0, 1),
|
||||||
|
AvgScheduleRobots = Math.Round(g.SumSchedules / 60.0, 1)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Алгоритм заполнения пропусков во времени (если сервер был выключен)
|
||||||
|
var filledPoints = new List<RobotHourlyPointDto>();
|
||||||
|
var currentHour = startHour;
|
||||||
|
int lastKnownMax = hourlyPoints.FirstOrDefault()?.MaxAllowedRobots ?? 0;
|
||||||
|
|
||||||
|
while (currentHour <= localEnd)
|
||||||
|
{
|
||||||
|
// Создаем временную точку со смещением для точного сопоставления
|
||||||
|
var targetOffsetDateTime = new DateTimeOffset(currentHour, TimeSpan.Zero);
|
||||||
|
var point = hourlyPoints.FirstOrDefault(p => p.Hour == targetOffsetDateTime);
|
||||||
|
|
||||||
|
if (point != null)
|
||||||
|
{
|
||||||
|
filledPoints.Add(point);
|
||||||
|
lastKnownMax = point.MaxAllowedRobots;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Если за этот час записей в БД нет — значит, сервер был полностью оффлайн.
|
||||||
|
// Выводим нули, чтобы график в Angular не прерывался, а плавно падал.
|
||||||
|
filledPoints.Add(new RobotHourlyPointDto
|
||||||
|
{
|
||||||
|
Hour = targetOffsetDateTime,
|
||||||
|
MaxAllowedRobots = lastKnownMax,
|
||||||
|
MaxTemplateRobots = 0,
|
||||||
|
AvgTemplateRobots = 0,
|
||||||
|
MaxScheduleRobots = 0,
|
||||||
|
AvgScheduleRobots = 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Шагаем строго на +1 час вперед по локальному времени клиента
|
||||||
|
currentHour = currentHour.AddHours(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(new ServerHourlyAnalyticsDto
|
||||||
|
{
|
||||||
|
Robot = userDto,
|
||||||
|
Snapshots = filledPoints.OrderBy(p => p.Hour).ToList()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
.OrderBy(t => t.Robot.Name)
|
||||||
|
.ThenBy(t => t.Robot.Ip)
|
||||||
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
16
PARR.Domain/DTOs/RobotSnapshotDTO/RobotAnalyticsQuery.cs
Normal file
16
PARR.Domain/DTOs/RobotSnapshotDTO/RobotAnalyticsQuery.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace PARR.Domain.DTOs.RobotSnapshotDTO
|
||||||
|
{
|
||||||
|
public record RobotAnalyticsQuery
|
||||||
|
{
|
||||||
|
public DateTimeOffset DateStart { get; init; }
|
||||||
|
|
||||||
|
public DateTimeOffset DateEnd { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Смещение часового пояса
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Offset { get; init; }
|
||||||
|
|
||||||
|
public string? Ip { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using PARR.Domain.DTOs.User;
|
||||||
|
|
||||||
|
namespace PARR.Domain.DTOs.RobotSnapshotDTO
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Аналитика по снапшотам роботов
|
||||||
|
/// </summary>
|
||||||
|
public record ServerHourlyAnalyticsDto
|
||||||
|
{
|
||||||
|
public UserBaseDto Robot { get; init; } = null!;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Почасовая статистика для гарфиков
|
||||||
|
/// </summary>
|
||||||
|
public List<RobotHourlyPointDto> Snapshots { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Почасовая статистика по снапшотам роботов
|
||||||
|
/// </summary>
|
||||||
|
public record RobotHourlyPointDto
|
||||||
|
{
|
||||||
|
public DateTimeOffset Hour { get; set; }
|
||||||
|
|
||||||
|
public int MaxAllowedRobots { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Максимальное кол-во роботов работающих одновременно по шаблонам
|
||||||
|
/// </summary>
|
||||||
|
public int MaxTemplateRobots { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сколько роботов в среднем, работало в течении часа
|
||||||
|
/// </summary>
|
||||||
|
public double AvgTemplateRobots { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Максимальное кол-во роботов работающих одновременно по расписаниям
|
||||||
|
/// </summary>
|
||||||
|
public int MaxScheduleRobots { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Сколько роботов в среднем, работало в течении часа
|
||||||
|
/// </summary>
|
||||||
|
public double AvgScheduleRobots { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user