Azure VM Performance Monitoring

Connect Azure VM to Log Analytics to Monitor Performance:

To Monitor the Azure VM Performance, your Windows or Linux Azure VMs to be connected to Log Analytics Workspace inorder to monitor the performance. You can use the below Azure PowerShell Script to Connect Azure VM to Log Analytics.

Select-AzSubscription -Subscription "_add_default_subscription_name_"
$resourceGroupName = "_add_rgroup_name_"
$Location = "_add_"

$workspace = Get-AzOperationalInsightsWorkspace -ResourceGroupName $resourceGroupName
$workspaceId = $workspace.CustomerId
$workspaceKey = Get-AzOperationalInsightsWorkspaceSharedKeys -ResourceGroupName $resourceGroupName -Name $Workspace.Name

$Publicsettings=@{"workspaceId" = $workspaceId}
$Protectedsettings=@{"workspaceKey" = $workspaceKey.primarysharedkey}

$VMs=Get-AzVM -ResourceGroupName $resourceGroupName | Where-Object {$_.name -match "DEMO"}

foreach($VM in $VMs){
  $vmName=$vm.name
  Set-AzVMExtension 
      -ExtensionName "Microsoft.EnterpriseCloud.Monitoring" `
      -ResourceGroupName $RGName `
      -VMName $ServerName `
      -Publisher "Microsoft.EnterpriseCloud.Monitoring" `
      -ExtensionType "MicrosoftMonitoringAgent" `
      -TypeHandlerVersion 1.0 `
      -Settings $PublicSettings `
      -ProtectedSettings $ProtectedSettings `
      -Location $Location
}

Monitoring and alerting for VM’s which is in a subscription and Region/Location with the following log queries to analyse logs of Disk Utilization, CPU Utilization, Heartbeat, Memory Utilization, Container Status.

Use Azure log analytics and the created the workspace. Virtual machines with OMS agent must be connected to the work space as a pre-requisite so that metrics can be flown from the VM's. Once the metrics populated, using those metrics query can be written in KQL (Kusto Query Language) which is the Log search window. Based on the output, for the required threshold alert must be configured. OMS is used to query the log analytics workspace and to create the required alerts.

Note: Alert will be created in the workspace and it will trigger only the condition in met.

Alert Configuration:

To configure any alert in Azure log analytics, we should follow the below steps:

Step 1: Login to Azure Portal and Navigate to Log Analytics Workspace.

Step 2: From the left pane, Search or Navigate to Logs and write your own kql query.

Step 3: Once the search is RUN, on the top right there will be New Alert Button.

Step 4: In the alert page, refer the condition and mention the threshold value and done the changes.

Step 5: Select the appropriate action group and severity of an alert.

Step 6: Save your Changes.

Azure Workspace Alert Examples:

Instance Down Alert: Alert will be triggered whenever any of the VM goes down.

KQL Query: Heartbeat | summarize LastHeartbeat=max(TimeGenerated) by Computer | where LastHeartbeat < ago(10m)

CPU Utilization Alert: Alert will search in each and every VM and trigger only when the utilization of processor exceeds 60%

KQL Query: Perf | where CounterName == "% Processor Time" | summarize AggregatedValue = max(CounterValue) by InstanceName,Computer | where AggregatedValue between ( 60 .. 70)

Memory Utilization Alert: Alert will search in each and every VM and trigger only when the utilization of Memory exceeds 60%

KQL Query: Perf | where ObjectName == 'Memory' | where CounterName == "% Used Memory" | summarize AggregatedValue = max(CounterValue) by InstanceName,Computer | where AggregatedValue between ( 60 .. 70 )

Disk/File system Utilization: Alert will search in each and every VM and trigger only when the utilization of Disk exceeds 60%

KQL Query: Perf | where CounterName == "% Used Space" | summarize AggregatedValue = avg(CounterValue) by InstanceName, Computer | where InstanceName == "/data" | where AggregatedValue between ( 60 .. 70 )

Azure VM Performance Reports:

KQL query to get Azure Virtual Machine CPU and MEMORY Utilization across all computers by OSType = "Windows/Linux":

Perf
| where ObjectName == "Processor" and CounterName == "% Processor Time" and InstanceName == "_Total"
| summarize MIN_CPU = min(CounterValue), AVG_CPU = avg(CounterValue), MAX_CPU = max(CounterValue) by bin(TimeGenerated, 30d), Computer
| join 
(
   Perf
    | where CounterName == "% Used Memory" or CounterName == "% Committed Bytes In Use" 
    | summarize AVG_MEM=avg(CounterValue), MIN_MEM=min(CounterValue), MAX_MEM=max(CounterValue) by bin(TimeGenerated, 30d), Computer
) on Computer
| join
(
VMComputer 
| summarize by Computer, AzureImageOffering, AzureLocation, AzureImageSku, 
OperatingSystemFamily, VirtualMachineNativeName, VirtualMachineType, DisplayName, HostName,  AzureResourceName, AzureSubscriptionId, AzureResourceGroup, OperatingSystemFullName, Cpus, AzureSize, AzureImagePublisher, AzureImageVersion) on Computer
| project HostName, AzureResourceGroup, Computer, OperatingSystemFamily, OperatingSystemFullName, AzureSize, Cpus, MIN_CPU, AVG_CPU, MAX_CPU, 
MIN_MEM, AVG_MEM, MAX_MEM

or

Perf
| where TimeGenerated > ago(30d)
| where ObjectName == "Processor" and CounterName == "% Processor Time" and InstanceName == "_Total"
| where Computer in ((Heartbeat | where OSType == "Linux" or OSType == "Windows" | distinct Computer))
| summarize MIN_CPU = min(CounterValue), AVG_CPU = avg(CounterValue), MAX_CPU = max(CounterValue) by Computer
| join 
Perf 
| where ObjectName == "Memory"
| where CounterName == "% Used Memory" or CounterName == "% Committed Bytes In Use"
| where TimeGenerated > ago(30d)
| summarize MIN_MEMORY = min(CounterValue), AVG_MEMOERY= avg(CounterValue), MAX_MEMORY = max(CounterValue) by Computer
) on Computer

or

Perf
| where CounterName == "% Processor Time"  and InstanceName == "_Total"
| where TimeGenerated > ago(30d)
| project bin(TimeGenerated, 30d), Computer, CPU=(CounterValue), _ResourceId, Type, SourceSystem
| join (
   Perf
    | where CounterName == "% Used Memory" or CounterName == "% Committed Bytes In Use" 
    | where TimeGenerated > ago(30d)
    | project bin(TimeGenerated, 30d), Computer, MEMORY=CounterValue, _ResourceId, Type, SourceSystem
) on TimeGenerated, Computer
| summarize MIN_CPU=min(CPU), AVG_CPU=avg(CPU), MAX_CPU=max(CPU), MIN_MEMORY=min(MEMORY), AVG_MEMORY=avg(MEMORY), MAX_MEMORY=max(MEMORY) 
by Computer, _ResourceId, Type = "Performance" , SourceSystem

KQL query to get Azure VM Memory Utilization across all the computers by Time Generated:

Perf
| where ObjectName == 'Memory' and CounterName == "% Used Memory"
| where TimeGenerated > ago(30d)
| summarize min(CounterValue), avg(CounterValue), max(CounterValue) by Computer

or

Perf 
| where ObjectName == "Memory"
| CounterName == "% Used Memory" or CounterName == "% Committed Bytes In Use"
| where TimeGenerated between(datetime(2021-05-01 00:00:00) .. datetime('2021-05-31 00:00:00'))
| summarize MINCPU = min(CounterValue), AVGCPU = avg(CounterValue), MAXCPU = min(CounterValue) by Computer, InstanceName

Monitor Utilization:

Example : Monitor CPU Utilization when above 60% and below 70%

Alert will search in each and every VM and trigger only when the utilization of processor exceeds 60%

Perf
| where CounterName == "% Processor Time"
| summarize AggregatedValue = max(CounterValue) by InstanceName,Computer
| where AggregatedValue between ( 60 .. 70)

Example : Monitor CPU Utilization when above 85% or equal to 85%

Alert will search in each and every VM and trigger only when the utilization of processor exceeds 85%

Perf
| where CounterName == "% Processor Time"
| summarize AggregatedValue = max(CounterValue) by InstanceName, Computer
| where AggregatedValue >= 85%

Monitor Memory Utilization

Example: Monitor Memory Utilization when above 60% and below 70%

Alert will search in each and every VM and trigger only when the utilization of Memory exceeds 60%

Perf
| where ObjectName == 'Memory'
| where CounterName == "% Used Memory"
| summarize AggregatedValue = max(CounterValue) by InstanceName,Computer
| where AggregatedValue between ( 60 .. 70 )

Example: Monitor Memory Utilization when above 70%

Alert will search in each and every VM and trigger only when the utilization of Memory exceeds 70%

Perf
| where ObjectName == 'Memory'
| where CounterName == "% Used Memory"
| summarize AggregatedValue = max(CounterValue) by InstanceName, Computer
| where AggregatedValue >=70%

Azure Application Gateway Logs to Troubleshoot Errors

Requests to which Application Gateway responded with an error -

AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS" and OperationName == "ApplicationGatewayAccess" and httpStatus_d > 399

Count of the Non-SSL requests on the Application Gateway -

AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS" and OperationName == "ApplicationGatewayAccess" and sslEnabled_s == "off"

Incoming requests on the Application Gateway -

AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS" and OperationName == "ApplicationGatewayAccess"

Application Gateway Firewall Logs -

AzureDiagnostics
| where ResourceType == "APPLICATIONGATEWAYS" and OperationName == "ApplicationGatewayFirewallLog"

Conditional Access Policy Alert

AuditLogs
| where Category has "Policy"
| where TargetResources contains "ConditionalAccessPolicy"
| project Service = "Conditional Access", Category, TargetResources[0].displayName, TargetResources[0].modifiedProperties[0].displayName

Monitor Azure VM Update Patch Jobs

Azure Servers Patch Deployment Status (Last 7 Days)

UpdateSummary
| where TimeGenerated > ago(7d)
| join kind=inner(UpdateRunProgress| where TimeGenerated > ago(7d) | project Computer, UpdateRunName) on Computer
| join kind=innerunique (Heartbeat | distinct Computer) on Computer
| project Resource, UpdateRunName, CriticalUpdatesMissing, SecurityUpdatesMissing
| order by CriticalUpdatesMissing, SecurityUpdatesMissing

Count of Azure Servers by UpdateRunName (Last 7 Days)

UpdateSummary
| where TimeGenerated > ago(7d)
| join kind=inner(UpdateRunProgress | where TimeGenerated > ago(7d) | project Computer, UpdateRunName) on Computer
| join kind=innerunique (Heartbeat | distinct Computer) on Computer
| summarize count() by UpdateRunName

Total Patch Deployments which were Succeeded - Last 7 Days

UpdateRunProgress
| where TimeGenerated > ago(7d)
| where InstallationStatus == "Succeeded"
| summarize arg_max(TimeGenerated, Title, InstallationStatus, Resource, UpdateRunName) by UpdateId
| project TimeGenerated, Resource, Title, InstallationStatus, UpdateRunName

UpdateRunProgress
| where TimeGenerated > ago(7d)
| summarize arg_max(TimeGenerated, Title, InstallationStatus, Resource) by UpdateId
| project TimeGenerated, Resource, displayName=Title, InstallationStatus
| summarize count() by InstallationStatus

Azure Servers Patch Deployment Status by Patch Schedule Name (Last 7 Days)

UpdateRunProgress
| where TimeGenerated > ago(7d)
| summarize arg_max(TimeGenerated, Title, InstallationStatus, Resource, UpdateRunName) by UpdateId
| project TimeGenerated, Resource, displayName=Title, InstallationStatus, UpdateRunName
| order by InstallationStatus desc
UpdateRunProgress
| where TimeGenerated > ago(7d) 
| where InstallationStatus == "NotStarted" 
| summarize by Title, InstallationStatus, SourceComputerId, UpdateId, Computer, Resource
| join kind=inner(
    UpdateRunProgress
    | where TimeGenerated > ago(7d) 
    | where InstallationStatus != "NotStarted" 
    | summarize by Title, InstallationStatus, SourceComputerId, UpdateId, Computer, Resource) on UpdateId 
| where InstallationStatus1 != "Succeed"
| summarize by Resource, Title, InstallationStatus, Computer

Update Run Progess where Installation Status = Failure

UpdateRunProgress
| where TimeGenerated > ago(7d)
| where InstallationStatus == "Failed"
| summarize arg_max(TimeGenerated, Title, InstallationStatus, Resource, UpdateRunName) by UpdateId
| project TimeGenerated, Resource, Title, InstallationStatus, UpdateRunName

Azure Update Management KQL Log Queries

Update | take 5

UpdateSummary | take 5

UpdateRunProgress | take 5
// Computers list 
// List of computers with Azure Update Management deployed.
Heartbeat
| where TimeGenerated>ago(12h) and OSType=="Linux" and notempty(Computer)
| summarize arg_max(TimeGenerated, Solutions, Computer, ResourceId, ComputerEnvironment, VMUUID) by SourceComputerId
| where Solutions has "updates"
| extend vmuuId=VMUUID, azureResourceId=ResourceId, osType=1, environment=iff(ComputerEnvironment=~"Azure", 1, 2), scopedToUpdatesSolution=true, lastUpdateAgentSeenTime=""
| join kind=leftouter
(
   Update
    | where TimeGenerated>ago(5h) and OSType=="Linux" and SourceComputerId in ((Heartbeat
    | where TimeGenerated>ago(12h) and OSType=="Linux" and notempty(Computer)
    | summarize arg_max(TimeGenerated, Solutions) by SourceComputerId
    | where Solutions has "updates"
    | distinct SourceComputerId))
    | summarize hint.strategy=partitioned arg_max(TimeGenerated, UpdateState, Classification, Product, Computer, ComputerEnvironment) by SourceComputerId, Product, ProductArch
    | summarize Computer=any(Computer), ComputerEnvironment=any(ComputerEnvironment), missingCriticalUpdatesCount=countif(Classification has "Critical" and UpdateState=~"Needed"), missingSecurityUpdatesCount=countif(Classification has "Security" and UpdateState=~"Needed"), missingOtherUpdatesCount=countif(Classification !has "Critical" and Classification !has "Security" and UpdateState=~"Needed"), lastAssessedTime=max(TimeGenerated), lastUpdateAgentSeenTime="" by SourceComputerId
    | extend compliance=iff(missingCriticalUpdatesCount > 0 or missingSecurityUpdatesCount > 0, 2, 1)
    | extend ComplianceOrder=iff(missingCriticalUpdatesCount > 0 or missingSecurityUpdatesCount > 0 or missingOtherUpdatesCount > 0, 1, 3)
)
on SourceComputerId
| project id=SourceComputerId, displayName=Computer, sourceComputerId=SourceComputerId, scopedToUpdatesSolution=true, missingCriticalUpdatesCount=coalesce(missingCriticalUpdatesCount, -1), missingSecurityUpdatesCount=coalesce(missingSecurityUpdatesCount, -1), missingOtherUpdatesCount=coalesce(missingOtherUpdatesCount, -1), compliance=coalesce(compliance, 4), lastAssessedTime, lastUpdateAgentSeenTime, osType=1, environment=iff(ComputerEnvironment=~"Azure", 1, 2), ComplianceOrder=coalesce(ComplianceOrder, 2)
| union(Heartbeat
| where TimeGenerated>ago(12h) and OSType=~"Windows" and notempty(Computer)
| summarize arg_max(TimeGenerated, Solutions, Computer, ResourceId, ComputerEnvironment, VMUUID) by SourceComputerId
| where Solutions has "updates"
| extend vmuuId=VMUUID, azureResourceId=ResourceId, osType=2, environment=iff(ComputerEnvironment=~"Azure", 1, 2), scopedToUpdatesSolution=true, lastUpdateAgentSeenTime=""
| join kind=leftouter
(
    Update
    | where TimeGenerated>ago(14h) and OSType!="Linux" and SourceComputerId in ((Heartbeat
    | where TimeGenerated>ago(12h) and OSType=~"Windows" and notempty(Computer)
    | summarize arg_max(TimeGenerated, Solutions) by SourceComputerId
    | where Solutions has "updates"
    | distinct SourceComputerId))
    | summarize hint.strategy=partitioned arg_max(TimeGenerated, UpdateState, Classification, Title, Optional, Approved, Computer, ComputerEnvironment) by Computer, SourceComputerId, UpdateID
    | summarize Computer=any(Computer), ComputerEnvironment=any(ComputerEnvironment), missingCriticalUpdatesCount=countif(Classification has "Critical" and UpdateState=~"Needed" and Approved!=false), missingSecurityUpdatesCount=countif(Classification has "Security" and UpdateState=~"Needed" and Approved!=false), missingOtherUpdatesCount=countif(Classification !has "Critical" and Classification !has "Security" and UpdateState=~"Needed" and Optional==false and Approved!=false), lastAssessedTime=max(TimeGenerated), lastUpdateAgentSeenTime="" by SourceComputerId
    | extend compliance=iff(missingCriticalUpdatesCount > 0 or missingSecurityUpdatesCount > 0, 2, 1)
    | extend ComplianceOrder=iff(missingCriticalUpdatesCount > 0 or missingSecurityUpdatesCount > 0 or missingOtherUpdatesCount > 0, 1, 3)
)
on SourceComputerId
| project id=SourceComputerId, displayName=Computer, sourceComputerId=SourceComputerId, scopedToUpdatesSolution=true, missingCriticalUpdatesCount=coalesce(missingCriticalUpdatesCount, -1), missingSecurityUpdatesCount=coalesce(missingSecurityUpdatesCount, -1), missingOtherUpdatesCount=coalesce(missingOtherUpdatesCount, -1), compliance=coalesce(compliance, 4), lastAssessedTime, lastUpdateAgentSeenTime, osType=2, environment=iff(ComputerEnvironment=~"Azure", 1, 2), ComplianceOrder=coalesce(ComplianceOrder, 2))
| order by ComplianceOrder asc, missingCriticalUpdatesCount desc, missingSecurityUpdatesCount desc, missingOtherUpdatesCount desc, displayName asc
| project-away ComplianceOrder
// Summary of updates available across machines 
// Count of updates available under various categories for each machine. 
UpdateSummary 
| where TimeGenerated>ago(14h) 
| summarize by Computer, CriticalUpdatesMissing, SecurityUpdatesMissing, OtherUpdatesMissing, TotalUpdatesMissing, ResourceId
// Missing updates summary 
// Get a summary of missing updates by category. 
Update
| where TimeGenerated>ago(5h) and OSType=="Linux" and SourceComputerId in ((Heartbeat
| where TimeGenerated>ago(12h) and OSType=="Linux" and notempty(Computer)
| summarize arg_max(TimeGenerated, Solutions) by SourceComputerId
| where Solutions has "updates"
| distinct SourceComputerId))
| summarize hint.strategy=partitioned arg_max(TimeGenerated, UpdateState, Classification) by Computer, SourceComputerId, Product, ProductArch
| where UpdateState=~"Needed"
| summarize by Product, ProductArch, Classification
| union (Update
| where TimeGenerated>ago(14h) and OSType!="Linux" and (Optional==false or Classification has "Critical" or Classification has "Security") and SourceComputerId in ((Heartbeat
| where TimeGenerated>ago(12h) and OSType=~"Windows" and notempty(Computer)
| summarize arg_max(TimeGenerated, Solutions) by SourceComputerId
| where Solutions has "updates"
| distinct SourceComputerId))
| summarize hint.strategy=partitioned arg_max(TimeGenerated, UpdateState, Classification, Approved) by Computer, SourceComputerId, UpdateID
| where UpdateState=~"Needed" and Approved!=false
| summarize by UpdateID, Classification )
| summarize allUpdatesCount=count(), criticalUpdatesCount=countif(Classification has "Critical"), securityUpdatesCount=countif(Classification has "Security"), otherUpdatesCount=countif(Classification !has "Critical" and Classification !has "Security")

 

// Azure Automation jobs that are Completed 
// List all automation jobs that got completed
AzureDiagnostics 
| where ResourceProvider == "MICROSOFT.AUTOMATION" and Category == "JobLogs" and ResultType == "Completed" 
| project TimeGenerated , RunbookName_s , ResultType , _ResourceId , JobId_g
// Azure Automation jobs that are failed, suspended, or stopped 
// List all the automation jobs that failed , suspended or stopped. 
AzureDiagnostics 
| where ResourceProvider == "MICROSOFT.AUTOMATION" and Category == "JobLogs" and (ResultType == "Failed" or ResultType == "Stopped" or ResultType == "Suspended") 
| project TimeGenerated , RunbookName_s , ResultType , _ResourceId , JobId_g
// Patch installation failure for your machines 
// List for each machine the installation status of the updates where the installation was not successful.
UpdateRunProgress
| where TimeGenerated>ago(1d) 
| where InstallationStatus == "NotStarted" 
| summarize by Title, InstallationStatus, SourceComputerId, UpdateId, Computer, ResourceId
| join kind= inner (
    UpdateRunProgress
    | where TimeGenerated>ago(1d) 
    | where InstallationStatus != "NotStarted"
    | summarize by Title, InstallationStatus, SourceComputerId, UpdateId, Computer
) on UpdateId 
| where InstallationStatus1 != "Succeed"
| summarize by Title, InstallationStatus, Computer, ResourceId
// Updates available for Linux machines 
// List the Linux package version updates available by their classification and for each Computer. 
Update
| where TimeGenerated>ago(14h) 
| where UpdateState =~ "Needed" and OSType == "Linux" 
| summarize by Computer, Classification, Product, ProductVersion, ResourceId
// Updates available for Windows machines 
// List the Windows update KBIDs available by their classification and for each Computer. 
Update
| where TimeGenerated>ago(14h) 
| where UpdateState =~ "Needed" and OSType != "Linux" 
| summarize by Computer, Classification, Product, KBID, ResourceId
// View historical job status 
// List all automation jobs. 
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.AUTOMATION" and Category == "JobLogs" and ResultType != "started"
| summarize AggregatedValue = count() by ResultType, bin(TimeGenerated, 1h) , RunbookName_s , JobId_g, _ResourceId

Azure VM CPU and Memory Utilization Performance Monitoring:

Here in this tutorial, I will be covering the following topics:

  • Checking azure virtual machine CPU utilization.
  • Checking azure virtual machine Memory utilization.
  • Creating consolidated performance report of azure virtual machines.

Pre-requisites:

  • Azure VMs you wish to monitor should be connected the log analytics workspace.
  • Performance counters should be enabled on the log analytics workspace configuration to capture the performance logs of both cpu and memory utilization.

Create Azure VM CPU Utilization and Memory Utilization Performance Report:

Azure VMs performance report can be generated using Azure Monitor Logs. The below KQL Query is the most efficient Azure monitoring log query to fetch the Azure virual machines CPU and Memory utilization report and also to export the results into a consolidated CSV file.

VMComputer
| where TimeGenerated > ago(30d)
| extend AzureSubscriptionName=case(
AzureSubscriptionId =~ 'add subscriptionId','add subscriptionName',
AzureSubscriptionId)
| summarize by Computer, HostName, AzureSize, Cpus, AzureSubscriptionName, OperatingSystemFamily, OperatingSystemFullName
| join kind=inner (Perf
| where TimeGenerated > ago(30d)
| where ObjectName == "Processor" and CounterName == "% Processor Time" and InstanceName == "_Total" 
| summarize MIN_CPU = round(min(CounterValue), 2), AVG_CPU = round(avg(CounterValue), 2), MAX_CPU = round(max(CounterValue), 2) by Computer
) on Computer
| join kind=inner (Perf
| where TimeGenerated > ago(30d)
| where CounterName == "% Used Memory" or CounterName == "% Committed Bytes In Use"
| summarize MIN_MEM = round(min(CounterValue), 2), AVG_MEM = round(avg(CounterValue), 2), MAX_MEM = round(max(CounterValue), 2) by Computer
) on Computer
| project Computer, HostName, MIN_CPU, AVG_CPU, MAX_CPU, MIN_MEM, AVG_MEM, MAX_MEM, AzureSize, Cpus, AzureSubscriptionName, OperatingSystemFamily, OperatingSystemFullName

Check Azure VM Availability

Copy and run this query in Azure Graph Explorer to check the Azure VM Availability for the Last 24 Hours or 1 day.


Heartbeat
| summarize heartbeatPerHour = count() by bin_at(TimeGenerated, 1h, ago(1d)), Resource, ResourceGroup, OSType
| extend availablePerHour = iff(heartbeatPerHour > 0, true, false)
| summarize totalAvailableHours = countif(availablePerHour == true) by Resource, ResourceGroup , OSType
| extend availabilityRate = (totalAvailableHours/24)*100.0 | sort by availabilityRate desc