Ei, Equipe de Scripts!Calculando o tempo de atividade do servidor

A Equipe de Scripts da Microsoft

Código disponível para download em: HeyScriptingGuy2008_12.exe(152 KB)

Atividade é atividade e inatividade é inatividade. Parece óbvio, mas não é bem assim quando se trata do tempo de atividade do servidor. Para saber o tempo de atividade, é preciso saber o tempo de inatividade. Quase todo administrador de rede se preocupa com o tempo de atividade do servidor (a não ser quando se preocupa com o tempo de inatividade). A maioria deles possui metas de tempo de atividade e precisa fornecer relatórios a respeito aos seus superiores.

E qual é a novidade? Parece que é possível usar a classe Win32_OperatingSystem WMI, que tem duas propriedades que devem tornar esse tipo de operação bem fácil: LastBootUpTime e LocalDateTime. Você acha que basta extrair LastBootUptime de LocalDateTime e pronto, está livre para bater uma bolinha antes do jantar.

Então, você aciona o Windows PowerShell para consultar a classe Win32_OperatingSystem e seleciona as propriedades, como é mostrado aqui:

PS C:\> $wmi = Get-WmiObject -Class Win32_OperatingSystem
PS C:\> $wmi.LocalDateTime - $wmi.LastBootUpTime

Mas, ao executar esses comandos, não é recebido com o amigável tempo de atividade do seu servidor, mas com uma triste mensagem de erro mostrada na Figura 1.

fig01.gif

Figura 1 Um erro é retornado ao tentar extrair os valores de tempo WMI UTC (clique na imagem para ampliá-la)

A mensagem de erro talvez seja um pouco confusa: "Constante numérica incorreta". Hein? Você sabe qual é o número e qual é a constante, mas o que isso tem a ver com o tempo?

Ao ser confrontado com mensagens de erro estranhas, o melhor a fazer é examinar diretamente os dados que o script está tentando analisar. Além disso, com o Windows PowerShell, em geral, é bom ver qual tipo de dados está sendo usado.

Para examinar os dados que o script está usando, você pode simplesmente imprimi-los na tela. Olha o que aparece:

PS C:\> $wmi.LocalDateTime
20080905184214.290000-240

O número parece um pouco estranho. Que tipo de data é essa? Para descobrir, use o método GetType. O bom de GetType é que ele quase sempre está disponível. Tudo o que você precisa fazer é chamá-lo. E eis a origem do problema: o valor LocalDateTime está sendo relatado como uma cadeia de caracteres, e não como um valor System.DateTime:

PS C:\> $wmi.LocalDateTime.gettype()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True     True     String
System.Object

Se for necessário subtrair um tempo de outro, verifique se você está lidando com valores de tempo e não com cadeias de caracteres. É fácil fazer isso usando o método ConvertToDateTime, que o Windows PowerShell adiciona a todas as classes WMI:

PS C:\> $wmi = Get-WmiObject -Class Win32_OperatingSystem
PS C:\> $wmi.ConvertToDateTime($wmi.LocalDateTime) –
$wmi.ConvertToDateTime($wmi.LastBootUpTime)

Ao subtrair um valor de tempo de outro, você fica com uma instância de um objeto System.TimeSpan. Isso significa que você pode escolher como exibir as informações de tempo de atividade sem ter de realizar um monte de operações aritméticas. Basta escolher a propriedade a ser exibida (e, com sorte, contar o tempo de atividade em TotalDays, e não em TotalMilliseconds). A exibição padrão do objeto System.TimeSpan é mostrada aqui:

Days              : 0
Hours             : 0
Minutes           : 40
Seconds           : 55
Milliseconds      : 914
Ticks             : 24559148010
TotalDays         : 0.0284249398263889
TotalHours        : 0.682198555833333
TotalMinutes      : 40.93191335
TotalSeconds      : 2455.914801
TotalMilliseconds : 2455914.801

O problema com este método é que ele diz apenas o tempo durante o qual o servidor esteve ativo desde a última reinicialização. Ele não calcula o tempo de inatividade. Neste caso, atividade é inatividade; para calcular o tempo de atividade, primeiro você deve saber o tempo de inatividade.

Então, como descobrir por quanto tempo o servidor esteve inativo? Para isso, é necessário saber quando ele é iniciado e quando é desligado. Você pode obter essas informações no log de eventos do Sistema. Um dos primeiros processos iniciados no servidor ou na estação de trabalho é o log de eventos; ele também é um dos últimos a parar quando um servidor é desligado. Cada um desses eventos de início/parada gera um eventID: 6005 quando o log de eventos é iniciado e 6006 quando é interrompido. A Figura 2 mostra um exemplo de um log de eventos sendo iniciado.

fig02.gif

Figura 2 O log de eventos é iniciado pouco depois da inicialização do computador (clique na imagem para ampliá-la)

Coletando os eventos 6005 e 6006 do Log do Sistema, classificando-os e subtraindo os inícios das paradas, é possível determinar o tempo durante o qual o servidor ficou inativo entre as reinicializações. Se, depois, você subtrair esse valor do número de minutos durante o período em questão, irá calcular a porcentagem de tempo de atividade do servidor. Essa é a abordagem obtida no script CalculateSystemUpTimeFromEventLog.ps1 mostrado na Figura 3.

Figura 3 CalculateSystemUpTimeFromEventLog 3

#---------------------------------------------------------------
# CalculateSystemUpTimeFromEventLog.ps1
# ed wilson, msft, 9/6/2008
# Creates a system.TimeSpan object to subtract date values
# Uses a .NET Framework class, system.collections.sortedlist to sort the events from eventlog.
#---------------------------------------------------------------
#Requires -version 2.0
Param($NumberOfDays = 30, [switch]$debug)

if($debug) { $DebugPreference = " continue" }

[timespan]$uptime = New-TimeSpan -start 0 -end 0
$currentTime = get-Date
$startUpID = 6005
$shutDownID = 6006
$minutesInPeriod = (24*60)*$NumberOfDays
$startingDate = (Get-Date -Hour 00 -Minute 00 -Second 00).adddays(-$numberOfDays)

Write-debug "'$uptime $uptime" ; start-sleep -s 1
write-debug "'$currentTime $currentTime" ; start-sleep -s 1
write-debug "'$startingDate $startingDate" ; start-sleep -s 1

$events = Get-EventLog -LogName system | 
Where-Object { $_.eventID -eq  $startUpID -OR $_.eventID -eq $shutDownID `
  -and $_.TimeGenerated -ge $startingDate } 

write-debug "'$events $($events)" ; start-sleep -s 1

$sortedList = New-object system.collections.sortedlist

ForEach($event in $events)
{
 $sortedList.Add( $event.timeGenerated, $event.eventID )
} #end foreach event
$uptime = $currentTime - $sortedList.keys[$($sortedList.Keys.Count-1)]
Write-Debug "Current uptime $uptime"

For($item = $sortedList.Count-2 ; $item -ge 0 ; $item -- )
{ 
 Write-Debug "$item `t `t $($sortedList.GetByIndex($item)) `t `
   $($sortedList.Keys[$item])" 
 if($sortedList.GetByIndex($item) -eq $startUpID)
 {
  $uptime += ($sortedList.Keys[$item+1] - $sortedList.Keys[$item])
  Write-Debug "adding uptime. `t uptime is now: $uptime"
 } #end if  
} #end for item 

"Total up time on $env:computername since $startingDate is " + "{0:n2}" -f `
  $uptime.TotalMinutes + " minutes."
$UpTimeMinutes = $Uptime.TotalMinutes
$percentDownTime = "{0:n2}" -f (100 - ($UpTimeMinutes/$minutesInPeriod)*100)
$percentUpTime = 100 - $percentDowntime

"$percentDowntime% downtime and $percentUpTime% uptime."

O script começa usando a instrução Param para definir dois parâmetros de linha de comando cujos valores você pode alterar ao executar o script na linha de comando. O primeiro, $NumberOfDays, permite especificar um número diferente de dias a ser usado no relatório de tempo de atividade. (Observe que forneci um valor padrão de 30 dias no script para que você possa executá-lo sem ter de fornecer um valor para o parâmetro. É claro que você pode alterá-lo se necessário.)

O segundo, [switch]$debug, é um parâmetro trocado que permitirá obter algumas informações de depuração do script se você incluí-lo na linha de comando ao executar o script. Essas informações podem ajudá-lo a sentir mais confiança nos resultados que obtém no script. Pode haver ocasiões em que a mensagem de interrupção de serviço do log de eventos 6006 não está presente, talvez como resultado de uma falha crítica do servidor em que não foi possível gravar os dados no log de eventos, fazendo com que o script subtraísse o valor do tempo de atividade de outro valor de tempo de atividade e distorcesse os resultados.

Depois que a variável $debug é fornecida na linha de comando, ela é apresentada em Variable: unidade. Nesse caso, o valor da variável $debugPreference é definido como continue, o que significa que o script continuará a ser executado, e qualquer valor fornecido para Write-Debug será visível. Observe que, por padrão, o valor de $debugPreference é silentlycontinue, por isso, se você não definir o valor de $debugPreference como continue, o script será executado, mas qualquer valor fornecido para Write-Debug será silent (ou seja, não será visível).

Quando o script é executado, a saída resultante lista cada ocorrência das entradas do log de eventos 6005 e 6006 (como você pode ver na Figura 4) e mostra o cálculo do tempo de atividade. Usando essas informações, você pode confirmar a precisão dos resultados.

fig04.gif

Figura 4 O modo de depuração exibe um rastreamento de cada valor de tempo que é adicionado ao cálculo do tempo de atividade (clique na imagem para ampliá-la)

A etapa seguinte é criar uma instância do objeto System.TimeSpan. Você poderia usar o cmdlet New-Object para criar um objeto timespan padrão a ser usado para executar os cálculos de diferença de data:

PS C:\> [timespan]$ts = New-Object system.timespan

Mas o Windows PowerShell já tem um novo cmdlet New-TimeSpan para criar um objeto timespan, portanto, é melhor usá-lo. O uso desse cmdlet torna a leitura do script mais fácil, e o objeto criado é equivalente ao objeto timespan criado com New-Object.

Agora, você pode inicializar algumas variáveis, começando com $currentTime, que é usada para manter o valor de data e hora atual. Você obtém essas informações com o cmdlet Get-Date:

$currentTime = get-Date

Em seguida, inicialize as duas variáveis que mantêm os números de eventID de inicialização e desligamento. Na verdade, não é preciso fazer isso, mas o código ficará mais legível e fácil de solucionar se você não incorporar as duas como valores literais da cadeia de caracteres.

A etapa seguinte é criar uma variável chamada $minutesInPeriod para manter o resultado do cálculo usado na descoberta do número de minutos durante o período em questão:

$minutesInPeriod = (24*60)*$NumberOfDays

Finalmente, é necessário criar a variável $startingDate, que manterá um objeto System.DateTime, que representa o tempo de início do período do relatório. A data será a meia noite da data inicial para o período:

$startingDate = (Get-Date -Hour 00 -Minute 00 -Second 00).adddays(-$numberOfDays)

Depois de criar variáveis, recupere os eventos do log de eventos e armazene os resultados da consulta na variável $events. Use o cmdlet Get-EventLog para consultar o log de eventos, especificando "system" como o nome do log. No Windows PowerShell 2.0, você pode usar um parâmetro a –source para reduzir a quantidade de informações a serem classificadas no cmdlet Where-Object. Mas, no Windows PowerShell 1.0, não existe essa opção e, portanto, você deve classificar todos os eventos não filtrados retornados pela consulta. Passe por pipeline os eventos para o cmdlet Where-Object a fim de filtrar as entradas do log de eventos apropriadas. Ao examinar o filtro Where-Object, você verá por que a Equipe de Scripts fez você criar as variáveis para manter os parâmetros.

O comando é muito mais fácil de ler do que seria se você usasse os valores literais da cadeia de caracteres. Os eventIDs obtidos são iguais a $startUpID ou a $shutDownID. Você também deve verificar se a propriedade timeGenerated da entrada do log de eventos é maior do que ou igual a $startingDate, desta forma:

$events = Get-EventLog -LogName system |
Where-Object { $_.eventID -eq  $startUpID -OR $_.eventID -eq $shutDownID -and $_.TimeGenerated -ge $startingDate }

Lembre-se de que este comando será executado apenas localmente. No Windows PowerShell 2.0, é possível usar o parâmetro –computerName para fazer o comando trabalhar remotamente.

A etapa seguinte é criar um objeto de lista classificada. Por quê? Porque, quando você analisa a coleção de eventos, não há como saber a ordem em que as entradas do log de eventos foram relatadas. Mesmo que você passe os objetos por pipeline para o cmdlet Sort-Object e armazene os resultados em uma variável, ao iterar os objetos e armazenar os resultados em uma tabela hash, não dá para saber se a lista manterá os resultados do procedimento de classificação.

Para contornar esses problemas frustrantes e difíceis de depurar, crie uma instância do objeto System.Collections.SortedList, usando o construtor padrão para o objeto. O construtor padrão instrui a lista classificada a classificar as datas cronologicamente. Armazene o objeto de lista classificada na variável $sortedList:

$sortedList = New-object system.collections.sortedlist

Depois de criar o objeto de lista classificada, é necessário preenchê-lo. Para isso, use a instrução ForEach e analise a coleção de entradas do log de eventos armazenada na variável $entries. Ao analisar a coleção, a variável $event mantém o controle da sua posição na coleção. Você pode usar o método add para adicionar duas propriedades ao objeto System.Collections.SortedList. A lista classificada permite adicionar uma chave e uma propriedade value (semelhante a um objeto Dictionary, exceto que ela também permite indexar na coleção, como uma matriz faria). Adicione a propriedade timegenerated como a chave e o eventID como a propriedade value:

ForEach($event in $events)
{
 $sortedList.Add( $event.timeGenerated,
 $event.eventID )
} #end foreach event

Depois, você calcula o tempo de atividade atual do servidor. Para isso, use a entrada do log de eventos mais recente na lista classificada. Observe que ela sempre será a instância 6005 porque, se a entrada mais recente fosse 6006, o servidor ainda estaria inativo. Como o índice é baseado em zero, a entrada mais recente será count -1.

Para recuperar o valor gerado pelo tempo, é necessário olhar para a propriedade-chave da lista classificada. Para obter o valor de índice, use a propriedade count e subtraia um dela. Depois subtraia o tempo em que o evento 6005 foi gerado do valor de data e hora armazenado na variável $currenttime que você preencher antes. Será possível imprimir os resultados desse cálculo somente se o script for executado no modo de depuração. Esse código é mostrado aqui:

$uptime = $currentTime -
$sortedList.keys[$($sortedList.Keys.Count-1)]
Write-Debug "Current uptime $uptime"

Agora está na hora de analisar o objeto de lista classificada e calcular o tempo de atividade para o servidor. Como você está usando o objeto de lista System.Collections.Sorted, aproveitará o fato de poder indexar na lista. Para isso, use a instrução for, começando em count -2 porque usamos count -1 antes para calcular o tempo de atividade atual.

Faremos a contagem regressiva para obter o tempo de atividade, assim, a condição especificada na segunda posição da instrução for será quando o item for maior do que ou igual a 0. Na terceira posição da instrução for, use --, que irá subtrair um do valor de $item. Use o cmdlet Write-Debug para imprimir o valor do número do índice se o script for executado com a opção –debug. Você também pode aplicar tab usando o caractere `t e imprimir o valor do tempo timegenerated. Esta seção de código é mostrada aqui:

For($item = $sortedList.Count-2 ; $item -ge 
  0 ; $item--)
{ 
 Write-Debug "$item `t `t $($sortedList.
 GetByIndex($item)) `t `
   $($sortedList.Keys[$item])" 

Se o valor eventID for igual a 6005, que é o valor de inicialização de eventID, você calcula o tempo de atividade subtraindo o tempo inicial do valor do tempo de inatividade anterior. Armazene esse valor na variável $uptime. Se estiver no modo de depuração, poderá usar o cmdlet Write-Debug para imprimir esses valores na tela:

 if($sortedList.GetByIndex($item) -eq $startUpID)
 {
  $uptime += ($sortedList.Keys[$item+1] 
  - $sortedList.Keys[$item])
  Write-Debug "adding uptime. `t uptime is now: $uptime"
 } #end if  
} #end for item

Por último, você deve gerar o relatório. Pegue o nome do computador na variável computer do ambiente do sistema. Use o tempo atual armazenado no valor $startingdate e exiba o total de minutos do tempo de atividade para o período. Use o especificador de formato {0:n2} para imprimir o número em dois dígitos. Em seguida, calcule a porcentagem de tempo de inatividade, dividindo o número de minutos de tempo de atividade pelo número de minutos no período incluído no relatório. Use o mesmo especificador de formato para imprimir o valor com duas casas decimais. Só por diversão, você também pode calcular a porcentagem de tempo de atividade e depois imprimir os dois valores, desta forma:

"Total up time on $env:computername since $startingDate is " + "{0:n2}" -f `
  $uptime.TotalMinutes + " minutes."
$UpTimeMinutes = $Uptime.TotalMinutes
$percentDownTime = "{0:n2}" -f (100 - ($UpTimeMinutes/$minutesInPeriod)*100)
$percentUpTime = 100 - $percentDowntime
"$percentDowntime% downtime and $percentUpTime% uptime."

Portanto, agora, a Equipe de Scripts retorna à questão inicial: Quando atividade é inatividade? Viu por que não dá para falar de tempo de atividade sem levar em conta o tempo de inatividade? Se você achou isso divertido, veja as outras colunas da "Equipe de Scripts" na TechNet ou visite a Central de Scripts.

Problemas de versão

Enquanto testava o script CalculateSystemUptimeFromEventLog.ps1 em seu laptop, o editor-colaborador Michael Murgolo encontrou um erro irritante. Dei o script para meu amigo Jit e ele encontrou o mesmo erro. Qual era o erro? Bem, aqui está ele:

PS C:\> C:\fso\CalculateSystemUpTimeFromEventLog.ps1
Cannot index into a null array.
At C:\fso\CalculateSystemUpTimeFromEventLog.ps1:36 char:43 + $uptime = 
$currentTime - $sortedList.keys[$ <<<< ($sortedList.Keys.Count-1)]
Total up time on LISBON since 09/02/2008 00:00:00 is 0.00 minutes.
100.00% downtime and 0% uptime.

O erro "Não é possível indexar uma matriz nula" é sinal de que a matriz não foi criada da forma correta. Por isso, pesquisei o código que cria a matriz:

ForEach($event in $events)
{
 $sortedList.Add( $event.timeGenerated,
 $event.eventID )
} #end foreach event

No fim, o código estava correto. O que estava causando o erro?

Em seguida, decidi examinar o objeto SortedList. Para isso, escrevi um script simples que cria uma instância da classe the System.Collections.SortedList e acrescenta algumas informações. Nesse ponto, usei a propriedade keys para imprimir a listagem de chaves. Este é o código:

$aryList = 1,2,3,4,5
$sl = New-Object Collections.SortedList
ForEach($i in $aryList)
{
 $sl.add($i,$i)
}

$sl.keys

No meu computador, o código funcionou. No computador de Jit, houve falha. Paciência. Mas, pelo menos, vi que estava no caminho certo. O problema, afinal, era que existe um bug com System.Collections.SortedList no Windows PowerShell 1.0. E eu estava executando a versão mais recente do ainda não lançado Windows PowerShell 2.0 em que esse bug foi corrigido, por isso, o código funcionou.

Então, como fica nosso script? Acontece que a classe SortedList tem um método chamado GetKey, e esse método funciona no Windows PowerShell 1.0 e no Windows PowerShell 2.0. Então, para a versão 1.0 do script, modificamos o código para usar GetKey em vez de fazer a iteração por meio da coleção de chaves. Na versão 2.0 do script, adicionamos uma marca que requer a versão 2.0 do Windows PowerShell. Se você tentar executar esse script em um computador com Windows PowerShell 1.0, ele simplesmente sairá e você não obterá o erro.

Michael também notou algo que não é um bug mas está relacionado a uma consideração de design. Ele observou que o script não detectará adequadamente o tempo de atividade se você colocar o computador para hibernar ou dormir. Isso é verdade, já que não detectamos nem procuramos esses eventos.

Na verdade, porém, não estou preocupado com o tempo de atividade no meu computador laptop ou desktop. Só estou interessado no tempo de atividade em um servidor, mas tenho de atender o servidor que entra no modo de dormir ou hibernar. Isso poderia acontecer, é claro, e pode ser uma forma interessante de economizar eletricidade no data center, mas não é algo que eu já tenha encontrado. Conte-me se colocar seus servidores para hibernar. Você pode entrar em contato comigo em Scripter@Microsoft.com.

O desafio de script do Dr. Scripto

O desafio mensal que testa não apenas sua habilidade de resolver quebra-cabeças, mas também de criar scripts.

Dezembro de 2008: Comandos do PowerShell

A lista a seguir contém 21 comandos do Windows PowerShell. O quadrado contém os mesmos comandos, mas eles estão ocultos. Cabe a você descobrir os comandos, que podem estar ocultos na vertical, na horizontal ou na diagonal (da esquerda para a direita ou da direita para a esquerda).

EXPORT-CSV FORMAT-LIST FORMAT-TABLE
GET-ACL GET-ALIAS GET-CHILDITEM
GET-LOCATION INVOKE-ITEM MEASURE-OBJECT
NEW-ITEMPROPERTY OUT-HOST OUT-NULL
REMOVE-PSSNAPIN SET-ACL SET-TRACESOURCE
SPLIT-PATH START-SLEEP STOP-SERVICE
SUSPEND-SERVICE WRITE-DEBUG WRITE-WARNING

\\msdnmagtst\MTPS\TechNet\issues\en\2008\12\HeyScriptingGuy - 1208\Figures\puzzle.gif

RESPOSTA:

O desafio de script do Dr. Scripto

Resposta: Dezembro de 2008: Comandos do PowerShell

fig12.gif

Ed Wilson é consultor sênior da Microsoft e um conhecido especialista em scripts. Também é um Microsoft Certified Trainer e apresenta um workshop prestigiado do Windows PowerShell para clientes Microsoft Premier ao redor do mundo. Escreveu oito livros, incluindo vários sobre scripts do Windows, e colaborou para quase doze outros livros. Ed possui mais de 20 certificados do setor.

Craig Liebendorfer é um lexicógrafo e editor experiente para a Web da Microsoft. Craig ainda não consegue acreditar que existe um trabalho que lhe paga para lidar com palavras todo dia. Uma das coisas que mais gosta é o humor irreverente, por isso acho que está no lugar certo. Ele considera sua bela filha a sua maior realização na vida.