🩹 失敗談・トラブル解決 2026.07.26 更新

ログから必要な行だけ抜く正規表現|日時・レベル・メッセージの実例

形式を固定した架空ログを使い、JavaScriptの正規表現で日時やERROR行を抽出し、名前付きグループと複数行ログを安全に扱う方法を解説します。

長いログから障害の手がかりを探すとき、最初から万能な正規表現を作る必要はありません。まず1行の形式を決め、日時、レベル、メッセージの順に分け、期待結果と照合します。この記事では、架空のログを使って「形式を固定する→1行を分解する→レベルで絞る→複数行の境界を決める→反例で検算する」という順番で進めます。

例はすべて架空です。実ログを試す前に、トークン(=合鍵になる文字列)、メールアドレス、利用者名、IPアドレス、端末内のパスを伏せてください。調査の入口はCodexが途中で止まるときのログの切り分け方でも整理しています。

正規表現を小さく組み立てる手順から確認したい場合は、先に正規表現の作り方を参照してください。

まずログ形式と期待結果を固定する

正規表現を書く前に、対象の形を1つに決めます。今回は「ISO 8601風の日時、半角空白、角括弧内のレベル、半角空白、1行のメッセージ」というこの記事限定の形式にします。ログの標準仕様であるOpenTelemetryのログデータモデルでも、ログは「時刻(Timestamp)・重大度(SeverityText)・本文(Body)」という部品に分けて扱います。この記事の分解はその考え方と同じです。

作業は次の番号順で進めます。

  1. 架空ログを3〜5行だけ用意する。
  2. 「全行を分解した結果」と「ERRORだけ残した結果」を先に手書きする(=期待結果)。
  3. わざと引っかかりそうな行(反例)を1行混ぜる。
  4. 正規表現を書き、期待結果と一致するまで直す。

今回の題材はこの4行です。4行目が反例で、INFO行のメッセージの中に文字列「[ERROR]」が入っています。

2026-07-23T09:14:05Z [INFO] server started
2026-07-23T09:14:08Z [ERROR] database connection failed
2026-07-23T09:14:10Z [WARN] retry scheduled
2026-07-23T09:14:12Z [INFO] previous [ERROR] was ignored

期待結果は「分解すると4件、ERRORで絞ると1件(database connection failed)」です。ミリ秒付きやタブ区切りが混ざるログなら、この前提を先に直します。

この記事で使う記号と機能の早見表です。

記号・機能役割この記事で使う場面注意点
^ / $先頭・末尾を示す1行全体の形式を固定するmの有無で「文字列全体」か「各行」かが変わる
g一致を繰り返し取得する全ログ行を列挙するmatchAll()へ正規表現を渡すときは必須
m^$を各行に対応させる複数行の文字列を行単位で調べる.が改行に一致するようになる指定ではない
s.を改行にも一致させる複数行を1ブロックとして扱う境界のない.*は次のログまで取りすぎる
(?<name>...)一致部分へ名前を付ける日時・レベル・メッセージを分けるES2018の機能。古い実行環境では対応を確認
matchAll()全一致を反復取得する一致とグループをまとめて読む戻り値は配列ではなくイテレーター(=順に取り出す窓口)

1行を日時・レベル・メッセージに分ける

1行を3つの値に分けるには、名前付きキャプチャグループを使います。MDNの解説にあるとおり、一致した部分を配列の番号ではなく名前で参照でき、matchAll()の結果のgroupsプロパティから読めます。番号指定より用途が明確です。

次のコードは、ログの定義から期待出力まで一続きで試せます。そのまま貼り付けて動かせます。

const log = [
  '2026-07-23T09:14:05Z [INFO] server started',
  '2026-07-23T09:14:08Z [ERROR] database connection failed',
  '2026-07-23T09:14:10Z [WARN] retry scheduled',
  '2026-07-23T09:14:12Z [INFO] previous [ERROR] was ignored',
].join('\n');

const pattern =
  /^(?<time>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z) \[(?<level>INFO|WARN|ERROR)\] (?<message>.+)$/gm;

const rows = [...log.matchAll(pattern)].map(({ groups }) => ({
  time: groups.time,
  level: groups.level,
  message: groups.message,
}));

console.log(rows.length); // 4
console.log(rows[1]);
// { time: '2026-07-23T09:14:08Z', level: 'ERROR', message: 'database connection failed' }

matchAll()は、TC39の公式仕様で「一致結果を順に返すイテレーターを返し、各結果は先頭が一致全体、その後ろがキャプチャグループ」と定義されています。MDNのmatchAll()の解説には、正規表現オブジェクトを渡す場合にgフラグがないとTypeError(=型の誤りエラー)になると明記されています。gを忘れて動かないときは、まずここを疑ってください。

名前付きグループはES2018で入った機能です。現行のNode.js(LTS版)と主要ブラウザで使えますが、古い実行環境で動かすときは、その環境の対応状況を実行時点の公式案内で確認してください。

日時については「形」と「正しさ」を分けます。上の(?<time>...)は桁の形を見るだけなので、2026-99-99T29:70:80Zのような実在しない日時にも一致します。実在するかの検証は、抽出した後に日時処理へ渡して行います。

また、この記事の「秒まで・末尾はZ固定」はあくまで今回のログ独自の前提です。RFC 3339の定義では、小数秒はピリオドの後ろに1桁以上の数字なら何桁でもよく、タイムゾーンはZのほかに+09:00のような数値オフセットも取れます。RFC 3339全般を受けたいなら、末尾を (?:\.\d+)?(?:Z|[+-]\d{2}:\d{2}) のように広げます。逆に「ミリ秒3桁固定」と書いたら、それは自分のログ専用の決めごとだと意識してください。

ERROR行だけを誤抽出せずに抜く

ERROR行の抽出でいちばん多い事故は、「行のどこかに[ERROR]があれば拾う」書き方です。一見動きますが、反例で崩れます。

const naive = [...log.matchAll(/^.*\[ERROR\].*$/gm)];
console.log(naive.length); // 2 ← INFO行の「previous [ERROR] was ignored」まで拾ってしまう

行内検索は、レベル欄の位置を固定していないのが原因です。おすすめは、前の節のように全行を先に分解してから、レベルの値で絞る方法です。「日時の直後にある角括弧」だけをレベルとして扱うので、メッセージ内の文字列に釣られません。

const errors = rows.filter((row) => row.level === 'ERROR');
console.log(errors.length); // 1
console.log(errors[0].message); // 'database connection failed'

期待結果(ERRORは1件)と一致しました。「検索条件を賢くする」より「構造に分けてから選ぶ」ほうが、意図を確認しやすく、条件の追加(WARNも見たい等)にも強くなります。確認の順番は自動投稿が失敗する原因と直し方でも整理しています。

複数行のスタックトレースとJSONログの扱い

例外のスタックトレース(=エラー発生までの経路の一覧)は、先頭行の後に複数行が続きます。OpenTelemetryのログデータモデルでも、本文(Body)は複数行の文字列になり得ると定義されており、「1行=1イベント」を無条件の前提にはできません。

sフラグで.*を改行まで伸ばす方法は、終わりの条件が決まらず、次のログまで取り込みがちです。代わりに「完全な日時+レベル欄で始まる行」を次エントリーの境界と決め、先にブロックへ分割します。ここでも反例を混ぜて検算します。4行目は、メッセージ本文の途中に日時らしき文字列で始まる行がある例です。

const multi = [
  '2026-07-23T09:14:08Z [ERROR] request failed',
  'TypeError: value is undefined',
  '    at handleRequest (app.js:42:7)',
  '2026-07-23T09:14:09Z deploy marker in message body',
  '2026-07-23T09:14:10Z [INFO] health check passed',
].join('\n');

// 悪い例: 日付らしい行頭だけで切る
const naiveEntries = multi.split(/(?=^\d{4}-\d{2}-\d{2}T)/m);
console.log(naiveEntries.length); // 3 ← 本文中の日時行でも切れてしまう

// 良い例: 日時+レベル欄まで確認して切る
const boundary =
  /(?=^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z \[(?:INFO|WARN|ERROR)\] )/m;
const entries = multi.split(boundary);
console.log(entries.length); // 2 ← ERRORブロックとINFO行に正しく分かれる

const errorEntries = entries.filter((entry) =>
  /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z \[ERROR\] /.test(entry)
);
console.log(errorEntries.length); // 1

もう1つ大事な線引きがあります。入力が1行1JSONのような構造化ログなら、正規表現で全文を解析せず、JSONとして読みます。Google CloudのCloud Loggingの解説では、JSONログのseverityが重大度に、messageが表示用メッセージに対応し、スタックトレースはmessageへ入れる形が案内されています。フィールド名や扱いはサービスごとに変わり得るので、利用時点の公式案内で確認してください。

const jsonLine = '{"severity":"ERROR","message":"database connection failed"}';
const record = JSON.parse(jsonLine);
console.log(record.severity); // 'ERROR'

JSON.parse()を使うのは、入力がJSONとして有効だと確認できる場合だけです。壊れたJSONや複数形式の混在があるログは、行ごとにtryで受けるなど別の前提で扱います。正規表現は「形式を決めたプレーンテキストから、決めた形を抜く道具」と覚えてください。

よくある質問

JavaScriptでログからERROR行だけ抽出するには?

まず全行を/^(?<time>...) \[(?<level>...)\] (?<message>...)$/gmの形で分解し、level === 'ERROR'で絞るのがおすすめです。行内に[ERROR]があるかだけを見る書き方は、メッセージ内の同じ文字列まで拾います。

match()とmatchAll()はどう使い分ける?

一致した文字列の一覧だけでよければmatch()でも扱えます。各行のtime・level・messageというキャプチャ結果を全件読むならmatchAll()が向いています。正規表現オブジェクトを渡す場合はgフラグが必須で、ないとTypeErrorになります(MDN)。

mフラグとsフラグの違いは?

m^$を各行の先頭・末尾へ対応させる指定です。s.を改行にも一致させる指定です。役割が別なので混同しないでください。行単位の抽出には主にmを使い、複数行ブロックはsに頼る前に「次のエントリーの境界」を決めます。

正規表現で日時が正しいかまで判定できる?

桁や区切りの形は確認できますが、存在しない月日まで1本の正規表現に詰め込むのは無理があります。正規表現は形式の抽出まで、日時として有効かは後段の処理で検証します。小数秒の桁数やタイムゾーンの前提はRFC 3339と自分のログで違い得るため、対象ログの仕様で確認してください。

JSON形式のログにも正規表現を使う?

JSONとして有効な入力なら、JSON.parse()で構造を読み、severitymessageをフィールドとして参照します。正規表現の出番は、形式を決めた未構造のテキストログです。

まとめ

手順を再掲します。「架空データで形式を固定→1行を名前付きグループで分解→levelで絞る→複数行の境界を決める→反例で検算」の順です。読者が次にやることは3つです。

  • この記事の4行の架空ログとコードをそのまま実行し、期待出力(4件→ERROR1件)が再現できるか確かめる。
  • 自分のログを1行だけ観察し、「日時・レベル・メッセージ」の並びと区切りを紙に書いてから、正規表現をその形に合わせて直す。
  • 実ログをAIや記事のコードへ渡す前に、トークン・メールアドレス・利用者名・IPアドレス・端末パスを伏せ、入力例・期待結果・除外例の3点だけを小さく渡す。

公式一次情報

確認日: 2026-07-26

続き(結論と実データ)はnoteに置いています

ここでは手順のところまで書きました。実際に出た数字、うまくいかなかった条件、そのまま使える設定ファイルは、 note の記事にまとめてあります。

noteで続きを読む Xをフォローする