Monday 16 March 2015

Getting errors from System Center Orchestrator Runbooks without subscribing multiple times.

It’s been quite annoying trying to get an error summary from a single runbook, or even a collection of runbooks, I didn’t want to resort to using a sql database for doing this at this stage. I came up with a quick proof of concept for our automated server build that would allow me to log errors and informational messages and get them all with one activity and not having to subscribe to hundreds of previous activities.

What I came up with was writing an event to the eventlog on the runbook server which contains the runbook job ID, the message text and the eventId of 100 or 200 based on info or error status.
Firstly we get the runbook job id using a sql query, this allows us to filter specifically on that instance of the runbook, rather than getting the previous attempts error messages.








This SQL query step contains the following query:

SELECT     POLICYINSTANCES.JobID 
FROM         POLICYINSTANCES INNER JOIN
                      ACTIONSERVERS ON POLICYINSTANCES.ActionServer = ACTIONSERVERS.UniqueID
WHERE     (POLICYINSTANCES.ProcessID = ) AND (ACTIONSERVERS.Computer = '') AND (POLICYINSTANCES.Status IS NULL)

Note that in the above you will need to subscribe to runbook server name and the Activity Process ID from the initialise data step.


The next step is to add a log Error step into your Runbook, or Log Info step as per below.

















This run .net step contains the following Powershell script:

$JobID = 'Subscribe_To_SQL_Query_Results'
$DateTime = 'Subscribe_To_Activity_End_Time_From_Previous_Step'
$message = 'Subscibe_To_Previous_Activity_Name Failed. Subscibe_To_Previous_Activity_ErrrorSummaryText'

# Check eventlog exists, creating if not
if (! [System.Diagnostics.EventLog]::Exists("ServerRunbook1234")) {
 New-EventLog –LogName "ServerRunbook1234" –Source "Orchestrator"
}

# Check source exists, creating if not
if (![System.Diagnostics.EventLog]::SourceExists("Orchestrator")){
 [System.Diagnostics.EventLog]::CreateEventSource("Orchestrator", "ServerRunbook1234")
}

$null = Write-Eventlog -Logname "ServerRunbook1234" `
-Source Orchestrator `
-EventID 100 `
-EntryType Information `
-Message "$($JobID)`n$($DateTime) $($message)"

For example, Something like this:


























Then at the end of your error rail you have a final step that gets all the errors and sends an email or whatever you choose to do.











The get all errors step is a little crude currently, but to minimise the query on the eventlog we query on events since the Runbook started, for this I take the initialise data step start time, and convert the utc format string to a datetime object.. I know this could be better but I was rushing.
It then turns out there is an issue when using en-GB culture to get messages – so if your outside of the UK – you may need to refine this!

Once we have the messages since the runbook started, we then select only the messages containing the runbook GUID, The guid and new line is then replaced with nothing to remove this from the log string that we output.

This crude “get all errors step” contains the following powershell script:

$activityStarttime = 'Subscribe_To_Activity_start_time_of_Initialise_Data_Step'
$jobID = 'Subscribe_To_SQL_Query_Results'

# Error 100, Info 200

# Create Constant for EventType
Set-Variable id -option Constant -value 100

# Crudely Convert Orchestrator date time to something we can search with
$activityStarttime = $activityStarttime.Replace("UTC ","")
[System.Globalization.CultureInfo]$Culture = New-Object "System.Globalization.CultureInfo" "en-US"
[DateTime]$converted = [DateTime]::ParseExact($activityStarttime,'s',$Culture)

# Change locale, as bug prevents getting localaised fields in some locales (like en-GB)
[System.Threading.Thread]::CurrentThread.CurrentCulture = New-Object "System.Globalization.CultureInfo" "en-US"

# Get corresponding Eventlog entries
$LogEntries = Get-WinEvent -FilterHashtable @{logname='ServerRunbook1234'; id=$id; StartTime=$Converted} -ErrorAction SilentlyContinue | where { $_.Message.Contains($jobID) -eq "true" }

if (!$LogEntries){throw "No events found"}

# Process relevant entries
$Errors
foreach ($entry in $LogEntries){
    $message = $entry.Message.Replace("$($jobID)`n","")
    $Errors += "$($message)`n"
}

The activity then publishes the $errors variable as per below:

























This can then be simply subscribed to on an email activity.