< Summary

Information
Class: Elsa.Dashboard.Api.Services.DefaultDashboardProvider
Assembly: Elsa.Dashboard.Api
File(s): /home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Dashboard.Api/Services/DefaultDashboardProvider.cs
Line coverage
93%
Covered lines: 258
Uncovered lines: 18
Coverable lines: 276
Total lines: 396
Line coverage: 93.4%
Branch coverage
66%
Covered branches: 48
Total branches: 72
Branch coverage: 66.6%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
GetOverviewAsync()100%11100%
GetWorkflowTrendsAsync()100%22100%
GetNeedsAttentionAsync()68.18%272278.12%
GetRecentActivityAsync()100%11100%
GetWorkflowHotspotsAsync()100%11100%
GetWorkflowMetricsAsync()100%22100%
GetRuntimeStatus()75%44100%
GetDiagnosticsSummaryAsync()100%11100%
GetStructuredLogSummaryAsync()100%2291.66%
GetConsoleLogSummaryAsync()100%2290.9%
CountAsync()100%11100%
CreateRangeFilter(...)91.66%1212100%
MapRecentActivity(...)50%22100%
CreateHotspot(...)40%101085%
NormalizeHotspotMetric(...)37.5%13857.14%
Finding(...)100%11100%
Min(...)50%22100%

File(s)

/home/runner/work/elsa-core/elsa-core/src/modules/Elsa.Dashboard.Api/Services/DefaultDashboardProvider.cs

#LineLine coverage
 1using ConsoleLogStreaming.Core;
 2using ConsoleLogStreaming.Core.Models;
 3using Elsa.Common.Entities;
 4using Elsa.Common.Models;
 5using Elsa.Dashboard.Api.Contracts;
 6using Elsa.Dashboard.Api.Models;
 7using Elsa.Diagnostics.StructuredLogs.Contracts;
 8using Elsa.Diagnostics.StructuredLogs.Models;
 9using Elsa.Workflows;
 10using Elsa.Workflows.Management;
 11using Elsa.Workflows.Management.Entities;
 12using Elsa.Workflows.Management.Enums;
 13using Elsa.Workflows.Management.Filters;
 14using Elsa.Workflows.Management.Models;
 15using Elsa.Workflows.Runtime;
 16using Microsoft.Extensions.DependencyInjection;
 17using Microsoft.Extensions.Hosting;
 18
 19namespace Elsa.Dashboard.Api.Services;
 20
 1021public class DefaultDashboardProvider(
 1022    IWorkflowInstanceStore workflowInstanceStore,
 1023    IWorkflowRuntimeAdminService runtimeAdminService,
 1024    DashboardRangeResolver rangeResolver,
 1025    IServiceProvider serviceProvider,
 1026    IHostEnvironment environment) : IDashboardProvider
 27{
 28    public async Task<DashboardOverview> GetOverviewAsync(DashboardQuery query, CancellationToken cancellationToken = de
 29    {
 430        var range = rangeResolver.Resolve(query.Range);
 431        var runtime = GetRuntimeStatus();
 432        var workflowMetrics = await GetWorkflowMetricsAsync(range, query.IncludeSystem, cancellationToken);
 433        var diagnostics = await GetDiagnosticsSummaryAsync(range, cancellationToken);
 34
 435        return new()
 436        {
 437            BackendName = environment.ApplicationName,
 438            EnvironmentName = environment.EnvironmentName,
 439            Runtime = runtime,
 440            WorkflowInstances = workflowMetrics,
 441            Diagnostics = diagnostics,
 442            AppliedRange = range.Key,
 443            From = range.From,
 444            To = range.To
 445        };
 446    }
 47
 48    public async Task<DashboardTrendResponse> GetWorkflowTrendsAsync(DashboardTrendRequest request, CancellationToken ca
 49    {
 150        var range = rangeResolver.Resolve(request.Range);
 151        var granularity = rangeResolver.ResolveGranularity(request.Granularity, range.Key);
 152        var bucketSize = rangeResolver.GetBucketSize(granularity);
 153        var buckets = new List<DashboardTrendBucket>();
 54
 5055        for (var bucketFrom = range.From; bucketFrom < range.To; bucketFrom = bucketFrom.Add(bucketSize))
 56        {
 2457            var bucketTo = Min(bucketFrom.Add(bucketSize), range.To);
 2458            buckets.Add(new()
 2459            {
 2460                From = bucketFrom,
 2461                To = bucketTo,
 2462                CreatedOrStarted = await CountAsync(request.IncludeSystem, nameof(WorkflowInstance.CreatedAt), bucketFro
 2463                Finished = await CountAsync(request.IncludeSystem, nameof(WorkflowInstance.FinishedAt), bucketFrom, buck
 2464                Faulted = await CountAsync(request.IncludeSystem, nameof(WorkflowInstance.UpdatedAt), bucketFrom, bucket
 2465                Suspended = await CountAsync(request.IncludeSystem, nameof(WorkflowInstance.UpdatedAt), bucketFrom, buck
 2466                IncidentBearing = await CountAsync(request.IncludeSystem, nameof(WorkflowInstance.UpdatedAt), bucketFrom
 2467            });
 68        }
 69
 170        return new()
 171        {
 172            Buckets = buckets,
 173            AppliedRange = range.Key,
 174            Granularity = granularity,
 175            From = range.From,
 176            To = range.To
 177        };
 178    }
 79
 80    public async Task<DashboardNeedsAttentionResponse> GetNeedsAttentionAsync(DashboardQuery query, int take, Cancellati
 81    {
 182        var range = rangeResolver.Resolve(query.Range);
 183        var overview = await GetOverviewAsync(query, cancellationToken);
 184        var findings = new List<DashboardFinding>();
 85
 186        if (overview.Runtime.Status == DashboardRuntimeStatusKeys.Paused)
 187            findings.Add(Finding("runtime-paused", DashboardFindingSeverity.Warning, "Runtime is paused", "Runtime", "ru
 088        else if (overview.Runtime.Status == DashboardRuntimeStatusKeys.Draining)
 089            findings.Add(Finding("runtime-draining", DashboardFindingSeverity.Warning, "Runtime is draining", "Runtime",
 90
 191        if (overview.Runtime.FailedIngressSourceCount > 0)
 192            findings.Add(Finding("ingress-source-failures", DashboardFindingSeverity.Warning, $"{overview.Runtime.Failed
 93
 194        if (overview.WorkflowInstances.Faulted > 0)
 195            findings.Add(Finding("workflow-faults", DashboardFindingSeverity.Error, $"{overview.WorkflowInstances.Faulte
 96
 197        if (overview.WorkflowInstances.Interrupted > 0)
 098            findings.Add(Finding("workflow-interrupted", DashboardFindingSeverity.Warning, $"{overview.WorkflowInstances
 99
 1100        if (overview.WorkflowInstances.IncidentBearing > 0)
 1101            findings.Add(Finding("workflow-incidents", DashboardFindingSeverity.Error, $"{overview.WorkflowInstances.Inc
 102
 1103        var structuredLogs = overview.Diagnostics.StructuredLogs;
 1104        if (structuredLogs.StaleSourceCount > 0)
 0105            findings.Add(Finding("structured-log-stale-sources", DashboardFindingSeverity.Warning, $"{structuredLogs.Sta
 1106        if (structuredLogs.DroppedWriteCount > 0)
 0107            findings.Add(Finding("structured-log-dropped-writes", DashboardFindingSeverity.Error, "Structured log storag
 1108        if (structuredLogs.RecentErrorOrCriticalCount > 0)
 1109            findings.Add(Finding("structured-log-errors", DashboardFindingSeverity.Error, $"{structuredLogs.RecentErrorO
 110
 1111        var consoleLogs = overview.Diagnostics.ConsoleLogs;
 1112        if (consoleLogs.StaleSourceCount > 0)
 0113            findings.Add(Finding("console-log-stale-sources", DashboardFindingSeverity.Warning, $"{consoleLogs.StaleSour
 1114        if (consoleLogs.DroppedLineCount > 0)
 0115            findings.Add(Finding("console-log-dropped-lines", DashboardFindingSeverity.Warning, "Console log capture dro
 116
 1117        return new()
 1118        {
 5119            Findings = findings.OrderBy(x => x.Priority).Take(Math.Clamp(take, 1, 50)).ToList(),
 1120            AppliedRange = range.Key
 1121        };
 1122    }
 123
 124    public async Task<DashboardRecentActivityResponse> GetRecentActivityAsync(DashboardQuery query, int take, Cancellati
 125    {
 1126        var range = rangeResolver.Resolve(query.Range);
 1127        var filter = CreateRangeFilter(query.IncludeSystem, nameof(WorkflowInstance.UpdatedAt), range.From, range.To);
 1128        var order = new WorkflowInstanceOrder<DateTimeOffset?>
 1129        {
 1130            KeySelector = x => x.UpdatedAt,
 1131            Direction = OrderDirection.Descending
 1132        };
 1133        var page = await workflowInstanceStore.SummarizeManyAsync(filter, PageArgs.FromPage(0, Math.Clamp(take, 1, 100))
 134
 1135        return new()
 1136        {
 1137            Items = page.Items.Select(MapRecentActivity).ToList(),
 1138            AppliedRange = range.Key,
 1139            From = range.From,
 1140            To = range.To
 1141        };
 1142    }
 143
 144    public async Task<DashboardWorkflowHotspotsResponse> GetWorkflowHotspotsAsync(DashboardWorkflowHotspotsRequest reque
 145    {
 1146        var range = rangeResolver.Resolve(request.Range);
 1147        var summaries = await workflowInstanceStore.SummarizeManyAsync(CreateRangeFilter(request.IncludeSystem, nameof(W
 1148        var metric = NormalizeHotspotMetric(request.Metric);
 1149        var hotspots = summaries
 3150            .GroupBy(x => x.DefinitionId)
 2151            .Select(x => CreateHotspot(x, metric))
 2152            .OrderByDescending(x => x.Value)
 2153            .ThenBy(x => x.WorkflowName)
 1154            .Take(Math.Clamp(request.Take, 1, 50))
 1155            .ToList();
 156
 1157        return new()
 1158        {
 1159            Items = hotspots,
 1160            AppliedRange = range.Key,
 1161            Metric = metric,
 1162            From = range.From,
 1163            To = range.To
 1164        };
 1165    }
 166
 167    private async Task<DashboardWorkflowInstanceMetrics> GetWorkflowMetricsAsync(DashboardRange range, bool includeSyste
 168    {
 4169        var completedSummaries = (await workflowInstanceStore.SummarizeManyAsync(
 4170            CreateRangeFilter(includeSystem, nameof(WorkflowInstance.FinishedAt), range.From, range.To, subStatus: Workf
 4171            cancellationToken)).ToList();
 4172        var durations = completedSummaries
 1173            .Where(x => x.FinishedAt != null)
 1174            .Select(x => x.FinishedAt!.Value - x.CreatedAt)
 1175            .Where(x => x >= TimeSpan.Zero)
 4176            .ToList();
 177
 4178        return new()
 4179        {
 4180            Running = await CountAsync(includeSystem, status: WorkflowStatus.Running, cancellationToken: cancellationTok
 4181            Completed = completedSummaries.Count,
 4182            Faulted = await CountAsync(includeSystem, nameof(WorkflowInstance.UpdatedAt), range.From, range.To, cancella
 4183            Suspended = await CountAsync(includeSystem, subStatus: WorkflowSubStatus.Suspended, cancellationToken: cance
 4184            Interrupted = await CountAsync(includeSystem, nameof(WorkflowInstance.UpdatedAt), range.From, range.To, canc
 4185            IncidentBearing = await CountAsync(includeSystem, hasIncidents: true, cancellationToken: cancellationToken),
 1186            AverageDuration = durations.Count == 0 ? null : TimeSpan.FromTicks(Convert.ToInt64(durations.Average(x => x.
 4187        };
 4188    }
 189
 190    private DashboardRuntimeStatus GetRuntimeStatus()
 191    {
 4192        var status = runtimeAdminService.GetStatus();
 4193        var state = status.State;
 4194        var runtimeStatus = state.IsAcceptingNewWork
 4195            ? DashboardRuntimeStatusKeys.AcceptingWork
 4196            : state.DrainStartedAt != null
 4197                ? DashboardRuntimeStatusKeys.Draining
 4198                : DashboardRuntimeStatusKeys.Paused;
 5199        var failedSourceCount = status.Sources.Count(x => x.LastError != null);
 200
 4201        return new()
 4202        {
 4203            Status = runtimeStatus,
 4204            IsAcceptingWork = state.IsAcceptingNewWork,
 4205            ActiveExecutionCycleCount = status.ActiveExecutionCycleCount,
 4206            IngressSourceCount = status.Sources.Count,
 4207            FailedIngressSourceCount = failedSourceCount,
 4208            PausedAt = state.PausedAt,
 4209            DrainStartedAt = state.DrainStartedAt,
 4210            Reason = state.Reason.ToString()
 4211        };
 212    }
 213
 214    private async Task<DashboardDiagnosticsSummary> GetDiagnosticsSummaryAsync(DashboardRange range, CancellationToken c
 215    {
 4216        var structuredLogs = await GetStructuredLogSummaryAsync(range, cancellationToken);
 4217        var consoleLogs = await GetConsoleLogSummaryAsync(range, cancellationToken);
 4218        return new()
 4219        {
 4220            StructuredLogs = structuredLogs,
 4221            ConsoleLogs = consoleLogs
 4222        };
 4223    }
 224
 225    private async Task<DashboardStructuredLogSummary> GetStructuredLogSummaryAsync(DashboardRange range, CancellationTok
 226    {
 4227        var provider = serviceProvider.GetService<IStructuredLogProvider>();
 4228        if (provider == null)
 1229            return new();
 230
 231        try
 232        {
 3233            var sources = await provider.ListSourcesAsync(cancellationToken);
 2234            var storageDiagnostics = serviceProvider.GetServices<IStructuredLogStorageDiagnostics>().ToList();
 2235            var recentErrors = await provider.GetRecentAsync(new()
 2236            {
 2237                Levels = [StructuredLogLevel.Error, StructuredLogLevel.Critical],
 2238                From = range.From,
 2239                To = range.To,
 2240                Take = 1000
 2241            }, cancellationToken);
 242
 2243            return new()
 2244            {
 2245                Capability = DashboardCapabilityStatus.Available,
 2246                SourceCount = sources.Count,
 1247                StaleSourceCount = sources.Count(x => x.Status == StructuredLogSourceStatus.Stale || x.Status == Structu
 2248                RecentErrorOrCriticalCount = recentErrors.Items.Count,
 1249                DroppedWriteCount = storageDiagnostics.Aggregate(0L, (total, x) => checked(total + x.DroppedWriteCount))
 2250                DroppedEventCount = recentErrors.DroppedEvents
 2251            };
 252        }
 1253        catch (UnauthorizedAccessException)
 254        {
 1255            return new() { Capability = new(DashboardCapabilityStatus.Unauthorized.Status, "No access to structured logs
 256        }
 0257        catch (Exception e) when (e is not OperationCanceledException)
 258        {
 0259            return new() { Capability = new(DashboardCapabilityStatus.Unavailable.Status, "Structured log summary is una
 260        }
 4261    }
 262
 263    private async Task<DashboardConsoleLogSummary> GetConsoleLogSummaryAsync(DashboardRange range, CancellationToken can
 264    {
 4265        var provider = serviceProvider.GetService<IConsoleLogProvider>();
 4266        if (provider == null)
 1267            return new();
 268
 269        try
 270        {
 3271            var sources = await provider.ListSourcesAsync(cancellationToken);
 2272            var recentStderr = await provider.GetRecentAsync(new()
 2273            {
 2274                Stream = ConsoleStream.Stderr,
 2275                From = range.From,
 2276                To = range.To,
 2277                Limit = 1000
 2278            }, cancellationToken);
 279
 2280            return new()
 2281            {
 2282                Capability = DashboardCapabilityStatus.Available,
 2283                SourceCount = sources.Count,
 1284                StaleSourceCount = sources.Count(x => x.Health is ConsoleLogSourceHealth.Stale or ConsoleLogSourceHealth
 2285                RecentStderrCount = recentStderr.Items.Count,
 0286                DroppedLineCount = recentStderr.Dropped.Aggregate(0L, (total, x) => checked(total + x.Count))
 2287            };
 288        }
 0289        catch (UnauthorizedAccessException)
 290        {
 0291            return new() { Capability = new(DashboardCapabilityStatus.Unauthorized.Status, "No access to console logs") 
 292        }
 1293        catch (Exception e) when (e is not OperationCanceledException)
 294        {
 1295            return new() { Capability = new(DashboardCapabilityStatus.Unavailable.Status, "Console log summary is unavai
 296        }
 4297    }
 298
 299    private async Task<long> CountAsync(
 300        bool includeSystem,
 301        string? timestampColumn = null,
 302        DateTimeOffset? from = null,
 303        DateTimeOffset? to = null,
 304        CancellationToken cancellationToken = default,
 305        WorkflowStatus? status = null,
 306        WorkflowSubStatus? subStatus = null,
 307        bool? hasIncidents = null)
 308    {
 140309        return await workflowInstanceStore.CountAsync(CreateRangeFilter(includeSystem, timestampColumn, from, to, status
 140310    }
 311
 312    private static WorkflowInstanceFilter CreateRangeFilter(
 313        bool includeSystem,
 314        string? timestampColumn,
 315        DateTimeOffset? from,
 316        DateTimeOffset? to,
 317        WorkflowStatus? status = null,
 318        WorkflowSubStatus? subStatus = null,
 319        bool? hasIncidents = null)
 320    {
 146321        var timestampFilters = new List<TimestampFilter>();
 146322        if (timestampColumn != null && from != null)
 134323            timestampFilters.Add(new() { Column = timestampColumn, Operator = TimestampFilterOperator.GreaterThanOrEqual
 146324        if (timestampColumn != null && to != null)
 134325            timestampFilters.Add(new() { Column = timestampColumn, Operator = TimestampFilterOperator.LessThan, Timestam
 326
 146327        return new()
 146328        {
 146329            IsSystem = includeSystem ? null : false,
 146330            WorkflowStatus = status,
 146331            WorkflowSubStatus = subStatus,
 146332            HasIncidents = hasIncidents,
 146333            TimestampFilters = timestampFilters.Count == 0 ? null : timestampFilters
 146334        };
 335    }
 336
 2337    private static DashboardRecentActivityItem MapRecentActivity(WorkflowInstanceSummary summary) => new()
 2338    {
 2339        InstanceId = summary.Id,
 2340        DefinitionId = summary.DefinitionId,
 2341        WorkflowName = summary.Name,
 2342        Status = summary.Status.ToString(),
 2343        SubStatus = summary.SubStatus.ToString(),
 2344        IncidentCount = summary.IncidentCount,
 2345        Duration = summary.FinishedAt == null ? null : summary.FinishedAt.Value - summary.CreatedAt,
 2346        CreatedAt = summary.CreatedAt,
 2347        UpdatedAt = summary.UpdatedAt,
 2348        FinishedAt = summary.FinishedAt
 2349    };
 350
 351    private static DashboardHotspot CreateHotspot(IGrouping<string, WorkflowInstanceSummary> group, string metric)
 352    {
 2353        var items = group.ToList();
 2354        var durations = items
 3355            .Where(x => x.FinishedAt != null)
 1356            .Select(x => x.FinishedAt!.Value - x.CreatedAt)
 1357            .Where(x => x >= TimeSpan.Zero)
 2358            .ToList();
 2359        var value = metric switch
 2360        {
 0361            DashboardHotspotMetric.Executions => items.Count,
 5362            DashboardHotspotMetric.Incidents => items.Sum(x => x.IncidentCount),
 0363            DashboardHotspotMetric.Duration => durations.Count == 0 ? 0 : Convert.ToInt64(durations.Average(x => x.Total
 0364            _ => items.LongCount(x => x.SubStatus == WorkflowSubStatus.Faulted)
 2365        };
 366
 2367        return new()
 2368        {
 2369            DefinitionId = group.Key,
 4370            WorkflowName = items.Select(x => x.Name).FirstOrDefault(x => !string.IsNullOrWhiteSpace(x)),
 2371            Value = value,
 1372            AverageDuration = durations.Count == 0 ? null : TimeSpan.FromTicks(Convert.ToInt64(durations.Average(x => x.
 2373        };
 374    }
 375
 376    private static string NormalizeHotspotMetric(string? metric) =>
 1377        metric?.Trim().ToLowerInvariant() switch
 1378        {
 0379            "executions" => DashboardHotspotMetric.Executions,
 1380            "incidents" => DashboardHotspotMetric.Incidents,
 0381            "duration" => DashboardHotspotMetric.Duration,
 0382            _ => DashboardHotspotMetric.Faults
 1383        };
 384
 5385    private static DashboardFinding Finding(string id, string severity, string message, string? targetKind, string? targ
 5386    {
 5387        Id = id,
 5388        Severity = severity,
 5389        Message = message,
 5390        TargetKind = targetKind,
 5391        Target = target,
 5392        Priority = priority
 5393    };
 394
 24395    private static DateTimeOffset Min(DateTimeOffset left, DateTimeOffset right) => left <= right ? left : right;
 396}