1. ホーム
  2. スクリプト・コラム
  3. パール

Perlで警告や例外を捕捉してログに書き込む

2022-01-03 16:44:07

すべてのPerlスクリプトとモジュールで警告をオンにすることが推奨されていますが、Perlからの警告をユーザーに見せたくない場合があります。

一方では、コードの前に 警告を使用する というセーフティネットがある一方で、画面には警告が表示されるのが普通です。多くの場合、お客様はこの警告をどのように扱えばいいのかわからない。運が良ければ、これらの警告はクライアントを驚かせるだけだが、もちろん、不幸にもクライアントはそれを修正しようとする...。(ここではPerlプログラマーの話ではない。)

第三に、これらの警告は後の分析のために保存しておくとよいでしょう。

また、Perlスクリプトやアプリケーションでは、use warningsを使用せず、#!で-wを使用しているところが多くあります。 use warningsを追加すると、多くの警告を発生させることができます。

長期的には、もちろんこれらの警告をなくすことが重要ですが、短期的にはどうでしょうか。

長期的な計画を立てたとしても、完全にバグのないコードを書くことはできませんし、アプリが今後絶対に警告メッセージを表示しないことを保証することもできません。

できる?

画面に出力される前に警告をキャプチャすることができます。

シグナル
Perl には %SIG という組み込みのハッシュ テーブルがあり、キーはオペレーティング システムのシグナルの名前になっています。対応する値は、特定のシグナルがトリガーされたときに呼び出される関数(ほとんどは関数リファレンス)です。

システムが提供する標準的なシグナルに加えて、Perlは2つの内部シグナルを追加しています"signal"。ひとつは <h__warn__< span=""> で、コードが warn() 関数を呼び出すたびにトリガーされます。もうひとつは __DIE__ で、die() を呼び出すたびに発生します。

今回は、これらが警告メッセージにどのような影響を与えるかを見ていきましょう。

匿名機能

sub { } は無名関数です。つまり、関数本体を持ち、名前がない関数です。(この例では関数本体も空ですが、意味はおわかりいただけると思います)。

キャッチ警告 - 未対応

以下のコードを追加した場合。

コピーコード コードは以下の通りです。

  local $SIG{__WARN__} = sub {
     # This is where you get the warning message
  };

これは、プログラムのどこかで警告メッセージが発生するたびに、それに対して何もしないことを効果的に意味します。基本的に、これはすべての警告を隠します。

警告をキャッチする - そして例外に変換する

書くこともできます。また、次のように書くこともできます。

コピーコード コードは以下の通りです。

  local $SIG{__WARN__} = sub {
    die;
  };

これにより、警告が発生するたびに die() が呼び出され、各警告は例外に変換されることになります。

例外に警告メッセージを含めたい場合は、このように書きます。

コピーコード コードは以下の通りです。

  local $SIG{__WARN__} = sub {
    my $message = shift;
    die $message;
  };

実際の警告メッセージは、無名関数への唯一のパラメータとして渡されます。

警告を捕捉し、ログに書き込む
その間に何か別のことをした方がいいかもしれません。

ノイズの多い警告メッセージをフィルタリングし、後で解析する:。

コピーコード コードは以下の通りです。

  local $SIG{__WARN__} = sub {
    my $message = shift;
    logger($message);
  };

ここでは、logger()を実装する書き込みロギング関数とします。

ログの書き込み

あなたのアプリケーションには、すでにログの仕組みがあることを前提としています。もしそうでなければ、それを追加するのがよいでしょう。追加できない場合でも、オペレーティング・システムの組み込みのロギング・メカニズムが必要です。例えば、Linuxのsyslog、MS WindowsのEvent Logger、その他のオペレーティング・システムには、独自の内部ロギング・メカニズムがあります。

この記事の例では、この考えを表すために自作のlogger()関数を使用しています。

ログの取り込みと書き込みの完全な例

コピーコード コードは以下の通りです。

  #! /usr/bin/perl
  use strict;
  use warnings;
  local $SIG{__WARN__} = sub {
    my $message = shift;
    logger('warning', $message);
  };
  my $counter;
  count();
  print "$counter\n";
  sub count {
    $counter = $counter + 42;
  }
  sub logger {
    my ($level, $msg) = @_;
    if (open my $out, '>>', 'log.txt') {
        chomp $msg;
        print $out "$level - $msg\n";
    }
  }

上記のコードにより、log.txt ファイルに以下の行が追加されます。

コピーコード コードは以下の通りです。

  Use of uninitialized value in addition (+) at code_with_warnings.pl line 14.

変数$counterと関数count()は、警告生成の例の一部でしかありません。

警告ハンドラ関数の警告メッセージ

WARN__ は、そのハンドラ関数の実行中に自動的に無効化されます。そのため、警告ハンドラ関数の実行中に生成された(新しい)警告メッセージは、無限ループを引き起こしません。

詳しくは、perlvarのドキュメントをご覧ください。
複数の警告を避ける
注意点としては、警告メッセージが重複しているとログファイルがいっぱいになってしまうことです。単純なキャッシュのような機能を使って、重複する警告メッセージの数を減らすことができるんだ。

コピーコード コードは以下の通りです。

#! /usr/bin/perl
  use strict;
  use warnings;
  my %WARNS;
  local $SIG{__WARN__} = sub {
      my $message = shift;
      return if $WARNS{$message}++;
      logger('warning', $message);
  };
  my $counter;
  count();
  print "$counter\n";
  $counter = undef;
  count();
  sub count {
    $counter = $counter + 42;
  }
  sub logger {
    my ($level, $msg) = @_;
    if (open my $out, '>>', 'log.txt') {
        chomp $msg;
        print $out "$level - $msg\n";
    }
  }

ご覧のように、変数$counterをundefに代入してから、再度count()関数を呼び出すと、同じ警告が発生します。

また、__WARN__ハンドラ関数を少し複雑なバージョンに置き換えると

コピーコード コードは以下の通りです。

  my %WARNS;
  local $SIG{__WARN__} = sub {
      my $message = shift;
      return if $WARNS{$message}++;
      logger('warning', $message);
  };

logger を呼び出す前に、現在の文字列が %WARNShash テーブルにすでにあるかどうかを確認します。ない場合、追加され、logger() が呼び出されます。すでに存在する場合は、return を呼び出し、同じイベントを 2 回ログに記録しないようにします。

配列の中の一意な値についても、同じような考え方をしたことを思い出してください。

ローカルとは何ですか?
上記のすべての例で、効果をローカライズ(警告処理)するためにlocal関数を使用しました。厳密には、これらの例では、メインスクリプトの最初の部分であると想定しているので、このようなことをする必要はありません。この場合は、結局のところグローバルスコープの中にあるので、問題ありません。

しかし、そのように使用したほうがよいでしょう。

local は、モジュールの変更を (警告に) 制限するために重要です。特に、公開される予定のモジュールについては。ローカライズしないと、アプリケーション全体に影響を及ぼします。limit は、それが含まれる閉じたコードブロックに影響を制限します。

グローバルな %WARNS の使用は避ける

Perl 5.10 以降をお使いの場合、スクリプトを use v5.10; で開始し、匿名関数内で state キーワードを使用して変数を宣言することで、グローバル変数 %WARNS を置き換えるようにコードを書き換えることができます。

コピーコード コードは以下の通りです。

  #! /usr/bin/perl
  use strict;
  use warnings;
  use v5.10;
  local $SIG{__WARN__} = sub {
      state %WARNS;
      my $message = shift;
      return if $WARNS{$message}++;
      logger('warning', $message);
  };