psLogAnalytics.psm1
function Write-PsLogAnalytics { <# .SYNOPSIS Query data sources and upload results to a Log Analytics workspace .DESCRIPTION Use to gather data local from a computer and send it to Microsoft Log Analytics .PARAMETER CustomerID The LogAnalytics Workspace ID number .PARAMETER SharedKey The LogAnalytics Workspace Primary Key .PARAMETER InformationType The type of information to gather (Exchange, WMI, SQL, AD) .PARAMETER Command The command used to gather the information (Get-ExchangeServer | ? serverrole -Like *Mailbox* | Get-Queue | select Identity,DeliveryType,Status,MessageCount,NextHopDomain | Sort -Descending MessageCount) .PARAMETER LogName Indicates the name of the log file to create in Log Analytics (ExchangeQueues, win32_logicaldisk, SQLProcessSpaceInfo) .PARAMETER Computer [only partially integrated] Indicates the computer where the command should be run and/or what computer data is listed as in Log Analytics .EXAMPLE Using WMI to gather disk space information Write-PsLogAnalytics -InformationType "WMI" -LogName "win32_logicaldisk" -Command "Get-WmiObject -Class Win32_LogicalDisk | Select-Object Name, VolumeName, FileSystem, Description, FreeSpace, Size" .EXAMPLE Using WMI to gather Physical Memory $Command = @' Get-WmiObject CIM_PhysicalMemory | Measure-Object -Property capacity -sum | % {[math]::round(($_.sum / 1GB),2)} '@ Write-PsLogAnalytics -InformationType "WMI" -LogName "PhysicalMemory" -Command $Command .EXAMPLE Using WMI to gather CPU information Write-PsLogAnalytics -InformationType "WMI" -LogName "win32processor" -Command "Get-WmiObject �class Win32_processor | Select-Object Name,DeviceID,NumberOfCores,NumberOfLogicalProcessors, Addresswidth" .EXAMPLE Using Microsoft Exchange Write-PsLogAnalytics -InformationType "Exchange" -LogName "ExchangeQueues" -Command "Get-ExchangeServer | ? serverrole -Like *Mailbox* | Get-Queue | select Identity,DeliveryType,Status,MessageCount,NextHopDomain | Sort -Descending MessageCount" .EXAMPLE Using Active Directory $Command = @' Get-ADUser -SearchBase 'OU=Launch,DC=CAT,DC=DEMO' -SearchScope 'Subtree' -properties "Created","Department","Displayname","EmailAddress","Manager","Modified","Name","Office","SamAccountName","Title","UserPrincipalName" -filter * '@ Write-PsLogAnalytics -InformationType "AD" -LogName "ActiveDirectory" -Command $Command .EXAMPLE Using SQL $Command = @' SELECT SERVERPROPERTY('MachineName') AS [MachineName] , physical_memory_in_use_kb / 1024 AS [SQLServerMemoryUsageMB] , locked_page_allocations_kb / 1024 AS [SQLServerLockedPagesAllocationMB] , large_page_allocations_kb / 1024 AS [SQLServerLargePagesAllocationMB] , page_fault_count , memory_utilization_percentage , available_commit_limit_kb , process_physical_memory_low , process_virtual_memory_low FROM sys.dm_os_process_memory WITH (NOLOCK) OPTION (RECOMPILE); '@ Write-PsLogAnalytics -InformationType "SQL" -LogName "SQLProcessSpaceInfo" -Command $Command -Computer "SQL1" .NOTES * Originally created as Write-LogAnalytics.ps1 by Cameron Fuller (Twitter: @CFullerMVP) * Available at https://gallery.technet.microsoft.com/PowerShell-script-to-0823e09d #> [CmdletBinding()] param ( [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $CustomerId, [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $SharedKey, [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $InformationType, [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $Command, [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $LogName, [parameter()][string] $Computer ) Write-Verbose "Specify the name of the record type that you'll be creating" $LogType = $LogName Write-Verbose "Specify a field with the created time for the records" $TimeStampField = Get-Date if (!$Computer) { $computername = $env:COMPUTERNAME } else { $computername = $Computer } # Create the function to create the authorization signature Function Build-Signature ($customerId, $sharedKey, $date, $contentLength, $method, $contentType, $resource) { $xHeaders = "x-ms-date:" + $date $stringToHash = $method + "`n" + $contentLength + "`n" + $contentType + "`n" + $xHeaders + "`n" + $resource $bytesToHash = [Text.Encoding]::UTF8.GetBytes($stringToHash) $keyBytes = [Convert]::FromBase64String($sharedKey) $sha256 = New-Object System.Security.Cryptography.HMACSHA256 $sha256.Key = $keyBytes $calculatedHash = $sha256.ComputeHash($bytesToHash) $encodedHash = [Convert]::ToBase64String($calculatedHash) $authorization = 'SharedKey {0}:{1}' -f $customerId, $encodedHash return $authorization } # Create the function to create and post the request Function Write-PsLogAnalyticsData { param ( [parameter()] $customerId, [parameter()] $sharedKey, [parameter()] $body, [parameter()] $logType ) $method = "POST" $contentType = "application/json" $resource = "/api/logs" $rfc1123date = [DateTime]::UtcNow.ToString("r") $contentLength = $body.Length $signature = Build-Signature ` -customerId $customerId ` -sharedKey $sharedKey ` -date $rfc1123date ` -contentLength $contentLength ` -fileName $fileName ` -method $method ` -contentType $contentType ` -resource $resource $uri = "https://" + $customerId + ".ods.opinsights.azure.com" + $resource + "?api-version=2016-04-01" $headers = @{ "Authorization" = $signature; "Log-Type" = $logType; "x-ms-date" = $rfc1123date; "time-generated-field" = $TimeStampField; } Write-Verbose "submitting web request" $response = Invoke-WebRequest -Uri $uri -Method $method -ContentType $contentType -Headers $headers -Body $body -UseBasicParsing return $response.StatusCode } if ($InformationType -match "Exchange") { Write-Verbose "Exchange pre-tasks" Add-PSSnapin *exchange* -ErrorAction SilentlyContinue } elseif ($InformationType -match "SQL") { # FUTURE: consider replacing with dbatools or sqlserver modules Write-Verbose "SQL pre-tasks" $database = 'master' $server = "$computername" $connection = New-Object System.Data.SqlClient.SQLConnection("Data Source=$server;Integrated Security=SSPI;Initial Catalog=$database"); $cmd = New-Object System.Data.SqlClient.SqlCommand($sqlquery, $connection); $connection.Open(); $reader = $cmd.ExecuteReader() $results = @() while ($reader.Read()) { $row = @{ } for ($i = 0; $i -lt $reader.FieldCount; $i++) { $row[$reader.GetName($i)] = $reader.GetValue($i) } $results += New-Object psobject -property $row } $connection.Close(); } elseif ($InformationType -match "WMI") { Write-Verbose "WMI pre-tasks" # Add any WMI specific precursor tasks here } elseif ($InformationType -match "AD") { # FUTURE: consider adsips module for cases w/o ActiveDirectory module Write-Verbose "ActiveDirectory pre-tasks. Must be run with Admin permissions initially" Import-Module ActiveDirectory } # Run the command which was send in with the Command parameter if ($InformationType -ne "SQL") { $results = Invoke-Expression $Command } foreach ($result in $results) { if ($InformationType -match "Exchange") { $result | Add-Member -MemberType NoteProperty -Name Computer -Value $computername } elseif ($InformationType -match "SQL") { # Add any SQL specific post tasks here } elseif ($InformationType -match "WMI") { $result | Add-Member -MemberType NoteProperty -Name Computer -Value $computername } elseif ($InformationType -match "AD") { # Add any AD specific post tasks here } Write-Verbose $result Write-Verbose "convert results to JSON and compressing" $json = $result | ConvertTo-Json -Compress Write-Verbose "Submitting the data to the API endpoint" Write-PsLogAnalyticsData -customerId $customerId -sharedKey $sharedKey -body ([System.Text.Encoding]::UTF8.GetBytes($json)) -logType $logType } } function Write-PsLogAnalyticsDeviceInventory { <# .SYNOPSIS Write Windows device inventory data to a Log Analytics workspace .PARAMETER CustomerID The LogAnalytics Workspace ID number .PARAMETER SharedKey The LogAnalytics Workspace Primary Key .PARAMETER InventoryMode Controls whether all inventory queries are run, just a selected query, or just list queries without uploading data The default is All queries .EXAMPLE Write-PsLogAnalyticsDeviceInventory -CustomerID "..." -SharedKey "..." .NOTES TBD #> [CmdletBinding()] param ( [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $CustomerId, [parameter(Mandatory)][ValidateNotNullOrEmpty()][string] $SharedKey, [parameter()][ValidateSet('All','Select','List')] $InventoryMode = 'All' ) try { $xmlsource = "$(Split-Path -Path $(Get-Module "psLogAnalytics").Path -Parent)\queries.xml" if (!(Test-Path $xmlsource)) { Write-Warning "Xml Source is missing: queries.xml"; break; } [xml]$xdata = Get-Content -Path $xmlsource -Raw -ErrorAction Stop $queries = $xdata.queries.query if ($InventoryMode -eq 'Select') { $choice = $queries | Select-Object name,type,comment | Sort-Object name | Out-GridView -Title "Select Query to Run" -OutputMode Single if ($null -ne $choice) { $queries = $queries | Where-Object {$_.name -eq $choice.name} Write-Verbose "query was selected, continuing process" } else { Write-Warning "No query selected. Done." break } } $index = 0 foreach ($query in $queries) { $cmd = $query.command $log = $query.name $cat = $query.type Write-Verbose "query: $log" switch ($InventoryMode) { 'List' { $query } Default { Write-PsLogAnalytics -InformationType $cat -LogName $log -Command $cmd -CustomerId $CustomerID -SharedKey $SharedKey } } $index++ } } catch { Write-Error $_.Exception.Message } } function Show-PsLogAnalyticsDeviceQuery { <# .SYNOPSIS Show stored Windows device inventory queries .PARAMETER GridView Display queries in GridView for selection and execution. If not provided, all queries are output to the console. .EXAMPLE Show-PsLogAnalyticsDeviceQuery .NOTES TBD #> [CmdletBinding()] param ( [parameter()][switch] $GridView ) try { $xmlsource = "$(Split-Path -Path $(Get-Module "psLogAnalytics").Path -Parent)\queries.xml" if (!(Test-Path $xmlsource)) { Write-Warning "Xml Source is missing: queries.xml"; break; } [xml]$xdata = Get-Content -Path $xmlsource -Raw -ErrorAction Stop $queries = $xdata.queries.query Write-Verbose "imported $($queries.Count) queries" if ($GridView) { $choice = $queries | Select-Object name,type,comment | Sort-Object name | Out-GridView -Title "Inventory Queries" -OutputMode Single if ($choice) { $queries | Where-Object {$_.name -eq $choice.name} | Select-Object -ExpandProperty command | Invoke-Expression } } else { $queries } } catch { Write-Error $_.Exception.Message } } function Export-PsLogAnalyticsQuery { <# .DESCRIPTION Invokes a query against the Log Analtyics Query API. .EXAMPLE Invoke-PsLogAnalyticsQuery -WorkspaceName my-workspace -SubscriptionId 0f991b9d-ab0e-4827-9cc7-984d7319017d -ResourceGroup my-resourcegroup -Query "union * | limit 1" -New-ObjectView .PARAMETER WorkspaceName The name of the Workspace to query against. .PARAMETER SubscriptionId The ID of the Subscription this Workspace belongs to. .PARAMETER ResourceGroup The name of the Resource Group this Workspace belongs to. .PARAMETER Query The query to execute. .PARAMETER Timespan The timespan to execute the query against. This should be an ISO 8601 timespan. .PARAMETER IncludeTabularView If specified, the raw tabular view from the API will be included in the response. .PARAMETER IncludeStatistics If specified, query statistics will be included in the response. .PARAMETER IncludeRender If specified, rendering statistics will be included (useful when querying metrics). .PARAMETER ServerTimeout Specifies the amount of time (in seconds) for the server to wait while executing the query. .PARAMETER Environment Internal use only. .EXAMPLE Export-PsLogAnalyticsQuery -WorkspaceName "my-workspace-123" ` -SubscriptionId "029340293840293402934809802" ` -ResourceGroup "my-rg123" -Query "DeviceInventory_CL" .EXAMPLE Export-PsLogAnalyticsQuery -WorkspaceName "my-workspace-123" ` -SubscriptionId "029340293840293402934809802" ` -ResourceGroup "my-rg123" ` -Query "DeviceInventory_CL | where Domain_s == "CONTOSO" " ` -IncludeTabularView -IncludeStatistics -IncludeRender ` -ServerTimeOut 30 -Environment int .NOTES Adapted heavily from Eli Shlomo example at https://www.eshlomo.us/query-azure-log-analytics-data-with-powershell/ #> [CmdletBinding()] param ( [parameter(Mandatory)][string] $WorkspaceName, [parameter(Mandatory)][guid] $SubscriptionId, [parameter(Mandatory)][string] $ResourceGroup, [parameter(Mandatory)][string] $Query, [parameter()][string] $Timespan, [parameter()][switch] $IncludeTabularView, [parameter()][switch] $IncludeStatistics, [parameter()][switch] $IncludeRender, [parameter()][int] $ServerTimeout, [parameter()][string][ValidateSet("", "int", "aimon")] $Environment = "", [parameter()][string] $apiVersion = "2017-01-01-preview" ) $ErrorActionPreference = "Stop" function Get-AccessToken { $azureCmdlet = Get-Command -Name Get-AzureRMContext -ErrorAction SilentlyContinue if ($null -eq $azureCmdlet) { $null = Import-Module AzureRM -ErrorAction Stop; } $AzureContext = & "Get-AzureRmContext" -ErrorAction Stop; $authenticationFactory = New-Object -TypeName Microsoft.Azure.Commands.Common.Authentication.Factories.AuthenticationFactory if ((Get-Variable -Name PSEdition -ErrorAction Ignore) -and ('Core' -eq $PSEdition)) { [Action[string]]$stringAction = {param($s)} $serviceCredentials = $authenticationFactory.GetServiceClientCredentials($AzureContext, $stringAction) } else { $serviceCredentials = $authenticationFactory.GetServiceClientCredentials($AzureContext) } # We can't get a token directly from the service credentials. Instead, we need to make a dummy message which we will ask # the serviceCredentials to add an auth token to, then we can take the token from this message. $message = New-Object System.Net.Http.HttpRequestMessage -ArgumentList @([System.Net.Http.HttpMethod]::Get, "http://foobar/") $cancellationToken = New-Object System.Threading.CancellationToken $null = $serviceCredentials.ProcessHttpRequestAsync($message, $cancellationToken).GetAwaiter().GetResult() $accessToken = $message.Headers.GetValues("Authorization").Split(" ")[1] # This comes out in the form "Bearer <token>" $accessToken } function Get-ArmHost { param ( [string] $environment ) switch ($environment) { "" { $armHost = "management.azure.com" } "aimon" { $armHost = "management.azure.com" } "int" { $armHost = "api-dogfood.resources.windows-int.net" } } $armHost } function Build-Uri { param ( [string] $armHost, [string] $subscriptionId, [string] $resourceGroup, [string] $workspaceName, [string] $queryParams ) "https://$armHost/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/" + ` "microsoft.operationalinsights/workspaces/$workspaceName/api/query?$queryParamString" } function Get-Headers { param ( [string] $AccessToken, [switch] $IncludeStatistics, [switch] $IncludeRender, [int] $ServerTimeout ) $preferString = "response-v1=true" if ($IncludeStatistics) { $preferString += ",include-statistics=true" } if ($IncludeRender) { $preferString += ",include-render=true" } if ($null -ne $ServerTimeout) { $preferString += ",wait=$ServerTimeout" } $headers = @{ "Authorization" = "Bearer $accessToken"; "prefer" = $preferString; "x-ms-app" = "LogAnalyticsQuery.psm1"; "x-ms-client-request-id" = [Guid]::NewGuid().ToString(); } $headers } function New-ObjectView { param ($data) # Find the number of entries we'll need in this array $count = 0 foreach ($table in $data.Tables) { $count += $table.Rows.Count } $objectView = New-Object object[] $count $i = 0; foreach ($table in $data.Tables) { foreach ($row in $table.Rows) { # Create a dictionary of properties $properties = @{} for ($columnNum=0; $columnNum -lt $table.Columns.Count; $columnNum++) { $properties[$table.Columns[$columnNum].name] = $row[$columnNum] } # Then create a PSObject from it. This seems to be *much* faster than using Add-Member $objectView[$i] = (New-Object PSObject -Property $properties) $null = $i++ } } $objectView } $accessToken = Get-AccessToken $armhost = Get-ArmHost $environment $queryParams = @("api-version=$apiVersion") $queryParamString = [string]::Join("&", $queryParams) $uri = Build-Uri $armHost $subscriptionId $resourceGroup $workspaceName $queryParamString $body = @{ "query" = $query; "timespan" = $Timespan } | ConvertTo-Json $headers = Get-Headers $accessToken -IncludeStatistics:$IncludeStatistics -IncludeRender:$IncludeRender -ServerTimeout $ServerTimeout $response = Invoke-WebRequest -UseBasicParsing -Uri $uri -Body $body -ContentType "application/json" -Headers $headers -Method Post if ($response.StatusCode -ne 200 -and $response.StatusCode -ne 204) { $statusCode = $response.StatusCode $reasonPhrase = $response.StatusDescription $message = $response.Content throw "Failed to execute query.`nStatus Code: $statusCode`nReason: $reasonPhrase`nMessage: $message" } $data = $response.Content | ConvertFrom-Json $result = New-Object PSObject $result | Add-Member -MemberType NoteProperty -Name Response -Value $response # In this case, we only need the response member set and we can bail out if ($response.StatusCode -eq 204) { $result return } $objectView = New-ObjectView $data $result | Add-Member -MemberType NoteProperty -Name Results -Value $objectView if ($IncludeTabularView) { $result | Add-Member -MemberType NoteProperty -Name Tables -Value $data.tables } if ($IncludeStatistics) { $result | Add-Member -MemberType NoteProperty -Name Statistics -Value $data.statistics } if ($IncludeRender) { $result | Add-Member -MemberType NoteProperty -Name Render -Value $data.render } $result } |