From b9343a1a203165ef6fe8159236df292b366340bf Mon Sep 17 00:00:00 2001 From: gurkanguran Date: Fri, 3 Mar 2023 11:44:07 +0100 Subject: [PATCH] Implemented workflow instance journal partially (#3749) * Implemented backend support for journal data partially * Extended journal design with new journal tab * Serialize JournalData for saving * Show directly the journal tab when clicked status * Added ExecutionLogMiddleware * Refactor * Replaced newtonsoft.json with built-in json library --- .../src/components.d.ts | 13 ++- .../src/models/core.ts | 1 + .../components/activity-properties.tsx | 106 +++++++++++++++++- .../workflow-instances/components/journal.tsx | 42 ++++--- .../workflow-instances/components/viewer.tsx | 38 ++++++- .../src/modules/workflow-instances/models.ts | 1 + .../Runtime/WorkflowExecutionLogStore.cs | 60 +++++++++- .../Behaviors/ExecutionLoggingBehavior.cs | 28 ----- .../Activities/ExceptionHandlingMiddleware.cs | 2 + .../Activities/ExecutionLogMiddleware.cs | 71 ++++++++++++ .../Elsa.Workflows.Core/Models/Activity.cs | 1 - .../ActivityExecutionPipeline.cs | 1 + 12 files changed, 305 insertions(+), 59 deletions(-) delete mode 100644 src/modules/Elsa.Workflows.Core/Behaviors/ExecutionLoggingBehavior.cs create mode 100644 src/modules/Elsa.Workflows.Core/Middleware/Activities/ExecutionLogMiddleware.cs diff --git a/src/designer/elsa-workflows-designer/src/components.d.ts b/src/designer/elsa-workflows-designer/src/components.d.ts index ff9ff2c29..d078ec414 100644 --- a/src/designer/elsa-workflows-designer/src/components.d.ts +++ b/src/designer/elsa-workflows-designer/src/components.d.ts @@ -6,7 +6,7 @@ */ import { HTMLStencilElement, JSXBase } from "@stencil/core/internal"; import { InputDefinition, OutputDefinition, WorkflowDefinition, WorkflowDefinitionSummary } from "./modules/workflow-definitions/models/entities"; -import { Activity, ActivityDeletedArgs, ActivitySelectedArgs, ChildActivitySelectedArgs, ContainerSelectedArgs, EditChildActivityArgs, GraphUpdatedArgs, IntellisenseContext, SelectListItem, TabChangedArgs, TabDefinition, Variable, WorkflowInstance, WorkflowInstanceSummary, WorkflowUpdatedArgs } from "./models"; +import { Activity, ActivityDeletedArgs, ActivitySelectedArgs, ChildActivitySelectedArgs, ContainerSelectedArgs, EditChildActivityArgs, GraphUpdatedArgs, IntellisenseContext, SelectListItem, TabChangedArgs, TabDefinition, Variable, WorkflowExecutionLogRecord, WorkflowInstance, WorkflowInstanceSummary, WorkflowUpdatedArgs } from "./models"; import { ActivityUpdatedArgs, DeleteActivityRequestedArgs, Widget, WorkflowDefinitionPropsUpdatedArgs, WorkflowDefinitionUpdatedArgs } from "./modules/workflow-definitions/models/ui"; import { NotificationType } from "./modules/notifications/models"; import { Button } from "./components/shared/button-group/models"; @@ -39,8 +39,11 @@ export namespace Components { } interface ElsaActivityProperties { "activity"?: Activity; + "activityExecutionLog": WorkflowExecutionLogRecord; + "activityPropertyTabIndex"?: number; "hide": () => Promise; "show": () => Promise; + "updateSelectedTab": (tabIndex: number) => Promise; } interface ElsaActivityPropertiesEditor { "activity"?: Activity; @@ -313,6 +316,7 @@ export namespace Components { "workflowInstance": WorkflowInstance; } interface ElsaWorkflowJournal { + "getExecutionLogByWorkflowInstanceId": (activityId: string) => Promise; "workflowDefinition": WorkflowDefinition; "workflowInstance": WorkflowInstance; } @@ -432,6 +436,10 @@ export interface ElsaWorkflowInstanceBrowserCustomEvent extends CustomEvent extends CustomEvent { + detail: T; + target: HTMLElsaWorkflowJournalElement; +} export interface ElsaWorkflowNavigatorCustomEvent extends CustomEvent { detail: T; target: HTMLElsaWorkflowNavigatorElement; @@ -896,6 +904,8 @@ declare namespace LocalJSX { } interface ElsaActivityProperties { "activity"?: Activity; + "activityExecutionLog"?: WorkflowExecutionLogRecord; + "activityPropertyTabIndex"?: number; } interface ElsaActivityPropertiesEditor { "activity"?: Activity; @@ -1170,6 +1180,7 @@ declare namespace LocalJSX { "workflowInstance"?: WorkflowInstance; } interface ElsaWorkflowJournal { + "onJournalItemStatusSelected"?: (event: ElsaWorkflowJournalCustomEvent) => void; "workflowDefinition"?: WorkflowDefinition; "workflowInstance"?: WorkflowInstance; } diff --git a/src/designer/elsa-workflows-designer/src/models/core.ts b/src/designer/elsa-workflows-designer/src/models/core.ts index 57298afd4..5146d039d 100644 --- a/src/designer/elsa-workflows-designer/src/models/core.ts +++ b/src/designer/elsa-workflows-designer/src/models/core.ts @@ -146,6 +146,7 @@ export interface WorkflowExecutionLogRecord { activityType: string; timestamp: Date; eventName: string; + payload?: any; } export enum SyntaxNames { diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/activity-properties.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/activity-properties.tsx index 53e76a507..aabe90c63 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/activity-properties.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/activity-properties.tsx @@ -1,26 +1,34 @@ -import {Component, h, Method, Prop, State} from '@stencil/core'; +import {Component, Event, EventEmitter, h, Listen, Method, Prop, State} from '@stencil/core'; import {camelCase} from 'lodash'; import { Activity, ActivityDescriptor, Lookup, TabChangedArgs, - TabDefinition + TabDefinition, + WorkflowExecutionLogRecord } from '../../../models'; import {InfoList} from "../../../components/shared/forms/info-list"; import descriptorsStore from "../../../data/descriptors-store"; +import moment from 'moment'; +import Container from 'typedi'; +import { ActivityIconRegistry } from '../../../services'; +import { ActivityIconSize } from '../../../components/icons/activities/models'; @Component({ tag: 'elsa-activity-properties', }) export class ActivityProperties { private slideOverPanel: HTMLElsaSlideOverPanelElement; + private readonly iconRegistry: ActivityIconRegistry; constructor() { + this.iconRegistry = Container.get(ActivityIconRegistry); } @Prop({mutable: true}) public activity?: Activity; - + @Prop() public activityExecutionLog: WorkflowExecutionLogRecord; + @Prop() public activityPropertyTabIndex?: number; @State() private selectedTabIndex: number = 0; @Method() @@ -33,6 +41,17 @@ export class ActivityProperties { await this.slideOverPanel.hide(); } + @Method() + public async updateSelectedTab(tabIndex : number): Promise { + this.selectedTabIndex = tabIndex; + } + + async componentWillLoad(): Promise { + if(this.activityPropertyTabIndex != null) { + this.selectedTabIndex = this.activityPropertyTabIndex; + } + } + public render() { const activity = this.activity; const activityDescriptor = this.findActivityDescriptor(); @@ -47,7 +66,12 @@ export class ActivityProperties { content: () => this.renderCommonTab() }; - const tabs = !!activityDescriptor ? [propertiesTab, commonTab] : []; + const journalTab: TabDefinition = { + displayText: 'Journal', + content: () => this.renderJournalTab() + }; + + const tabs = !!activityDescriptor ? [propertiesTab, commonTab, journalTab] : []; const mainTitle = activity.id; const subTitle = activityDescriptor.displayName; @@ -93,4 +117,78 @@ export class ActivityProperties { return
}; + + private renderJournalTab = () => { + const log = this.activityExecutionLog; + if(log == null) return; + + const exception = log.payload?.exception; + const statusColor = log.eventName == "Completed" ? "bg-blue-100" : log.eventName == "Faulted" ? "bg-red-100" : "bg-green-100"; + const icon = this.iconRegistry.getOrDefault(log.activityType)({size: ActivityIconSize.Small}); + return ( +
+
+
+
+ + {icon} + +
+
+
+

+ {log.activityType} +

+
+
+ + + + {log.eventName} + +
+
+ {moment(log.timestamp).format('DD-MM-YYYY HH:mm:ss')} +
+
+
+
+
+
+
+
+ Activity ID + +
+
{log.activityId}
+
+ {!!exception ? ( + [
+
+ Exception + +
+
+ {exception.message} +
+
, +
+
+ Exception Details + +
+
+
{JSON.stringify(exception, null, 1)}
+
+
] + ) : undefined} +
+
+
+
+
+ ) + }; } diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/journal.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/journal.tsx index 08b659f05..7e1b5a4e0 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/journal.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/journal.tsx @@ -1,9 +1,8 @@ -import {Component, h, Prop, State, Watch} from "@stencil/core"; -import {ActivityDescriptor, Workflow, WorkflowExecutionLogRecord, WorkflowInstance} from "../../../models"; +import {Component, h, Method, Prop, State, Watch, Event, EventEmitter} from "@stencil/core"; +import {Workflow, WorkflowExecutionLogRecord, WorkflowInstance} from "../../../models"; import {Container} from "typedi"; import {ActivityIconRegistry, ActivityNode, createActivityNodeMap, flatten, walkActivities} from "../../../services"; import {durationToString, formatTime, getDuration, Hash, isNullOrWhitespace} from "../../../utils"; -import descriptorsStore from '../../../data/descriptors-store'; import {ActivityExecutionEventBlock} from "../models"; import {ActivityIconSize} from "../../../components/icons/activities"; import {WorkflowDefinition} from "../../workflow-definitions/models/entities"; @@ -33,6 +32,7 @@ export class Journal { @State() workflowExecutionLogRecords: Array = []; @State() rootBlocks: Array = []; @State() expandedBlocks: Array = []; + @Event() journalItemStatusSelected: EventEmitter; @Watch('workflowInstance') async onWorkflowInstanceChanged(value: string) { @@ -47,20 +47,18 @@ export class Journal { await this.loadJournalPage(0); } - async componentWillLoad() { + @Method() + public async getExecutionLogByWorkflowInstanceId(activityId: string): Promise { + const logRecords = this.workflowExecutionLogRecords.filter(r => r.activityId === activityId); + return logRecords.find(r => r.eventName === "Faulted" || r.eventName === "Completed") ?? logRecords.find(r => r.eventName === "Started"); + } + + async componentWillLoad(): Promise { this.createGraph(); await this.loadJournalPage(0); } render() { - const workflowInstance = this.workflowInstance; - const workflowDefinition = this.workflowDefinition; - const nodeMap = this.nodeMap; - const activityDescriptors: Array = descriptorsStore.activityDescriptors; - const blocks = this.rootBlocks; - const iconRegistry = this.iconRegistry; - const expandedBlocks = this.expandedBlocks; - return (
@@ -107,16 +105,17 @@ export class Journal { const nodeMap = this.nodeMap; const iconRegistry = this.iconRegistry; const expandedBlocks = this.expandedBlocks; - return blocks.map((block, index) => { + const activityNode = nodeMap[block.activityId]; const activity = activityNode.activity; const activityMetadata = activity.metadata; const activityDisplayText = isNullOrWhitespace(activityMetadata.displayText) ? activity.id : activityMetadata.displayText; const duration = durationToString(block.duration); - const status = block.completed ? 'Completed' : 'Started'; + const status = block.completed ? 'Completed' : block.faulted ? 'Faulted' : 'Started'; const icon = iconRegistry.getOrDefault(activity.type)({size: ActivityIconSize.Small}); const expanded = !!expandedBlocks.find(x => x == block); + const statusColor = block.completed ? "bg-blue-100" : block.faulted ? "bg-red-100" : "bg-green-100"; const toggleIcon = expanded ? ( @@ -154,7 +153,9 @@ export class Journal {
- {status} + {block.children.length == 0 ? + ( this.onStatusClick(e, block)} class={`inline-flex rounded-full ${statusColor} px-2 text-xs font-semibold leading-5 text-green-800`}>{status}) + : {status}} {duration} , expanded ? this.renderBlocks(block.children) : undefined] @@ -187,7 +188,7 @@ export class Journal { return; const workflowInstanceId = this.workflowInstance.id; - const pageOfRecords = await this.workflowInstancesApi.getJournal({page, pageSize: PAGE_SIZE, workflowInstanceId: workflowInstanceId}) + const pageOfRecords = await this.workflowInstancesApi.getJournal({page, pageSize: PAGE_SIZE, workflowInstanceId: workflowInstanceId}); const blocks = this.createBlocks(pageOfRecords.items); const rootBlocks = blocks.filter(x => !x.parentActivityInstanceId); this.workflowExecutionLogRecords = [...this.workflowExecutionLogRecords, ...pageOfRecords.items]; @@ -197,9 +198,11 @@ export class Journal { private createBlocks = (records: Array): Array => { const startedEvents = records.filter(x => x.eventName == 'Started'); const completedEvents = records.filter(x => x.eventName == 'Completed'); + const faultedEvents = records.filter(x => x.eventName == 'Faulted'); const blocks = startedEvents.map(startedRecord => { const completedRecord = completedEvents.find(x => x.activityInstanceId == startedRecord.activityInstanceId); + const faultedRecord = faultedEvents.find(x => x.activityInstanceId == startedRecord.activityInstanceId); const duration = !!completedRecord ? getDuration(completedRecord.timestamp, startedRecord.timestamp) : null; return { @@ -207,10 +210,12 @@ export class Journal { activityInstanceId: startedRecord.activityInstanceId, parentActivityInstanceId: startedRecord.parentActivityInstanceId, completed: !!completedRecord, + faulted: !!faultedRecord, timestamp: startedRecord.timestamp, duration: duration, startedRecord: startedRecord, completedRecord: completedRecord, + faultedRecord: faultedRecord, children: [] }; }); @@ -243,4 +248,9 @@ export class Journal { const existingBlock = this.expandedBlocks.find(x => x == block); this.expandedBlocks = existingBlock ? this.expandedBlocks.filter(x => x != existingBlock) : [...this.expandedBlocks, block]; }; + + private onStatusClick = async (e: MouseEvent, block: ActivityExecutionEventBlock) => { + e.preventDefault(); + this.journalItemStatusSelected.emit(block.activityId); + }; } diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/viewer.tsx b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/viewer.tsx index 24642b411..4f28774a5 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/viewer.tsx +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/components/viewer.tsx @@ -6,7 +6,8 @@ import { ActivitySelectedArgs, ContainerSelectedArgs, GraphUpdatedArgs, - WorkflowInstance + WorkflowInstance, + WorkflowExecutionLogRecord } from '../../../models'; import {PluginRegistry, ActivityNameFormatter, ActivityDriverRegistry, EventBus} from '../../../services'; import {MonacoEditorSettings} from "../../../services/monaco-editor-settings"; @@ -23,6 +24,8 @@ export class WorkflowInstanceViewer { private readonly activityNameFormatter: ActivityNameFormatter; private flowchartElement: HTMLElsaFlowchartElement; private container: HTMLDivElement; + private workflowJournalElement: HTMLElsaWorkflowJournalElement; + private activityPropertiesElement: HTMLElsaActivityPropertiesElement; constructor() { this.eventBus = Container.get(EventBus); @@ -37,6 +40,8 @@ export class WorkflowInstanceViewer { @State() private workflowDefinitionState: WorkflowDefinition; @State() private workflowInstanceState: WorkflowInstance; @State() private selectedActivity?: Activity; + @State() private selectedActivityExecutionLog?: WorkflowExecutionLogRecord; + @State() private activityPropertyTabIndex?: number; @Watch('monacoLibPath') private handleMonacoLibPath(value: string) { @@ -73,6 +78,26 @@ export class WorkflowInstanceViewer { @Listen('activitySelected') private async handleActivitySelected(e: CustomEvent) { this.selectedActivity = e.detail.activity; + this.selectedActivityExecutionLog = await this.workflowJournalElement.getExecutionLogByWorkflowInstanceId(this.selectedActivity.id); + } + + @Listen('journalItemStatusSelected') + private async handleJournalStatusSelected(e: CustomEvent) { + const activityId = e.detail; + const graph = await this.flowchartElement.getGraph(); + const node = graph.getNodes().find(n => n.id == activityId) + + if(node != null) { + graph.resetSelection(node); + this.selectedActivity = node.data; + this.selectedActivityExecutionLog = await this.workflowJournalElement.getExecutionLogByWorkflowInstanceId(this.selectedActivity.id); + if(this.activityPropertiesElement == null) { + this.activityPropertyTabIndex = 2; + } + else { + this.activityPropertiesElement.updateSelectedTab(2); + } + } } @Listen('graphUpdated') @@ -127,9 +152,8 @@ export class WorkflowInstanceViewer { private renderSelectedObject = () => { const activity = this.selectedActivity; - if (!!activity) - return ; + return this.activityPropertiesElement = el}/>; } private getWorkflowInternal = async (): Promise => { @@ -159,14 +183,18 @@ export class WorkflowInstanceViewer { public render() { const workflowDefinition = this.workflowDefinitionState; const workflowInstance = this.workflowInstanceState; - return ( +
this.container = el}> this.onActivityPickerPanelStateChanged(e.detail)}> - + this.workflowJournalElement = el} + /> this.flowchartElement = el} diff --git a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/models.ts b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/models.ts index 200582c3e..8e0001602 100644 --- a/src/designer/elsa-workflows-designer/src/modules/workflow-instances/models.ts +++ b/src/designer/elsa-workflows-designer/src/modules/workflow-instances/models.ts @@ -42,6 +42,7 @@ export interface ActivityExecutionEventBlock { activityInstanceId: string; parentActivityInstanceId: string; completed: boolean; + faulted: boolean; timestamp: Date; duration?: moment.Duration; startedRecord: WorkflowExecutionLogRecord; diff --git a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowExecutionLogStore.cs b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowExecutionLogStore.cs index 63d7805ff..605f0841e 100644 --- a/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowExecutionLogStore.cs +++ b/src/modules/Elsa.EntityFrameworkCore/Modules/Runtime/WorkflowExecutionLogStore.cs @@ -1,27 +1,44 @@ +using System.Text.Json.Serialization; +using Elsa.EntityFrameworkCore.Common; +using Elsa.Workflows.Core.Serialization; using Elsa.Common.Entities; using Elsa.Common.Models; -using Elsa.EntityFrameworkCore.Common; using Elsa.Workflows.Runtime.Entities; using Elsa.Workflows.Runtime.Services; +using Microsoft.EntityFrameworkCore; +using JsonSerializer = System.Text.Json.JsonSerializer; namespace Elsa.EntityFrameworkCore.Modules.Runtime; /// public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore { + private readonly SerializerOptionsProvider _serializerOptionsProvider; + private readonly IDbContextFactory _dbContextFactory; private readonly EntityStore _store; /// /// Constructor /// - - public EFCoreWorkflowExecutionLogStore(EntityStore store) => _store = store; + public EFCoreWorkflowExecutionLogStore( + EntityStore store, + IDbContextFactory dbContextFactory, + SerializerOptionsProvider serializerOptionsProvider) + { + _store = store; + _serializerOptionsProvider = serializerOptionsProvider; + _dbContextFactory = dbContextFactory; + } + /// public async Task SaveAsync(WorkflowExecutionLogRecord record, CancellationToken cancellationToken = default) => await _store.SaveAsync(record, cancellationToken); /// - public async Task SaveManyAsync(IEnumerable records, CancellationToken cancellationToken = default) => await _store.SaveManyAsync(records, cancellationToken); + public async Task SaveManyAsync(IEnumerable records, CancellationToken cancellationToken = default) + { + await _store.SaveManyAsync(records, Save, cancellationToken); + } /// public async Task> FindManyByWorkflowInstanceIdAsync(string workflowInstanceId, PageArgs? pageArgs = default, CancellationToken cancellationToken = default) @@ -31,8 +48,43 @@ public class EFCoreWorkflowExecutionLogStore : IWorkflowExecutionLogStore x => x.Timestamp, OrderDirection.Ascending, pageArgs, + Load, cancellationToken); + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + foreach (var record in records.Items) + { + var entry = dbContext.Entry(record); + var json = entry.Property("PayloadData").CurrentValue; + if (!string.IsNullOrEmpty(json)) + { + record.Payload = JsonSerializer.Deserialize(json); + } + } + return records; } + + private WorkflowExecutionLogRecord Save(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord entity) + { + var options = _serializerOptionsProvider.CreatePersistenceOptions(ReferenceHandler.Preserve); + var json = JsonSerializer.Serialize(entity.Payload, options); + + dbContext.Entry(entity).Property("PayloadData").CurrentValue = json; + return entity; + } + + private WorkflowExecutionLogRecord? Load(RuntimeElsaDbContext dbContext, WorkflowExecutionLogRecord? entity) + { + if (entity is not null) + { + var json = dbContext.Entry(entity).Property("PayloadData").CurrentValue; + if (!string.IsNullOrEmpty(json)) + { + entity.Payload = JsonSerializer.Deserialize(json); + } + } + return entity; + } } \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Behaviors/ExecutionLoggingBehavior.cs b/src/modules/Elsa.Workflows.Core/Behaviors/ExecutionLoggingBehavior.cs deleted file mode 100644 index 2cb165060..000000000 --- a/src/modules/Elsa.Workflows.Core/Behaviors/ExecutionLoggingBehavior.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Elsa.Extensions; -using Elsa.Workflows.Core.Models; -using Elsa.Workflows.Core.Services; -using Elsa.Workflows.Core.Signals; - -namespace Elsa.Workflows.Core.Behaviors; - -/// -/// Records Start and Completed entries of the owning activity. -/// -public class ExecutionLoggingBehavior : Behavior -{ - public ExecutionLoggingBehavior(IActivity owner) : base(owner) - { - OnSignalReceived(OnActivityCompleted); - } - - protected override void Execute(ActivityExecutionContext context) - { - context.AddExecutionLogEntry("Started"); - } - - private void OnActivityCompleted(ActivityCompleted signal, SignalContext context) - { - if (context.IsSelf) - context.SenderActivityExecutionContext.AddExecutionLogEntry("Completed"); - } -} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs index 8bf5ef4cf..4a0140843 100644 --- a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExceptionHandlingMiddleware.cs @@ -1,6 +1,8 @@ +using Elsa.Extensions; using Elsa.Workflows.Core.Models; using Elsa.Workflows.Core.Pipelines.ActivityExecution; using Elsa.Workflows.Core.Services; +using Elsa.Workflows.Core.Signals; using Microsoft.Extensions.Logging; namespace Elsa.Workflows.Core.Middleware.Activities; diff --git a/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExecutionLogMiddleware.cs b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExecutionLogMiddleware.cs new file mode 100644 index 000000000..48e0d07e7 --- /dev/null +++ b/src/modules/Elsa.Workflows.Core/Middleware/Activities/ExecutionLogMiddleware.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Elsa.Extensions; +using Elsa.Workflows.Core.Models; +using Elsa.Workflows.Core.Pipelines.ActivityExecution; +using Elsa.Workflows.Core.Services; + +namespace Elsa.Workflows.Core.Middleware.Activities; + +/// +/// Adds extension methods to . +/// +public static class ExecutionLogMiddlewareExtensions +{ + /// + /// Installs the component in the activity execution pipeline. + /// + public static IActivityExecutionPipelineBuilder UseExecutionLogging(this IActivityExecutionPipelineBuilder pipelineBuilder) => pipelineBuilder.UseMiddleware(); +} + +/// +/// An activity execution middleware component that extracts execution details as . +/// +public class ExecutionLogMiddleware : IActivityExecutionMiddleware +{ + private readonly ActivityMiddlewareDelegate _next; + + /// + /// Constructor. + /// + public ExecutionLogMiddleware(ActivityMiddlewareDelegate next) + { + _next = next; + } + + /// + public async ValueTask InvokeAsync(ActivityExecutionContext context) + { + context.AddExecutionLogEntry("Started"); + + try + { + await _next(context); + + var payload = new JsonObject(); + + foreach (var entry in context.JournalData) + { + payload[entry.Key] = entry.Value != null ? JsonSerializer.Deserialize(JsonSerializer.Serialize(entry.Value)) : JsonNode.Parse("null"); + } + + context.AddExecutionLogEntry("Completed", payload: payload); + } + catch (Exception exception) + { + context.AddExecutionLogEntry("Faulted", + payload: new + { + Exception = new + { + exception.Message, + exception.Source, + exception.Data, + Type = exception.GetType() + } + }); + + throw; + } + } +} \ No newline at end of file diff --git a/src/modules/Elsa.Workflows.Core/Models/Activity.cs b/src/modules/Elsa.Workflows.Core/Models/Activity.cs index ddbfba21b..a4046d188 100644 --- a/src/modules/Elsa.Workflows.Core/Models/Activity.cs +++ b/src/modules/Elsa.Workflows.Core/Models/Activity.cs @@ -24,7 +24,6 @@ public abstract class Activity : IActivity, ISignalHandler Line = line; Type = ActivityTypeNameHelper.GenerateTypeName(GetType()); Version = 1; - Behaviors.Add(this); Behaviors.Add(this); } diff --git a/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionPipeline.cs b/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionPipeline.cs index 8916ce11d..26c16e1b6 100644 --- a/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionPipeline.cs +++ b/src/modules/Elsa.Workflows.Core/Pipelines/ActivityExecution/ActivityExecutionPipeline.cs @@ -35,6 +35,7 @@ public class ActivityExecutionPipeline : IActivityExecutionPipeline private ActivityMiddlewareDelegate CreateDefaultPipeline() => Setup(x => x .UseExceptionHandling() + .UseExecutionLogging() .UseDefaultActivityInvoker() ); } \ No newline at end of file