PowerShell 指定した期間のタスクログとイベントログを取得する

# はじめに

自分のPCの調子が悪く決まった時間帯に動作が停止しているようです。
そこで怪しい時間帯に何が起こっているかPowerShellを使ってタスクスケジューラのログとイベントログを取得することにしました。

# 環境

OS

```:OS
システムの種類	64 ビット オペレーティング システム、x64 ベース プロセッサ
エディション	Windows 10 Pro
バージョン	20H2
インストール日	‎2020/‎12/‎27
OS ビルド	19042.870
エクスペリエンス	Windows Feature Experience Pack 120.2212.551.0
```

$PSVersionTable

```powershell:$PSVersionTable
Name                           Value
----                           -----
PSVersion                      7.1.3
PSEdition                      Core
GitCommitId                    7.1.3
OS                             Microsoft Windows 10.0.19042
Platform                       Win32NT
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0…}
PSRemotingProtocolVersion      2.3
SerializationVersion           1.1.0.1
WSManStackVersion              3.0
```

# One Liner

取り合えず結果から、1行で書いた場合のコマンドを示します。(改行入れていますが)
管理者権限で実行してください。
そうしないと、タスクスケジューラの方は問題ないようですがイベントログの方は取得できないログが出てきます。

## Task

タスクスケジューラのログを取得するコマンドです。

```powershell:Task
Get-ScheduledTask |
  Get-ScheduledTaskInfo |
    Where-Object { $_.LastRunTime -ge (Get-Date "2021-04-03T04:30:00") -and $_.LastRunTime -lt (Get-Date "2021-04-03T05:00:00") } |
      Sort-Object -Property LastRuntime, LastTaskResult |
        Format-Table TaskPath, TaskName, LastTaskResult, LastRunTime
```

変数で渡すなりして日付けを修正すれば任意の期間のログを取得できます。
日付けの形式は、ISO 8601の形式で指定していますが"2021/04/04 12:00:00"などでも認識してくれます。
またわざわざ`Get-Date`を呼び出して変換していますが文字列をそのまま渡しても認識してくれます。

## EventLog

イベントログを取得するコマンドです。

```powershell:EventLog
Get-WinEvent -ListLog * |
  Where-Object RecordCount -gt 0 |
    ForEach-Object { Get-WinEvent -FilterHashtable @{LogName = $_.LogName; StartTime = (Get-Date "2021-04-03T04:30:00"); EndTime = (Get-Date "2021-04-03T09:30:00") } -ErrorAction SilentlyContinue } |
      Sort-Object -Property TimeCreated |
        Format-Table  ProviderName, LogName, TimeCreated, Id, LevelDisplayName, Message
```

日付けに関してはタスクスケジューラと同様になります。
Windows10ですので`Get-WinEvent`を使用していますが、Vista以前の古いOSならば`Get-EventLog`を使用した方が良いかもしれません。
詳細は[docs.microsoft.com](https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.management/get-eventlog?view=powershell-5.1)を見ていただければと思います。
そこには以下のように記述されています。

```
PowerShell cmdlets that contain the EventLog noun work only on Windows classic event logs such as Application, System, or Security. To get logs that use the Windows Event Log technology in Windows Vista and later Windows versions, use Get-WinEvent.
Get-EventLog uses a Win32 API that is deprecated. The results may not be accurate. Use the Get-WinEvent cmdlet instead.
```

また`Get-WinEvent`に`-ErrorAction SilentlyContinue`を渡しているのは該当するログが見つからなかった場合のエラーメッセージを表示されないためです。
そのため、`Get-WinEvent`で何らかのエラーが発生しても出力されないので注意してください。

# Script File

後々の使い勝手を考えてスクリプトファイルも用意しておきます。
こちらも管理者権限で実行してください。
蛇足かもしれませんが追加の処理を入れています。
メッセージから改行コードを削除しています。
元のままのメッセージが必要ならば該当処理をコメントアウトなりしてください。

```powershell:GetLogByTime
param ([parameter(mandatory)][String] $start, [String] $end, [Int] $width, [String] $csv)
function GetLogByTime {
  param ([parameter(mandatory)][String] $start, [String] $end, [Int] $width, [String] $csv)
  $start = Get-Date $start -Format "yyyy-MM-ddThh:mm:ss"
  if ($end -eq "") {
    $end = Get-Date -Format "yyyy-MM-ddThh:mm:ss"
  }
  else {
    $end = Get-Date $end -Format "yyyy-MM-ddThh:mm:ss"
  }
  # Format-Table | Out-String -Width にWindowSize.Widthを渡しても思惑通りに出力されない
  # if ($width -eq 0) {
  #   $width = (Get-Host).UI.RawUI.WindowSize.Width
  # }

  Write-Output "${start} to ${end}, ${width}, ${csv}"
  $task = Get-ScheduledTask | Get-ScheduledTaskInfo |
    Where-Object { $_.LastRunTime -ge (Get-Date $start) -and $_.LastRunTime -lt (Get-Date $end) } |
      Sort-Object -Property LastRuntime, LastTaskResult
  $eventLog = Get-WinEvent -ListLog * |
    Where-Object RecordCount -gt 0 |
      ForEach-Object { Get-WinEvent -FilterHashtable @{LogName = $_.LogName; StartTime = (Get-Date $start); EndTime = (Get-Date $end) } -ErrorAction SilentlyContinue } |
        Sort-Object -Property TimeCreated

  # Messageから改行を削除する
  for ($i = 0; $i -lt $eventLog.Count; $i++) {
    $eventLog[$i].Message = $eventLog[$i].Message -replace "`n", " "
    $eventLog[$i].Message = $eventLog[$i].Message -replace "`r", ""
  }
    
  $formattedTask = Format-Table TaskPath, TaskName, LastTaskResult, LastRunTime -AutoSize -InputObject $task
  $formattedEventLog = Format-Table ProviderName, LogName, TimeCreated, Id, LevelDisplayName, Message -AutoSize -InputObject $eventLog
  if ($width -gt 2) {
    $formattedTask = Out-String -Width $width -InputObject $formattedTask
    $formattedEventLog = Out-String -Width $width -InputObject $formattedEventLog
  }
  Write-Output $formattedTask
  Write-Output $formattedEventLog
  
  if ($csv -ne "") {
    $task | Select-Object TaskPath, TaskName, LastTaskResult, LastRunTime |
      Export-Csv -Path "${csv}1.csv"
    $eventLog | Select-Object  ProviderName, LogName, TimeCreated, Id, LevelDisplayName, Message |
      Export-Csv -Path "${csv}2.csv"
  }
}

GetLogByTime $start $end $width $csv
```

これを例えば、`GetLogByTime.ps1`と保存して、
`./GetLogByTime.ps1 -start "2021-04-04T12:00:00" -end "2021-04-05T12:00:00"`
`./GetLogByTime.ps1 "2021-04-04T12:00:00" "2021-04-05T12:00:00"`
のように使用してください。

メッセージを省略させたくなければ、
`./GetLogByTime.ps1 "2021-04-04T12:00:00" "2021-04-05T12:00:00" -width 1000`
のように`-width`に大きな数値を指定してください。

また、出力をCSVで出力させたければ、
`./GetLogByTime.ps1 "2021-04-04T12:00:00" "2021-04-05T12:00:00" -csv "logFileName"`
のように指定してください。

# 最後に

これで調べたいログが簡単に取得できるようになりましたが、問題を解決できるかはまた別の話だったりします。
普段からログを見ているわけではないので何が怪しいのか分かりませんし、ハードウェアの問題の可能性も大きいので辛いです。
まあ、この記事がどなたかの一助になれば幸いです。

---

# 備考

## Select-Object

`Select-Object`にオブジェクトのコレクションを`-InputObject`で渡した場合は1つのオブジェクトとして扱われるので注意すること。
基本的に`Select-Object`には、パイプでオブジェクトを渡す。

https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/select-object?view=powershell-7.1

```
-InputObject
Specifies objects to send to the cmdlet through the pipeline.
This parameter enables you to pipe objects to Select-Object.

When you pass objects to the InputObject parameter, instead of using the pipeline, Select-Object treats the InputObject as a single object, even if the value is a collection.
It is recommended that you use the pipeline when passing collections to Select-Object.
```