Using Inbound Email Services and Regex to Monitor Apex Error Emails

Source code:

Problem

You want to accept, parse, store and analyze unhandled Apex exception emails as error messages in the context of your environment, perhaps even integrating License Management Organization (LMO) information.

[img emimage]

Solution

The general solution has 3 steps:

This recipe looks at the second step in detail. Read [url http://wiki.developerforce.com/index.php/An_Introduction_To_Email_Services_on_Force.com] An Introduction to Email Services on Force.com[/url] to learn how to create an email service. Setup forwarding from the email address currently receiving unhandled Apex exception emails to the unique address of the Inbound Email Service you create.

You'll also need to create an object with fields to store the exception data you parse. The code below uses one called *Error_Message__c*.

You then need to create a class implementing *Messaging.InboundEmailHandler*, mapping this class to the email address. Now, whenever an error email is sent, the *handleInboundEmail* method on that class will be invoked.

Your class will need to implement the standard interface:

[code apex] global class NPSPErrorProcessor implements Messaging.InboundEmailHandler { global Messaging.InboundEmailResult handleInboundEmail(Messaging.InboundEmail email, Messaging.InboundEnvelope envelope) { [/code]

Using the InboundEmailResult object, you can parse and store the body of the email using regular expressions ([url http://download.oracle.com/javase/1.4.2/docs/api/java/util/regex/Pattern.html]regex[/url]) and Apex string methods to create and fill our custom object. First though, we use Apex string methods to find basic information about our message

[code apex] Messaging.InboundEmailResult result = new Messaging.InboundEmailresult(); //create a new instance of our custom object to hold the error data Error_Message__c em = new Error_Message__c(Message__c = email.plainTextBody); //convert email body to lowercase to avoid case mismatches string lcEmailBody = email.plainTextBody.toLowerCase().trim(); //First, determine the error type string emErrorContext = ''; if (lcEmailBody.contains('custom_validation')) emErrorContext = emErrorContext + 'Validation Error;'; if (lcEmailBody.contains('apex script unhandled trigger exception')) emErrorContext = emErrorContext + 'Apex Trigger;'; if (lcEmailBody.contains('batch')) emErrorContext = emErrorContext + 'Batch Apex;'; if (lcEmailBody.contains('visualforce')) emErrorContext = emErrorContext + 'Visualforce;'; if (lcEmailBody.contains('apex script unhandled exception')) emErrorContext = emErrorContext + 'Apex Class;'; //Finally, if we haven't found anything, dump it in 'other' if (emErrorContext.length() < 2) emErrorContext = 'Other;'; //Place the Error Context text into our custom object em em.Error_Context__c = emErrorContext; [/code]

You can now define a regular expression string that identifies the patterns in the email you want to match. Once you've created the string, create a new Pattern object and call the static Pattern *compile* method, passing in our regex string. Finally, create a new Matcher object to hold the results, and call the Pattern instance method *matcher*, passing the body of the email as the parameter.

[code apex] string regex = '005[A-Za-z0-9]{12}+/(00D[A-Za-z0-9]{12}+)[\\r\\n]+(.+?)\\r\\n]+caused by:[\\s]*System\\.([^:]*)[\\s]*:(.+)[\\r\\n]+(?:(?:Class)|(?:Trigger))\\.([\\w]*)\\.([\\w]*)\\.?([\\w]*):'; Pattern emailPattern = Pattern.compile(regex); Matcher emailMatcher = emailPattern.matcher(email.plainTextBody); [/code]

This regular expression will parse the body of the exception email and capture the following items from the error message:

Note how the expression stores each parsed set of information in a regular expression group (given by the number next to item above). You could expand this to include other available information, such as User ID.

[tip]The regex string above is the actual production string used for the [url http://wiki.developerforce.com/index.php/Nonprofit_Starter_Pack]Nonprofit Starter Pack[/url]. You may need or want to adjust the values if you're not using namespaces or a managed package.[/tip]

With the regular expression in had, you can check the results from the Matcher object, and using the group method, assign results to the custom object. In this case, we use SOQL to identify associated License and Account objects in the LMA (optional), and store the rest of the data in our *error_message__c* object.

[code apex] if (emailMatcher.find()){ sfLma__License__c errorLicense = [select id, sfLma__Package_Version__r.sfLma__Package__r.id, sfLma__Package_Version__r.id from sflma__License__c where sfLma__Subscriber_Org_ID__c = :emailMatcher.group(1).trim() and sflma__Status__c = 'Active' and sfLma__Package_Version__r.sfLma__Package__r.Namespace__c = :emailMatcher.group(5).trim()]; em.License__c = errorLicense.id; em.Package__c = errorLicense.sfLma__Package_Version__r.sfLma__Package__r.id; em.Package_Version__c = errorLicense.sfLma__Package_Version__r.id; em.Account__c = [select id from Account where Organization_ID__c = :emailMatcher.group(1).trim()].id; em.Exception_Type__c = emailMatcher.group(3).trim(); em.Short_Message__c = emailMatcher.group(4).trim(); em.Class_Name__c = emailMatcher.group(6).trim(); em.Method_Name__c = emailMatcher.group(7).trim(); insert em; } } //close class [/code]

The group method takes an integer corresponding to the regex pattern from our regex string to identify which data we want from the Apex error email. When complete, we insert our new error message object.

[img emobject]

Once you have all your exceptions recorded, you can use the data to create dashboards and detail views to monitor an instance or package eco-system.

[img emdashboard]

Discussion

References