Monday, January 22, 2018

Keep Performance Counters Enabled After Installing Sitecore 9 Using PowerShell

After figuring out the Sitecore Installation Framework and successfully installing Sitecore 9 - I was finally getting to enjoy that fresh CMS smell.

I decided to take a peek at the logs (which to my surprise were now located inside the Website root's App_Data folder) and was greeted an error:


 ManagedPoolThread #0 22:19:56 WARN Failed to create counter 'Sitecore.System\Events | Events Raised / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #1 22:19:56 WARN Failed to create counter 'Sitecore.System\IO | File Watcher Events / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #2 22:19:56 WARN Failed to create counter 'Sitecore.System\Logging | Errors Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #3 22:19:56 WARN Failed to create counter 'Sitecore.System\Logging | Fatals Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #4 22:19:56 WARN Failed to create counter 'Sitecore.System\Logging | Informations Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #5 22:19:57 WARN Failed to create counter 'Sitecore.System\Logging | Warnings Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #6 22:19:57 WARN Failed to create counter 'Sitecore.System\Logging | Audits Logged / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #7 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Methods Invoked / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #8 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Objects Created / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #9 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Objects Not Created / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #10 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Types Resolved / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #11 22:19:57 WARN Failed to create counter 'Sitecore.System\Reflection | Types Not Resolved / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #12 22:19:58 WARN Failed to create counter 'Sitecore.System\Threading | Background Threads Started / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #13 22:19:58 WARN Failed to create counter 'Sitecore.System\Xml | Packets Created / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 3692 22:19:58 INFO HttpModule is being initialized  
 ManagedPoolThread #13 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Jobs | Jobs Executed / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #12 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Pipelines | Pipelines Aborted / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #11 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Pipelines | Pipelines Executed / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #10 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Pipelines | Processors Executed / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #9 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Publishing | Items Queued / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #8 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Publishing | Replacements / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #7 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | File Cleanups / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #6 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | Html Cache Clearings / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #5 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | Publishings / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #4 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | Reminders Sent / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  
 ManagedPoolThread #3 22:19:58 WARN Failed to create counter 'Sitecore.Jobs\Tasks | Tasks Executed / sec'. Sitecore has no necessary permissions for reading/creating counters.  
 Message: Access to the registry key 'Global' is denied.  

Given that I was installing Sitecore 9 on a Windows 10 VM, I must have missed a requirement/prerequisite somewhere along the line - but this looked really familiar.

I've seen this before.

Oh yeah...

From https://kb.sitecore.net/articles/404548:
Possible solution:   A Sitecore application pool user has to be a member of the system “Performance Monitor Users” group to have access to the performance counters.
Adding the user to this group and restarting IIS should resolve the problem.

That's right - Windows Performance Counters!


Turns out, this is actually mentioned in the appendix of the Sitecore Experience Platform 9.0 Sitecore Experience Platform Installation Guide under Windows Performance Counters section, too (1 page before the end of the document!):


Sitecore XP contains a built-in functionality that reads and updates the Windows performance counters that you can use to monitor and troubleshoot the Sitecore application. This functionality requires access to Windows registry keys. 

So the obvious two options -

1) Set Counters.Enabled setting to false in \App_Config\Sitecore.config
Booorrring

2) Grant access by making the application pool identity a member of the built-in Performance Monitor Users group.

Instead of simply just disabling the counters, I wanted to see exactly what it takes to keep them on.

This should be as easy as adding the Application Pool Identity of my Sitecore 9 instance as a Performance Monitor Users group.  Unfortunately for me and my VM - the Local Users and Groups option wasn't available for me:
WHERE IS IT?!


Rather than figuring out why Windows 10 isn't showing that option - I figured, why can't this just be automated?

So in the spirit of scripting everything...

I put together the following post-installation script that assigns the environment's AppPoolIdentity account to the Performance Monitor Users group, then resets IIS for the changes to take effect.

All that needs to be configured is the $accountName variable - then simply run the script as in an elevated PowerShell window:



Write-Host 

# Configure this to the same value as the Sitecore Site Name (eg. sc90.local). 
# If you're using Network Service account, set to 'NT AUTHORITY\Network Service'
$accountName = "sc90.local"  
# Get the Performance Monitor Users group policy
$group = [ADSI]"WinNT://$Env:ComputerName/Performance Monitor Users,group"

# Create Account Object
$ntAccount = New-Object System.Security.Principal.NTAccount($accountName)
Write-Host "Account to add to Performance Monitor Users: $ntAccount"

# Translates the account name represented by the NTAccount object into another IdentityReference-derived type.
$strSID = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier])

#Create the user
$user = [ADSI]"WinNT://$strSID"

try
{
        # Set user to group policy
 $group.Add($user.Path)
 Write-Host -ForegroundColor Green "$accountName has been successfully added as a member of the Performance Monitor Users group."
 
 # Reset IIS for changes to take effect
 iisreset
}
catch
{
     Write-Host -ForegroundColor Green "$accountName is already a member of Performance Monitor Users."
}


Write-Host "Process complete."
Write-Host 


After running this script, the logs no longer contained "Access to the registry key 'Global' is denied. / Sitecore has no necessary permissions for reading/creating counters" errors - indicating that Sitecore could now read the permissions.

Feel free to include this in your own set of post-installation steps and modify to fit your needs.

Happy scripting!

Wednesday, January 3, 2018

Sitecore Powershell: Valid Page URLs Report

Preface: I love the Sitecore Powershell Extensions Module, and I opt to use it every chance I get.


One of my clients had a simple request:
"Please provide a list in Excel of every single valid URL on the live global site, please." 

After running ScreamingFrog and obtaining a report with missing URLs (the final list returned was faulty – likely due to the software’s inability to hit specific links only available via AJAX rendered components) - we had a couple options on the table:

  1. Similar to functionality found in a Sitemap component, we need a simple ASPX page that loops through the content that filters out everything but the global English version, generate the URLs, trigger a web request to determine the URL's web status, and display it on the page (or create a Download button to get the list).  Code this, deploy it, etc.
  2. Do all of the above - but with Powershell - which happened to already be installed on the CMS

GUESS which I opted for? :)
Yeah!...you guessed it!

Let's get right into it.

Using the Get-ChildItem command and targetting a specific part of the content tree (explicitly using the Web DB), we get the initial list of English versioned items.
 $itemsWithMatchingCondition = Get-ChildItem 
          -Path web:'/sitecore/content/WebsiteName/Home' 
          -Language 'en' 
          -Version * 
          -Recurse 

With this specific implementation, I was lucky enough to have a stable template naming convention where all items using a template that ended with "Page" were always going to be...well...pages.
(Without this luck, I may have had to check if the item contained at least a main layout within the renderings).

To filter this, we'll use a simple IF statement with a LIKE operator against the initial item list's item:
 iif ($item.Template.Name -like $script:pageString)

Now that we have a list of page items we want to process, we need to generate the item's URL.

This handy function that sets the site context, configures the UrlOptions, and gets the URL via the LinkManager does just that:
function Get-ItemUrl($itemToProcess){
     [Sitecore.Context]::SetActiveSite("website")
     $urlop = New-Object ([Sitecore.Links.UrlOptions]::DefaultOptions)
     $urlop.AddAspxExtension = $false
     $urlop.AlwaysIncludeServerUrl = $true
     $linkUrl = [Sitecore.Links.LinkManager]::GetItemUrl($itemToProcess,$urlop)
     $linkUrl
}

Here's the fun part!

Per the requirement, we'll need to validate that the URLs Sitecore was generating were actually functioning.  Any non-functioning URLs (if any) shouldn't be included in the final report (only status code 200).

Powershell lets us make web requests - which we could then check the status of.
All we need to do here is pass in the URL we generated and expect a true or false value in return:

function IsValidPageStatus($urStr){
    $return = $false;
    $HTTP_Request = [System.Net.WebRequest]::Create($urStr)
    $HTTP_Response = $HTTP_Request.GetResponse()
    $HTTP_Status = [int]$HTTP_Response.StatusCode
    if ($HTTP_Status -eq 200) {
        $return = $true
    }
    else {
        Write-Host $urStr
        Write-Host "Response: " $HTTP_Status
        $return = $false
    }
    $HTTP_Response.Close()
    return $return
}

(Note: Any page URL that fails will be listed in the console after the script completes.)

After every URL goes through this check, we add the item to the array list:

if($isValidUrl){
      $script:itemIDsWithPassedCriteria.Add($item) > $null 
}

Finally, build out the report - which can then be exported via the Powershell ISE in CSV/Excel format:

if ($script:itemIDsWithPassedCriteria.Count -eq 0)
{
    Write-Warning "No page items found."
}else{
$props = @{
 InfoTitle = "Live Page Urls"
 InfoDescription = "Provides a list of all valid page URLs "
 PageSize = 100
}
    $script:itemIDsWithPassedCriteria|Show-ListView @props -Property 
       @{ Label = "Url"; Expression = { Get-ItemUrl ($_) } }
    Close-Window 
}


Here's the full script:

<#
.SYNOPSIS
  Provides a list report of all valid page URLs  
.AUTHOR
Written by Gabe Streza
#>
# Variables
$script:pageString = "* Page" #page string
function GetItemsWhichUsePageTemplate()
{
    $itemsWithMatchingCondition = Get-ChildItem -Path web:'/sitecore/content/WebsiteName/Home' 
                                                        -Language 'en' -Version * -Recurse 
    { 
        if ($item.Template.Name -like $script:pageString)
        {
            $linkUrl = Get-ItemUrl($item)
            $isValidUrl = IsValidPageStatus($linkUrl)
            if($isValidUrl){
                $script:itemIDsWithPassedCriteria.Add($item) > $null # The output of the Add is ignored
            }
        }
    }
}
function Get-ItemUrl($itemToProcess){
     [Sitecore.Context]::SetActiveSite("website")
     $urlop = New-Object ([Sitecore.Links.UrlOptions]::DefaultOptions)
     $urlop.AddAspxExtension = $false
     $urlop.AlwaysIncludeServerUrl = $true
     $linkUrl = [Sitecore.Links.LinkManager]::GetItemUrl($itemToProcess,$urlop)
     $linkUrl
}
function IsValidPageStatus($urStr){
    $return = $false;
    $HTTP_Request = [System.Net.WebRequest]::Create($urStr)
    $HTTP_Response = $HTTP_Request.GetResponse()
    $HTTP_Status = [int]$HTTP_Response.StatusCode
    if ($HTTP_Status -eq 200) {
        $return = $true
    }
    else {
        Write-Host $urStr
        Write-Host "Response: " $HTTP_Status
        $return = $false
    }
    $HTTP_Response.Close()
    return $return
}

$script:itemIDsWithPassedCriteria = New-Object System.Collections.ArrayList
GetItemsWhichUsePageTemplate

if ($script:itemIDsWithPassedCriteria.Count -eq 0)
{
    Write-Warning "No page items found."
}else{
$props = @{
 InfoTitle = "Live Page Urls"
 InfoDescription = "Provides a list of all valid page URLs "
 PageSize = 100
}
    $script:itemIDsWithPassedCriteria|Show-ListView @props 
                                                            -Property @{ Label = "Url"; Expression = { Get-ItemUrl ($_) } }
    Close-Window 
}
Write-Host "Done."

This took about 8 minutes to process a 2000 page site - which is good for a one-time run - but there are certainly some optimizations we should make if this was a report the client would use repeatedly in order to make it a bit snappier.  For this purpose, we're all set!

Feel free to grab this, tinker with it, and make it your own!

Let me know in the comments if this has helped - or if you have any additional recommendations.