awkを使用してログファイルから特定のエントリを取得する

awkを使用してログファイルから特定のエントリを取得する

私は現在、大きなログファイルから特定の基準に一致するエントリを取得するためにawkを使用しようとしています。デフォルトでは、コマンドに含まれる情報(通常はコマンドの他の場所にある可能性があります)に基づいて、トランザクションIDでタグ付けされたコマンド全体を抽出できる必要があります。下のサンプルログ(高濃縮)。送信されたコマンドは1行でも、複数行(00001や00002など)にまたがる可能性があり、コマンドを必ずしも一緒にグループ化する必要はなく、コマンド間に異なるIDを挿入できます。

(NAME, 486, 00001) <xml><command:name>target</command:name></xml>
(NAME, 486, 00001)   <response>
(NAME, 486, 00001)     <result code="200">
(NAME, 486, 00001)       <msg>Command failed</msg>
(NAME, 486, 00001)     </result>
(NAME, 486, 00001)  </response>
(FOO, 486, 00002) <xml>
(FOO, 486, 00002) <differentCommand:name>This is another sent command</differentCommand:name></xml>
(FOO, 486, 00002) </xml>
(FOO, 486, 00002)   <response>
(FOO, 486, 00002)     <result code="400">
(FOO, 486, 00002)       <msg>Command completed successfully</msg>
(FOO, 486, 00002)     </result>
(FOO, 486, 00002)  </response>
(ANOTHERNAME, 486, 00003) <xml><command:name>target</command:name></xml>
(ANOTHERNAME, 486, 00003)   <response>
(ANOTHERNAME, 486, 00003)     <result code="400">
(ANOTHERNAME, 486, 00003)       <msg>Command completed successfully</msg>
(ANOTHERNAME, 486, 00003)     </result>
(ANOTHERNAME, 486, 00003)   </response>
(FOO, 486, 00004) <xml>
(FOO, 486, 00004) <command:name>This is another sent command</command:name></xml>
(FOO, 486, 00004) </xml>
(FOO, 486, 00004)   <response>
(FOO, 486, 00004)     <result code="400">
(FOO, 486, 00004)       <msg>Command completed successfully</msg>
(FOO, 486, 00004)     </result>
(FOO, 486, 00004)  </response>

デフォルトでは、応答(括弧内の5桁の数字はトランザクションID)を含む完全なコマンドである名前を返したいのですが、成功した場合(結果コード= "400")だけを返したいと思います。

これが私が今まで持っているものです:

BEGIN { FS="[(,)]"; }
$4 ~ "<command:name" { id[$3] = $3 }

{ for (i in id) {
        if ($3 == i) {
                if ($5 ~ "Command completed success")
                        success[i] = i;
                }
        }
}

$4 in success { print $0 }

しかし、明らかにこれは戻らないでしょう。戻る検索が成功すると、アイテムの残りの部分を取得できます。次のみが返されます。

(ANOTHERNAME, 486, 00003)       <msg>Command completed successfully</msg>
(ANOTHERNAME, 486, 00003)     </result>
(ANOTHERNAME, 486, 00003)   </response>
(FOO, 486, 00004)       <msg>Command completed successfully</msg>
(FOO, 486, 00004)     </result>
(FOO, 486, 00004)  </response>

BEGINステートメント内にループを入れようとしましたが、時間がかかり、そのサイズの配列を使用しようとするとメモリの問題が発生します(このファイルは1 GBを超える)。

私が返したいのは次のとおりです。

(ANOTHERNAME, 486, 00003) <xml><command:name>target</command:name></xml>
(ANOTHERNAME, 486, 00003)   <response>
(ANOTHERNAME, 486, 00003)     <result code="400">
(ANOTHERNAME, 486, 00003)       <msg>Command completed successfully</msg>
(ANOTHERNAME, 486, 00003)     </result>
(ANOTHERNAME, 486, 00003)   </response>
(FOO, 486, 00004) <xml>
(FOO, 486, 00004) <command:name>This is another sent command</command:name></xml>
(FOO, 486, 00004) </xml>
(FOO, 486, 00004)   <response>
(FOO, 486, 00004)     <result code="400">
(FOO, 486, 00004)       <msg>Command completed successfully</msg>
(FOO, 486, 00004)     </result>
(FOO, 486, 00004)  </response>

私が試してみることがawkで可能かどうか疑問に思います。私はしばらくこの作業にどのツールを使用するかを調べようとしてきました。スピードが私の主な関心事です。今日のファイルだけがプレーンテキストとして使用できますが(それで十分速いです)、残りはgzipで圧縮されています(だからそうしていますzcat filename | awk -f test.awk) - ファイルを何度も読み取ることを避けようとしています、そして大きすぎてメモリに保存できません。

答え1

</response>これをレコード終了マークとして使用できます。たとえば、

$ awk -F'[ )]' '{record[$3] = record[$3] "\n" $0};

                /<\/response>/ {
                  if (record[$3] ~ /completed successfully/) {
                    # optional: remove leading newline if you don't want
                    # a blank line before each output record:
                    # sub(/\n/,"",record[$3])

                    print record[$3]
                  };
                  delete record[$3]
                }' input.log 

(FOO, 486, 00002) <xml>
(FOO, 486, 00002) <differentCommand:name>This is another sent command</differentCommand:name></xml>
(FOO, 486, 00002) </xml>
(FOO, 486, 00002)   <response>
(FOO, 486, 00002)     <result code="400">
(FOO, 486, 00002)       <msg>Command completed successfully</msg>
(FOO, 486, 00002)     </result>
(FOO, 486, 00002)  </response>

(ANOTHERNAME, 486, 00003) <xml><command:name>target</command:name></xml>
(ANOTHERNAME, 486, 00003)   <response>
(ANOTHERNAME, 486, 00003)     <result code="400">
(ANOTHERNAME, 486, 00003)       <msg>Command completed successfully</msg>
(ANOTHERNAME, 486, 00003)     </result>
(ANOTHERNAME, 486, 00003)   </response>

(FOO, 486, 00004) <xml>
(FOO, 486, 00004) <command:name>This is another sent command</command:name></xml>
(FOO, 486, 00004) </xml>
(FOO, 486, 00004)   <response>
(FOO, 486, 00004)     <result code="400">
(FOO, 486, 00004)       <msg>Command completed successfully</msg>
(FOO, 486, 00004)     </result>
(FOO, 486, 00004)  </response>

これは、以下のsed + perlおよびsed + awkのバージョンと似ていますが、という配列の適切な要素(つまり、id番号)に各行(前に改行文字が付いている)を追加して各レコード自体を構成しますrecord。行が表示されたら、</response>「成功した」と一致する場合は、要素を印刷してから要素を削除します。

これはsed + awkまたはsed + perlのバージョンより少し遅いです(なぜなら各入力行を配列要素に追加します。sed- 空の行を頻繁に挿入するよりも多くのCPUリソースを使用し、より多くのメモリを使用しますが(</response>行があるまで各レコードがメモリに保持されるため)、あまり多くはありません。各レコードをメモリーに保持します。必要なだけ削除してから削除してください。

ただし、このバージョンは、特定のIDのレコードが他のIDのレコードとインターリーブされている場合にも機能します。

これはPerlと同等です:

perl -F'[\h)]' -e '
  $record{$F[2]} .= $_;

  if (/<\/response>/) {
    if ($record{$F[2]} =~ /completed successfully/) {
      # print blank line between records
      print "\n" if $not_first_record++;

      print $record{$F[2]}
    }
    delete $record{$F[2]};
  }' input.log

私のテスト(100,000個のサンプルデータのコピーを含む120MBの入力ファイルを使用)では、awkバージョンはほぼ2倍速いことがわかりました。私のテストシステム(古代AMD Phenom II 1090T)では、awkバージョンは約4.6秒で実行され、Perlバージョンは約7.4秒かかりました。

修正する

最適化されたPerlバージョンは次のとおりです。

水平スペースまたは閉じ括弧()をフィールド区切り文字として[\h)]使用する正規表現を使用する代わりに、Perlのデフォルトのスペース区切り文字を使用します。 3番目のフィールドから各レコードのキーを抽出し、最後の文字())を切り捨てます。

このバージョンは約3.9秒で実行され、これはほぼ2倍速い速度です。これは、自動分割モードで正規表現を使用した-F場合の膨大なパフォーマンス低下を示しています。

ところで、レコードに連想配列の代わりにインデックス配列を使用してみましたが(つまり、文字列キーの代わりに数値@recordインデックスを使用%record)、パフォーマンスに顕著な違いはありませんでした。また、index()正規表現のマッチングの代わりにindex($record{$key},"completed successfully")その関数を試してみましたが、顕著な$record{$F[2]} =~ /completed successfully/パフォーマンスの違いはありませんでした。

perl -ane '
  chop($key = $F[2]);
  $record{$key} .= $_;

  if (/<\/response>/) {
    if ($record{$key} =~ /completed successfully/) {
      print "\n" if $not_first_record++;
      print $key, $record{$key};
    }
    delete $record{$key}
  }' input.log

同じ最適化は劇的ではありませんが、awkのパフォーマンスも向上します。

chop()awkには機能はありませんが、substr()同じことを行うために使用できます。

awk '{
       key = substr($3, 1, length($3)-1);
       record[key] = record[key] "\n" $0
     };

     /<\/response>/ {
       if (record[key] ~ /completed successfully/) {
         sub(/^\n/,"",record[key])
         print record[key]
       };
       delete record[key]
     }' input.log

このバージョンは約3.5秒で実行されます(以前のawkバージョンの4.6秒より約30%高速です)。

全体的に更新されたawkとperlのバージョンはパフォーマンスがはるかに似ていますが、awkはまだ約12%高速です。

コードを少し変更すると、パフォーマンスが大きく変わる可能性があります。


または:

ログエントリは常にこのようなIDできれいに区切られていますか、それとも別のIDとインターリーブされていますか?

きちんと区切られている場合、最も簡単な方法の1つは、sed空白行を挿入して「段落」に分割することです(たとえば、1つ以上の空白行で区切ります)。今後<xml>ライン。

sedその後、出力は「短絡モード」でログにパイプまたはawk読み込まれます。perlawkの場合はRS=""BEGINブロックで設定するか、オプションを使用し-v、Perlの場合は-00コマンドラインオプションを使用します。その後、awkまたはperlスクリプトは、レコードに「成功的に完了しました」が含まれていることを確認します。その場合は、記録を印刷します。

上記のawkのみのバージョンよりはるかに速く実行され、少ないメモリを使用しますが、ロギング時にのみ正しく機能します。いいえ他のレコードとインターリーブされました。

$ sed '/) <xml>/i\\n' input.log |
    perl -00 -ne 'print if /completed successfully/m'
(FOO, 486, 00002) <xml>
(FOO, 486, 00002) <differentCommand:name>This is another sent command</differentCommand:name></xml>
(FOO, 486, 00002) </xml>
(FOO, 486, 00002)   <response>
(FOO, 486, 00002)     <result code="400">
(FOO, 486, 00002)       <msg>Command completed successfully</msg>
(FOO, 486, 00002)     </result>
(FOO, 486, 00002)  </response>

(ANOTHERNAME, 486, 00003) <xml><command:name>target</command:name></xml>
(ANOTHERNAME, 486, 00003)   <response>
(ANOTHERNAME, 486, 00003)     <result code="400">
(ANOTHERNAME, 486, 00003)       <msg>Command completed successfully</msg>
(ANOTHERNAME, 486, 00003)     </result>
(ANOTHERNAME, 486, 00003)   </response>

(FOO, 486, 00004) <xml>
(FOO, 486, 00004) <command:name>This is another sent command</command:name></xml>
(FOO, 486, 00004) </xml>
(FOO, 486, 00004)   <response>
(FOO, 486, 00004)     <result code="400">
(FOO, 486, 00004)       <msg>Command completed successfully</msg>
(FOO, 486, 00004)     </result>
(FOO, 486, 00004)  </response>

またはawkを使用してください:

sed '/) <xml>/i\\n' input.log | awk -v RS='' '/completed successfully/'

このバージョンの出力はほぼ同じですが、各出力レコード間に空白行はありません。

個人的には、各出力レコードの間に空行がある場合はすでに「短絡モード」になっているので、必要に応じて出力をより簡単に処理できるので便利です。もちろんこれは主観的な好みにすぎません。


関連情報