Skip to content

Commit

Permalink
adding new dailyStats endpont
Browse files Browse the repository at this point in the history
Signed-off-by: Neil South <[email protected]>
  • Loading branch information
neildsouth committed May 14, 2024
1 parent 76cef11 commit ba3ef1d
Show file tree
Hide file tree
Showing 6 changed files with 190 additions and 54 deletions.
26 changes: 26 additions & 0 deletions src/WorkflowManager/Contracts/Models/ApplicationReviewStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2023 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

namespace Monai.Deploy.WorkflowManager.Common.Contracts.Models
{
public enum ApplicationReviewStatus
{
Approved,
Rejected,
Cancelled,
AwaitingReview
}
}
1 change: 0 additions & 1 deletion src/WorkflowManager/Contracts/Models/ExecutionStatDTO.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,4 @@ public ExecutionStatDTO(ExecutionStats stats)
public double ExecutionDurationSeconds { get; set; }
public string Status { get; set; } = "Created";
}

}
39 changes: 39 additions & 0 deletions src/WorkflowManager/Contracts/Models/ExecutionStatDayOverview.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2023 MONAI Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System;
using Newtonsoft.Json;

namespace Monai.Deploy.WorkflowManager.Common.Contracts.Models
{
public class ExecutionStatDayOverview
{
[JsonProperty("date")]
public DateOnly Date { get; set; }
[JsonProperty("total_executions")]
public int TotalExecutions { get; set; }
[JsonProperty("total_failures")]
public int TotalFailures { get; set; }
[JsonProperty("total_approvals")]
public int TotalApprovals { get; set; }
[JsonProperty("total_rejections")]
public int TotalRejections { get; set; }
[JsonProperty("total_cancelled")]
public int TotalCancelled { get; set; }
[JsonProperty("total_awaiting_review")]
public int TotalAwaitingReview { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ public interface ITaskExecutionStatsRepository
/// <returns></returns>
Task UpdateExecutionStatsAsync(TaskCancellationEvent taskCanceledEvent, string workflowId, string correlationId);

/// <summary>
/// Returns all entries between the two given dates
/// </summary>
/// <param name="startTime">start of the range.</param>
/// <param name="endTime">end of the range.</param>
/// <param name="workflowId">optional workflow id.</param>
/// <param name="taskId">optional task id.</param>
/// <returns>a collections of stats</returns>
Task<IEnumerable<ExecutionStats>> GetAllStatsAsync(DateTime startTime, DateTime endTime, string workflowId = "", string taskId = "");
/// <summary>
/// Returns paged entries between the two given dates
/// </summary>
Expand All @@ -62,7 +71,7 @@ public interface ITaskExecutionStatsRepository
/// <param name="workflowId">optional workflow id.</param>
/// <param name="taskId">optional task id.</param>
/// <returns>a collections of stats</returns>
Task<IEnumerable<ExecutionStats>> GetStatsAsync(DateTime startTime, DateTime endTime, int pageSize = 10, int pageNumber = 1, string workflowId = "", string taskId = "");
Task<IEnumerable<ExecutionStats>> GetStatsAsync(DateTime startTime, DateTime endTime, int? pageSize = 10, int? pageNumber = 1, string workflowId = "", string taskId = "");

/// <summary>
/// Return the count of the entries with this status, or all if no status given.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Ardalis.GuardClauses;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Monai.Deploy.Messaging.Events;
Expand All @@ -40,11 +39,7 @@ public TaskExecutionStatsRepository(
IOptions<ExecutionStatsDatabaseSettings> databaseSettings,
ILogger<TaskExecutionStatsRepository> logger)
{
if (client == null)
{
throw new ArgumentNullException(nameof(client));
}

_ = client ?? throw new ArgumentNullException(nameof(client));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var mongoDatabase = client.GetDatabase(databaseSettings.Value.DatabaseName, null);
_taskExecutionStatsCollection = mongoDatabase.GetCollection<ExecutionStats>("ExecutionStats", null);
Expand Down Expand Up @@ -149,17 +144,24 @@ await _taskExecutionStatsCollection.UpdateOneAsync(o =>
}
}

public async Task<IEnumerable<ExecutionStats>> GetStatsAsync(DateTime startTime, DateTime endTime, int pageSize = 10, int pageNumber = 1, string workflowId = "", string taskId = "")
public async Task<IEnumerable<ExecutionStats>> GetAllStatsAsync(DateTime startTime, DateTime endTime, string workflowId = "", string taskId = "")
{
return await GetStatsAsync(startTime, endTime, null, null, workflowId, taskId);
}

public async Task<IEnumerable<ExecutionStats>> GetStatsAsync(DateTime startTime, DateTime endTime, int? pageSize = 10, int? pageNumber = 1, string workflowId = "", string taskId = "")
{
CreateFilter(startTime, endTime, workflowId, taskId, out var builder, out var filter);

filter &= builder.Where(GetExecutedTasksFilter());

var result = await _taskExecutionStatsCollection.Find(filter)
.Limit(pageSize)
.Skip((pageNumber - 1) * pageSize)
.ToListAsync();
return result;
var result = _taskExecutionStatsCollection.Find(filter);
if (pageSize is not null)
{
result = result.Limit(pageSize).Skip((pageNumber - 1) * pageSize);
}

return await result.ToListAsync();
}

private static ExecutionStats ExposeExecutionStats(ExecutionStats taskExecutionStats, TaskExecution taskUpdateEvent)
Expand Down
141 changes: 101 additions & 40 deletions src/WorkflowManager/WorkflowManager/Controllers/TaskStatsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,59 @@ public async Task<IActionResult> GetOverviewAsync([FromQuery] DateTime startTime
}
}

/// <summary>
/// Get execution daily stats for a given time period.
/// </summary>
/// <param name="filter">TimeFiler defining start and end times, plus paging options.</param>
/// <param name="workflowId">WorkflowId if you want stats just for a given workflow. (both workflowId and TaskId must be given, if you give one).</param>
/// <returns>a paged obect with all the stat details.</returns>
[ProducesResponseType(typeof(StatsPagedResponse<List<ExecutionStatDTO>>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status500InternalServerError)]
[HttpGet("dailystats")]
public async Task<IActionResult> GetDailyStatsAsync([FromQuery] TimeFilter filter, string workflowId = "")
{
SetUpFilter(filter, out var route, out var pageSize, out var validFilter);

try
{
var allStats = await _repository.GetAllStatsAsync(filter.StartTime, filter.EndTime, workflowId, string.Empty);
var statsDto = allStats
.OrderBy(a => a.StartedUTC)
.GroupBy(s => s.StartedUTC.Date)
.Select(g => new ExecutionStatDayOverview
{
Date = DateOnly.FromDateTime(g.Key.Date),
TotalExecutions = g.Count(),
TotalFailures = g.Count(i => string.Compare(i.Status, "Failed", true) == 0),
TotalApprovals = g.Count(i => string.Compare(i.Status, ApplicationReviewStatus.Approved.ToString(), true) == 0),
TotalRejections = g.Count(i => string.Compare(i.Status, ApplicationReviewStatus.Rejected.ToString(), true) == 0),
TotalCancelled = g.Count(i => string.Compare(i.Status, ApplicationReviewStatus.Cancelled.ToString(), true) == 0),
TotalAwaitingReview = g.Count(i => string.Compare(i.Status, ApplicationReviewStatus.AwaitingReview.ToString(), true) == 0),
});

var pagedStats = statsDto.Skip((filter.PageNumber - 1) * pageSize).Take(pageSize);

var res = CreateStatsPagedResponse(pagedStats, validFilter, statsDto.Count(), _uriService, route);
var (avgTotalExecution, avgArgoExecution) = await _repository.GetAverageStats(filter.StartTime, filter.EndTime, workflowId, string.Empty);

res.PeriodStart = filter.StartTime;
res.PeriodEnd = filter.EndTime;
res.TotalExecutions = allStats.Count();
res.TotalSucceeded = statsDto.Sum(s => s.TotalApprovals);
res.TotalFailures = statsDto.Sum(s => s.TotalFailures);
res.TotalInprogress = statsDto.Sum(s => s.TotalAwaitingReview);
res.AverageTotalExecutionSeconds = Math.Round(avgTotalExecution, 2);
res.AverageArgoExecutionSeconds = Math.Round(avgArgoExecution, 2);

return Ok(res);
}
catch (Exception e)
{
_logger.GetStatsAsyncError(e);
return Problem($"Unexpected error occurred: {e.Message}", $"tasks/stats", InternalServerError);
}
}

/// <summary>
/// Get execution stats for a given time period.
/// </summary>
Expand All @@ -133,63 +186,71 @@ public async Task<IActionResult> GetStatsAsync([FromQuery] TimeFilter filter, st
return Problem("Failed to validate ids, not a valid guid", "tasks/stats/", BadRequest);
}

if (filter.EndTime == default)
{
filter.EndTime = DateTime.Now;
}
SetUpFilter(filter, out var route, out var pageSize, out var validFilter);

if (filter.StartTime == default)
try
{
filter.StartTime = new DateTime(2023, 1, 1);
}

var route = Request?.Path.Value ?? string.Empty;
var pageSize = filter.PageSize ?? Options.Value.EndpointSettings?.DefaultPageSize ?? 10;
var max = Options.Value.EndpointSettings?.MaxPageSize ?? 20;
var validFilter = new PaginationFilter(filter.PageNumber, pageSize, max);
var allStats = await _repository.GetStatsAsync(filter.StartTime, filter.EndTime, pageSize, filter.PageNumber, workflowId, taskId);
var statsDto = allStats
.OrderBy(a => a.StartedUTC)
.Select(s => new ExecutionStatDTO(s));

try
var res = await GatherPagedStats(filter, workflowId, taskId, route, validFilter, statsDto);
return Ok(res);
}
catch (Exception e)
{
workflowId ??= string.Empty;
taskId ??= string.Empty;
var allStats = _repository.GetStatsAsync(filter.StartTime, filter.EndTime, pageSize, filter.PageNumber, workflowId, taskId);
_logger.GetStatsAsyncError(e);
return Problem($"Unexpected error occurred: {e.Message}", $"tasks/stats", InternalServerError);
}
}

var successes = _repository.GetStatsStatusSucceededCountAsync(filter.StartTime, filter.EndTime, workflowId, taskId);
private async Task<StatsPagedResponse<IEnumerable<T>>> GatherPagedStats<T>(TimeFilter filter, string workflowId, string taskId, string route, PaginationFilter validFilter, IEnumerable<T> statsDto)
{
workflowId ??= string.Empty;
taskId ??= string.Empty;

var fails = _repository.GetStatsStatusFailedCountAsync(filter.StartTime, filter.EndTime, workflowId, taskId);
var successes = _repository.GetStatsStatusSucceededCountAsync(filter.StartTime, filter.EndTime, workflowId, taskId);

var rangeCount = _repository.GetStatsTotalCompleteExecutionsCountAsync(filter.StartTime, filter.EndTime, workflowId, taskId);
var fails = _repository.GetStatsStatusFailedCountAsync(filter.StartTime, filter.EndTime, workflowId, taskId);

var stats = _repository.GetAverageStats(filter.StartTime, filter.EndTime, workflowId, taskId);
var rangeCount = _repository.GetStatsTotalCompleteExecutionsCountAsync(filter.StartTime, filter.EndTime, workflowId, taskId);

var running = _repository.GetStatsStatusCountAsync(filter.StartTime, filter.EndTime, TaskExecutionStatus.Accepted.ToString(), workflowId, taskId);
var stats = _repository.GetAverageStats(filter.StartTime, filter.EndTime, workflowId, taskId);

await Task.WhenAll(allStats, fails, rangeCount, stats, running);
var running = _repository.GetStatsStatusCountAsync(filter.StartTime, filter.EndTime, TaskExecutionStatus.Accepted.ToString(), workflowId, taskId);

ExecutionStatDTO[] statsDto;
await Task.WhenAll(fails, rangeCount, stats, running);

statsDto = allStats.Result
.OrderBy(a => a.StartedUTC)
.Select(s => new ExecutionStatDTO(s))
.ToArray();
var res = CreateStatsPagedResponse(statsDto, validFilter, rangeCount.Result, _uriService, route);

var res = CreateStatsPagedResponse(statsDto, validFilter, rangeCount.Result, _uriService, route);
res.PeriodStart = filter.StartTime;
res.PeriodEnd = filter.EndTime;
res.TotalExecutions = rangeCount.Result;
res.TotalSucceeded = successes.Result;
res.TotalFailures = fails.Result;
res.TotalInprogress = running.Result;
res.AverageTotalExecutionSeconds = Math.Round(stats.Result.avgTotalExecution, 2);
res.AverageArgoExecutionSeconds = Math.Round(stats.Result.avgArgoExecution, 2);
return res;
}

res.PeriodStart = filter.StartTime;
res.PeriodEnd = filter.EndTime;
res.TotalExecutions = rangeCount.Result;
res.TotalSucceeded = successes.Result;
res.TotalFailures = fails.Result;
res.TotalInprogress = running.Result;
res.AverageTotalExecutionSeconds = Math.Round(stats.Result.avgTotalExecution, 2);
res.AverageArgoExecutionSeconds = Math.Round(stats.Result.avgArgoExecution, 2);
return Ok(res);
private void SetUpFilter(TimeFilter filter, out string route, out int pageSize, out PaginationFilter validFilter)
{
if (filter.EndTime == default)
{
filter.EndTime = DateTime.Now;
}
catch (Exception e)

if (filter.StartTime == default)
{
_logger.GetStatsAsyncError(e);
return Problem($"Unexpected error occurred: {e.Message}", $"tasks/stats", InternalServerError);
filter.StartTime = new DateTime(2023, 1, 1);
}

route = Request?.Path.Value ?? string.Empty;
pageSize = filter.PageSize ?? Options.Value.EndpointSettings?.DefaultPageSize ?? 10;
var max = Options.Value.EndpointSettings?.MaxPageSize ?? 20;
validFilter = new PaginationFilter(filter.PageNumber, pageSize, max);
}
}
}

0 comments on commit ba3ef1d

Please sign in to comment.