Tuesday, November 6, 2012

Workflow SQL Tracking

If you do not like the ETW tracking idea, you can actually keep your tracking data into SQL server. How? We can create a custom tracking participant that write tracking record into database.
Here are the list of tracking record type that you can capture:
  1. ActivityStateRecord
  2. ActivityScheduledRecord
  3. CancelRequestedRecord
  4. FaultPropagationRecord
  5. WorkflowInstanceRecord
  6. BookmarkResumptionRecord
  7. CustomTrackingRecord
In today's topic, I will share how to track activity state record. The other tracking record type can be easily track with the same pattern of code.

First, we identify the properties of the activity state tracking record from MSDN, then see what record data are useful to us, then base on that data, we create a database table to store them.
So, I have created a new database call WorkflowTrackingStore.
Then, I create a new table call ActivityStateRecords.

And, Here is my table design:


Then, I use LASG to generate the code to insert tracking record into my table. Again, if you do not know what is LASG, you can refer back to my previous post. Or, you can still write code manually to create a method to insert data into the database by using ADO.net. So, here is one good reason of using LASG, it automate the repetitive coding work and get you straight and focus on your problem.

Now, let's start writing custom tracking participant. I have put up all the tracking record type which are available for capturing. Just un-comment the code and do some slight modification to make it run.

    public class SqlTrackingParticipant : TrackingParticipant
    {
        protected override void Track(TrackingRecord record, TimeSpan timeout)
        {
            //ActivityScheduledRecord activityScheduledRecord = record as ActivityScheduledRecord;
            //CancelRequestedRecord cancelRequestedRecord = record as CancelRequestedRecord;
            //FaultPropagationRecord faultPropagationRecord = record as FaultPropagationRecord;
            //WorkflowInstanceRecord workflowInstanceRecord = record as WorkflowInstanceRecord;
            //BookmarkResumptionRecord bookmarkResumptionRecord = record as BookmarkResumptionRecord;
            //CustomTrackingRecord customTrackingRecord = record as CustomTrackingRecord;

            if (record is ActivityStateRecord)
            {
                ActivityStateRecord activityStateRecord = record as ActivityStateRecord;
                CustomActivityStateRecord entity = new CustomActivityStateRecord();

                entity.InstanceId = activityStateRecord.InstanceId;
                entity.State = activityStateRecord.State;
                entity.EventTime = activityStateRecord.EventTime;
                entity.TraceLevel = (byte)activityStateRecord.Level;
                entity.ActivityId = activityStateRecord.Activity.Id;
                entity.ActivityName = activityStateRecord.Activity.Name;
                entity.Arguments = CustomSerializer.SerializeData(activityStateRecord.Arguments);
                entity.Variables = CustomSerializer.SerializeData(activityStateRecord.Variables);

                CustomActivityStateRecordDAC dac = new CustomActivityStateRecordDAC();
                dac.Create(entity);
            }
        }
    }

Note that we can track the workflow activity arguments and variables, but they are in IDictionary<string, object> type. Therefore, we need to serialize them into XML then only able to save them into the database. So, I have a custom data serializer to do the job.

    public class CustomSerializer
    {
        NetDataContractSerializer dataSerializer;

        public CustomSerializer()
        {
            dataSerializer = new NetDataContractSerializer();
        }

        public static string SerializeData(IDictionary data)
        {
            //Source from WCF WF sample from MSDN http://msdn.microsoft.com/en-us/library/dd483375(VS.100).aspx
            NetDataContractSerializer dataSerializer = new NetDataContractSerializer();
            if (data.Count == 0)
            {
                return string.Empty;
            }

            StringBuilder builder = new StringBuilder();
            XmlWriterSettings settings = new XmlWriterSettings()
            {
                OmitXmlDeclaration = true
            };
            using (XmlWriter writer = XmlWriter.Create(builder, settings))
            {
                if (dataSerializer == null)
                {
                    dataSerializer = new NetDataContractSerializer();
                }
                try
                {
                    dataSerializer.WriteObject(writer, data);
                }
                catch (Exception e)
                {
                    Trace.WriteLine(String.Format(CultureInfo.InvariantCulture, "Exception during serialization of data: {0}", e.Message));
                }

                writer.Flush();
                return builder.ToString();
            }
        }

        public static IDictionary DeserializeData(string data)
        {
            NetDataContractSerializer dataSerializer = new NetDataContractSerializer();
            IDictionary obj = default(IDictionary);

            byte[] byteArray = Encoding.ASCII.GetBytes(data);
            MemoryStream stream = new MemoryStream(byteArray);

            obj = (IDictionary)dataSerializer.Deserialize(stream);

            return obj;
        }
    }

We are done with creating custom tracking participant. Now, we should have this custom tracking participant to be added into Workflow Service Host as an extension. Therefore, I have to write a custom configuration and service behavior extension for my workflow service host. Note: I have to do this is because I am using Windows Process Activation Service host. If you use Self-Hosted Service, please refer to MSDN to see how to add extension to Self-Hosted Service, then you can skip writing the custom configuration code.

The extension element:
    
    //Source from WCF WF sample from MSDN http://msdn.microsoft.com/en-us/library/dd483375(VS.100).aspx
    //Modified to cater for Enterprise Library database factory
    public class SqlTrackingExtensionElement : BehaviorExtensionElement
    {
        [ConfigurationProperty("connectionStringName", DefaultValue = "", IsKey = false, IsRequired = true)]
        public string ConnectionStringName
        {
            get { return (string)this["connectionStringName"]; }
            set { this["connectionStringName"] = value; }
        }

        [ConfigurationProperty("profileName", DefaultValue = "", IsKey = false, IsRequired = false)]
        public string ProfileName
        {
            get { return (string)this["profileName"]; }
            set { this["profileName"] = value; }
        }

        public override Type BehaviorType { get { return typeof(SqlTrackingBehavior); } }
        protected override object CreateBehavior() { return new SqlTrackingBehavior(ConnectionStringName, ProfileName); }
    }

The behavior:
    
    public class SqlTrackingBehavior : IServiceBehavior
    {
        string profileName;
        string connectionStringName;

        public SqlTrackingBehavior(string connectionStringName, string profileName)
        {
            this.connectionStringName = connectionStringName;
            this.profileName = profileName;
        }

        public virtual void ApplyDispatchBehavior(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase)
        {
            WorkflowServiceHost workflowServiceHost = serviceHostBase as WorkflowServiceHost;
            if (null != workflowServiceHost)
            {
                string workflowDisplayName = workflowServiceHost.Activity.DisplayName;
                TrackingProfile trackingProfile = GetProfile(this.profileName, workflowDisplayName);

                workflowServiceHost.WorkflowExtensions.Add(()
                        => new SqlTrackingParticipant
                        {
                            ConnectionStringName = connectionStringName,
                            TrackingProfile = trackingProfile
                        });

            }
        }

        TrackingProfile GetProfile(string profileName, string displayName)
        {
            TrackingProfile trackingProfile = null;
            TrackingSection trackingSection = (TrackingSection)WebConfigurationManager.GetSection("system.serviceModel/tracking");
            if (trackingSection == null)
            {
                return null;
            }

            if (profileName == null)
            {
                profileName = "";
            }

            //Find the profile with the specified profile name in the list of profile found in config
            var match = from p in new List(trackingSection.TrackingProfiles)
                        where (p.Name == profileName) && ((p.ActivityDefinitionId == displayName) || (p.ActivityDefinitionId == "*"))
                        select p;

            if (match.Count() == 0)
            {
                //return an empty profile
                trackingProfile = new TrackingProfile()
                {
                    ActivityDefinitionId = displayName
                };

            }
            else
            {
                trackingProfile = match.First();
            }

            return trackingProfile;
        }

        public virtual void AddBindingParameters(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase, Collection endpoints, BindingParameterCollection bindingParameters) { }
        public virtual void Validate(ServiceDescription serviceDescription, ServiceHostBase serviceHostBase) { }
    }

Warning: The above SqlTrackingBehavior and SqlTrackingExtensionElement code are sourced from MSDN Workflow Sample project. I am Enterprise Library user and I have modified the above code to work with my Enterprise Library database factory. If you code with ADO.net, please download the MSDN sample project and get the original source code from there.

Once the custom behavior extension code are ready, we need to add them into the web.config.


One important thing to make sure is you actually have <activityStateQueries> specified in the tracking profile config section. For my sample project, I only capture activity state record. If you track any other tracking record, make sure you put them into the tracking profile config section. Above sample web.config, my tracking profile actually track workflow instance, activity state and custom tracking only. You can also specify which individual activity to track by replacing the asterisk (*) value in the activityName XML attribute with your individual activity name.
P/S: asterisk (*) mean all activities.

Now, let's build and run your workflow host.
Common error that you would get:

Server Error in '/' Application.

Configuration Error

Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.

Parser Error Message: The type 'PersistenceStoreSample.Frameworks.SqlTrackingExtensionElement,
PersistenceStoreSample.Frameworks, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' registered for extension 'sqlTracking' could not be loaded.



Root cause: 
Missing assembly reference.

Resolution:
Add the custom tracking participant project into the workflow service host project reference.


Lastly, test your application, purposely create some error. Then, see whether the tracking record get inserted into the database correctly. I wish to clarify that the source code are from MSDN WCF WF sample projects. I learned how tracking participant work and how to create SQL participant from the sample project. Then, base on my understanding, I make the code simpler and able to work with Enterprise Library. If you do not use Enterprise Library, you can download the original sample code which use ADO.net with stored procedures and contains complete and working implementation.

If you want to see the complete implementation, feel free to download my sample project from HERE.

Next topic, I will share about Workflow Visual Tracking.


No comments:

Post a Comment

Send Transactional SMS with API

This post cover how to send transactional SMS using the Alibaba Cloud Short Message Service API. Transactional SMS usually come with One Tim...