PowerShellでログファイルからエラーが発生している時間帯を特定する方法

PowerShellでログファイルからエラーが発生している時間帯を特定する方法を現在の環境で使うなら「error時間帯の特定は、error行を抽出する前にlogのtimestamp形式、timezone、複数行eventを確認し、TryParseExactで時刻を検証して一定bucketへ集計します。parse失敗行を捨てず別に数えます。」が基本方針です。対象はlog形式ごとにtimestamp patternとtimezoneを定義し、先頭tokenを時刻と決め打ちしないケースです。原logを変えない抽出から始め、生成CSVの検証項目と破棄条件を先に決めます。

目次

timestamp書式と明示offsetを最初に固定する

ログの障害時間帯を絞るときは、タイムスタンプ形式、タイムゾーン、夏時間、複数行イベントの境界を最初に確定します。代表行をDateTimeOffsetとして解析し、解析不能行を別に数えます。UTCへ正規化した時刻を一定間隔で集計し、元行の前後関係と件数を再確認してピーク時間帯を特定します。

  • Get-Itemでsizeと更新時刻、Get-Content -TotalCountで形式を確認する
  • timestampのformat、culture、timezone、日付跨ぎを特定する
  • ERRORがlevel欄かmessage中の文字かをschemaで区別する
  • rotation fileを含む期間と重複の扱いを決める

原logを変えず候補行だけ抽出する

log分析は読み取りですが、全文にはcredential、query、個人情報が含まれ得ます。作業copyを限定ACLで作り、抽出結果は時刻・event IDなど必要最小限にします。元logを整形やmaskで上書きしません。script変更はsample copyで試し、parse率と件数が合格してから本番read-only分析へ使います。失敗時は生成CSVだけを破棄し、原logを保持します。

malformed行をLineNumber付きで隔離する

  • Splitの先頭を無条件にtimestampとする
  • local timeとUTCを混在させる
  • parse失敗行を黙って捨てる
  • ERROR文字列をseverityと断定する
  • rotation重複と複数行eventを見落とす

TryParseExact後のUTCだけをbucket化する

候補行とline numberを読む

Select-String -LiteralPath 'C:\Logs\app.log' -SimpleMatch 'ERROR' |
  Select-Object Path,LineNumber,Line | Select-Object -First 20

まず少量を確認し、ERROR文字列が本当にseverityを表すかを見ます。機密値を含む行は共有しません。

ISO 8601 timestampを厳密parse

$culture=[Globalization.CultureInfo]::InvariantCulture
$format="yyyy-MM-dd'T'HH:mm:ss.FFFFFFFK"
$rows=Select-String -LiteralPath '.\app.log' -Pattern '^\d{4}-\d{2}-\d{2}T.*\sERROR\s'
$parsed=[Collections.Generic.List[object]]::new()
$rejected=[Collections.Generic.List[object]]::new()
foreach($row in $rows){
  $token=($row.Line -split '\s+',2)[0]
  $dt=[datetimeoffset]::MinValue
  $hasExplicitOffset=$token -match '(?:Z|[+-]\d{2}:\d{2})$'
  if($hasExplicitOffset -and [datetimeoffset]::TryParseExact($token,$format,$culture,[Globalization.DateTimeStyles]::None,[ref]$dt)){
    $parsed.Add([pscustomobject]@{Time=$dt;LineNumber=$row.LineNumber;Token=$token})
  } else {
    $rejected.Add([pscustomobject]@{LineNumber=$row.LineNumber;Token=$token;Reason='FormatOrOffset'})
  }
}
[pscustomobject]@{Candidates=$rows.Count;Accepted=$parsed.Count;Rejected=$rejected.Count}

ISO 8601のZまたは±hh:mm offsetを必須にし、TryParseExactへ通らないtokenはLineNumber付きのRejected一覧へ分けます。

15分bucketへ集計

$bucketed=$parsed | ForEach-Object {
  $utc=$_.Time.UtcDateTime
  $minute=[int]([math]::Floor($utc.Minute/15)*15)
  $bucket=[datetime]::new($utc.Year,$utc.Month,$utc.Day,$utc.Hour,$minute,0,[DateTimeKind]::Utc)
  [pscustomobject]@{BucketUtc=$bucket;LineNumber=$_.LineNumber}
}
$bucketed | Group-Object BucketUtc | ForEach-Object {
  [pscustomobject]@{BucketUtc=$_.Group[0].BucketUtc;Count=$_.Count}
} | Sort-Object BucketUtc

UTCへ正規化し15分単位へまとめます。local業務時間へ戻す表示ではtimezone名とDSTを明記します。

最初と最後のerrorを確認

$ordered=$parsed | Sort-Object Time
$ordered | Select-Object -First 1
$ordered | Select-Object -Last 1

単一clusterと断定せず、bucket間のgapや複数peakを確認します。

CSVへ最小項目を保存

$parsed | Select-Object Time,LineNumber,Token | Export-Csv '.\error-times-accepted.csv' -NoTypeInformation -Encoding utf8
$rejected | Export-Csv '.\error-times-rejected.csv' -NoTypeInformation -Encoding utf8

message本文を出さず時刻と行番号だけにします。原logはread-onlyのまま、出力は新規fileです。

文字列名ではなくDateTimeで時系列sortする

エラー行の最小・最大時刻だけでは連続障害を意味しません。bucket count、連続gap、deploymentやrestart時刻との相関を見ます。複数行stack traceは先頭行だけにtimestampがある場合があり、一行ずつ独立eventとして扱うと誤集計します。rotationで同じeventが重複するか、clock skewがあるかも確認します。

parse成功数と拒否数を候補総数へ照合する

total error候補、parse成功、parse失敗、bucket合計が一致することを確認します。既知の5行を手計算し、UTC変換とbucket境界を照合します。Event logや監視alert、deployment記録と同時刻を比較し、相関を原因と断定せず追加evidenceを集めます。

UTC範囲・bucket合計・Rejected件数を検収する

ERROR行のLineNumberとtimestamp文字列を取得し、ParseExactでformat・culture・DateTimeStylesを固定してからUTCへ揃えます。parse成功/失敗を別件数にし、15分bucketの最初・最後・件数から集中時間帯を判断します。

offset必須のISO 8601を厳密parseする

$pattern='yyyy-MM-ddTHH:mm:ss.fffK'
$c=[Globalization.CultureInfo]::InvariantCulture
$dto=[DateTimeOffset]::ParseExact('2026-07-17T12:34:56.789+09:00',$pattern,$c)
$utc=$dto.UtcDateTime
$bucket=Get-Date -Date $utc -Minute ([math]::Floor($utc.Minute/15)*15) -Second 0 -Millisecond 0
[pscustomobject]@{Utc=$utc;Bucket=$bucket.ToString('o')}

Accepted・Rejected・read errorを分類する

  • timestamp受理:AcceptedとRejectedの合計が候補行数に一致し、AcceptedのUTC順とbucket合計が再計算でも一致する
  • 書式不一致を隔離:ERROR 0件は正常な空結果として検索期間とfile hashを記録する
  • log読取・集計error:timestamp欠落、DST曖昧、encoding error、read errorはRejected行としてLineNumber付きで残す

Get-Dateへの暗黙parseはlocaleで月日を逆に解釈し得ます。文字列sortを時刻順とせず、offsetなしlocal時刻はRejectedへ送り、別途timezone mappingを合意しない限り補完しません。複数logのclock skewとrotation重複を無視しません。

offsetなし・DST境界・malformedを試す

try { [DateTimeOffset]::ParseExact('17/07/2026 12:00','yyyy-MM-ddTHH:mm:ssK',[Globalization.CultureInfo]::InvariantCulture) } catch { [pscustomobject]@{State='Rejected';Value='17/07/2026 12:00'} }

offset有無、DST重複時刻、millisecond欠落、malformed、rotation境界をtestします。候補数、parse成功/失敗、最初/最後UTC、bucket合計、source hashを保存します。

候補行数はAcceptedとRejectedの合計へ一致させます。offsetなし時刻を端末localとして補完せずRejectedへ送り、bucketはUTCのDateTime propertyでsortしてlocale表示の順序へ依存しません。

公式情報・参考資料

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次