Thursday, October 10, 2013

Powershell Script - Remote Drivespace (Mountpoints) Emailed Report

10/09/2013: Today's post will outline a remote drivespace reporting script that I routinely use to monitor and report on the available Transaction Log space on my Exchange servers. The script leverages WMI to remotely query for the current freespace & volume size metrics (on either specified drive letters, or all Volume Mount Points on the target system), calculates the space in terms of gigabytes and percentage-free, outputs a report file, and emails the results to an admin mailbox.

I'd generally design this type of script to leverage Exchange Management Shell and dynamically query all servers in the appropriate site in the mailbox role (to automatically accommodate server adds and removals over time). But in this case I wanted this script to be 'blind' to Exchange version differences and also avoid a dependency on Exchange Management Shell. A specific goal in this case was to provide a tool to enable junior admins to evaluate the drive status after backup failure alerts, without the need to run the queries from a machine with a specific version of the Exchange Management Tools installed. So I instead use semicolon-delmited string variables to statically store arrays of server names for processing within the script itself.

Here's a broad outline of the script's processing:
  1. Prompt for any missing parameters
  2. Split my pre-defined delimited string varibles of target servers per site into arrays
  3. Use the Get-Wmiobject cmdlet to retrieve the name, driveletter, capacity, & freespace attributes of matching local drives (drivetype=3), filtering on either the drive letter (for non-mountpoint local drives), or on no-drive letter, to identify mountpoint drives, and then re-filter for a distintive Transaction-Log-identifying folder name substring, to target the Tlog drives in isolation from the array of all mountpoints (including database drives) on the mountpoint root drive.
  4. For flexibility, I chose to build my WMI query as a string -- adding and dropping appropriate syntax for the query, depending on the type of target server -- and then use the Invoke-Expression cmdlet to execute the command. Note: I use the tee cmdlet to feed the resulting output to both the console and an email-able attachment file.
  5. From there, the script uses the standard send-mailmessage cmdlet to email the output report to the specified admin address. 
UPDATE: 11/30/2016: Going through and updating the old code to Gists. Also updated posted version to the version I'm using these days.

Static-source follows (for folks in RSS readers, or disabling scripting):
(check the linked Gist/Github links below for _current_ versions).

get-Tlogs-FreeSpace-Report.ps1:

# get-Tlogs-FreeSpace-Report.ps1

<#
.SYNOPSIS
get-Tlogs-FreeSpace-Report-Ex10.ps1 - run daily logparser top-EAS report, and email the results (also usable on-demand by IOC staff, via schedtask)
.NOTES
Written By: Todd Kadrie
Website: http://tinstoys.blogspot.com
Twitter: http://twitter.com/tostka
Change Log
vers: 2:59 PM 12/23/2013 COMPANY port completed, basic function in; needs subdir/db id for SITE&SITE; could use tlog counts, for all (since edb & tlogs are comingled)
  for tracking actual tlog accumulation, in isolation from replic & edb footprint
vers: 10:49 AM 12/23/2013 COMPANY port
vers: 12:11 PM 10/25/2013 shifted to include files
vers: 8:00 AM 10/10/2013 did some cleanup, tracked down & disabled debugging code that was reporting falacious errors.
vers: 10:14 AM 9/24/2013 updated send-emailnofication() to latest vers
Vers: 8:35 AM 9/16/2013 added timestamp to console for ref
# vers: 1:01 PM 8/30/2013 replaced win32_perfformatteddata_perfdisk_logicaldisk code with win32_volume calls, to get Capacity (not supported in perfdisk)
# VERS: 11:50 AM 8/30/2013 completed, and added tweak: removed version-specific path code and replaced with generic filter: '*:\ML*'
# VERS: 2:28 PM 8/28/2013 pretty complete
# VERS: 12:55 PM 8/28/2013 fixed output to log, and mailing. Also removed split versions, now all in one. Also added parameters, prmpting and validation
# VERS: 1:03 PM 8/16/2013 - strip back EMS items - to make it run on mailexp, with or without Ex2010 ems avail
# vers: 1:28 PM 8/15/2013 - baseically working, except it's seeing dag servers in susyd & gbmk
# vers: 1:24 PM 8/7/2013 then did coding a daily logparser output, top 50 EAS users on prior 12hrs of logs
# vers: 11:07 AM 8/14/2013 first did a straight port of the tlog reporter for UNI, runs fine remotely
# vers: 2:12 PM 2/10/2012 forgot to change tom's addr on $smtptonotify
# vers: 8:03 AM 2/9/2012, shifted outfile to reports subdir, and made it read targsrvr from local comp name (run on any, not just 52 now), and added an email to TomG
# vers: 2:34 PM 11/18/2011: debugged function under schtask
# vers: 11:11 AM 11/18/2011 added freespace percentage calc
# vers: 11:45 AM 11/17/2011
# pulls tlog status info from the DAG, normally run on ex52
# # TASK ADD CMD:
#schtasks /create /s Server /tn "get-Tlogs-FreeSpace-Report-Ex10" /tr E:\SCRIPTS\get-Tlogs-FreeSpace-Report-Ex10-ps1.cmd /sc DAILY /ST 04:00 /ru na\ea-backup /rp
# TESTFIRE
# schtasks /run /tn "get-Tlogs-FreeSpace-Report-Ex10"
# update pw:
#/RP  [password]    Specifies the password for the "run as" user.
#                    To prompt for the password, the value must be either
#                    "*" or none. This password is ignored for the
#                    system account. Must be combined with either /RU or
#                    /XML switch.
#SCHTASKS /Change /s Servern2 /TN "task-mbx-size-rpt-e2k3" /RP <password>
.DESCRIPTION
Run daily tlog space report, and email the results (also usable on-demand by IOC staff, via schedtask)
.PARAMETER  $TargetServer
Infrastructure subset to be analyzed [NA|SITE|AP|EU]
.INPUTS
None. Does not accepted piped input.
.OUTPUTS
None. Returns no objects or output.
System.Boolean
            True if the current Powershell is elevated, false if not.
[use a | get-member on the script to see exactly what .NET obj TypeName is being returning for the info above]
.EXAMPLE
.\get-Tlogs-FreeSpace-Report.ps1 -SiteName Server$3 -ExTargVers 2007
.LINK
#>

#------------- Declaration and Initialization of Global Variables ------------- 

PARAM(
  [alias("s")]
  [string] $SiteName=$(Read-Host -prompt "Specify Site to analyze [SiteName: NA | EU | AU ]"),
  [alias("v")]
  [string] $ExTargVers="2010"
) # PARAM BLOCK END

## immed after param, setup to stop executing the script on the first error
trap { break; }

$bDebug = $TRUE
#$bDebug = $FALSE

If ($bDebug -eq $TRUE) {write-host "*** DEBUGGING MODE ***"}

# ----- V INCLUDES BLOCK V------
$ScriptDir = (Split-Path -parent $MyInvocation.MyCommand.Definition) + "\"
# load standard includes...
write-host -foregroundcolor darkgray ((get-date).ToString("HH:mm:ss") + ":LOADING INCLUDES:")
# constants file
# 10:50 AM 12/23/2013: updated for CO
$sLoad=($ScriptDir + "incl-CO-consts.ps1") ; if (test-path $sLoad) {
  write-host -foregroundcolor darkgray ((get-date).ToString("HH:mm:ss") + ":"+ $sLoad) ; . $sLoad } else {write-warning ((get-date).ToString("HH:mm:ss") + ":MISSING"+ $sLoad + " EXITING...") ; exit}
# functions file
# 10:50 AM 12/23/2013: updated for CO
$sLoad=($ScriptDir + "incl-funcs-base-CO.ps1") ; if (test-path $sLoad) {
  write-host -foregroundcolor darkgray ((get-date).ToString("HH:mm:ss") + ":"+ $sLoad) ; . $sLoad } else {write-warning ((get-date).ToString("HH:mm:ss") + ":MISSING"+ $sLoad + " EXITING...") ; exit}
# ----- ^ INCLUDES BLOCK ^------

$SiteName=$SiteName.ToUpper()
$ExTargVers=$ExTargVers.ToUpper()
# 12:35 PM 8/28/2013 validate the inputs
# NA|EU|AU
if (!($SiteName -match "^(NA|EU|AU)$")) {
  write-warning ("INVALID SiteName SPECIFIED: " + $SiteName + ", EXITING...")
  exit 1
}
if (!($ExTargVers -match "^(2007|2010)$")) {
  write-warning ("INVALID ExTargVers SPECIFIED: " + $ExTargVers + ", EXITING...")
  exit 1
}

# remote task exec server
$runsServer = "Server7"

$ScriptBaseName = [system.io.path]::GetFilename($MyInvocation.InvocationName)
$ScriptNameNoExt = [system.io.path]::GetFilenameWithoutExtension($MyInvocation.InvocationName)  

If ($bDebug -eq $TRUE) {
  write-host ("`$ScriptDir: " + $ScriptDir)
  write-host ("`$ScriptNameNoExt: " + $ScriptNameNoExt)
  write-host ("`$ScriptBaseName: " + $ScriptBaseName)
}

$TimeStampNow = get-date -uformat "%Y%m%d-%H%M" 

write-host " "
# confirm log dir exists
$testPath= $ScriptDir + "logs\" 
if (!(test-path $testPath)) {
  write-host "Creating " $testPath
  New-Item $testPath -type directory    
}

# 10:59 AM 12/23/2013 COMPANY upd
$SMTPFrom = (($ScriptBaseName.replace(".","-")) + "@DOMAIN.com") ; 
$SMTPServer = "Server0.domain.com" ; 
$SmtpToAdmin="todd.kadrie@DOMAIN.com" ; 
$SMTPTo=$SmtpToAdmin ; 
$SmtpToNotify="address@domain.com" ; 
$SMTPTo2=$SmtpToNotify ; 

# setup body as a hash
$SmtpBody = @() ; 


#*================v FUNCTION LISTINGS v================

#---------------------------v Function Cleanup v---------------------------
Function Cleanup {
  # clear all objects and exit
  # Clear-item doesn't seem to work as a variable release 

  exit
}
#*----------------^ END Function Cleanup ^----------------
#*================^ END FUNCTION LISTINGS ^================

#*================v SCRIPT BODY v================
#*---------------------------v Function Get-TLogSpaceRegion v---------------------------
Function Get-TLogSpaceRegion ($ExTargVers,$SiteName) {

 #start-transcript -path $outransfile
   
  If ($bDebug -eq $TRUE) {
      Write-Host ("Get-TLogSpaceRegion,ExTargVers: " + $ExTargVers)
  }
 # lose site looping - when there's an issue, it's regional at best, use explicit passes per Site, and poll all matching servers.

  Write-Host -ForegroundColor White ("Site: " + $SiteName)
  
  #$ExTargVers = 14 
  # NA | EU | AU
  #if ($ExTargVers -eq 2010 -and $SiteName -ne 'Server$3') {
  if ($SiteName -eq 'NONE') {
  
    Clear-Variable outfile -ErrorAction SilentlyContinue
    
    Write-Host -ForegroundColor Yellow ("No Ex2010 in site: " + $SiteName + " skipping...")
    $SmtpBody +="No Ex2010 in site: " + $SiteName + " skipping..."
    #$attachment = $ScriptDir + "logs\" + $ScriptNameNoExt + "-" + $targServer + "-" + $TimeStampNow + ".txt"
    #$SMTPSubj= ("Daily Rpt: "+ (Split-Path $attachment -Leaf) + " " + [System.DateTime]::Now)
    $SMTPSubj= ("Daily Rpt: Site:" + $SiteName + " " + " ExRev: " + $ExTargVers  + ", " + [System.DateTime]::Now)
    
    Send-EmailNotif
    
  } Else {
    
    # 1:06 PM 8/28/2013 drop role
    $sMsg = "Checking Exch vers " + $ExTargVers 
    if ($ExClstr -ne $null) {$sMsg += " (" + $ExClstr + " replic)" }
    $sMsg += " servers in site " + $SiteName
    Write-Host -ForegroundColor green ($sMsg)
    
    
    # $ExTargVers [8|14],$SiteName as per above
    If ($bDebug -eq $TRUE) {
      #write-host "ServersMbxNAF: " $ServersMbxNAF
      write-host "SiteName: " $SiteName
      write-host "ExTargVers: " $ExTargVers
    } # if-block end
    
    # NA|EU|AU
    switch ($SiteName)
    {
      "NA" {
        If ($bDebug -eq $TRUE) { write-host "US BLOCK"}
        switch ($ExTargVers.ToSTring()){
          "2007" {$targServers=$ServersMbxNA}
          "2010" {$targServers=$ServersMbxNA}
        } # switch block end
      } # switch entry end
      "EU" {$targServers=$ServersMbxEU}
      "AU" {$targServers=$ServersMbxAU}
    } # switch block end
    
    If ($bDebug -eq $TRUE) { write-host "targServers: " $targServers }
    
    if ($targServers.Count -eq $null) {
      Write-Host -ForegroundColor Yellow ("No matching servers in site " + $SiteName)
    } else {
            
      Write-Host ("targServers count: " + $targServers.Count)
      If ($bDebug -eq $TRUE) {$targServers}
      foreach ($targServer in $targServers) {
        # 8:34 AM 9/16/2013 timestamp added
        Write-Host ("Time: " + (get-date).ToString("HH:mm:ss"))
        #$outransfile=$ScriptDir + "logs\" + $ScriptNameNoExt + "-" + $TimeStampNow + "-trans.txt"
        $outransfile=$ScriptDir + "logs\" + $ScriptNameNoExt + "-" + $targServer + $TimeStampNow + "-trans.txt"
        # delete any existing transcript file
        # 11:59 AM 11/18/2011 check first, avoids an error
        if (test-path $outransfile) {remove-item $outransfile -force }

        write-host  -ForegroundColor Yellow ("`$targServer: " + $targServer)
        Clear-Variable outfile -ErrorAction SilentlyContinue
        
        $attachment = $ScriptDir + "logs\" + $ScriptNameNoExt + "-" + $targServer + "-" + $TimeStampNow + ".txt"
        if (test-path $attachment) {write-host -foregroundcolor yellow "removing existing" $attachment ; remove-item $attachment -force }
        write-host ("outfile: " + $attachment)
        
        # dash add to body
        $SmtpBody += ('-' * 50)
        $SmtpBody +=$targServer + " pass started," + (((get-date).ToString("HH:mm:ss")))
        
        $SMTPSubj= ("Daily Rpt: "+ (Split-Path $attachment -Leaf) + " " + [System.DateTime]::Now)

        # exception for PF server (no MPs)
        # 2:26 PM 12/23/2013 EXCEPTION for SITE-site dag; uses vmp's, named
        if (($targServer).ToUpper() -match '(?i:((SITE|BCC)MS64\d))') {

            
               # Volume-mount point; non-PF mailbox servers; those with luns with names with distinct substrings to target
              #$sExcCmd = "Get-Wmiobject -query 'select name,driveletter,capacity,freespace from win32_volume where drivetype=3 AND driveletter=NULL' -computer " + $targServer 
              # 2:20 PM 12/23/2013 COMPANY 
              $sExcCmd = "Get-Wmiobject -query 'select name,driveletter,capacity,freespace from win32_volume where drivetype=3' -computer " + $targServer 
              #$sExcCmd = $sExcCmd + " | where {(`$_.Name -like '*:\ML*')}"
              # F:\Server2
              # matches e:|f: SITE|bcc, mail str and any digit
              $sExcCmd = $sExcCmd + " | where {(`$_.Name -match '(?i:((E|F):\\(SITE|SITE).*MAIL0\d\\))')}"
        
                    # match SITE|SITE servers
        } elseif  (($targServer).ToUpper() -match '(?i:((SITE|SITE)MS64\d))' ) {
          # 12:36 PM 8/30/2013 original win32_perfformatteddata_perfdisk_logicaldisk no-capacity code 
          # pull the drive letter filter (which will come back F:; name comes back F:\
          $sExcCmd = "Get-Wmiobject -query 'select name,driveletter,capacity,freespace from win32_volume where drivetype=3' -computer " + $targServer 
          # regex e-h:
          $sExcCmd = $sExcCmd + " | where {(`$_.Name -match '(?i:((E|F|G|H):\\))')}"

              
        } # if-block end
        
        <#   test commands...
        #get-wmiobject win32_perfformatteddata_perfdisk_logicaldisk -ComputerName $targServer| where {($_.Name -ne "F:") -AND ($_.Name -ne "E:") -AND ($_.Name -ne "_Total")} | sort Name | select Name, @{ Label ="FreeSpaceGB"; Expression={($_.FreeMegabytes/1024).tostring("F0")}}, @{ Label ="FreeSpace%"; Expression={'{0:P1}' -f ($_.percentfreespace/100)}}| ft -autosize| out-file -filepath $attachment
        # testing command, to retrieve all drives/luns/WMP's on the target box  
        #get-wmiobject win32_perfformatteddata_perfdisk_logicaldisk -ComputerName "Server$3-fedexch2"| select Name  
        
        #>
        
        # 12:00 PM 8/30/2013 sub in win32_volume code, which supports capacity attrib (but doesn't appear to return freespace as % (has to be calcd)
        $sExcCmd = $sExcCmd + " | Sort Name | Select Name,@{Name='VolSize(gb)';Expression={[decimal]('{0:N1}' -f(`$_.capacity/1gb))}},@{Name='Freespace(gb)';Expression={[decimal]('{0:N1}' -f(`$_.freespace/1gb))}},@{Name='Freespace(%)';Expression={'{0:P2}' -f((`$_.freespace/1gb)/(`$_.capacity/1gb))}}"
        
        # 10:57 AM 8/28/2013 fresh tee use, putting tee inside the invoked string
        $sExcCmd=$sExcCmd + " | tee -FilePath $attachment" 
        
        If ($bDebug -eq $TRUE) {
          write-host "sExcCmd at Invoke:"
          $sExcCmd 
        }

        Invoke-Expression $sExcCmd 
        
        If ($bDebug -eq $TRUE) {
          Write-Host  -ForegroundColor Yellow "Pass completed"

          Write-Host "---"
        }
        else {
        
        }

        
        $SmtpBody +=$targServer + " pass completed," + (((get-date).ToString("HH:mm:ss"))) + "`nResults Attached: " + (gci $attachment).Name
        # add dash divider 
        $SmtpBody += ('-' * 50)

        Send-EmailNotif
        Write-Host ('='*50)
        
      } # server for-loop end
    }  # if-block end NoServers test
  }  # if-block end Vers/Site exclusion

 
 #stop-transcript

 Cleanup

}
#*---------------------------^ End Function Get-TLogSpaceRegion ^---------------------------

# 12:13 PM 10/25/2013 shifted to include
#*----------------v Function Send-EmailNotif v---------------- ; 
#Function Send-EmailNotif {

<# 
    .SYNOPSIS
Send-EmailNotif() - Generic send-mailmessage wrapper function

    .NOTES
Written By: Todd Kadrie
Website: http://tinstoys.blogspot.com
Twitter: http://twitter.com/tostka

Additional Credits: [REFERENCE]
Website: [URL]
Twitter: [URL]

Change Log
  vers: 8:28 AM 9/24/2013 - functional, from the get-db-freespace-report.ps1. Added '*Unable to connect*' error testing

-----------
Code used within main body [declarations etc]
$SMTPFrom = (($ScriptBaseName.replace(".","-")) + "@unisys.com") ; 
#$SMTPSubj= ("Daily Rpt: "+ (Split-Path $Attachment -Leaf) + " " + [System.DateTime]::Now)
# putting a crlf into the msgbody use 'n (equals CrLf)
#$objMailMessage.Body = "Hello `nThis is a second line."
#$SMTPBody="Pass Completed "+ [System.DateTime]::Now + "`nResults Attached: " +$Attachment
$SMTPServer = "NA-MAILRELAY-T3.na.uis.unisys.com" ; 
#$SMTPTo="ExchangeTaskTeam@unisys.com"
$SmtpToAdmin="todd.kadrie@unisys.com" ; 
$SMTPTo=$SmtpToAdmin ; 
$SmtpToNotify="Tom.Gunstad@chrobinson.com" ; 
$SMTPTo2=$SmtpToNotify ; 
# setup body as a hash
$SmtpBody = @()
# to accumulate the body as a log of outputs: 
$SmtpBody += 'Group Share'
# add dash divider 
$SmtpBody += ('-' * 50)
# -----------
  
    .DESCRIPTION
Send-EmailNotif() - Generic send-mailmessage wrapper function
    
    .PARAMETER  SMTPFrom
SMTP From address (alias "from")

    .PARAMETER  SmtpTo
SMTP To address (alias "to")

    .PARAMETER  SMTPSubj
SMTP Subject (alias "subj")

    .PARAMETER  SMTPServer
SMTP Server (alias "server")

    .PARAMETER  SmtpBody
Smtp Body text (alias "body")

    .PARAMETER  Attachment
SMTP File Attachment (alias "attach")

    .INPUTS
None. Does not accepted piped input.

    .OUTPUTS
None. Returns no objects or output.

    .EXAMPLE
Send-EmailNotif 
*----------^ END Comment-based Help  ^---------- #>

  <#
  PARAM(
    [parameter(Mandatory=$true)]
    [alias("from")]
    [string] $SMTPFrom,
    [parameter(Mandatory=$true)]
    [alias("to")]
    [string] $SmtpTo,
    [parameter(Mandatory=$true)]
    [alias("subj")]
    [string] $SMTPSubj,
    [parameter(Mandatory=$true)]
    [alias("server")]
    [string] $SMTPServer = "NA-MAILRELAY-T3.na.uis.unisys.com" ,
    [parameter(Mandatory=$true)]
    [alias("body")]
    [string] $SmtpBody,
    [parameter(Mandatory=$false)]
    [alias("attach")]
    [string] $Attachment 
  )
  #>

  <#
  # 9:49 AM 9/24/2013 save time and don't bother trying to mail from my laptop - 25 is blocked
  if ($bDebug) {
    write-host -foregroundcolor yellow "Funct: Send-EmailNotif()..."
    write-host -foregroundcolor darkgray "`$ComputerName: " $ComputerName
  }
  
  
  if ($ComputerName -ne 'Server$3-KADRIETS2') {
    #write-warning "no match" ; write-host " "
    
    # before you email conv to str & add CrLf:
    $SmtpBody = $SmtpBody | out-string
      
    # define/update variables into $email splat for params
    if ($Attachment -AND (test-path $Attachment)) {
      # attachment send
      $email = @{
      From = $SMTPFrom
      To = $SmtpToAdmin
      Subject = $SMTPSubj
      SMTPServer = $SMTPServer
      Body = $SmtpBody
      Attachments = $Attachment
      } 
    } else {
      # no attachment
      $email = @{
      From = $SMTPFrom
      To = $SmtpToAdmin
      Subject = $SMTPSubj
      SMTPServer = $SMTPServer
      Body = $SmtpBody
      } 
    }
    
    # 11:59 AM 8/28/2013 mailing debugging code
    # write-host  -ForegroundColor Yellow "Emailing with following parameters:"
    # $email
    # write-host "body:"
    # $SmtpBody
    # write-host ("-" * 5)
    # write-host "body.length: " $SmtpBody.length
    write-host ((get-date).ToString("HH:mm:ss") + ": sending mail...")
    $error.clear() 
    send-mailmessage @email 

    # then pipe just the errors out to console
    if($error.count -gt 0){
      write-host -foregroundcolor red ((get-date).ToString("HH:mm:ss") + ": " + $error )
      if ($error -like '*Unable to connect*') {write-host -foregroundcolor yellow ((get-date).ToString("HH:mm:ss") + ": ***" + $SMTPServer + " is unreachable on port 25.`nMake sure " + $ComputerName + " can telnet the host on port 25!***") }
    }
    
  } else {
    write-warning ($ComputerName + " is blocked for SMTP access, skipping email")
  }  # if-block end
  #>
#}#*----------------^ END Function Send-EmailNotif ^---------------- ;
#>

#--------------------------- Invocation of Get-TLogSpaceRegion ---------------------------
 # spec version:8|14) and Sitename:*Server$3*|*AUSYD*|*GBMK* 
 If ($bDebug -eq $TRUE) {write-host Get-TLogSpaceRegion -ExTargVers $ExTargVers -SiteName $SiteName}
 Get-TLogSpaceRegion -ExTargVers $ExTargVers -SiteName $SiteName
 # Turn off Tracing
 #Set-PSDebug -Trace 0
#*================^ END SCRIPT BODY ^================

And the current revision can always be found at Github: get-Tlogs-FreeSpace-Report.ps1

Wednesday, October 9, 2013

Powershell Script - Hub Queue Live Monitoring Script (Exch2007/Exch2010)

10/09/2013: Here's a script I've used in various forms since around 2007: A basic rolling Exchange Hubtransport Queue Depth monitor.

I've found over time, that regardless of SCOM or other active monitoring in place for your Exchange infrastructure, you can often see problems show up early and first in queuing behavior on your HubTransport servers:
  • Experiencing mail server storage latency issues? You'll probably see increasing patterns of queuing in the MapiDelivery queues to the problem stores, as you move into peak business primetime hours and increasing mail load delivers into the hubs, but can't be delivered into the mailbox server transaction logs and stores, fast enough to keep pace with the mail flow.
  • Experiencing cpu-saturation, or other capacity issues on your hubs, or Hub-CAS hybrid systems? You'll probably see the footprint on the CPU-dependant AV-scanning processes (you ARE running antivirus scanning on your hubs, correct? :D), which will lead to mail queuing in the Submissions queues.
    --in my case, I most recently saw this as a product of CAS Exchange Web Services load -- from Lync -- soaking up more CPU than the systems had been provisioned for, in 2007, well before the Lync infrastructure had been tacked onto the Exchange infrastructure -- looong story behind that one, that predated my arrival at the firm. :P))

So in a nutshell, I've found it a pretty good backup-monitoring option, to run a self-refreshing console display on the current queue status of all hubs in a given Exchange revision.

Toward that end, here's the current revision of my tool for the purpose, 'HT-queues-info-loop-E710.ps1', which does nothing more than output the current queue status for all hubs at a given Exchange revision level, and refresh the display every 15 seconds.

The functional outline for the script is the following:

Note:  The script is designed to accommodate use in either Exchange 2007 or Exchange 2010 (under both Powershell1 & Powershell2), and dynamically detect the current hosting Exchange version it is running on.
  1. The script dynamically tests for and loads the most-current Exchange Management Shell version available on the system.
  2. It then pulls a list of all HubTransport servers on the matching Exchange revision, into an array. 
  3. And loops that array every 15 seconds, pulling and displaying the current active queues -- by default I tend to run it displaying only non-zero queues (configured via the $bNonZero variable).
Pretty simple stuff, but I do some reformatting of the fields retrieved, and the output, to shrink the status display down, to permit me to run it out of the way on my desktop (or in an RDP to an admin workstation).

HT-queues-info-loop-E710.ps1 - Reports HubTrans queue depth on a loop every 15secs - hybrid Ex2007/Ex2010 version
Language: Powershell
Target: EMS or Powershell
 
# HT-queues-info-loop-E710.ps1

#    .SYNOPSIS
#HT-queues-info-loop-E710.ps1 - Reports HubTrans queue depth on a loop every 15secs - hybrid Ex2007/Ex2010 version
#
#    .NOTES
#Written By: Todd Kadrie
#Website: http://tinstoys.blogspot.com
#Twitter: http://twitter.com/tostka
#
# NOTE: THIS SAMPLE CODE HAS BEEN LINE-WRAPPED AT PIPE ( | ) COMMANDS 
#   AND MID-LINE SEMICOLONS ( ; ), TO IMPROVE READABILITY ON THE WEB SITE. 
#   The code will work without issues with these line-wraps, 
#   but I traditionally have these sections un-wrapped for production use.
#  
#Change Log
# rev: 8:40 AM 10/9/2013 initial public version
#
# Script to display queue information of Hub Transport Servers
# Dynamicly selects Exchange Hub Transport Servers from Exchange Organization
# Selects server's location by Site
# Output queue information
#
#    .DESCRIPTION
#HT-queues-info-loop-E710.ps1 - Reports HubTrans queue depth on a loop every 15secs
#
#   .INPUTS
#None. Does not accepted piped input.
#
#    .OUTPUTS
#Outputs to console.
#
#    .EXAMPLE
#.\HT-queues-info-loop-E710.ps1

#------------- Declaration and Initialization of Global Variables ------------- 
## immed after param, setup to stop executing the script on the first error
trap { break; }

# Set pause period (secs) & scale up to ticks
$sleeptime = 15 ; $sleeptime *= 1000

# Set to $TRUE to display only NONZERO queues. False, displays all queue depths (also overridden at EMSVers level below)
$bNonZero = $True

# debugging flag
#$bDebug = $TRUE
$bDebug = $FALSE

If ($bDebug -eq $TRUE) {
  write-host ((get-date).ToString("HH:mm:ss") + ": *** DEBUGGING MODE ***")
}

# quote & singlequote characters
$sQot = [char]34 ; 
$sQotS = [char]39 ; 

#*================v FUNCTION LISTINGS v================

#*----------v Function EMSLoadLatest v----------
#  Checks local machine for registred E20[13|10|07] EMS, and then loads the newest one found
#  Returns the string 2013|2010|2007 for reuse for version-specific code. Note, the E13 code requires further testing
#  # vers: 2:05 PM 7/19/2013 typo fix in 2013 code
#  # vers: 1:46 PM 7/19/2013
#    .NOTES
#Written By: Todd Kadrie
#Website: http://tinstoys.blogspot.com
#Twitter: http://twitter.com/tostka
#
function EMSLoadLatest {
  # compare registered vs loaded plugins;
  $SnapsReg=Get-PSSnapin -Registered ;
  $SnapsLoad=Get-PSSnapin ;
  # check/load E2013, E2010, or E2007, stop at newest (servers wouldn't be running multi-versions)
  if (($SnapsReg | 
      where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.E2013"}))
  {
    if (!($SnapsLoad | 
      where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.E2013"}))
    {
      Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2013 -ErrorAction SilentlyContinue ; 
      return "2013" ;
    } else {
      return "2013" ;
    }
  } elseif (($SnapsReg | 
      where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.E2010"})) 
  { 
    if (!($SnapsLoad | 
      where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.E2010"})) 
    {
      Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 -ErrorAction SilentlyContinue ; 
      return "2010" ;
    } else {
      return "2010" ;
    }
  } elseif (($SnapsReg | 
      where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.Admin"}))
  {
    if (!($SnapsLoad | 
        where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.Admin"}))
    {
      Add-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin -ErrorAction SilentlyContinue ; 
      return "2007" ;
    } else {
      return "2007" ;
    }
  }
}
#*----------^END Function EMSLoadLatest ^----------

#*================^ END FUNCTION LISTINGS ^================

#--------------------------- Invocation of SUB_MAIN ---------------------------

# invoke EMS module load
$EMSVers = EMSLoadLatest  # return's a string ["2013"|"2010"|"2007"]; 
# test $EMSVers for approp code in multi-version scripts (2007|2010|2013)
write-host ((get-date).ToString("HH:mm:ss") + ": `$EMSVers: " + $EMSVers)

$ComputerName = ($env:COMPUTERNAME)

# Dynamicly set variables for each site's hub transport servers, and sort by server name
# sample syntax to gather site names:
# [PS] C:\Windows\System32>Get-ExchangeServer | Where { $_.isHubTransportServer -eq $true } | sort name
# returns two sites as dns, "domain.com/Configuration/Sites/Site1" & "domain.com/Configuration/Sites/DisasterRecoveryCenter"

# switch on running EMSvers, and configure options to suit
switch ($EMSVers){
  "2007" {
      if ($bDebug) {write-host "2007"}
      # if we're on E2007, force nonzero - too many hubs/queues displayed otherwise
      $bNonZero = $TRUE
      $hubs = Get-ExchangeServer | 
        where {$_.isHubTransportServer -eq $true -AND $_.admindisplayversion.major -eq 8 } | 
        sort name       
  }
  "2010" {
    if ($bDebug) {write-host "2010"}
    $bNonZero = $TRUE
    #$E10Hub = Get-ExchangeServer |where {$_.isHubTransportServer -eq $true -AND $_.admindisplayversion.major -eq 14 } | sort name 
    $hubs = Get-ExchangeServer | 
      where {$_.isHubTransportServer -eq $true -AND $_.admindisplayversion.major -eq 14 } | 
      sort name 
  }
  "2013" {
    if ($bDebug) {write-host "2013"}
    # Note: The E2013 code for this script is largely a placemarker and unimplemented; requires further revisions...
    #$E13Hub = Get-ExchangeServer | where {$_.isHubTransportServer -eq $true -AND $_.admindisplayversion.major -eq 16 } | sort name
    # 2:11 PM 10/7/2013 E2013 messes up the admindisplayversion - MS changed the variable type from Version to String! Requires a workaround to function.
    # unimplemented yet.
  }
} # switch block end

# run a for-based infinite loop
for (;;)
{
 write-host " "
 
  # echo time per pass
  $sMsg =  (get-date).toshortdatestring() + "-" + (get-date).toshorttimestring() 
  Write-Host $sMsg -foregroundcolor "gray"
  
  # configure colors for queue displays
 [console]::ForegroundColor = "Green"
 [console]::ResetColor()  
 
 # run a list of all hub stats in $hubs site
   foreach ( $hub in $hubs )
  {
      [console]::ForegroundColor = "Green"
      write-host "----" $hub ":"   
      
      # samples of the hub queue queries being run below (formats the output to shrink the displayed footprint for full-time desktop monitoring loop display):
      # prod Exch2010 version, note: using only single quotes
      # get-exchangeserver us-hubcas1 -erroraction silentlycontinue | get-queue -erroraction silentlycontinue | where {$_.MessageCount -gt 0} |  select  @{Label='Identity';Expression={(($_.Identity.ToString())).(0,[System.Math]::Min(15, $_.Identity.Length)) + '...'}},@{Label='Type';Expression={(($_.DeliveryType.ToString())).substring(0,6) + '...'}},Status,@{Label='Count';Expression={$_.MessageCount}},@{Label='NxtHop';Expression={(($_.NextHopDomain.ToString())).substring(0,[System.Math]::Min(16, $_.NextHopDomain.Length)) + '...'}} | ft -auto | out-default
      # prod Exch2007 version single quotes
      # get-exchangeserver us-hubcas1 -erroraction silentlycontinue | get-queue -erroraction silentlycontinue | where {$_.MessageCount -gt 0} |  select  @{Name='Identity';Expression={(($_.Identity.ToString())).(0,[System.Math]::Min(15, $_.Identity.Length)) + '...'}},@{Name='Type';Expression={(($_.DeliveryType.ToString())).substring(0,6) + '...'}},Status,@{Name='Count';Expression={$_.MessageCount}},@{Name='NxtHop';Expression={(($_.NextHopDomain.ToString())).substring(0,[System.Math]::Min(16, $_.NextHopDomain.Length)) + '...'}} | ft -auto
      
      # begin building the queue query string, a piece at a time...
      $sCmd = "get-exchangeserver " + $hub + " -erroraction silentlycontinue | get-queue -erroraction silentlycontinue"
      if ($bNonZero) {
         # filter/display only non-zero queues
        $sCmd +=" | where {`$_.MessageCount -gt 0}"        
      }
      
      # append approp SELECT block (defaulting to Exch2010 syntax, correcting for E2007 below)
      $sCmd +=" |  select @{Label='Type';Expression={((`$_.DeliveryType.ToString())).substring(0,6) + '...'}},Status,@{Label='Count';Expression={`$_.MessageCount}},@{Label='NxtHop';Expression={((`$_.NextHopDomain.ToString())).substring(0,[System.Math]::Min(16, `$_.NextHopDomain.Length)) + '...'}}"
      
      # for 2007 Powershell Ver1, we need to replace LABEL custom field strings with NAME to avoid an error...
      if (($EMSVers -eq "2007") -AND ($host.version.major -eq "1")) {
        if ($bDebug) {write-host "doing Exch2007 Label:Name swap..."}
        $sCmd=$sCmd.Replace('Label', 'Name')
      }
      
      # append the output formatting
      $sCmd +=" | ft -auto"
      if ($EMSVers -eq "2010"){
          # 2010 needs added out-default to workaround error piping format-table:
          $sCmd +=" | ft -auto | out-default"
      }
      
      if ($bDebug) {
        # echo the current query syntax being run
        write-host -foregroundcolor darkgray ((get-date).ToString("HH:mm:ss") + ": `$sCmd: " + $sCmd)
        write-host -foregroundcolor darkgray ((get-date).ToString("HH:mm:ss") + ": Invoking `$sCmd...")
      }
      
      # pre-clear errors, invoke the command, and then test for and echo any errors...
      $error.clear() 
      Invoke-Expression $sCmd -verbose
      if($error.count -gt 0){
        write-host -foregroundcolor red ((get-date).ToString("HH:mm:ss") + " ERROR: " + $error ) 
      } 
      
  } # HUB for-loop end
  
 write-host "======" -foregroundcolor "gray"

 $sMsg = "pausing (" + ($sleeptime / 1000) + " secs)..."
 Write-Host $sMsg -foregroundcolor "gray"
 Start-Sleep -m $sleeptime
} # MAIN for-loop end

 
Typical output:

10/9/2013-4:36 PM

---- US-HUBCAS1 :

Type      Status Count NxtHop
----      ------ ----- ------
SmtpRe... Active    81 site1...
SmtpRe... Active    36 site2...


---- US-hubcas2 :

Type      Status Count NxtHop
----      ------ ----- ------
SmtpRe... Active     2 site2...
SmtpRe... Active     1 site1...
Undefi...  Ready     1 Submission...


---- US-HUBCAS3 :

Type      Status Count NxtHop
----      ------ ----- ------
SmtpRe... Active     1 site2...
SmtpRe... Active     5 site1...
SmtpRe... Active     1 hub version 14...
Undefi...  Ready     3 Submission...
======
pausing (15 secs)...

Wednesday, October 2, 2013

Powershell Script - Using EMS Get-MessageTrackingLog + LogParser to Retrieve Top-Traffic Metrics for the Hubs in a Site

10/02/2013:All mail admins will occasionally see "odd" Exchange HubTransport queuing behavior for "unknown" reasons (I'll generally notice these type of events in response to monitoring alerts or via a looping get-queue monitoring script I run 24/7).

In most cases these little 'surges' come and go in a matter of minutes with no material impact or footprint. But on other occasions, you see something about the queuing that warrants further investigation into exactly what occurred:
  • Possibly your interest is piqued due to the amount of mail that queued up (say a sudden spike into hundreds if not thousands of messages in normal delivery queues)
  • Or possibly you want to check further due to mail rapidly piling up in queues where you traditionally should see little to no traffic (e.g. in the Unreachable queue).
This is a bit more complicated than my usual posting. It represents a full-scale script I use to parse through and report on Exchange 2007 or Exchange 2010 HubTransport Message Tracking Logs, on the following filtered subsets of data to try to identify exactly what was the source of the odd queuing behavior.
It's a longish script but it's process can be summarized as follows:
  1. Interactively prompt for filtering criteria:
    • Exchange Site to analyze [SiteName: US | EU | AP ]
    • Exchange Revision to target: [ExTargVers: 2007 | 2010 ]
    • 'Top' event type to filter: [TrkTarget: SENDER | RECIPIENT | BOUNCE | DEFERRED]  
    • Minutes of recent traffic to filter: [TrkMins: [integer]]
  2. Run an Exchange Management Shell get-messagetrackinglog pass on all HubTransport servers in the specified Site & Exchange revision, to export all traffic in the specified recent number of recent minutes, into a csv file.
  3. Run a Microsoft LogParser query at the output file above, to return a report on the Top-20 Senders or Recipients (depending on the filter), related to the matching traffic.Export the results to CSV report file, and console.
 Here's the code...

get-HT-MsgTrk-TopTraffic-LastXMin.ps1
Language: Powershell
Target: EMS, Powershell and MS LogParser, parsing Exchange 2007/2010 HT Server Message Tracking logs
 
# get-HT-MsgTrk-TopTraffic-LastXMin.ps1

#*----------V Comment-based Help (leave blank line below) V---------- 

<# 
    .SYNOPSIS
get-HT-MsgTrk-TopTraffic-LastXMin.ps1 - Runs msgtracking on the local HubTransports for the designated Site & Exchange Revision, for the designated minutes of recent of traffic, then runs logparser to produce top-20 addresses in as either Sender, or Recipient in four categories of traffic.

    .NOTES
Written By: Todd Kadrie
Website: http://tinstoys.blogspot.com
Twitter: http://twitter.com/tostka

Change Log
  # 2:00 PM 10/2/2013 initial revision

    .DESCRIPTION
get-HT-MsgTrk-TopTraffic-LastXMin.ps1 - Runs msgtracking on the local HubTransports for the designated Site & Exchange Revision, for the designated minutes of recent of traffic, then runs logparser to produce top-20 addresses in as either Sender, or Recipient in four categories of traffic.

    .INPUTS
None. Does not accepted piped input.

    .OUTPUTS
None. Returns no objects or output.

    .EXAMPLE
.\get-HT-MsgTrk-TopTraffic-LastXMin.ps1 -SiteName US -ExTargVers 2007 -TrkTarget RECIPIENT -TrkMins 60

*----------^ END Comment-based Help  ^---------- #>

#------------- Declaration and Initialization of Global Variables ------------- 

PARAM(
  [alias("s")]
  [string] $SiteName=$(Read-Host -prompt "Specify Site to analyze [SiteName: US | EU | AP ]"),
  [alias("v")]
  [string] $ExTargVers=$(Read-Host -prompt "Specify Exchange Revision to target: [ExTargVers: 2007 | 2010 ]"),
  [alias("e")]
  [string] $TrkTarget=$(Read-Host -prompt "Specify 'Top' event type to filter: `n[TrkTarget: SENDER | RECIPIENT | BOUNCE | DEFERRED]"),
  [alias("m")]
  [string] $TrkMins=$(Read-Host -prompt "Specify minutes of recent traffic to filter: [TrkMins: [integer]]")
) # PARAM BLOCK END


## immed after param, setup to stop executing on first error
trap { break; }

# debugging output control
#$bDebug = $TRUE
$bDebug = $FALSE
If ($bDebug -eq $TRUE) {write-host ((get-date).ToString("HH:mm:ss") + ": *** DEBUGGING MODE ***")}

# *----------------v DELIMITED CONSTANTS v----------------
$SitesNameList = "US;AP;EU" ; $SitesNameList=$SitesNameList.split(";") ; 
$SitesList = "*US*;*AP*;*EU*" ; $SitesList=$SitesList.split(";") ; 
$ServersMbxNA2010="US-E2010EXCH1;US-E2010EXCH2" ; $ServersMbxNA2010=$ServersMbxNA2010.split(";") ; 
$ServersHCNAF="US-E2010HUBCAS1;US-E2010HUBCAS2" ; $ServersHCNAF=$ServersHCNAF.split(";") ; 
$ServersMbxNA="US-EXCH10;US-EXCH7;US-EXCH8" ; $ServersMbxNA=$ServersMbxNA.split(";") ; 
$ServersHCNA="US-HUBCAS1;US-HUBCAS2;US-HUBCAS3" ; $ServersHCNA=$ServersHCNA.split(";") ; 
$ServersMbxAP="AP-EXCH3" ; $ServersMbxAP=$ServersMbxAP.split(";") ; 
$ServersHCAP="AP-HUBCAS1;AP-HUBCAS2" ; $ServersHCAP=$ServersHCAP.split(";") ; 
$ServersMbxEU="EU-EXCH5;EU-EXCH6" ; $ServersMbxEU=$ServersMbxEU.split(";") ; 
$ServersHCEU="EU-HUBCAS1;EU-HUBCAS2" ; $ServersHCEU=$ServersHCEU.split(";") ; 
$IisLogsHCNA="\\US-HUBCAS1\e$\Weblogs\W3SVC1\;\\US-HUBCAS2\e$\Weblogs\W3SVC1\;\\US-HUBCAS3\e$\Weblogs\W3SVC1\" ; $IisLogsHCNA=$IisLogsHCNA.split(";") ; 
$IisLogsHCNAF="\\US-E2010HUBCAS1\e$\IIS Weblog\W3SVC1\;\\US-E2010HUBCAS2\e$\IIS Weblog\W3SVC1\" ; $IisLogsHCNAF=$IisLogsHCNAF.split(";") ; 
$IisLogsHCAP="\\AP-HUBCAS1\E$\WEBLOGS\W3SVC1\;\\AP-HUBCAS2\E$\WEBLOGS\W3SVC1\" ; $IisLogsHCAP=$IisLogsHCAP.split(";") ; 
$IisLogsHCEU="\\EU-HUBCAS1\E$\WEBLOGS\W3SVC1\;\\EU-HUBCAS1\E$\WEBLOGS\W3SVC1\" ; $IisLogsHCEU=$IisLogsHCEU.split(";") ; 
$MsgTrkLogsHCNA="\\US-HUBCAS1\F$\Program Files\Microsoft\Exchange Server\Logs\MessageTracking\;\\US-hubcas2\F$\Program Files\Microsoft\Exchange Server\Logs\MessageTracking\;\\US-HUBCAS3\e$\Weblogs\W3SVC1\" ; $MsgTrkLogsHCNA=$MsgTrkLogsHCNA.split(";") ; 
$MsgTrkLogsHCNAF="\\US-E2010HUBCAS1\e$\IIS Weblog\W3SVC1\;\\US-E2010HUBCAS2\e$\IIS Weblog\W3SVC1\" ; $MsgTrkLogsHCNAF=$MsgTrkLogsHCNAF.split(";") ; 
$MsgTrkLogsHCEU="\\EU-HUBCAS1\E$\Program Files\Microsoft\Exchange Server\Logs\MessageTracking\;\\EU-HUBCAS2\E$\Program Files\Microsoft\Exchange Server\Logs\MessageTracking\" ; $MsgTrkLogsHCEU=$MsgTrkLogsHCEU.split(";") ; 
$MsgTrkLogsHCAP="\\AP-HUBCAS1\E$\Program Files\Microsoft\Exchange Server\Logs\MessageTracking\;\\AP-HUBCAS2\E$\Program Files\Microsoft\Exchange Server\Logs\MessageTracking\" ; $MsgTrkLogsHCAP=$MsgTrkLogsHCAP.split(";") ;
$MsgTrkLogsHCNATemplate="\\US-HUBCASX\E$\WEBLOGS\W3SVC1\" ; 
$MsgTrkLogsHCNAFTemplate="\\US-E2010HUBCASX\E$\IIS WEBLOG\W3SVC1\" ;
$MsgTrkLogsHCAPTemplate="\\AP-HUBCASX\E$\Program Files\Microsoft\Exchange Server\Logs\MessageTracking\" ;
$MsgTrkLogsHCEUTemplate="\\EU-HUBCASX\E$\Program Files\Microsoft\Exchange Server\Logs\MessageTracking\" ;
# *----------------^ DELIMITED UNI CONSTANTS ^----------------

#*================v FUNCTION LISTINGS v================

#*----------v Function EMSLoadLatest v----------
<#
  Checks local machine for registred E20[13|10|07] EMS, and then loads the newest one found
  Returns the string 2013|2010|2007 for reuse for version-specific code
#>
function EMSLoadLatest {
  # check registred vS loaded ;
  $SnapsReg=Get-PSSnapin -Registered ;
  $SnapsLoad=Get-PSSnapin ;
  # check/load E2013, E2010, or E2007, stop at newest (servers wouldn't be running multi-versions)
  if (($SnapsReg| where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.E2013"}))
  {
    if (!($SnapsLoad | where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.E2013"}))
    {
      Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2013 -ErrorAction SilentlyContinue ; return "2013" ;
    } else {
      return "2013" ;
    }
  } elseif (($SnapsReg| where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.E2010"})) 
  { 
    if (!($SnapsLoad | where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.E2010"})) 
    {
      Add-PSSnapin Microsoft.Exchange.Management.PowerShell.E2010 -ErrorAction SilentlyContinue ; return "2010" ;
    } else {
      return "2010" ;
    }
  } elseif (($SnapsReg| where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.Admin"}))
  {
    if (!($SnapsLoad | where {$_.Name -eq "Microsoft.Exchange.Management.PowerShell.Admin"}))
    {
      Add-PSSnapin Microsoft.Exchange.Management.PowerShell.Admin -ErrorAction SilentlyContinue ; return "2007" ;
    } else {
      return "2007" ;
    }
  }
}
#*----------^END Function EMSLoadLatest ^----------

#---------------------------v Function Cleanup v---------------------------
Function Cleanup {
  # clear all objects and exit
  # Clear-item doesn't seem to work as a variable release 

  exit
} #*----------------^ END Function Cleanup ^----------------

#*----------------v Function ProcessLogs() v----------------
Function ProcessLogs {

 
PARAM(
  [alias("s")]
  [string] $SiteName=$(Read-Host -prompt "Specify Site to analyze [SiteName: US | EU | AP ]"),
  [alias("v")]
  [string] $ExTargVers=$(Read-Host -prompt "Specify Exchange Revision to target: [ExTargVers: 2007 | 2010 ]"),
  [alias("e")]
  [string] $TrkTarget=$(Read-Host -prompt "Specify 'Top' event type to filter: `n[TrkTarget: SENDER | RECIPIENT | BOUNCE | DEFERRED]"),
  [alias("m")]
  [string] $TrkMins=$(Read-Host -prompt "Specify minutes of recent traffic to filter: [TrkMins: [integer]")
) # PARAM BLOCK END
  
  # switch as per specified settings
  switch ($SiteName)
  {
    "US" {
      switch ($ExTargVers) 
      {
        "2007" {$Hubs = $ServersHCNA ; $MTLogs = $MsgTrkLogsHCNA }
        "2010" {$Hubs = $ServersHCNAF ;$MTLogs = $MsgTrkLogsHCNAF}
      } # switch block end
    } 
    "AP" {$Hubs = $ServersHCAP ; $MTLogs = $MsgTrkLogsHCAP}
    "EU" {$Hubs = $ServersHCEU ; $MTLogs = $MsgTrkLogsHCEU}
    defAPlt {write-warning "Invalid Site spec '$SiteName' Exiting." ; exit 1}
  } # switch block end
 
  write-host -ForegroundColor White ((get-date).ToString("HH:mm:ss") + ": Site: " + $SiteName)

  foreach ($Hub in $Hubs) {
    write-host -foregroundcolor White ("-" * 10)
    If ($bDebug -eq $TRUE) {write-host ((get-date).ToString("HH:mm:ss") + ": `$Hub: " + $Hub)}
    $ServerName = $Hub
    write-host -foregroundcolor yellow ((get-date).ToString("HH:mm:ss") + ": Parsing Server: " + $ServerName + " for the Top " + $TopUsers + " MsgTracking users in the last " + $TrkMins + " minutes (" + (get-date -format "yyMMdd") + ")...")
    
    #1. Setup search parameters and file names
    # Setup the messagetracking end time string (now):
    $TimeStampEnd= get-date -uformat "%m/%d/%Y %I:%M:%S %p"
    # strip any zeropadding
    if (($TimeStampEnd.ToSTring()).StartsWith('0')) {$TimeStampEnd=$TimeStampEnd.substring(1,$TimeStampEnd.length-1)}
    # setup a filename-compatible variant string
    $TimeStampEndFN= get-date -uformat "%Y%m%d-%H%M"
    
    # calculate a string for the designated number of minutes prior
    $TimeStampStart=(get-date).AddMinutes((-1 * $TrkMins))
    # and format it into the format necessary for the get-messagetrackinglog cmdlet
    $TimeStampStart=get-date ($TimeStampStart.ToString()) -uformat "%m/%d/%Y %I:%M:%S %p"
    
    if ($bDebug) { write-host -foregroundcolor darkgray ((get-date).ToString("HH:mm:ss") + ":`$TimeStampStart: " + $TimeStampStart + "`n`$TimeStampEnd: " + $TimeStampEnd ) }
    # strip any zeropadding
    if (($TimeStampStart.ToSTring()).StartsWith('0')) {$TimeStampStart=$TimeStampStart.substring(1,$TimeStampStart.length-1)}
    # setup a filename-compatible variant string
    $TimeStampStartFN=get-date ($TimeStampStart.ToString()) -uformat "%Y%m%d-%H%M"
    # gen filename from script 
    $TimeStampNow = get-date -uformat "%Y%m%d-%H%M"
    # construct the temporary MessageTracking output file name
    $outTrkFile=$HTTempDir + $ServerName + "-TRACK-" + $TimeStampStartFN + "to" + $TimeStampEndFN + ".csv"
    # construct the Logparser output report file name
    $outRptFile = $HTTempDir + $ServerName + "-" + $TimeStampStartFN + "to" + $TimeStampEndFN + "-top" + $TopUsers + "-" + $TrkTarget + "Addrs.csv"
    
    # echo the settings to console
    write-host ((get-date).ToString("HH:mm:ss") + ": TimeStampStart:" + $TimeStampStart)
    write-host ((get-date).ToString("HH:mm:ss") + ": TimeStampEnd:" + $TimeStampEnd)
    write-host ((get-date).ToString("HH:mm:ss") + ": `$outTrkFile:" + $outTrkFile)
    write-host ((get-date).ToString("HH:mm:ss") + ": `$outRptFile:" + $outRptFile)

    #2. Use msgtrack to pull out the time range of receive events (make sure to use no-type):
    # events: DEFER | DELIVER | DSN | FAIL | POISONMESSAGE | RECEIVE | SEND
    # build the Message Tracking syntax as a string 
    $sExcCmd = 'get-messagetrackinglog -ResultSize Unlimited -server ' + $ServerName + ' -Start "' + $TimeStampStart + '" -End "' + $TimeStampEnd + '" |  select Timestamp,ClientIp,ClientHostname,ServerIp,ServerHostname,SourceContext,ConnectorId,Source,EventId,InternalMessageId,MessageId,@{Name="Recipient-Addrs";Expression={$_.recipients}},RecipientStatus,TotalBytes,RecipientCount,RelatedRecipientAddress,Reference,MessageSubject,Sender,ReturnPath,MessageInfo | export-csv ' + $outTrkFile + ' �notype' 
    
    write-host -foregroundcolor Yellow "---"
    write-host -foregroundcolor Yellow ((get-date).ToString("HH:mm:ss") + ": Running initial messagetracking query(collect " + $TrkMins + "min traffic)...")
    
    # start a stopwatch to time the the process
    $sw = [Diagnostics.Stopwatch]::StartNew()
    write-host " " 
    write-host ((get-date).ToString("HH:mm:ss") + ": get-messagetracking query at Invoke:" ); $sExcCmd 
    
    # clear any existing errors
    $error.clear() 
    # Invoke the get-messagetrackinglog command to retrieve the raw traffic for the specified number of minutes
    Invoke-Expression $sExcCmd -verbose

    # check for errors
    if($error.count -gt 0){
      write-host -foregroundcolor red ((get-date).ToString("HH:mm:ss") + ": " + $error )
      if ($error -like '*The task has failed with an exception. The error message is: The parameter is incorrect*') {write-host -foregroundcolor yellow ((get-date).ToString("HH:mm:ss") + ": *** You cannot run Exch2010 MessageTracks from Exch2007 EMS.`nMove execution of this script to a machine with Exch2010 EMS installed***") ; exit 1}
    } # if-block end

    # confirm that the CSV tracking output file was produced, before moving on to logparsing
    if (!(test-path $outTrkFile)){
      write-warning ("NO " + $outTrkFile + " file written! ABORTING logparser pass on this system")
    } else {
      If ($bDebug -eq $TRUE) {write-host -ForegroundColor Yellow ((get-date).ToString("HH:mm:ss") + ": `$outTrkFile: " + $outTrkFile + " file present") }
      
      #3. Logparse out the top-20 items from the messagetracking csv output:
      
      # Switch matching the type of traffic we want to evaluate this pass; build suitable LogParser SQL query syntax to suit...
      switch ($TrkTarget) {
        "SENDER" {
          $sSQL =  "SELECT TOP 10 Sender, Count(*) AS Msgs INTO " + $outRptFile + " FROM " + $outTrkFile + " WHERE eventid = 'RECEIVE' GROUP BY Sender ORDER BY Msgs DESC"
        }
        "RECIPIENT" {
         # 12:50 PM 10/1/2013 using the new Recipient-Addrs custom field from the msgtrack
         $sSQL =  "SELECT TOP 20 Recipient-Addrs, Count(*) AS Msgs INTO " + $outRptFile + " FROM " + $outTrkFile + " WHERE eventid = 'DELIVER' OR eventid = 'SEND' OR eventid = 'FAIL' OR eventid = 'DSN' GROUP BY Recipient-Addrs ORDER BY Msgs DESC"
          
        }
        "BOUNCE" {
         # 12:50 PM 10/1/2013 using the new Recipient-Addrs custom field from the msgtrack
         $sSQL = "SELECT TOP 20 Recipient-Addrs, Count(*) AS Msgs INTO " + $outRptFile + " FROM " + $outTrkFile + " WHERE eventid = 'FAIL' OR eventid = 'DSN' GROUP BY Recipient-Addrs ORDER BY Msgs DESC"
        }
        "DEFERRED"  {
          # 12:50 PM 10/1/2013 using the new Recipient-Addrs custom field from the msgtrack
          $sSQL =  "SELECT TOP 20 Recipient-Addrs, Count(*) AS Msgs INTO " + $outRptFile + " FROM " + $outTrkFile + " WHERE eventid = 'DEFER' GROUP BY Recipient-Addrs ORDER BY Msgs DESC"
        }
      } # if-block end
       
      # build the LogParser commandline syntax, wrapping the SQL sytnax in double-quotes
      $sExcCmd =  $LOGPAREXE + ' -headerRow ON -iDQuotes APto ' + $sQot + $sSQL + $sQot + ' -i:CSV -o:csv'
      
      write-host -foregroundcolor Yellow "---"
      write-host -foregroundcolor Yellow ((get-date).ToString("HH:mm:ss") + ": Running secondary LogParser query(Top " + $TopUsers + ",on " + $TrkTarget + ")...")
      
      write-host ((get-date).ToString("HH:mm:ss") + ": " + $TrkTarget + " Query at Invoke:" ); $sExcCmd 
      
      # Invoke the LogParser command
      Invoke-Expression $sExcCmd -verbose

      # evaluate that the proper Logparser output csv file was produced (exit the script if no output)
      if (!(test-path $outRptFile)){
          write-warning ("NO " + $outRptFile + " file written!")
          exit 1
      } else {
        If ($bDebug -eq $TRUE) {write-host -ForegroundColor Yellow ((get-date).ToString("HH:mm:ss") + ": `$outRptFile: " + $outRptFile + " file present")}
      } # if-block end
      
      # if we're using CSV output of the report, pull the report csv in and redisplay it to console...
      if ($bOutputToCSV -eq $TRUE) {
        import-csv $outRptFile | ft -wrap | out-defAPlt
      } # if-block end
      
    } # if-block end (logparser block)
    
    # then move the processing files ($outTrkFile & $outRptFile csv's to the permanent reporting $DestDir
    write-host -foregroundcolor Yellow "---"
    write-host ((get-date).ToString("HH:mm:ss") + ": Pass Results..." )
    write-host ((get-date).ToString("HH:mm:ss") + ": input file: " + $outTrkFile )
    write-host ((get-date).ToString("HH:mm:ss") + ": output file: " + $outRptFile)
    write-host ((get-date).ToString("HH:mm:ss") + ": Moving output file to : " + $DestDir)
    
    # Only copy if not already running on the reporting archive server 
    if ($ComputerName -eq $runsServer) {
      write-host ((get-date).ToString("HH:mm:ss") + ": Script is executing on " + $runsServer + " skipping move of result files.")
    } else {
      # remote server pass, move results & processing files to central storage location.
        $error.clear() 
        move-item $outTrkFile $DestDir #-whatif
        move-item $outRptFile $DestDir #-whatif
    } # if-block end
    
    
    write-host -foregroundcolor green ((get-date).ToString("HH:mm:ss") + ": PASS COMPLETED")
   # stop stopwatch & echo time
    $sw.Stop()
    # simple output
    write-host -foregroundcolor green ((get-date).ToString("HH:mm:ss") + ": Elapsed Time: (HH:MM:SS.ms)" + $sw.Elapsed.ToString())
    write-host -foregroundcolor Yellow ("=" * 10)
        
  } # for-loop end HUBS

} #*----------------^ END Function ProcessLogs() ^----------------

#*================^ END FUNCTION LISTINGS ^================

#--------------------------- Invocation of SUB_MAIN ---------------------------

$EMSVers = EMSLoadLatest  # return's a string ["2013"|"2010"|"2007"]
write-host ((get-date).ToString("HH:mm:ss") + ": `$EMSVers: " + $EMSVers)
# then test $EMSVers for approp code in multi-version scripts (2007|2010|2013)

$ComputerName = ($env:COMPUTERNAME)

# assign specs per params: 
$SiteName=$SiteName.ToUpper()
$ExTargVers=$ExTargVers.ToUpper()
$TrkTarget=$TrkTarget.ToUpper()

# 12:35 PM 8/28/2013 validate the inputs (uses regular expression matching)
if (!($SiteName -match "^(US|EU|AP)$")) {
  write-warning ("INVALID SiteName SPECIFIED: " + $SiteName + ", EXITING...")
  exit 1
} # if-block end
if (!($ExTargVers -match "^(2007|2010)$")) {
  write-warning ("INVALID ExTargVers SPECIFIED: " + $ExTargVers + ", EXITING...")
  exit 1
} # if-block end
# DEFERRED | DELIVER | DSN | FAIL | POISONMESSAGE | RECEIVE | SEND
if (!($TrkTarget -match "^(SENDER|RECIPIENT|BOUNCE|DEFERRED)$")) {
  write-warning ("INVALID TrkTarget SPECIFIED: " + $TrkTarget + ", EXITING...")
  exit 1
} # if-block end
# TrkMins integer range 0-200
if (!($TrkMins -match "^([1-9]|[1-9][0-9]|[1][0-9][0-9]|20[0-0])$")) {
  write-warning ("INVALID TrkMins SPECIFIED: " + $TrkMins + ", EXITING...")
  exit 1
} # if-block end
# specify the number of 'Top ##' entries to return via logparser
$TopUsers = 20

write-host -foregroundcolor darkgray ((get-date).ToString("HH:mm:ss") + ": `$ExTargVers:" + $ExTargVers)

# Switch as per specified Exchange version - test to ensure we don't try to run Ex2010 EMS cmdlets from Ex2007 EMS...
switch ($EMSVers){
  "2007" {
    if ($ExTargVers -ne "2007") {
      write-warning ("There is a mismatch between the specified Exch Rev (`$ExTargVers:" + $ExTargVers + ") and the detected EMS version (`$EMSVers:" + $EMSVers + "). `nEMS2007 can only be used for 2007 queries. `nExiting...")
     exit 1
    }
  }
  "2010" {
    write-host "2010"
  }
  "2013" {
    write-host "2010"
  }
} # switch block end

<# $TrkTarget Exchane Message Tracking EventID options:
DEFER Message delivery delayed
DELIVER Message delivered to a mailbox
DSN "A delivery status notification was generated.
Messages quarantined by the Content Filter are also delivered as DSNs. the recipients field has the SMTP address of the quarantine mailbox."
EXPAND Distribution Group expanded. The RelatedRecipientAddress field has the SMTP address of the Distribution Group.
FAIL Delivery failed. The RecipientStatus field has more information about the failure, including the SMTP response code. You should also look at the Source and Recipients fields when inspecting messages with this event.
POISONMESSAGE Message added to or removed from the poison queue
RECEIVE "Message received. The Source field is STOREDRIVER for messages submitted by Store Driver (from a Mailbox server), or  SMTP for messages�
a) received from another Hub/Edge
b) received from an external (non-Exchange) host using SMTP
c) submitted by SMTP clients such as POP/IMAP users."
REDIRECT Message redirected to alternate recipient
RESOLVE Generally seen when a message is received on a proxy address and resolved to the defAPlt email address. The RelatedRecipientAddress field has the proxy address the message was sent to. The recipients field has the defAPlt address it was resolved (and delivered) to.
SEND Message sent by SMTP. The ServerIP and ServerHostName parameters have the IP address and hostname of the SMTP server.
SUBMIT "The Microsoft Exchange Mail Submission service on a Mailbox server successfully notified a Hub Transport server that a message is awaiting submission (to the Hub). These are the events you'll see on a Mailbox server.
The SourceContext property provides the MDB Guid, Mailbox Guid, Event sequence number, Message class, Creation timestamp, and Client type. Client type can be User (Outlook MAPI), RPCHTTP (Outlook Anwhere), OWA, EWS, EAS, Assistants, Transport."
TRANSFER Message forked because of content conversion, recipient limits, or transport agents
#>

# specify the location of temporary processing files
$HTTempDir = "e:\scripts\logs\"
# specify the UNC path to the archive location for the resulting reports
$DestDir = "\\US-MAILUTILS\e$\scripts\logs"

# flag to indicate whether we're LogParsing to CSV (used to pull the csv back in and display it to console)
$bOutputToCSV = $TRUE

$runsServer = "US-MAILUTILS"
# shift to dynamic on the local box

# Derive paths from script path support
$ScriptDir = (Split-Path -parent $MyInvocation.MyCommand.Definition) + "\"
$ScriptBaseName = [system.io.path]::GetFilename($MyInvocation.InvocationName)
$ScriptNameNoExt = [system.io.path]::GetFilenameWithoutExtension($MyInvocation.InvocationName)  


# log parser is assumed to be in the directory from which the script is being run
$LOGPAREXE = ".\logparser.exe"
# 9:20 AM 9/27/2013 test logpar loc

if (!(test-path $LOGPAREXE)) {
  if (test-path .\logparser.exe) {
    $LOGPAREXE = ".\logparser.exe"
  } else {
    write-warning ("NO COPY OF LOGPARSER FOUND AT EITHER OF:`n" + $LOGPAREXE + "`n" + (Get-Location).path)
    write-warning "Exiting"
    exit 1
  }  # if-block end
  write-host ((get-date).ToString("HH:mm:ss") + ": Creating " + $HTTempDir)
  New-Item $HTTempDir �type directory    
} # if-block end

$TimeStampNow = get-date -uformat "%Y%m%d-%H%M" 

write-host " "

# 2:37 PM 9/25/2013 it's making it to here, but no further, just drops out

# confirm log dir exists
$testPath= $ScriptDir + "logs\" 
if (!(test-path $testPath)) {
  write-host ((get-date).ToString("HH:mm:ss") + ": Creating " + $testPath)
  New-Item $testPath -type directory    
} # if-block end

If ($bDebug -eq $TRUE) {
  write-host -foregroundcolor darkgray (((get-date).ToString("HH:mm:ss") + ": `$ScriptDir: " + $ScriptDir))
  write-host -foregroundcolor darkgray (((get-date).ToString("HH:mm:ss") + ": `$ScriptNameNoExt: " + $ScriptNameNoExt))
  write-host -foregroundcolor darkgray (((get-date).ToString("HH:mm:ss") + ": `$ScriptBaseName: " + $ScriptBaseName))
  write-host -foregroundcolor darkgray (((get-date).ToString("HH:mm:ss") + ": `$LOGPAREXE: " + $LOGPAREXE))
  write-host -foregroundcolor darkgray (((get-date).ToString("HH:mm:ss") + ": `$HTTempDir: " + $HTTempDir))
} # if-block end

# 8:49 AM 5/8/2013 check logparser.exe
if (!(test-path $LOGPAREXE)) {write-warning ("ERROR LOGPARSER NOT FOUND AT " + $LOGPAREXE + ". EXITING") ; exit 32}

# setup variables to hold Quote and Single-Quote characters
$sQot = [char]34 ; $sQotS = [char]39 ;

# description of the script's purpose
$sAppDesc = "Running msgtracking on the specified regional HTs, for the last 30mins of traffic, then running logparser to produce top-" + $TopUsers + " " + $TrkTarget + " traffic sources." + "..."
write-host -foregroundcolor darkgray ((get-date).ToString("HH:mm:ss") + ": " + $sAppDesc)
# LAUNCH THE PROCESSLOGS FUNCTION
ProcessLogs -Site $SiteName -ExTargVers $ExTargVers -TrkTarget $TrkTarget -TrkMins $TrkMins

# call the generic cleanup function  
Cleanup

#*================^ END SCRIPT BODY ^================

 
Typical interactive run with output:
[PS] E:\scripts>.\get-HT-MsgTrk-TopTraffic-LastXMin.ps1 Specify Site to analyze [SiteName: US | EU | AP ]: US Specify Exchange Revision to target: [ExTargVers: 2007 | 2010 ]: 2007 Specify 'Top' event type to filter: [TrkTarget: SENDER | RECIPIENT | BOUNCE | DEFERRED]: sender Specify minutes of recent traffic to filter: [TrkMins: [integer]]: 30 10:35:59: $EMSVers: 2007 10:35:59: $ExTargVers:2007 10:35:59: Running msgtracking on the specified regional HTs, for the last 30mins  of traffic, then running logparser to produce top-20 SENDER traffic sources.... 10:35:59: Site: US ---------- 10:35:59: Parsing Server: US-HUBCAS1 for the Top 20 MsgTracking users in the  last 30 minutes (131002)... 10:35:59: TimeStampStart:10/02/2013 10:05:59 AM 10:35:59: TimeStampEnd:10/02/2013 10:35:59 AM 10:35:59: $outTrkFile:e:\scripts\logs\US-HUBCAS1-TRACK-20131002-1005to201310 02-1035.csv 10:35:59: $outRptFile:e:\scripts\logs\US-HUBCAS1-20131002-1005to20131002-103 5-top20-SENDERAddrs.csv --- 10:35:59: Running initial messagetracking query(coolect 30min traffic)... 10:35:59: SENDER Query at Invoke: get-messagetrackinglog -ResultSize Unlimited -server US-HUBCAS1 -Start "10/ 02/2013 10:05:59 AM" -End "10/02/2013 10:35:59 AM" |  select Timestamp,ClientIp ,ClientHostname,ServerIp,ServerHostname,SourceContext,ConnectorId,Source,EventI d,InternalMessageId,MessageId,@{Name="Recipient-Addrs";Expression={$_.recipient s}},RecipientStatus,TotalBytes,RecipientCount,RelatedRecipientAddress,Reference ,MessageSubject,Sender,ReturnPath,MessageInfo | export-csv e:\scripts\logs\US -NAHUBCAS1-TRACK-20131002-1005to20131002-1035.csv â��notype --- 10:42:22: Running secondary LogParser query(Top 20,on SENDER)... 10:42:22: SENDER Query at Invoke: .\logparser.exe -headerRow ON -iDQuotes Auto "SELECT TOP 10 Sender, Count(*) AS  Msgs INTO e:\scripts\logs\US-HUBCAS1-20131002-1005to20131002-1035-top20-SE NDERAddrs.csv FROM e:\scripts\logs\US-HUBCAS1-TRACK-20131002-1005to20131002 -1035.csv WHERE eventid = 'RECEIVE' GROUP BY Sender ORDER BY Msgs DESC" -i:CSV -o:csv 

Sender                                  Msgs ------                                  ---- 
do-not-reply@domain.com                 14629 noreplysharepointsystem@domain.com      629 Status@domain.com                       516 Application@domain.com                  447 Process@domain.com                      407 
customer@do-not-reply.com               295 
noreply@portal.domain.com               292 noreply@software.com                    272 Noreplysystem@domain.com                246 MicrosoftExchange32...                  203 b6ce41109e@domain.com --- 10:42:23: Pass Results... 10:42:23: input file: e:\scripts\logs\US-HUBCAS1-TRACK-20131002-1005to201310 02-1035.csv 10:42:23: output file: e:\scripts\logs\US-HUBCAS1-20131002-1005to20131002-10 35-top20-SENDERAddrs.csv 10:42:23: Moving output file to : \\US-MAILUTILS\e$\scripts\logs 10:42:23: Script is executing on US-MAILUTILS skipping move of result files. 10:42:23: PASS COMPLETED 10:42:23: Elapsed Time: (HH:MM:SS.ms)00:06:24.1628999 ========== ---------- ...

Powershell Snippet - Copy/Distribute a File to Every Exchange Server, With Preference Order as to Which Target Drive

10/02/2013: Here's another handy Powershell script snippet: This time, a chunk of code I use for quickly distributing a new or updated file to all or a subset of Exchange servers using Powershell+Exchange Management Shell

Pretty simple process going on below:
  1. I store a list of drive letters to be tested into an array (in preference order)
  2. Then utilize the EMS get-exchangeserver cmdlet to collect a filtered set of all exchange servers (in this case, using
    get-exchangeserver | where{$_.isHubTransportServer -eq $true} to return only HubTransports anywhere in the Org)
  3. I then loop the exchange servers list against the drives array, testing for a functional path (which indicates a suitable target drive), and using the {break} command to drop out of the loop at the first matching drive in the list. 
  4. Whereuopon the designated file is copied to the matching UNC path

Copy/Distribute file to every exchange server, with preference order  as to which target drive: (1-line below; the command skips tests and copies to the first match)
Language: Powershell
Target: EMS or Powershell,
 
# 1-line, wrapped at pipes and semi-colons for readability 

$drvs="e","f","c" ; get-exchangeserver |
 where{$_.isHubTransportServer -eq $true} |
 foreach{ 
  write-host $_ ; 
  foreach ($drv in $drvs) { 
      if (test-path \\$_\$drv`$\scripts\) { break ; } 
  } ; 
  copy \\SourceServer\e$\scripts\get-HT-MsgTrk-TopTraffic-LastXMin.ps1 \\$_\$drv`$\scripts\ -whatif ; 
  }

 

Friday, September 20, 2013

Powershell Script - More Logparsing Multiple Servers

9/20/2013: I've been putting in a fair amount of time recently repetitively Logparsing the IIS logs on several regions of CAS servers

-- I'm attempting to guage MS Lync 'Exchange Web Services' load and demand on our production CAS's, ahead of new Lync capacity rollouts overseas --

So here's another quicky code chunk that I routinely grab and put to use for Logparsing multiple CAS servers of IIS logs.

It's a simple variant of my prior post "Powershell Script - Tip: Delimited String "Contants" & Multi-server Logparser scripts", only this time, rather than substitute in the entire LogPath, I'm foreach looping through the CAS server names.

Here's a specific example I just used this morning that builds a multi-server set of looped LogParser queries to pull out and group the Client 'agent' strings for all EWS traffic, and output the result into a csv file, leveraging substitution of the proper $CAS server name within the target UNC path to a log file...

Logparsing multiple servers, by foreach-ing an array variable
Language/Tools: Powershell, MS Logparser
Target: EMS or Powershell, parsing Exchange 2007/2010 CAS Server IIS logs
 
# comma-delimited server name array, 
#   foreach into logparser command
# Note: edit/update the logname string, to specify variant dates
# one-line follows (manually wrapped for clarity)
$CASs="US-HUBCAS1","US-HUBCAS2","US-HUBCAS3" ; 
foreach ($CAS in $CASs){
write-host ("`n" + $CAS) ; 
.\logparser.exe "SELECT cs(User-Agent) as Client, count(*) as Hits INTO $CAS-EWS-ALLClients.csv FROM '\\$CAS\e$\Weblogs\W3SVC1\ex130919*.log' WHERE cs-uri-stem LIKE '%/EWS/%' GROUP BY Client ORDER BY Hits DESC" -o:csv 
}


Powershell Script - Comment-Based Help Template (Powershell v2)

9/20/2013: Another frequently useful, but occasionally fiddly, feature of Powershell (as of revision 2), is the Comment-Based Help system. The features intention is to make it quick & easy to include the proper values within your script to provide self-assembling get-help output, to make your script as well-documented for other users, as the stock Powershell Cmdlets.

Unfortunately, the area that most frequently breaks when trying to use what I'll refer to as 'CBH', is in the specifics of formatting and placement and exactly which of the various options you may want to include in most of your scripts.

Like a lot of folks I spent some time poking around in the docs and on the web, and figured out the details to make things function, and generally distilled that material down into a simple boilerplate 'Template' that I use within in every new script I write. And since my goal is is to 'share' with the world, I figured I'd post up my template, along with some useful notes on keeping it functioning trouble-free. :)



Todd's Comment-Based Help Template

 
#*----------V Comment-based Help (leave blank line below) V---------- 

<# 
    .SYNOPSIS
[NAME].ps1 - [1-LINE-DESC]

    .NOTES
Written By: Todd Kadrie
Website: http://tinstoys.blogspot.com
Twitter: http://twitter.com/tostka

Additional Credits: [REFERENCE]
Website: [URL]
Twitter: [URL]

Change Log
[VERSIONS]

    .DESCRIPTION
[DESC]

    .PARAMETER  <Parameter-Name>
<parameter comment>

    .INPUTS
None. Does not accepted piped input.

    .OUTPUTS
None. Returns no objects or output.
System.Boolean
                True if the current Powershell is elevated, false if not.
[use a | get-member on the script to see exactly what .NET obj TypeName is being returning for the info above]

    .EXAMPLE
.\[SYNTAX EXAMPLE]
[use an .EXAMPLE keyword per syntax sample]

    .LINK
< name of a related topic, one .LINK keyword per related topic. Can also include a URI to online help (start with http:// or https://)>
*----------^ END Comment-based Help  ^---------- #>



Comment-Based Help Notes:


  • ALWAYS INDENT KEYWORDS WITH SPACES NOT TABS: For Keywords, the period must be the first character on the line, excepting spaces or # (hashmark).
    NO LEADING TABS!

  • A blank line must appear between any unrelated leading comments, and the start of the CBH <#...#> block:
  • Within Comment-Blocks (<#...#>) a .KEYWORD must be the first item on the line immediately below the opening <# 

Powershell Script - Tip: Delimited String "Contants" & Multi-server Logparser scripts

9/19/2013: This is a useful tip that I get some solid routine mileage out of: Using a delimited string variable, to function as an Array Constant. Primary usage is when I want to loop through a set of variant values, and execute the same commands against each value.

In the code below (which returns some sample sorted information on the various Exchange versions, and roles) I'm setting up a $Sites variable, containing a semi-colon-delimited string, that is then split and reassigned to the variable as an Array. The resulting array is then fed through a simple foreach loop to output information about the servers and roles in each AD Site.


Array Stored as Semi-colon-delimited string. 

Language/Tools: Powershell
Target: Exchange 2007/2010 Management Console

$Sites = "US;AU;EU" ; 
$Sites=$Sites.split(";") ; 
foreach ($Site in $Sites) {
  write-host -foregroundcolor yellow "SITE: " $Site; get-exchangeserver | 
    where { $_.Site -like "*$Site" } | 
    sort AdminDisplayVersion,ServerRole,Name | 
    select Name,ServerRole,AdminDisplayVersion 
}


 
Typical output:
SITE:  US

Name                                                                 ServerRole AdminDisplayVersion
----                                                                 ---------- -------------------
US-ServerUM1                                                   UnifiedMessaging Version 14.2 (Build 247.5)
US-MbxServer2                                                           Mailbox Version 14.3 (Build 123.4)
US-MbxServer2                                                           Mailbox Version 14.3 (Build 123.4)
US-HUBCAS1                                           ClientAccess, HubTransport Version 14.3 (Build 123.4)
US-HUBCAS2                                           ClientAccess, HubTransport Version 14.3 (Build 123.4)
US-PFServer1                                                            Mailbox Version 8.3 (Build 83.6)
US-MbxServer1                                                           Mailbox Version 8.3 (Build 83.6)




Multi-server Logparser scripts (using Delimited-String Constants):

The above is a simple contrived example to get the concept across. More frequently I use delimited-variable strings for storing and looping through arrays of log directories (for get-messagetracking or logparser processing). For example: Below I have a script that runs a Microsoft Logparser query (summarizing hourly EWS hits) from the IIS logs matching today's date -- pretty common stuff.
But this version has the added benefit of executing the command against a series of different CAS servers (by walking a delimited-string constant containing the UNC paths to each server's logs)...

Logparsing multiple CAS server IIS log directories for EWS traffic per hour. 

Language: Powershell /MS LogParser
Target: EMS or Powershell, parsing Exchange 2007/2010 CAS Server IIS logs

# Array of IIS paths, stored as semi-colon-delimited string
$LogDirs="\\US-HUBCAS1\e$\Weblogs\W3SVC1\;\\US-HUBCAS2\e$\Weblogs\W3SVC1\" ; $LogDirs=$LogDirs.split(";") ; 
# constants for single & double quotes
$sQuot = [char]34 ; $sQuotS = [char]39
foreach ($LogDir in $LogDirs) {
  # build the log path to today's logs
  $sTodaysLogsStr = $LogDir
  # Ex2007 IIS log filename variant
  #$sTodaysLogsStr += "ex" 
  # Ex2010 variant
  $sTodaysLogsStr += "u_ex" 
  $sTodaysLogsStr += (Get-Date -format "yMMdd") + "*.log"
  # build the logparser sql command
  $strSQL =  "SELECT QUANTIZE(TO_LOCALTIME(time),3600) AS Hour, count(*) AS Hits" ;    
  $strSQL += " FROM " + ($sQuotS + $sTodaysLogsStr + $sQuotS) 
  $strSQL += " WHERE cs-uri-stem LIKE '%/EWS/%' GROUP BY Hour ORDER BY Hour" ; 
  # assemble the logparser commandline
  $strExecCmd = ". logparser.exe " + $sQuot + $strSQL + $sQuot + " -rtp:-1" ;
  $strExecCmd
  # execute the assembled command
  Invoke-Expression  $strExecCmd 
}
 
Which produces two passes of Logparser output like so:
[PS] C:\usr\local\bin>c:\tmp\tmp1.ps1
. logparser.exe "SELECT QUANTIZE(TO_LOCALTIME(time),3600) AS Hour, count(*) AS Hits FROM '\\US-HUBCAS1\e$\IIS Weblog\W3SVC1\u_ex130920*.log' WHERE cs-uri-stem LIKE '%/EWS/%' GROUP BY Hour ORDER BY Hour" -rtp:-1
Hour     Hits
-------- ----
00:00:00 477
01:00:00 2289
02:00:00 2809
03:00:00 3300
04:00:00 3655
05:00:00 5657
06:00:00 7350
07:00:00 9135
08:00:00 9340
09:00:00 9053
10:00:00 8615
11:00:00 8894
12:00:00 6568
18:00:00 1132
19:00:00 758
20:00:00 749
21:00:00 612
22:00:00 661
23:00:00 571

Statistics:
-----------
Elements processed: 392382
Elements output:    19
Execution time:     12.12 seconds

. logparser.exe "SELECT QUANTIZE(TO_LOCALTIME(time),3600) AS Hour, count(*) AS Hits FROM '\\US-HUBCAS2\e$\IIS Weblog\W3SVC1\u_ex130920*.log' WHERE cs-uri-stem LIKE '%/EWS/%' GROUP BY Hour ORDER BY Hour" -rtp:-1
Hour     Hits
-------- -----
00:00:00 3205
01:00:00 2979
02:00:00 3133
03:00:00 3554
04:00:00 3833
05:00:00 5564
06:00:00 9284
07:00:00 10352
08:00:00 10016
09:00:00 9733
10:00:00 9869
11:00:00 10096
12:00:00 7526
18:00:00 5183
19:00:00 3445
20:00:00 2850
21:00:00 3447
22:00:00 3367
23:00:00 3387

Statistics:
-----------
Elements processed: 495761
Elements output:    19
Execution time:     14.72 seconds