| | | 1 | | using Elsa.Dashboard.Abstractions.Contracts; |
| | | 2 | | using Elsa.Dashboard.Abstractions.Models; |
| | | 3 | | using Microsoft.Extensions.Hosting; |
| | | 4 | | |
| | | 5 | | namespace Elsa.Dashboard.Api.Services; |
| | | 6 | | |
| | 10 | 7 | | public class DefaultDashboardProvider( |
| | 10 | 8 | | IEnumerable<IDashboardContributor> contributors, |
| | 10 | 9 | | DashboardRangeResolver rangeResolver, |
| | 10 | 10 | | IHostEnvironment environment) : IDashboardProvider |
| | | 11 | | { |
| | | 12 | | public async Task<DashboardOverview> GetOverviewAsync(DashboardQuery query, CancellationToken cancellationToken = de |
| | | 13 | | { |
| | 4 | 14 | | var range = rangeResolver.Resolve(query.Range); |
| | 4 | 15 | | var context = CreateContext(range, query.IncludeSystem, cancellationToken); |
| | 4 | 16 | | var contributions = new List<DashboardOverviewContribution>(); |
| | | 17 | | |
| | 17 | 18 | | foreach (var contributor in OrderedContributors) |
| | | 19 | | { |
| | 10 | 20 | | var contribution = await ExecuteContributorAsync(contributor, x => x.GetOverviewAsync(context).AsTask(), can |
| | 4 | 21 | | if (contribution != null) |
| | 3 | 22 | | contributions.Add(contribution); |
| | | 23 | | } |
| | | 24 | | |
| | 3 | 25 | | return new() |
| | 3 | 26 | | { |
| | 3 | 27 | | BackendName = environment.ApplicationName, |
| | 3 | 28 | | EnvironmentName = environment.EnvironmentName, |
| | 3 | 29 | | Runtime = MergeRuntime(contributions), |
| | 3 | 30 | | WorkflowInstances = MergeWorkflowMetrics(contributions), |
| | 3 | 31 | | Diagnostics = MergeDiagnostics(contributions), |
| | | 32 | | Metrics = contributions.SelectMany(x => x.Metrics).OrderBy(x => x.Order).ThenBy(x => x.Id, StringComparer.Or |
| | | 33 | | Panels = contributions.SelectMany(x => x.Panels).OrderBy(x => x.Order).ThenBy(x => x.Id, StringComparer.Ordi |
| | 3 | 34 | | AppliedRange = range.Key, |
| | 3 | 35 | | From = range.From, |
| | 3 | 36 | | To = range.To |
| | 3 | 37 | | }; |
| | 3 | 38 | | } |
| | | 39 | | |
| | | 40 | | public async Task<DashboardTrendResponse> GetWorkflowTrendsAsync(DashboardTrendRequest request, CancellationToken ca |
| | | 41 | | { |
| | 1 | 42 | | var range = rangeResolver.Resolve(request.Range); |
| | 1 | 43 | | var granularity = rangeResolver.ResolveGranularity(request.Granularity, range.Key); |
| | 1 | 44 | | var context = new DashboardTrendContext(range, granularity, request.IncludeSystem, cancellationToken, Environmen |
| | 3 | 45 | | var responses = await CollectAsync(contributor => contributor.GetWorkflowTrendsAsync(context).AsTask(), cancella |
| | 1 | 46 | | var buckets = responses |
| | | 47 | | .SelectMany(x => x.Buckets) |
| | | 48 | | .GroupBy(x => new { x.From, x.To }) |
| | | 49 | | .Select(x => new DashboardTrendBucket |
| | | 50 | | { |
| | | 51 | | From = x.Key.From, |
| | | 52 | | To = x.Key.To, |
| | | 53 | | CreatedOrStarted = x.Sum(y => y.CreatedOrStarted), |
| | | 54 | | Finished = x.Sum(y => y.Finished), |
| | | 55 | | Faulted = x.Sum(y => y.Faulted), |
| | | 56 | | Suspended = x.Sum(y => y.Suspended), |
| | | 57 | | IncidentBearing = x.Sum(y => y.IncidentBearing) |
| | | 58 | | }) |
| | | 59 | | .OrderBy(x => x.From) |
| | 1 | 60 | | .ToList(); |
| | | 61 | | |
| | 1 | 62 | | return new() |
| | 1 | 63 | | { |
| | 1 | 64 | | Buckets = buckets, |
| | 1 | 65 | | AppliedRange = range.Key, |
| | 1 | 66 | | Granularity = granularity, |
| | 1 | 67 | | From = range.From, |
| | 1 | 68 | | To = range.To |
| | 1 | 69 | | }; |
| | 1 | 70 | | } |
| | | 71 | | |
| | | 72 | | public async Task<DashboardNeedsAttentionResponse> GetNeedsAttentionAsync(DashboardQuery query, int take, Cancellati |
| | | 73 | | { |
| | 2 | 74 | | var range = rangeResolver.Resolve(query.Range); |
| | 2 | 75 | | var context = CreateContext(range, query.IncludeSystem, cancellationToken); |
| | 6 | 76 | | var findings = await CollectManyAsync(contributor => contributor.GetFindingsAsync(context).AsTask(), cancellatio |
| | | 77 | | |
| | 2 | 78 | | return new() |
| | 2 | 79 | | { |
| | 2 | 80 | | Findings = findings |
| | | 81 | | .OrderBy(x => x.Priority) |
| | | 82 | | .ThenBy(x => x.Id, StringComparer.Ordinal) |
| | 2 | 83 | | .Take(Math.Clamp(take, 1, 50)) |
| | 2 | 84 | | .ToList(), |
| | 2 | 85 | | AppliedRange = range.Key |
| | 2 | 86 | | }; |
| | 2 | 87 | | } |
| | | 88 | | |
| | | 89 | | public async Task<DashboardRecentActivityResponse> GetRecentActivityAsync(DashboardQuery query, int take, Cancellati |
| | | 90 | | { |
| | 1 | 91 | | var range = rangeResolver.Resolve(query.Range); |
| | 1 | 92 | | var context = new DashboardListContext(range, Math.Clamp(take, 1, 100), query.IncludeSystem, cancellationToken, |
| | 3 | 93 | | var responses = await CollectAsync(contributor => contributor.GetRecentActivityAsync(context).AsTask(), cancella |
| | 1 | 94 | | var items = responses |
| | | 95 | | .SelectMany(x => x.Items) |
| | | 96 | | .OrderByDescending(x => x.UpdatedAt ?? x.FinishedAt ?? x.CreatedAt) |
| | | 97 | | .ThenBy(x => x.InstanceId, StringComparer.Ordinal) |
| | 1 | 98 | | .Take(context.Take) |
| | 1 | 99 | | .ToList(); |
| | | 100 | | |
| | 1 | 101 | | return new() |
| | 1 | 102 | | { |
| | 1 | 103 | | Items = items, |
| | 1 | 104 | | AppliedRange = range.Key, |
| | 1 | 105 | | From = range.From, |
| | 1 | 106 | | To = range.To |
| | 1 | 107 | | }; |
| | 1 | 108 | | } |
| | | 109 | | |
| | | 110 | | public async Task<DashboardWorkflowHotspotsResponse> GetWorkflowHotspotsAsync(DashboardWorkflowHotspotsRequest reque |
| | | 111 | | { |
| | 0 | 112 | | var range = rangeResolver.Resolve(request.Range); |
| | 0 | 113 | | var metric = NormalizeHotspotMetric(request.Metric); |
| | 0 | 114 | | var take = Math.Clamp(request.Take, 1, 50); |
| | 0 | 115 | | var context = new DashboardHotspotsContext(range, metric, take, request.IncludeSystem, cancellationToken, Enviro |
| | 0 | 116 | | var responses = await CollectAsync(contributor => contributor.GetWorkflowHotspotsAsync(context).AsTask(), cancel |
| | 0 | 117 | | var items = responses |
| | | 118 | | .SelectMany(x => x.Items) |
| | | 119 | | .GroupBy(x => x.DefinitionId) |
| | | 120 | | .Select(x => new DashboardHotspot |
| | | 121 | | { |
| | | 122 | | DefinitionId = x.Key, |
| | | 123 | | WorkflowName = x.Select(y => y.WorkflowName).FirstOrDefault(y => !string.IsNullOrWhiteSpace(y)), |
| | | 124 | | Value = x.Sum(y => y.Value), |
| | | 125 | | AverageDuration = AverageDuration(x.Select(y => y.AverageDuration)) |
| | | 126 | | }) |
| | | 127 | | .OrderByDescending(x => x.Value) |
| | | 128 | | .ThenBy(x => x.WorkflowName, StringComparer.Ordinal) |
| | 0 | 129 | | .Take(take) |
| | 0 | 130 | | .ToList(); |
| | | 131 | | |
| | 0 | 132 | | return new() |
| | 0 | 133 | | { |
| | 0 | 134 | | Items = items, |
| | 0 | 135 | | AppliedRange = range.Key, |
| | 0 | 136 | | Metric = metric, |
| | 0 | 137 | | From = range.From, |
| | 0 | 138 | | To = range.To |
| | 0 | 139 | | }; |
| | 0 | 140 | | } |
| | | 141 | | |
| | | 142 | | private IReadOnlyCollection<IDashboardContributor> OrderedContributors => |
| | 8 | 143 | | contributors |
| | 12 | 144 | | .OrderBy(x => x.Order) |
| | 12 | 145 | | .ThenBy(x => x.Id, StringComparer.Ordinal) |
| | 8 | 146 | | .ToList(); |
| | | 147 | | |
| | | 148 | | private DashboardContext CreateContext(DashboardRange range, bool includeSystem, CancellationToken cancellationToken |
| | 6 | 149 | | new(range, includeSystem, cancellationToken, EnvironmentName: environment.EnvironmentName); |
| | | 150 | | |
| | | 151 | | private async Task<IReadOnlyCollection<T>> CollectAsync<T>( |
| | | 152 | | Func<IDashboardContributor, Task<T?>> action, |
| | | 153 | | CancellationToken cancellationToken) |
| | | 154 | | where T : class |
| | | 155 | | { |
| | 2 | 156 | | var results = new List<T>(); |
| | 12 | 157 | | foreach (var contributor in OrderedContributors) |
| | | 158 | | { |
| | 4 | 159 | | var result = await ExecuteContributorAsync(contributor, action, cancellationToken); |
| | 4 | 160 | | if (result != null) |
| | 4 | 161 | | results.Add(result); |
| | | 162 | | } |
| | | 163 | | |
| | 2 | 164 | | return results; |
| | 2 | 165 | | } |
| | | 166 | | |
| | | 167 | | private async Task<IReadOnlyCollection<T>> CollectManyAsync<T>( |
| | | 168 | | Func<IDashboardContributor, Task<IReadOnlyCollection<T>>> action, |
| | | 169 | | CancellationToken cancellationToken) |
| | | 170 | | { |
| | 2 | 171 | | var results = new List<T>(); |
| | 12 | 172 | | foreach (var contributor in OrderedContributors) |
| | | 173 | | { |
| | 4 | 174 | | var result = await ExecuteContributorAsync(contributor, action, cancellationToken); |
| | 4 | 175 | | if (result != null) |
| | 3 | 176 | | results.AddRange(result); |
| | | 177 | | } |
| | | 178 | | |
| | 2 | 179 | | return results; |
| | 2 | 180 | | } |
| | | 181 | | |
| | | 182 | | private static async Task<T?> ExecuteContributorAsync<T>( |
| | | 183 | | IDashboardContributor contributor, |
| | | 184 | | Func<IDashboardContributor, Task<T>> action, |
| | | 185 | | CancellationToken cancellationToken) |
| | | 186 | | { |
| | | 187 | | try |
| | | 188 | | { |
| | 13 | 189 | | return await action(contributor); |
| | | 190 | | } |
| | 1 | 191 | | catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) |
| | | 192 | | { |
| | 1 | 193 | | throw; |
| | | 194 | | } |
| | 2 | 195 | | catch |
| | | 196 | | { |
| | 2 | 197 | | return default; |
| | | 198 | | } |
| | 12 | 199 | | } |
| | | 200 | | |
| | | 201 | | private static DashboardRuntimeStatus MergeRuntime(IEnumerable<DashboardOverviewContribution> contributions) => |
| | 3 | 202 | | contributions |
| | 2 | 203 | | .Select(x => x.Runtime) |
| | 2 | 204 | | .FirstOrDefault(x => x != null && x.Status != DashboardRuntimeStatusKeys.Unavailable) |
| | 3 | 205 | | ?? new(); |
| | | 206 | | |
| | | 207 | | private static DashboardWorkflowInstanceMetrics MergeWorkflowMetrics(IEnumerable<DashboardOverviewContribution> cont |
| | | 208 | | { |
| | 6 | 209 | | var metrics = contributions.Select(x => x.WorkflowInstances).OfType<DashboardWorkflowInstanceMetrics>().ToList() |
| | 3 | 210 | | return new() |
| | 3 | 211 | | { |
| | 2 | 212 | | Running = metrics.Sum(x => x.Running), |
| | 2 | 213 | | Completed = metrics.Sum(x => x.Completed), |
| | 2 | 214 | | Faulted = metrics.Sum(x => x.Faulted), |
| | 2 | 215 | | Suspended = metrics.Sum(x => x.Suspended), |
| | 2 | 216 | | Interrupted = metrics.Sum(x => x.Interrupted), |
| | 2 | 217 | | IncidentBearing = metrics.Sum(x => x.IncidentBearing), |
| | 2 | 218 | | AverageDuration = AverageDuration(metrics.Select(x => x.AverageDuration)) |
| | 3 | 219 | | }; |
| | | 220 | | } |
| | | 221 | | |
| | | 222 | | private static DashboardDiagnosticsSummary MergeDiagnostics(IEnumerable<DashboardOverviewContribution> contributions |
| | | 223 | | { |
| | 6 | 224 | | var summaries = contributions.Select(x => x.Diagnostics).OfType<DashboardDiagnosticsSummary>().ToList(); |
| | 3 | 225 | | return new() |
| | 3 | 226 | | { |
| | 2 | 227 | | StructuredLogs = summaries.Select(x => x.StructuredLogs).FirstOrDefault(x => x.Capability.Status != Dashboar |
| | 2 | 228 | | ConsoleLogs = summaries.Select(x => x.ConsoleLogs).FirstOrDefault(x => x.Capability.Status != DashboardCapab |
| | 3 | 229 | | }; |
| | | 230 | | } |
| | | 231 | | |
| | | 232 | | private static TimeSpan? AverageDuration(IEnumerable<TimeSpan?> durations) |
| | | 233 | | { |
| | 3 | 234 | | var values = durations.OfType<TimeSpan>().Where(x => x >= TimeSpan.Zero).ToList(); |
| | 3 | 235 | | return values.Count == 0 ? null : TimeSpan.FromTicks(Convert.ToInt64(values.Average(x => x.Ticks))); |
| | | 236 | | } |
| | | 237 | | |
| | | 238 | | private static string NormalizeHotspotMetric(string? metric) => |
| | 0 | 239 | | metric?.Trim().ToLowerInvariant() switch |
| | 0 | 240 | | { |
| | 0 | 241 | | "executions" => DashboardHotspotMetric.Executions, |
| | 0 | 242 | | "incidents" => DashboardHotspotMetric.Incidents, |
| | 0 | 243 | | "duration" => DashboardHotspotMetric.Duration, |
| | 0 | 244 | | _ => DashboardHotspotMetric.Faults |
| | 0 | 245 | | }; |
| | | 246 | | } |