How to: Parse Health Analyzer [OrphanApp] Entries

This post is going to provide a quick overview of how we can parse through messages in the health analyzer in order to identify all of the [OrphanApp] references that can be found in the ‘Missing server side dependencies’ problem that is reported by the health analyzer in SharePoint.

 

Background

I was recently working with a customer who had an abnormally large number of [OrphanApp] entries in their health analyzer rule.  This produced a very large number of instances of a message similar to the following – so much so that manually reading through and validating each of these entries became a very cumbersome task

[OrphanApp] App [af6f2dd4-c702-406d-a792-11c2355749db] is orphaned in the database [O365Hybrid_ContentDB1] on site collection [9c64e5d0-aaac-4037-867e-0b77c334529f]. Orphaned Apps are not accessible, cause unnecessary resource and license consumption and may fail upgrade. Try to uninstall this App. App [af6f2dd4-c702-406d-a792-11c2355749db] is orphaned in the database [O365Hybrid_ContentDB1] on site collection [9c64e5d0-aaac-4037-867e-0b77c334529f]. Try to uninstall this App. If the App uninstallation is failing, it needs to be unblocked before the orphan App can be removed.

This was further complicated by the fact that any of the site collections that we verified (Get-SPSite –Identity 9c64e5d0-aaac-4037-867e-0b77c334529f) confirmed that the site did not exist in the environment

 

Approach

The overall approach in this case was pretty straightforward

  1. Get the contents of the list item that contains all of the [OrphanApp] entries
  2. Parse the message to isolate the site collection ID, the app ID and the content database name
  3. Verify that the site collection exists by using Get-SPSite
  4. Export the results to a human-readable format using Export-CSV

 

Solution

Part 1

Let’s start with getting the contents of the list item that contains all of the [OrphanApp] entries.  These live within a list called ‘Review Problems and Solutions’, which exists in the root web of your central administration site collection, so we can retrieve that pretty easily with the following PowerShell:

$HealthList = (get-spweb https://CentralAdminURL).Lists["Review Problems and Solutions"]

Next we would want to target the individual list item that contains our [OrphanApp] entries, in this case, the title will be ‘Missing server side dependencies.’.  We can retrieve this item pretty easily by using the following PowerShell

$HealthListItem = $HealthList.getitems() | ? {($_['Severity'] -ne "4 - Success") -and ($_['Title'] -eq "Missing server side dependencies.")}

This will return the entire list item, but in this case we are only interested in the ‘Explanation’ field.  Again, this is easily accessible using the following PowerShell:

$HealthExplanation = $HealthListItem[‘Explanation’]

Part 2

Now we have the entire string of data that we need to parse, which will include multiple entries.  These are separated with hard carriage returns, so we can actually split the ‘Explanation’ into all of the individual messages in this case by using Split as shown the following PowerShell:

$Messages = $HealthExplanation.Split(“`r`n”)

This will then result in a collection of messages, which we can iterate through by using a foreach loop

Once we are in our foreach loop, we’re going to use regular expression (or RegEx) to extract the information that we need.  Let’s start with the database name.  If we refer to the message above, we can see that the content database name is the only instance where we see words enclosed in square brackets after the [OrphanApp] entry, so we would use the following regular expression in PowerShell to extract that information from the string:

$DatabaseName = ([Regex]"\`[\w*\`]").Matches($Message)[1].Value.replace("[","").Replace("]","")

What this is doing is finding the [1] (or second) position in the array of objects (strings) that are returned when we look for a pattern that matches ‘[some words]’.  This accounts for the [OrphanApp] entry at the start of the array, which would be position [0].  The two instances of ‘.replace’ are used to strip the leading and trailing square brackets so that we return only the string.

Next we will extract the app ID and the site collection ID from the message using two very similar Powershell examples.  Again we will use regular expression.  Let’s take a look at how we extract the app ID in the first example.  The format of these messages is pretty standard and uniform.  The first GUID that we see in the message is going to be the app ID.  So we’re going to use the following PowerShell example to extract the first GUID from the message:

$AppGUID = ([Regex]"\`[[a-fA-F0-9]{8}`-([a-fA-F0-9]{4}`-){3}[a-fA-F0-9]{12}\`]").Matches($Message)[0].Value.Replace("[","").Replace("]","")

Let’s look at the RegEx formula that we’re using and break it down into its components quickly before moving on.  Each of these patterns must be matched in succession:

  1. \`[   : We need to escape the ‘[‘ character, because this has special meaning in regular expression
  2. [a-fA-F0-9]{8} : This includes all characters that could be used in a GUID, upper and lower case.  We’re looking specifically for 8 characters that match this pattern
  3. `- : We need to escape the ‘-‘ character, because this has special meaning in a regular expression
  4. ([a-fA-F0-9]{4}`-){3} : Again, this includes all characters that could be used in a GUID, we are looking for 3 successive instances of 4 acceptable characters, each followed by a ‘-‘
  5. [a-fA-F0-9]{12} : Again, this includes all acceptable characters that could be used in a GUID.  We are looking specifically for 12 characters that match this pattern
  6. \`] : We need to escape the ‘]’ character, because this has special meaning in regular expression

All put together, this regular expression (‘\`[[a-fA-F0-9]{8}`-([a-fA-F0-9]{4}`-){3}[a-fA-F0-9]{12}\`]’) will match any GUID which is enclosed in square brackets.

In this case, as we mentioned, the app ID will be the first GUID extracted from the message that is provided by the health analyzer, this is denoted by .Matches($Message) [0] in the example above.

The site collection ID will be the second GUID extracted from the message, so in that case the RegEx we use doesn’t change dramatically:

$SCGUID = ([Regex]"\`[[a-fA-F0-9]{8}`-([a-fA-F0-9]{4}`-){3}[a-fA-F0-9]{12}\`]").Matches($Message)[1].Value.Replace("[","").Replace("]","")

The only difference in this case is that we target the second match returned, as denoted by .Matches($Message) [1] .

Part 3

Now that we have extracted the site collection ID, it’s pretty straightforward for us to use that information in a simple Get-SPSite script

Get-SPSite $SCGUID -ErrorAction 0

I use the –ErrorAction 0 (silently continue) in this case because I know that many of the site collection IDs that are referenced do not correspond to actual sites in the environment.  It’s not really error handling, but it’s preventing a flood of red from being output to the screen.  If this returns a site collection object, the site exists. If it returns nothing, then the site collection does not exist in the environment.

Part 4

Now that we have all of the information we need to collect, outputting the information to a CSV is a pretty simple way to make it human readable and portable.  We can do this by putting all of the information we need into a PSCustomObject.  We do this by using the following PowerShell script:

$LoggingInformation = [PSCustomObject] @{
        DatabaseName = $DatabaseName
        AppGUID = $AppGUID
        SiteCollectionGUID = $SCGUID
        SiteExists = $SiteExists
        }

This will effectively declare a new variable (PSCustomObject class) named LoggingInformation and set various properties (DatabaseName, AppGUID, SiteCollectionGUID, SiteExists) to values that were previously retrieved.  We can then export this object to a CSV using the following PowerShell script:

$LoggingInformation | Export-Csv –Path “c:\sharepoint\output.csv -Append –NoTypeInformation

In this case, the LogingInformation variable is considered the input object.  I’m using the ‘Append’ switch to ensure that the information is added to the end of the CSV and the ‘NoTypeInformation’ switch just so that we don’t export type information for every object exported

 

Download the Script

I’ve created a script that puts all of this together for you, and you can download it from the TechNet script gallery.  There you will be able to find usage details that will help you execute the script.

Download Get-OrphanedApps.ps1

 

Feedback

As usual, if you do happen to have any feedback or suggestions regarding how I could improve this script, feel free to reach out to me.

You can also follow me on twitter:

@Rcormier_MSFT