Tracking Workflows
One trade-off when working with a declarative modeling framework (compared to writing traditional code) is the ability to understand each step of execution as the corresponding line of code executes at runtime. Without this insight into how things are being processed, it becomes extremely difficult to manage software and make enhancements for existing business processes, especially when they become large and complex. While working with WF, insight into a workflow’s execution quickly becomes a necessity. Some common concerns that come up while working with WF are
Windows Workflow tracking is the solution WF provides for answering questions like these. Tracking workflows provides a way for configuring standard workflow events and data that needs to be captured using the WF runtime.
Just as workflows provide transparency into logic that is applied to business processes, WF tracking provides the foundation for tracking a workflow instance as it executes through a workflow. All the way from when a workflow starts to when it finishes or is aborted, there are additional events within each activity within a workflow.
WF tracking comes ready to use out of the box, so there is nothing extra required for implementing it. It just needs to be configured based on the data that is required to be tracked. Each version of WF since its inception has come with a version of tracking, as the original authors of WF saw early on the importance of being able to track and gather important data on how a workflow executes. However, since the release of WF4 significant changes have been made in making the tracking service more efficient and the implementation easier to understand and set up.
Essentially, data pertaining to a workflow’s execution is always being generated from the WF runtime. Tracked data can be subscribed to and filtered based on the necessary data that is required to be collected. After obtaining the workflow instance’s tracked data, it can be stored within a database or data file.
Tracking Overview
When WF was originally released, tracking workflow instance data was automatically stored within SQL Server; this changed when WF4 was released and storing tracked data became more generic. Instead of WF handling where the data is stored, WF4 requires the developer to decide the best option for where tracked workflow instance data should be stored based on a given workflow solution. WF4 does, however, provide tracing of a workflow instance by logging tracked data to the Event Tracing for Windows (ETW), as illustrated within Figure 9-1. ETW was first provided with Windows 2000. Through the years, ETW has been enlisted to instrument internal system event tracing tied to the operating system because ETW is an efficient way for high-velocity tracing. ETW also provides support for tracing events for running applications. ETW can be configured dynamically so that applications being traced are not aware, nor are affected, while making changes about specific data that is required to be traced. The traced data that is sent to the ETW is logged in chronological order.
Figure 9-1. Windows Workflow tracking overview
The WF runtime emits information about a workflow instance’s execution called a tracking record. There are different types of tracking records that the WF runtime provides; they are represented in Table 9-1.
Table 9-1. Different Types of Tracking Records
Tracking Record | Detail |
---|---|
Workflow life cycle | Emitted as a workflow instance reaches events for a workflow’s lifecycle. |
Activity life cycle | Emitted as a workflow instance reaches events for an activity’s life cycle. |
Bookmark resumption | Emitted when a bookmark is resumed for a workflow instance. |
Custom tracking | Issued when custom data needs to be tracked within a custom activity. |
Each tracking record that is emitted from the WF runtime inherits from the abstract class TrackingRecord. Table 9-2 presents the properties provided with the TrackingRecord object that can be used to define metadata about the event being tracked.
Table 9-2. TrackingRecord Properties
Property | Description |
---|---|
Annotations | Name/value pairs collection of type IDictionary<string,string> that are added when additional information needs to be supplied. |
EventTime | Defines a date and time for when a tracking record occurred. |
InstanceId | Defines the ID that is a System.Guid type for a workflow instance. |
Level | Defines a System.Diagnostics.TraceLevel type for a tracked event. The level represents the tracking record’s purpose. TraceLevel members include Off, Error, Warning, Info, and Verbose. |
RecordNumber | Sequential number that represents the order for a generated tracking record. |
Workflow Lifecycle
Tracking records are used to give transparency into a workflow instance’s execution path, but it is also important to understand the flow of the workflow’s lifecycle. A workflow’s tracked events include when a workflow instance
Therefore there are tracking records that are emitted to indicate when these events occur.
When the WF runtime is tracking events within a workflow’s lifecycle, the WF runtime uses the tracking record WorkflowInstanceRecord, which inherit from the TrackingRecord object. Along with properties mentioned in Table 9-2 which are already inherited through TrackingRecord, WorkflowInstanceRecord also includes some additional properties (see Table 9-3).
Table 9-3. Unique WorkflowInstanceRecord Properties
Property | Description |
---|---|
ActivityDefinitionId | Represents the root activity represented as the workflow that produced the tracking record. |
State | The current stage of the workflow’s life cycle when the tracking record was created. |
WorkflowDefinitionIdentity | A new property within WF4.5 of type System.Activities.WorkflowIdentity that represents the Name, Package, and Version for a workflow. |
As the state changes for a workflow instance, WorkflowInstanceRecord is emitted from the WF runtime; however, other events within the workflow life cycle trigger additional tracking records to be emitted that inherit from WorkflowInstanceRecord. Each of the tracking records indicated below inherits from WorkflowInstanceRecord, and therefore inherits the properties from Tables 9-2 and 9-3:
Tracking records are also used to provide transparency into a workflow activity’s lifecycle. A workflow’s tracked events include the following:
Therefore these events have a tracking record that is emitted to indicate when an event occurs. The tracking records mentioned in the next section also inherit from TrackingRecord, so they also inherit the same properties indicated in Table 9-4.
Table 9-4. Unique ActivityStateRecord Properties
Property | Description |
---|---|
Activity | Represents characteristics about the activity that produced the tracking record. The property type is System.Activites.Tracking.ActivityInfo and it provides the following properties: |
|
|
|
|
|
|
|
|
Arguments | Represents the type IDictionary<string,Object> as the collection of arguments associated with an activity when the tracking record is emitted. |
Variables | Represents the type IDictionary<string,Object> as the collection of variables associated with an activity when the tracking record is emitted. |
State | Represents the current stage of the activity when the tracking record is emitted. |
As a workflow instance runs, it is equally important to gain transparency into each activity that is executed within a workflow. The ActivityStateRecord is emitted when an activity is executed. The ActivityStateRecord provides detailed data about an activity to easily understand the flow of a workflow; it also includes the properties described in Table 9-4.
ActivityScheduledRecord
When a workflow starts its execution, the WF runtime schedules the root activity of the workflow. After a workflow starts, activities that contain other child activities can also schedule their children activities. The WF runtime maintains scheduled activities within a queue/stack of activities. ActivityScheduledRecord is emitted when an activity is scheduled. Table 9-5 shows the properties that distinguish the tracking record.
Table 9-5. Unique ActivityScheduledRecord Properties
Property | Description |
---|---|
Activity | Represents characteristics about the scheduling activity that produced the tracking record. The property type is System.Activites.Tracking.ActivityInfo and it provides the following properties: |
|
|
|
|
|
|
|
|
Child | Represents characteristics about the child activity that is scheduled that produced the tracking record. The property is also a type of System.Activites.Tracking.ActivityInfo. |
FaultPropagatedRecord
When code has an exception, it bubbles up the exception until it is finally handled or it simply fails. However, there is a software trail that is built during this process called the StackTrace, and the same concept is applied when the WF runtime emits a FaultPropagateRecord. A FaultPropagateRecord contains data about a fault that occurred within a workflow activity. Table 9-6 identifies the key properties of the tracking record.
Table 9-6. Unique FaultPropagatedRecord Properties
Property | Description |
---|---|
Fault | Represents the System.Exception that produced the tracking record. |
FaultHandler | Gets the fault handler. The property type is System.Activites.Tracking.ActivityInfo and it provides the following properties: |
|
|
|
|
|
|
|
|
FaultSource | Represents the activity that generated the fault. The property type is also System.Activites.Tracking.ActivityInfo. |
IsFaultSource | A Boolean type value that represents if the handler was the first handler for the fault. |
BookmarkResumptionRecord
Bookmarks are used within a workflow when a workflow instance requires an event, sometimes an event that provides data that a workflow instance requires to continue processing. BookmarkResumptionRecord is emitted when a bookmark is resumed within a workflow instance. Table 9-7 identifies the key properties of the tracking record.
Table 9-7. Unique BookmarkResumptionRecord Properties
Property | Description |
---|---|
BookmarkName | Name of the bookmark that is resumed when producing the tracking record. |
BookmarkScope | Gets the scope ID, which is a System.Guid type tied to the bookmark. |
Owner | Represents the activity that was waiting on the bookmark to resume. The property type is System.Activites.Tracking.ActivityInfo and it provides the following properties: |
|
|
|
|
|
|
|
|
Payload | A System.Object type representing the value that was passed with the bookmark as it is resumed. |
CustomTrackingRecord
There are times when custom information needs to be returned within a tracking record. A CustomTrackingRecord is a tracking record used within custom activities for tracking custom data deemed important to a workflow author. Table 9-8 describes the properties used for tracking custom data.
Table 9-8. Unique CustomTrackingRecord Properties
Property | Description |
---|---|
Data | Represents the collection of data that is defined as type IDictionary<string,Object>. |
Name | The unique name that identifies the custom tracking record. |
Activity | Represents the custom activity that custom data is gathered. It’s type is System.Activites.Tracking.ActivityInfo and it provides the following properties: |
|
|
|
|
|
|
|
State machine workflows also have their own tracking record called StateMachineStateRecord. It was introduced with the release of the Microsoft .NET Framework 4 Platform Update 1; however, it also is provided with WF4.5. It inherits from CustomTrackingRecord but provides its own unique properties to track specific state machine information (see Table 9-9).
Table 9-9. Unique StateMachineStateRecord Properties
Property | Description |
---|---|
StateMachineName | Represents the name of the state machine activity that contains the state. |
StateName | Gets the state name for the executing state when the tracking record is created. |
Note The ReceiveMessageRecord and SendMessageRecord also inherit from CustomTrackingRecord and are used for tracking when messages are received and sent within a workflow service instance.
Tracking Profile
All of the tracking records mentioned above are published through the WF runtime, but in order to subscribe to tracking orders, a tracking profile has to be built. A tracking profile indicates which tracking records need to be published based on workflow instances. Tracking profiles contain the queries used to select which tracking records are needed as well as filtering for specific information within a particular tracking record.
There are two approaches for building a tracking profile. One is a standard approach that requires a tracking profile that subscribes to a generic set of tracking records, and the other is tailored around a subset of data that is specific for understanding the exact flow of workflow instances.
Tracking profiles can be built using XML elements, placed within a standard .NET configuration like a Web.config or App.config file when using workflows hosted as WCF services, using the WorkflowServiceHost. However, tracking profiles are built through code when the workflows are hosted using WorkflowInvoker or WorkflowApplication. The main advantage of building tracking profiles within a configuration file is that the profile can be configured or modified during runtime without affecting the running workflow service’s execution. Listing 9-1 indicates a tracking profile used to track all of a workflow instance’s life cycle stages while being hosted as a WF service or using the WorkflowServiceHost for hosting a workflow.
Listing 9-1. Tracking Profile Tracking Workflow Lifecycle
<tracking>
<profiles>
<trackingProfile name="Custom Tracking Profile">
<workflow>
<workflowInstanceQueries>
<workflowInstanceQuery>
<states>
<state name="*"/>
</states>
</workflowInstanceQuery>
</workflowInstanceQueries>
</workflow>
</trackingProfile>
</profiles>
</tracking>
As a workflow instance processes, each state of the workflow instance lifecycle is tracked. The reason why each state is tracked is because of the syntax <state name="*"/> and the wildcard syntax it uses, which indicates to get all states. The same tracking profile can be built using the C# syntax in Listing 9-2.
Listing 9-2. CustomTracking Profile
static TrackingProfile BuildTrackingProfile()
{
try
{
var profile = new TrackingProfile
{
Name = "Custom Tracking Profile",
Queries =
{
new WorkflowInstanceQuery
{
States = {"*"}
}
}
};
return profile;
}
catch (Exception ex)
{
throw ex;
}
}
Tracking profiles have a way of subscribing to child activity tracking records that are emitted for children activities contained within composite activities; however, by default it is turned off. ImplementationVisibility is the property that can be found of the following tracking profiles:
The ImplementationVisibility property can be set to either
Listing 9-3 shows a TrackingProfile object supplied with an ImplementationVisibility property changed to System.Activities.Tracking.ImplementationVisibility.All.
Listing 9-3. Changing the Default ImplementationVisibility Property to All
var profile = new TrackingProfile
{
Name = "Custom Tracking Profile",
ImplementationVisibility =
System.Activities.Tracking.ImplementationVisibility.All
}
The same can be done through the configuration for a tracking profile, as indicated in Table 9-4.
<tracking>
<trackingProfile name="Custom Tracking Profile" implementationVisibility="All">
TrackingQuery
An earlier section covered the different types of tracking records that are emitted through the WF runtime; however, to subscribe to certain types of tracking records, a TrackingQuery is required to be enlisted for identifying the type of tracking record that should be subscribed to as well as what characteristics of the tracking record should be filtered. Listing 9-2 illustrates that all WorkflowInstanceRecord types should be subscribed to by using the following syntax:
Queries =
{
new WorkflowInstanceQuery
{
States = {"*"}
}
}
Each type of tracking query that is used for subscribing to tracking records inherits from System.Activities.Tracking.TrackingQuery:
In order to track multiple tracking records, the following C# syntax can be used:
Queries =
{
new WorkflowInstanceQuery
{
States = {"*"}
},
new ActivityStateQuery
{
States={"*"}
}
}
And when the tracking profile is configured using XML, the queries can be stacked, like so:
<workflowInstanceQueries>
<workflowInstanceQuery>
<states>
<state name="*"/>
</states>
</workflowInstanceQuery>
</workflowInstanceQueries>
<activityStateQueries>
<activityStateQuery>
<states>
<state name="*"/>
</states>
</activityStateQuery>
</activityStateQueries>
Note The default value for the ImplementationVisibility property for a tracking record is RootScope.
Tracking Participant
Tracking participants are the vehicle for delivering tracking records. In WF3.x, tracking was delivered out of the box to SQL Server, but in WF4.x this method is no longer supplied out of the box. Instead, a tracking participant can be created for pushing tracking records to SQL Server or any other database or file system. Tracking participants are added to the WF runtime as an extension, so in order to add a tracking participant to a workflow application hosted either by WorkflowInvoker or WorkflowApplication, the workflow extension needs to be added through code. For workflows that are either hosted as WCF services or hosted using WorkflowServiceHost, extensions can be added through the configuration. Tracking participants execute within the same process as the workflow instance, so it is good practice to make sure code within a tracking participant does not interfere with objects within the workflow or contain processes which may run for long periods of time.
Note System.Activities.Tracking.EtwTrackingParticipant is the one tracking participant that is provided out of the box with WF4.x. It loads tracking records that are subscribed to using a tracking profile and emits an Event Tracing for Windows (ETW) event that consumes the tracking record data.
BASIC TRACKING
This exercise demonstrates how to track a workflow using the following:
This example demonstrates tracking a workflow application so the tracking profile and tracking participant will be wired up using C# code.
Figure 9-2. Writeline, Delay, Writeline workflow
Figure 9-3. Running the workflow
using System;
using System.Activities.Tracking;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace WorkflowConsoleApplication1
{
public sealed class ConsoleTrackingParticipant:TrackingParticipant
{
protected override void Track(TrackingRecord record, TimeSpan timeout)
{
try
{
if (record != null)
{
if(record is WorkflowInstanceRecord)
{
WorkflowInstanceRecord InstanceRecord = record as WorkflowInstanceRecord;
Console.WriteLine(string.Format("{0} InstanceId: {1} Workflow instance state {2}",
InstanceRecord.ActivityDefinitionId,
record.InstanceId,
InstanceRecord.State));
}
else if(record is ActivityStateRecord)
{
ActivityStateRecord ActivityRecord = record as ActivityStateRecord;
Console.WriteLine(string.Format("Activity: {0} ActivityInstanceId: {1} State : {2}",
ActivityRecord.Activity.Name,
record.InstanceId,
ActivityRecord.State));
}
}
}
catch (Exception ex)
{
throw ex;
}
}
}
}
using System;
using System.Linq;
using System.Activities;
using System.Activities.Statements;
using System.Activities.Tracking;
namespace WorkflowConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// Create and cache the workflow definition
Activity workflow1 = new Workflow1();
ConsoleTrackingParticipant tracker = new ConsoleTrackingParticipant();
tracker.TrackingProfile = BuildTrackingProfile();
WorkflowInvoker invoker = new WorkflowInvoker(workflow1);
invoker.Extensions.Add(tracker);
invoker.Invoke();
Console.ReadLine();
}
static TrackingProfile BuildTrackingProfile()
{
try
{
var profile = new TrackingProfile
{
ImplementationVisibility= System.Activities.Tracking.ImplementationVisibility.All,
Name = "Custom Tracking Profile",
Queries =
{
new WorkflowInstanceQuery
{
States = {"*"}
},
new ActivityStateQuery
{
States={"*"}
}
}
};
return profile;
}
catch (Exception ex)
{
throw ex;
}
}
}
}
Figure 9-4. Console tracking data
//ImplementationVisibility = System.Activities.Tracking.ImplementationVisibility.All,
Figure 9-5. Selecting multiple activities
Figure 9-6. Child activities within a Sequence activity
Figure 9-7. Auto-connecting the Sequence activity
Figure 9-8. Flowchart containing a composite activity
//ImplementationVisibility = System.Activities.Tracking.ImplementationVisibility.All,
Figure 9-9. Tracking of composite activity
ImplementationVisibility = System.Activities.Tracking.ImplementationVisibility.All,
This exercise demonstrated how to implement tracking for a workflow by using a custom tracking profile and tracking participant for tracking a workflow instance that was hosted using WorkflowInvoker. The tracking record information was sent to the console to demonstrate the flow of the workflow. This exercise also demonstrated how to use the ImplementationVisibility property of a TrackingRecord to control whether the root activity of a composite activity is the only activity subscribed to for getting tracking records or if children tracking records also should be subscribed.
The Basic Tracking exercise demonstrated building the tracking profile and adding a tracking participant as a workflow extension through code because WorkflowInvoker was used to host the workflow as an application. But when a workflow is either hosted as a WCF service or the workflow is hosted using the WCF service host WorkflowServiceHost, tracking participants and tracking profiles can be configured using XML, as mentioned earlier. There is one caveat: a tracking participant still requires custom code for adding it as a workflow extension. Therefore a custom service behavior is required, which can be built within a custom class that implements the interface System.ServiceModel.Description.IServiceBehavior. In order to use the custom service behavior, a custom behavior extension element is required, too, that inherits from System.ServiceModel.Configuration.BehaviorExtensionElement.
Before a custom tracking participant can be configured through a configuration file for gathering data on subscribed tracking records from a workflow hosted through WorkflowServiceHost, a tracking participant must be added as an extension using a custom service behavior. The IService interface provides a way to insert and edit custom extensions for a WCF service. The interface exposes three methods:
In the next exercise, a generic tracking behavior will be built that can be used for configuring custom tracking participants. The goal for doing this is so different tracking participants can be used and changed out during runtime for a workflow instance.
BehaviorExtensionElement
System.ServiceModel.Configuration.BehaviorExtensionElement enables developers to add their own custom configuration within the System.ServiceModel section for a configuration file. It works with the .NET runtime, so assemblies can use dynamic parameters through configuration. BehaviorExtensionElement provides a way to define custom configuration elements that can be used to customize service or endpoint behaviors. To build a custom behavior extension, a custom class must inherit from BehaviorExtensionElement. There are two members of the object that need to overridden:
Tip EtwTrackingParticipant does not require any custom service behaviors or extension elements when configured as a tracking participant for either a workflow service or workflow hosted using WorkflowServiceHost.
TRACKING CONFIGURATION
This exercise will demonstrate how to configure tracking for workflows hosted through the WorkflowServiceHost. A custom tracking extension element and tracking behavior will be created to show how they are used to configure a custom tracking participant.
Figure 9-10. Setting the OperationName property
Figure 9-11. Setting CanCreateInstance and ServiceContractName
Figure 9-12. ProcessWork Workflow
Figure 9-13. Adding a composite activity to a Receive activity
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ServiceModel.Configuration;
using System.Configuration;
using System.Activities.Tracking;
namespace WorkflowConsoleApplication2
{
public class GenericTrackingExtensionElement:BehaviorExtensionElement
{
[ConfigurationProperty("profileName", DefaultValue= null,IsKey=false, IsRequired = false)]
public string ProfileName
{
get { return (string)this["profileName"]; }
set { this["profileName"] = value; }
}
[ConfigurationProperty("participantTypeName", DefaultValue = null, IsKey = false, IsRequired = false)]
public string ParticipantTypeName
{
get { return (string)this["participantTypeName"]; }
set { this["participantTypeName"] = value; }
}
public override Type BehaviorType
{
get
{
return typeof(GenericTrackingBehavior);
}
}
protected override object CreateBehavior()
{
return new GenericTrackingBehavior(ProfileName, ParticipantTypeName);
}
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ServiceModel.Description;
using System.ServiceModel.Activities;
using System.ServiceModel;
using System.Activities.Tracking;
using System.ServiceModel.Activities.Tracking.Configuration;
using System.Web.Configuration;
using System.Configuration;
namespace WorkflowConsoleApplication2
{
public class GenericTrackingBehavior:IServiceBehavior
{
private string trackingProfileName { get; set; }
public string ParticipantTypeName { get; set; }
//Constructor which passes in the tracking profile name
public GenericTrackingBehavior(string profileName,string participantTypeName)
{
try
{
ParticipantTypeName = participantTypeName;
trackingProfileName = profileName;
}
catch (Exception ex)
{
throw ex;
}
}
//method that is implemented through the IServiceBehavior contract
//Handles adding the tracking participant to the WorkflowServiceHost
public virtual void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
{
WorkflowServiceHost workflowServiceHost = serviceHostBase as WorkflowServiceHost;
try
{
if (null != workflowServiceHost)
{
string workflowDisplayName = workflowServiceHost.Activity.DisplayName;
TrackingProfile trackingProfile = GetTrackingProfileFromConfig(trackingProfileName, workflowDisplayName);
var participant = Activator.CreateInstance(Type.GetType(ParticipantTypeName));
TrackingParticipant tp = participant as TrackingParticipant;
tp.TrackingProfile = trackingProfile;
workflowServiceHost.WorkflowExtensions.Add(tp);
}
}
catch (Exception ex)
{
throw ex;
}
}
TrackingProfile GetTrackingProfileFromConfig(string profileName, string displayName)
{
TrackingProfile trackingProfile = null;
TrackingSection trackingSection = (TrackingSection)WebConfigurationManager.GetSection("system.serviceModel/tracking");
if (trackingSection == null)
{
return null;
}
//Find the profile with the specified profile name in the list of profile found in config
var match = from p in new List<TrackingProfile>(trackingSection.TrackingProfiles)
where p.Name == profileName
select p;
if (match.Count() == 0)
{
throw new ConfigurationErrorsException(string.Format("Could not find a profile with name '{0}'", profileName));
}
else
{
trackingProfile = match.First();
}
//No matching profile with the specified name was found
if (trackingProfile == null)
{
//return an empty profile
trackingProfile = new TrackingProfile()
{
ActivityDefinitionId = displayName
};
}
return trackingProfile;
}
public virtual void AddBindingParameters(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase, System.Collections.ObjectModel.Collection<ServiceEndpoint> endpoints, System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
{
}
public virtual void Validate(ServiceDescription serviceDescription, System.ServiceModel.ServiceHostBase serviceHostBase)
{
}
}
}
using System;
using System.Linq;
using System.Activities;
using System.Activities.Statements;
using System.ServiceModel.Activities;
using System.ServiceModel;
namespace WorkflowConsoleApplication2
{
[ServiceContract]
public interface ITrackingDemoService
{
[OperationContract(IsOneWay = true)]
void StartWorkflow();
}
class Program
{
static void Main(string[] args)
{
// Create and cache the workflow definition
Activity workflow1 = new Workflow1();
using (var host = new WorkflowServiceHost(workflow1))
{
host.Open();
// Create a client that sends a message to create an instance of the workflow.
ITrackingDemoService client = ChannelFactory<ITrackingDemoService>.CreateChannel(new BasicHttpBinding(), new EndpointAddress(" http://localhost:8080/TrackingDemoService "));
client.StartWorkflow();
Console.ReadLine();
}
}
}
}
Figure 9-14. Framework references
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<system.serviceModel>
<services>
<service name="Workflow1" behaviorConfiguration="">
<endpoint address="" binding="basicHttpBinding" contract="ITrackingDemoService" />
<host>
<baseAddresses>
<add baseAddress="http://localhost:8080/TrackingDemoService"/>
</baseAddresses>
</host>
</service>
</services>
<extensions>
<behaviorExtensions>
<add name="ConsoleTracking" type="WorkflowConsoleApplication2.GenericTrackingExtensionElement, WorkflowConsoleApplication2, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</behaviorExtensions>
</extensions>
<behaviors>
<serviceBehaviors>
<behavior>
<ConsoleTracking profileName="CustomTrackingProfile" participantTypeName="WorkflowConsoleApplication2.ConsoleTrackingParticipant" />
</behavior>
</serviceBehaviors>
</behaviors>
<tracking>
<profiles>
<trackingProfile name="CustomTrackingProfile">
<workflow>
<workflowInstanceQueries>
<workflowInstanceQuery>
<states>
<state name="*"/>
</states>
</workflowInstanceQuery>
</workflowInstanceQueries>
</workflow>
</trackingProfile>
</profiles>
</tracking>
</system.serviceModel>
</configuration>
Figure 9-15. Tracking data sent to the console
Filtering Tracking Records
The Basic Tracking exercise demonstrated subscribing to the published tracking records, WorkflowInstanceRecord and ActivityStateRecord. Filtering tracking records is important for subscribing to specific information about a workflow instance’s execution. This section will look at each of the tracking records to understand how specific workflow instance data can be tracked. The Tracking Configuration exercise demonstrated how to track workflows through configuration, which makes it easier for customizing tracking data by changing tracking profiles on the fly.
In order to demonstrate filtering of tracking records, the workflow that will be used will build on the tracking infrastructure that was implemented within the Tracking Configuration exercise; however, I did change out the tracking participant, ConsoleTrackingParticipant, and created another one so the entire tracking record could be serialized to XML and written to the console window. The only thing that needed to change in order to do this was the code within the override Track method, which was replaced with the code in Listing 9-4.
Listing 9-4. Serializing the Tracking Records
protected override void Track(TrackingRecord record, TimeSpan timeout)
{
string x = string.Empty;
try
{
var serialize = new DataContractSerializer(record.GetType());
XmlTextWriter writer = new XmlTextWriter(Console.Out)
{
Formatting= Formatting.Indented
};
serialize.WriteObject(writer, record);
writer.WriteRaw(Environment.NewLine);
writer.Flush();
}
catch (Exception ex)
{
throw ex;
}
}
The examples for filtering tracking will build on the simple workflow service illustrated in Figure 9-16.
Figure 9-16. Simple tracking workflow
The WorkflowInstanceRecord uses the WorkflowInstanceQuery for filtering tracking records. However, let’s first demo all of the records that will be returned when “*” is used as a wildcard, demonstrated with the tracking profile illustrated in Listing 9-5 for pulling all of the workflow instance tracking records with the new tracking participant.
Listing 9-5. Serializing the Tracking Records
<trackingProfile name="CustomTrackingProfile">
<workflow>
<workflowInstanceQueries>
<workflowInstanceQuery>
<states>
<state name="*"/>
</states>
</workflowInstanceQuery>
</workflowInstanceQueries>
</workflow>
</trackingProfile>
Figure 9-17 shows that the workflow instance tracking records have been serialized, showing the properties that are provided on the tracking record.
Figure 9-17. Serialized workflow instance tracking record
To filter the tracking records for when the workflow instance starts, the tracking profile can be changed to
<states>
<state name="Started"/>
</states>
The result is
<WorkflowInstanceRecord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="
http://schemas.datacontract.org/2004/07/System.Activities.Tracking">
<EventTime>2012-03-24T00:32:56.4870156Z</EventTime>
<InstanceId>3d4cc291-864c-40c3-9772-e5769ec7db60</InstanceId>
<Level>Info</Level>
<RecordNumber>0</RecordNumber>
<ActivityDefinitionId>Workflow1</ActivityDefinitionId>
<State>Started</State>
<WorkflowDefinitionIdentity xmlns:d2p1="
http://schemas.datacontract.org/2004/07/System.Activities" i:nil="true" />
</WorkflowInstanceRecord>
Tracking when the workflow also ends requires the following change:
<states>
<state name="Started"/>
<state name="Completed"/>
</states>
The result is
<WorkflowInstanceRecord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Activities.Tracking">
<EventTime>2012-03-24T00:45:00.3395546Z</EventTime>
<InstanceId>026aa5d3-5457-4b1e-8d65-7d37904e2077</InstanceId>
<Level>Info</Level>
<RecordNumber>0</RecordNumber>
<ActivityDefinitionId>Workflow1</ActivityDefinitionId>
<State>Started</State>
<WorkflowDefinitionIdentity xmlns:d2p1="http://schemas.datacontract.org/2004/07/System.Activities" i:nil="true" />
</WorkflowInstanceRecord>
<WorkflowInstanceRecord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Activities.Tracking">
<EventTime>2012-03-24T00:45:00.4176796Z</EventTime>
<InstanceId>026aa5d3-5457-4b1e-8d65-7d37904e2077</InstanceId>
<Level>Info</Level>
<RecordNumber>5</RecordNumber>
<ActivityDefinitionId>Workflow1</ActivityDefinitionId>
<State>Completed</State>
<WorkflowDefinitionIdentity xmlns:d2p1="http://schemas.datacontract.org/2004/07/System.Activities" i:nil="true" />
</WorkflowInstanceRecord>
This type of tracking filter is more practical for tracking starting and completing of a workflow instance. Note that the EventTime property is set and can be used to determine the length of time it took for one workflow instance over others, but what happens when unanticipated issues happen to a workflow instance? Without either tracking all workflow instance records or specifying aborted workflow instances within the tracking record, they won’t be seen. In fact, if the workflow throws an exception, the only indication that something might be wrong is that the Completed state tracking record is never received, therefore the tracking profile needs to add Aborted as a filtered state as well.
<states>
<state name="Started"/>
<state name="Aborted"/>
<state name="Completed"/>
</states>
To test this, add a Throw activity that will simulate an exception within the workflow (see Figure 9-18).
Figure 9-18. Adding a Throw activity
Now the WorkflowInstanceAbortedRecord will be tracked, as illustrated in the following results:
<WorkflowInstanceRecord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Activities.Tracking">
<EventTime>2012-03-24T01:54:38.0475625Z</EventTime>
<InstanceId>285ff549-67e2-4074-a56d-2c3ed596f014</InstanceId>
<Level>Info</Level>
<RecordNumber>0</RecordNumber>
<ActivityDefinitionId>Workflow1</ActivityDefinitionId>
<State>Started</State>
<WorkflowDefinitionIdentity xmlns:d2p1="http://schemas.datacontract.org/2004/07/System.Activities" i:nil="true" />
</WorkflowInstanceRecord>
<WorkflowInstanceAbortedRecord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/System.Activities.Tracking">
<EventTime>2012-03-24T01:54:38.1881875Z</EventTime>
<InstanceId>285ff549-67e2-4074-a56d-2c3ed596f014</InstanceId>
<Level>Info</Level>
<RecordNumber>6</RecordNumber>
<ActivityDefinitionId>Workflow1</ActivityDefinitionId>
<State>Aborted</State>
<WorkflowDefinitionIdentity xmlns:d2p1="http://schemas.datacontract.org/2004/07/System.Activities" i:nil="true" />
<Reason>Here is an exception</Reason>
</WorkflowInstanceAbortedRecord>
Notice that there is not a Completed state record, because the workflow aborts rather than completes. The WorkflowInstanceAbortedRecord contains the reason, which maps back to the exception message. If an exception caused a workflow to abort, than the WorkflowInstanceUnhandledExceptionRecord can be tracked too by querying its state
<states>
<state name="UnhandledExeption"/>
</states>
so additional information can be provided for debugging.
Tracking can get even more granular by tracking workflow activities. A custom activity called DoSomeWork has been added to the workflow using the following code:
public class DoSomeWork:CodeActivity
{
[RequiredArgument]
public InArgument<int> MillaSecondsToWait{ get; set; }
protected override void Execute(CodeActivityContext context)
{
Thread.Sleep(context.GetValue(SecondsToWait));
}
}
Adding the new activity, the milliseconds are set to 5000, simulating 5 seconds for work being processed (see Figure 9-19).
Figure 9-19. Adding a custom code activity DoSomeWork
A new tracking profile is added to the configuration for just tracking activity records:
<trackingProfile name="ActivityTrackingProfile">
<workflow>
<activityStateQueries>
<activityStateQuery>
<states>
<state name="*"/>
</states>
</activityStateQuery>
</activityStateQueries>
</workflow>
</trackingProfile>
The service behaviors custom property profileName is changed to ActivityTrackingProfile so the activity records can be tracked.
Note Tracking profiles can contain different tracking queries. For example, a tracking profile can contain a workflow instance and activity state queries together.
After running the workflow using the changed profile, all of the activity states for each activity in the workflow get tracked, but you’re interested in tracking just the custom DoSomeWork activity. So instead of getting all of the other records, you can just see the custom activity’s execution. The tracking profile needs to be changed to
<activityStateQuery activityName="DoSomeWork">
<states>
<state name="*"/>
</states>
</activityStateQuery>
The results within the console show the DoSomeWork activity's execution.
<ActivityStateRecord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="
http://schemas.datacontract.org/2004/07/System.Activities.Tracking">
<EventTime>2012-03-24T16:29:04.2771797Z</EventTime>
<InstanceId>5562743c-be60-428c-9751-d933636ee2de</InstanceId>
<Level>Info</Level>
<RecordNumber>4</RecordNumber>
<Activity>
<Id>1.3</Id>
<InstanceId>7</InstanceId>
<Name>DoSomeWork</Name>
<TypeName>CustomTrackingQueries.DoSomeWork</TypeName>
</Activity>
<State>Executing</State>
<arguments xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" />
<variables xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" />
</ActivityStateRecord>
<ActivityStateRecord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="
http://schemas.datacontract.org/2004/07/System.Activities.Tracking">
<EventTime>2012-03-24T16:29:09.3240547Z</EventTime>
<InstanceId>5562743c-be60-428c-9751-d933636ee2de</InstanceId>
<Level>Info</Level>
<RecordNumber>5</RecordNumber>
<Activity>
<Id>1.3</Id>
<InstanceId>7</InstanceId>
<Name>DoSomeWork</Name>
<TypeName>CustomTrackingQueries.DoSomeWork</TypeName>
</Activity>
<State>Closed</State>
<arguments xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" />
<variables xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays" />
</ActivityStateRecord>
Notice that the EventTime property is exactly 5 seconds, which represents the duration used to sleep the thread that simulated work being processed. The value MilliSecondsToWait is not included, so the tracking profile can be changed to query the arguments value, like so:
<arguments>
<argument name="MilliSecondsToWait"/>
</arguments>
The results are
<arguments xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<d2p1:KeyValueOfstringanyType>
<d2p1:Key>MilliSecondsToWait</d2p1:Key>
<d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:int"
>5000</d2p1:Value>
</d2p1:KeyValueOfstringanyType>
</arguments>
including the value of 5000 for the argument. Variables can also be tracked if required using the <variables> collection query.
Caution Make sure to get the Name properties spelling and case right when querying tracking records. The value MillisecondsToWait for the argument to track would not have returned the argument because the argument is MilliSecondsToWait.
Custom Data Tracking
Custom activities can also have custom data tracked that is neither a variable nor an argument. These are scenarios where data needs to be tracked because it either is retrieved from other sources outside of the workflow or it is data that is important to track so it can be referenced later—like transaction numbers or date stamps. A custom activity can create a CustomTrackingRecord and its data property can be used to hold data that needs to be published and later subscribed to using a CustomTrackingQuery.
The DoSomeWork activity has been changed and now includes a new using statement using System.Activities.Tracking; and the following code was added within the Execute method:
var record = new CustomTrackingRecord("WorkingLevel")
{
Data =
{
{"Scale", 10},
{"ScaleDescription", "Pretty dawg on hard!"}
}
};
context.Track(record);
This code builds a new CustomTrackingRecord and sets the Data property to two IDictionary<string, Object> values, Scale and ScaleDescription. The tracking profile is changed to query all custom tracking records, like so:
<customTrackingQueries>
<customTrackingQuery activityName="*"/>
</customTrackingQueries>
The results are
<CustomTrackingRecord xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns=
"http://schemas.datacontract.org/2004/07/System.Activities.Tracking">
<EventTime>2012-03-24T17:25:26.4978828Z</EventTime>
<InstanceId>5b64b301-201b-49c4-9ee1-4f5d10cc4234</InstanceId>
<Level>Info</Level>
<RecordNumber>4</RecordNumber>
<Activity>
<Id>1.5</Id>
<InstanceId>7</InstanceId>
<Name>DoSomeWork</Name>
<TypeName>CustomTrackingQueries.DoSomeWork</TypeName>
</Activity>
<Name>WorkingLevel</Name>
<data xmlns:d2p1="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<d2p1:KeyValueOfstringanyType>
<d2p1:Key>Scale</d2p1:Key>
<d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:int">10</d2p1:Value>
</d2p1:KeyValueOfstringanyType>
<d2p1:KeyValueOfstringanyType>
<d2p1:Key>ScaleDescription</d2p1:Key>
<d2p1:Value xmlns:d4p1="http://www.w3.org/2001/XMLSchema" i:type="d4p1:string">Pretty dawg on hard!</d2p1:Value>
</d2p1:KeyValueOfstringanyType>
</data>
</CustomTrackingRecord>
This shows the data values that were set within the custom activity DoSomeWork. If there were other custom activities but only custom data from the DoSomeWork activity is required, the tracking profile’s activtyName can be changed, like so:
<customTrackingQuery activityName="DoSomeWork"/>
Or, if other custom activities within the workflow have a tracking record called WorkingLevel that needs to be tracked, the profile can be changed to
<customTrackingQuery activityName="*" name="WorkingLevel"/>
which will return back tracked data for WorkingLevel for each activity.
Record Annotations
Tracking stores that are either kept within files or database stores will more than likely contain large amounts of records based on a couple of factors. There can be a large number of workflow instances being tracked depending on the activity for workflow hosted application, or a single tracking store can store tracking for multiple workflow types. For instance, one file could contain all unhandled exceptions, or a general approach for subscribing to all published tracking records may be taken where all workflow activity is tracked. With a high volume of tracking data, there might be times when tracked data needs to be flagged. Some examples could be workflows that are running a particular server and the server IP needs to be added, or indicating workflows that are processing work for certain clients. Annotations can be added within tracking queries, so once tracking information is tracked, it can be given annotations describing what the tracked information is so it can be flagged and referenced for later use. Annotations can be added within a tracking profile in the following manner:
<annotations>
<annotation name="LevelOfWork" value="These records indicate how hard the author is working!"/>
</annotations>
Annotations can even be used to categorize records, like so:
<annotations>
<annotation name="Publisher" value="Apress"/>
<annotation name="BookName" value="Pro WF4.5"/>
<annotation name="LevelOfWork" value="These records indicate how hard the author is working!"/>
</annotations>
This way other tracking records can share the same categories like Publisher and BookName for consolidating how hard other authors are working.
ETW Tracking Participant
Tracking workflows within Event Tracing for Windows (ETW) is provided out of the box with WF4.x and provides a natural way for viewing tracked workflows within the Event Viewer, a centralized place that provides visualization into the health for the system and custom applications. Many internal processes are logged to the Event Viewer, so it can be used to monitor the health of internal and external processes.
There are two ways of setting up the ETW tracking participant while using it to track workflow services or workflows hosted using WorkflowServiceHost. One way is to use code for workflow applications, hosted by WorkflowApplication or WorkflowInvoker, as demonstrated here:
Activity workflow1 = new Workflow1();
WorkflowInvoker invoker = new WorkflowInvoker(workflow1);
var tracker =
new EtwTrackingParticipant
{
TrackingProfile = BuildTrackingProfile()
};
Invoker.Extensions.Add(tracker);
Invoker.Invoke();
Another way is through a configuration file when using WorkflowServiceHost:
<behaviors>
<serviceBehaviors>
<behavior>
<etwTracking profileName="Sample Tracking Profile" />
</behavior>
</serviceBehaviors>
<behaviors>
Setting up the configuration for the ETW tracking participant is different than setting up other custom tracking participants because there is no need to add it as a custom extension to the WF runtime. Therefore, custom service behavior and element extensions do not need to be built.
The Event Viewer is provided within the operating system and can be found by clicking Start on Windows and typing “Event Viewer” (see Figure 9-20).
Figure 9-20. Searching for the Event Viewer
After opening the Event Viewer, tracking records can be found by expanding the Applications and Services Logs Microsoft Windows Application Server-Applications Microsoft-Windows-Application Server Applications/Analytic node (see Figure 9-21).
Figure 9-21. Event Viewer location for WF tracking
If the logs are disabled, they can be enabled by right-clicking on the node and selecting Enable Log (see Figure 9-22).
Figure 9-22. Enabling the logs
Using the same project as before to demonstrate tracking queries, the custom service behavior is commented out and the etwTracking is included as well as the same tracking profile for tracking custom tracking data (see Figure 9-23).
Figure 9-23. Configuring ETW tracking
Running the project again will query the same tracking records for custom data like before, but this time the records will be viewable through the Event Viewer (see Figure 9-24).
Figure 9-24. Viewing the tracked records within the log
Summary
Workflow instances run within a black box called the WF runtime, which provides limited information based on the current state within a workflow instance’s life cycle. Workflow tracking publishes different types of tracking records based on most of the characteristics that are important to track, and this is why it is important to have a good understanding of what data is available and how to track workflow instance execution.
Three core concepts for tracking workflow instances were covered in this chapter. The different tracking records provide the detailed data that can be tracked. Tracking participants provide a way to touch, save, and view tracked data. And tracking profiles allow customization of what tracking information is subscribed to from tracking records.
Tracking workflows is also a good way to see how WF4.5 works under the hood, so things like scheduling workflows and asynchronous activity execution can be monitored. Debugging is also greatly improved by tracking workflows because issues can be pinpointed to better understand different flow patterns. Tracked information in WF4.x can be sent to ETW, which provides a natural way for tracking workflow instances; however, using a custom tracking participant, tracked data can also be formatted to XML and stored within the file system or even sent to a database store.
A tracking store is different from using a line-of-business application for storing data, so there are many factors to consider when deciding where tracked data should be stored. WF4.x does provide a nice infrastructure for obtaining tracked information for a workflow, and it should be used as a way to query the flow for workflow instances, debugging issues within workflows, and even providing metadata to other line-of-business applications that need to feed off the workflow’s events and data.
The next chapter will change direction and dive into rehosting the WF designer so that workflows can be authored outside of Visual Studio.