Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
}
Loading