< Summary

Information
Class: Elsa.Workflows.Runtime.Dashboard.WorkflowDashboardContributor
Assembly: Elsa.Workflows.Runtime.Dashboard
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Runtime.Dashboard/Dashboard/WorkflowDashboardContributor.cs
Line coverage
1%
Covered lines: 3
Uncovered lines: 181
Coverable lines: 184
Total lines: 268
Line coverage: 1.6%
Branch coverage
0%
Covered branches: 0
Total branches: 50
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_Id()100%210%
get_Order()100%210%
GetOverviewAsync()100%210%
GetFindingsAsync()0%156120%
GetWorkflowTrendsAsync()0%620%
GetRecentActivityAsync()100%210%
GetWorkflowHotspotsAsync()100%210%
GetWorkflowMetricsAsync()0%620%
GetRuntimeStatus()0%2040%
CountAsync()100%210%
CreateRangeFilter(...)0%156120%
MapRecentActivity(...)0%620%
CreateHotspot(...)0%110100%
GetBucketSize(...)0%2040%
Finding(...)100%210%
Min(...)0%620%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Workflows.Runtime.Dashboard/Dashboard/WorkflowDashboardContributor.cs

#LineLine coverage
 1using Elsa.Common.Entities;
 2using Elsa.Common.Models;
 3using Elsa.Dashboard.Abstractions.Contracts;
 4using Elsa.Dashboard.Abstractions.Models;
 5using Elsa.Workflows.Management;
 6using Elsa.Workflows.Management.Entities;
 7using Elsa.Workflows.Management.Enums;
 8using Elsa.Workflows.Management.Filters;
 9using Elsa.Workflows.Management.Models;
 10
 11namespace Elsa.Workflows.Runtime.Dashboard;
 12
 313public class WorkflowDashboardContributor(
 314    IWorkflowInstanceStore workflowInstanceStore,
 315    IWorkflowRuntimeAdminService runtimeAdminService) : IDashboardContributor
 16{
 017    public string Id => "workflows";
 18
 019    public int Order => 100;
 20
 21    public async ValueTask<DashboardOverviewContribution?> GetOverviewAsync(DashboardContext context)
 22    {
 023        return new()
 024        {
 025            Runtime = GetRuntimeStatus(),
 026            WorkflowInstances = await GetWorkflowMetricsAsync(context.Range, context.IncludeSystem, context.Cancellation
 027        };
 028    }
 29
 30    public async ValueTask<IReadOnlyCollection<DashboardFinding>> GetFindingsAsync(DashboardContext context)
 31    {
 032        var runtime = GetRuntimeStatus();
 033        var workflowMetrics = await GetWorkflowMetricsAsync(context.Range, context.IncludeSystem, context.CancellationTo
 034        var findings = new List<DashboardFinding>();
 35
 036        if (runtime.Status == DashboardRuntimeStatusKeys.Paused)
 037            findings.Add(Finding("runtime-paused", DashboardFindingSeverity.Warning, "Runtime is paused", "Runtime", "ru
 038        else if (runtime.Status == DashboardRuntimeStatusKeys.Draining)
 039            findings.Add(Finding("runtime-draining", DashboardFindingSeverity.Warning, "Runtime is draining", "Runtime",
 40
 041        if (runtime.FailedIngressSourceCount > 0)
 042            findings.Add(Finding("ingress-source-failures", DashboardFindingSeverity.Warning, $"{runtime.FailedIngressSo
 43
 044        if (workflowMetrics.Faulted > 0)
 045            findings.Add(Finding("workflow-faults", DashboardFindingSeverity.Error, $"{workflowMetrics.Faulted} workflow
 46
 047        if (workflowMetrics.Interrupted > 0)
 048            findings.Add(Finding("workflow-interrupted", DashboardFindingSeverity.Warning, $"{workflowMetrics.Interrupte
 49
 050        if (workflowMetrics.IncidentBearing > 0)
 051            findings.Add(Finding("workflow-incidents", DashboardFindingSeverity.Error, $"{workflowMetrics.IncidentBearin
 52
 053        return findings;
 054    }
 55
 56    public async ValueTask<DashboardTrendResponse?> GetWorkflowTrendsAsync(DashboardTrendContext context)
 57    {
 058        var bucketSize = GetBucketSize(context.Granularity);
 059        var buckets = new List<DashboardTrendBucket>();
 60
 061        for (var bucketFrom = context.Range.From; bucketFrom < context.Range.To; bucketFrom = bucketFrom.Add(bucketSize)
 62        {
 063            var bucketTo = Min(bucketFrom.Add(bucketSize), context.Range.To);
 064            buckets.Add(new()
 065            {
 066                From = bucketFrom,
 067                To = bucketTo,
 068                CreatedOrStarted = await CountAsync(context.IncludeSystem, nameof(WorkflowInstance.CreatedAt), bucketFro
 069                Finished = await CountAsync(context.IncludeSystem, nameof(WorkflowInstance.FinishedAt), bucketFrom, buck
 070                Faulted = await CountAsync(context.IncludeSystem, nameof(WorkflowInstance.UpdatedAt), bucketFrom, bucket
 071                Suspended = await CountAsync(context.IncludeSystem, nameof(WorkflowInstance.UpdatedAt), bucketFrom, buck
 072                IncidentBearing = await CountAsync(context.IncludeSystem, nameof(WorkflowInstance.UpdatedAt), bucketFrom
 073            });
 74        }
 75
 076        return new()
 077        {
 078            Buckets = buckets,
 079            AppliedRange = context.Range.Key,
 080            Granularity = context.Granularity,
 081            From = context.Range.From,
 082            To = context.Range.To
 083        };
 084    }
 85
 86    public async ValueTask<DashboardRecentActivityResponse?> GetRecentActivityAsync(DashboardListContext context)
 87    {
 088        var filter = CreateRangeFilter(context.IncludeSystem, nameof(WorkflowInstance.UpdatedAt), context.Range.From, co
 089        var order = new WorkflowInstanceOrder<DateTimeOffset?>
 090        {
 091            KeySelector = x => x.UpdatedAt,
 092            Direction = OrderDirection.Descending
 093        };
 094        var page = await workflowInstanceStore.SummarizeManyAsync(filter, PageArgs.FromPage(0, context.Take), order, con
 95
 096        return new()
 097        {
 098            Items = page.Items.Select(MapRecentActivity).ToList(),
 099            AppliedRange = context.Range.Key,
 0100            From = context.Range.From,
 0101            To = context.Range.To
 0102        };
 0103    }
 104
 105    public async ValueTask<DashboardWorkflowHotspotsResponse?> GetWorkflowHotspotsAsync(DashboardHotspotsContext context
 106    {
 0107        var summaries = await workflowInstanceStore.SummarizeManyAsync(CreateRangeFilter(context.IncludeSystem, nameof(W
 0108        var hotspots = summaries
 0109            .GroupBy(x => x.DefinitionId)
 0110            .Select(x => CreateHotspot(x, context.Metric))
 0111            .OrderByDescending(x => x.Value)
 0112            .ThenBy(x => x.WorkflowName)
 0113            .Take(context.Take)
 0114            .ToList();
 115
 0116        return new()
 0117        {
 0118            Items = hotspots,
 0119            AppliedRange = context.Range.Key,
 0120            Metric = context.Metric,
 0121            From = context.Range.From,
 0122            To = context.Range.To
 0123        };
 0124    }
 125
 126    private async Task<DashboardWorkflowInstanceMetrics> GetWorkflowMetricsAsync(DashboardRange range, bool includeSyste
 127    {
 0128        var completedSummaries = (await workflowInstanceStore.SummarizeManyAsync(
 0129            CreateRangeFilter(includeSystem, nameof(WorkflowInstance.FinishedAt), range.From, range.To, subStatus: Workf
 0130            cancellationToken)).ToList();
 0131        var durations = completedSummaries
 0132            .Where(x => x.FinishedAt != null)
 0133            .Select(x => x.FinishedAt!.Value - x.CreatedAt)
 0134            .Where(x => x >= TimeSpan.Zero)
 0135            .ToList();
 136
 0137        return new()
 0138        {
 0139            Running = await CountAsync(includeSystem, status: WorkflowStatus.Running, cancellationToken: cancellationTok
 0140            Completed = completedSummaries.Count,
 0141            Faulted = await CountAsync(includeSystem, nameof(WorkflowInstance.UpdatedAt), range.From, range.To, cancella
 0142            Suspended = await CountAsync(includeSystem, subStatus: WorkflowSubStatus.Suspended, cancellationToken: cance
 0143            Interrupted = await CountAsync(includeSystem, nameof(WorkflowInstance.UpdatedAt), range.From, range.To, canc
 0144            IncidentBearing = await CountAsync(includeSystem, hasIncidents: true, cancellationToken: cancellationToken),
 0145            AverageDuration = durations.Count == 0 ? null : TimeSpan.FromTicks(Convert.ToInt64(durations.Average(x => x.
 0146        };
 0147    }
 148
 149    private DashboardRuntimeStatus GetRuntimeStatus()
 150    {
 0151        var status = runtimeAdminService.GetStatus();
 0152        var state = status.State;
 0153        var runtimeStatus = state.IsAcceptingNewWork
 0154            ? DashboardRuntimeStatusKeys.AcceptingWork
 0155            : state.DrainStartedAt != null
 0156                ? DashboardRuntimeStatusKeys.Draining
 0157                : DashboardRuntimeStatusKeys.Paused;
 0158        var failedSourceCount = status.Sources.Count(x => x.LastError != null);
 159
 0160        return new()
 0161        {
 0162            Status = runtimeStatus,
 0163            IsAcceptingWork = state.IsAcceptingNewWork,
 0164            ActiveExecutionCycleCount = status.ActiveExecutionCycleCount,
 0165            IngressSourceCount = status.Sources.Count,
 0166            FailedIngressSourceCount = failedSourceCount,
 0167            PausedAt = state.PausedAt,
 0168            DrainStartedAt = state.DrainStartedAt,
 0169            Reason = state.Reason.ToString()
 0170        };
 171    }
 172
 173    private async Task<long> CountAsync(
 174        bool includeSystem,
 175        string? timestampColumn = null,
 176        DateTimeOffset? from = null,
 177        DateTimeOffset? to = null,
 178        CancellationToken cancellationToken = default,
 179        WorkflowStatus? status = null,
 180        WorkflowSubStatus? subStatus = null,
 181        bool? hasIncidents = null)
 182    {
 0183        return await workflowInstanceStore.CountAsync(CreateRangeFilter(includeSystem, timestampColumn, from, to, status
 0184    }
 185
 186    private static WorkflowInstanceFilter CreateRangeFilter(
 187        bool includeSystem,
 188        string? timestampColumn,
 189        DateTimeOffset? from,
 190        DateTimeOffset? to,
 191        WorkflowStatus? status = null,
 192        WorkflowSubStatus? subStatus = null,
 193        bool? hasIncidents = null)
 194    {
 0195        var timestampFilters = new List<TimestampFilter>();
 0196        if (timestampColumn != null && from != null)
 0197            timestampFilters.Add(new() { Column = timestampColumn, Operator = TimestampFilterOperator.GreaterThanOrEqual
 0198        if (timestampColumn != null && to != null)
 0199            timestampFilters.Add(new() { Column = timestampColumn, Operator = TimestampFilterOperator.LessThan, Timestam
 200
 0201        return new()
 0202        {
 0203            IsSystem = includeSystem ? null : false,
 0204            WorkflowStatus = status,
 0205            WorkflowSubStatus = subStatus,
 0206            HasIncidents = hasIncidents,
 0207            TimestampFilters = timestampFilters.Count == 0 ? null : timestampFilters
 0208        };
 209    }
 210
 0211    private static DashboardRecentActivityItem MapRecentActivity(WorkflowInstanceSummary summary) => new()
 0212    {
 0213        InstanceId = summary.Id,
 0214        DefinitionId = summary.DefinitionId,
 0215        WorkflowName = summary.Name,
 0216        Status = summary.Status.ToString(),
 0217        SubStatus = summary.SubStatus.ToString(),
 0218        IncidentCount = summary.IncidentCount,
 0219        Duration = summary.FinishedAt == null ? null : summary.FinishedAt.Value - summary.CreatedAt,
 0220        CreatedAt = summary.CreatedAt,
 0221        UpdatedAt = summary.UpdatedAt,
 0222        FinishedAt = summary.FinishedAt
 0223    };
 224
 225    private static DashboardHotspot CreateHotspot(IGrouping<string, WorkflowInstanceSummary> group, string metric)
 226    {
 0227        var items = group.ToList();
 0228        var durations = items
 0229            .Where(x => x.FinishedAt != null)
 0230            .Select(x => x.FinishedAt!.Value - x.CreatedAt)
 0231            .Where(x => x >= TimeSpan.Zero)
 0232            .ToList();
 0233        var value = metric switch
 0234        {
 0235            DashboardHotspotMetric.Executions => items.Count,
 0236            DashboardHotspotMetric.Incidents => items.Sum(x => x.IncidentCount),
 0237            DashboardHotspotMetric.Duration => durations.Count == 0 ? 0 : Convert.ToInt64(durations.Average(x => x.Total
 0238            _ => items.LongCount(x => x.SubStatus == WorkflowSubStatus.Faulted)
 0239        };
 240
 0241        return new()
 0242        {
 0243            DefinitionId = group.Key,
 0244            WorkflowName = items.Select(x => x.Name).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)),
 0245            Value = value,
 0246            AverageDuration = durations.Count == 0 ? null : TimeSpan.FromTicks(Convert.ToInt64(durations.Average(x => x.
 0247        };
 248    }
 249
 250    private static TimeSpan GetBucketSize(string granularity) =>
 0251        granularity.Equals(DashboardTrendGranularity.Minute, StringComparison.OrdinalIgnoreCase)
 0252            ? TimeSpan.FromMinutes(1)
 0253            : granularity.Equals(DashboardTrendGranularity.Day, StringComparison.OrdinalIgnoreCase)
 0254                ? TimeSpan.FromDays(1)
 0255                : TimeSpan.FromHours(1);
 256
 0257    private static DashboardFinding Finding(string id, string severity, string message, string? targetKind, string? targ
 0258    {
 0259        Id = id,
 0260        Severity = severity,
 0261        Message = message,
 0262        TargetKind = targetKind,
 0263        Target = target,
 0264        Priority = priority
 0265    };
 266
 0267    private static DateTimeOffset Min(DateTimeOffset left, DateTimeOffset right) => left <= right ? left : right;
 268}