1# ----------------------------------------------------------------------------- 2# $Id: Raw.pm 11365 2008-05-10 14:58:28Z topia $ 3# ----------------------------------------------------------------------------- 4package Log::Raw; 5use strict; 6use warnings; 7use IO::File; 8use File::Spec; 9use Tiarra::Encoding; 10use base qw(Module); 11use Module::Use qw(Tools::DateConvert Log::Writer); 12use Tools::DateConvert; 13use Log::Writer; 14use ControlPort; 15use Mask; 16 17sub new { 18 my $class = shift; 19 my $this = $class->SUPER::new(@_); 20 $this->{matching_cache} = {}; # <servername,fname> 21 $this->{writer_cache} = {}; # <server,Log::Writer> 22 $this->{sync_command} = do { 23 my $sync = $this->config->sync; 24 if (defined $sync) { 25 uc $sync; 26 } 27 else { 28 undef; 29 } 30 }; 31 $this; 32} 33 34sub sync { 35 my $this = shift; 36 $this->flush_all_file_handles; 37 RunLoop->shared->notify_msg("Raw logs synchronized."); 38} 39 40sub control_requested { 41 my ($this,$request) = @_; 42 if ($request->ID eq 'synchronize') { 43 $this->sync; 44 ControlPort::Reply->new(204,'No Content'); 45 } 46 else { 47 die ref($this)." received control request of unsupported ID ".$request->ID."\n"; 48 } 49} 50 51sub message_arrived { 52 my ($this,$message,$sender) = @_; 53 54 # syncは有効で、クライアントから受け取ったメッセージであり、かつ今回のコマンドがsyncに一致しているか? 55 if (defined $this->{sync_command} && 56 $sender->isa('IrcIO::Client') && 57 $message->command eq $this->{sync_command}) { 58 # 開いているファイルを全てflush。 59 # 他のモジュールも同じコマンドでsyncするかも知れないので、 60 # do-not-send-to-servers => 1は設定するが 61 # メッセージ自体は破棄してしまわない。 62 $this->sync; 63 $message->remark('do-not-send-to-servers',1); 64 return $message; 65 } 66 $message; 67} 68 69sub message_io_hook { 70 my ($this,$message,$io,$type) = @_; 71 72 # break with last 73 while (1) { 74 last unless $io->server_p; 75 last unless Mask::match_deep([Mask::array_or_all( 76 $this->config->command('all'))], $message->command); 77 my $msg = $message->clone; 78 if ($this->config->resolve_numeric && $message->command =~ /^\d{3}$/) { 79 $msg->command( 80 (NumericReply::fetch_name($message->command)||'undef'). 81 '('.$message->command.')'); 82 } 83 my $server = $io->network_name; 84 my $dirname = $this->_server_match($server); 85 if (defined $dirname) { 86 my $prefix = sprintf '(%s/%s) ', $server, do { 87 if ($type eq 'in') { 88 'recv'; 89 } elsif ($type eq 'out') { 90 'send'; 91 } else { 92 '----'; 93 } 94 }; 95 96 my $charset = do { 97 if ($msg->have_raw_params) { 98 $msg->encoding_params('binary'); 99 'binary'; 100 } elsif ($io->can('out_encoding')) { 101 $io->out_encoding; 102 } else { 103 $this->config->charset; 104 } 105 }; 106 $this->_write($server, $dirname, $msg->time, $prefix . 107 $msg->serialize($charset)); 108 } 109 last; 110 } 111 112 return $message; 113} 114 115sub _server_match { 116 my ($this,$server) = @_; 117 118 my $cached = $this->{matching_cache}->{$server}; 119 if (defined $cached) { 120 if ($cached eq '') { 121 # cache of not found 122 return undef; 123 } 124 else { 125 return $cached; 126 } 127 } 128 129 foreach my $line ($this->config->server('all')) { 130 my ($name, $mask) = split /\s+/, $line, 2; 131 if (Mask::match($mask,$server)) { 132 # マッチした。 133 my $fname_format = $this->config->filename || '%Y.%m.%d.txt'; 134 my $fpath_format = $name."/$fname_format"; 135 136 $this->{matching_cache}->{$server} = $fpath_format; 137 return $fpath_format; 138 } 139 } 140 $this->{matching_cache}->{$server} = ''; 141 undef; 142} 143 144sub _write { 145 # 指定されたログファイルにヘッダ付きで追記する。 146 # ディレクトリ名の日付のマクロは置換される。 147 my ($this,$channel,$abstract_fpath,$time,$line) = @_; 148 my $concrete_fpath = do { 149 my $basedir = $this->config->directory; 150 if (defined $basedir) { 151 Tools::DateConvert::replace("$basedir/$abstract_fpath", $time); 152 } 153 else { 154 Tools::DateConvert::replace($abstract_fpath, $time); 155 } 156 }; 157 my $header = Tools::DateConvert::replace( 158 $this->config->header || '%H:%M', 159 $time, 160 ); 161 my $always_flush = do { 162 if ($this->config->keep_file_open) { 163 if ($this->config->always_flush) { 164 1; 165 } else { 166 0; 167 } 168 } else { 169 1; 170 } 171 }; 172 # ファイルに追記 173 my $make_writer = sub { 174 Log::Writer->shared_writer->find_object( 175 $concrete_fpath, 176 always_flush => $always_flush, 177 file_mode_oct => $this->config->mode, 178 dir_mode_oct => $this->config->dir_mode, 179 ); 180 }; 181 my $writer = sub { 182 # キャッシュは有効か? 183 if ($this->config->keep_file_open) { 184 # このチャンネルはキャッシュされているか? 185 my $cached_elem = $this->{writer_cache}->{$channel}; 186 if (defined $cached_elem) { 187 # キャッシュされたファイルパスは今回のファイルと一致するか? 188 if ($cached_elem->uri eq $concrete_fpath) { 189 # このファイルハンドルを再利用して良い。 190 #print "$concrete_fpath: RECYCLED\n"; 191 return $cached_elem; 192 } 193 else { 194 # ファイル名が違う。日付が変わった等の場合。 195 # 古いファイルハンドルを閉じる。 196 #print "$concrete_fpath: recached\n"; 197 eval { 198 $cached_elem->flush; 199 $cached_elem->unregister; 200 }; 201 # 新たなファイルハンドルを生成。 202 $cached_elem = $make_writer->(); 203 if (defined $cached_elem) { 204 $cached_elem->register; 205 } 206 return $cached_elem; 207 } 208 } 209 else { 210 # キャッシュされていないので、ファイルハンドルを作ってキャッシュ。 211 #print "$concrete_fpath: *cached*\n"; 212 my $cached_elem = 213 $this->{writer_cache}->{$channel} = 214 $make_writer->(); 215 if (defined $cached_elem) { 216 $cached_elem->register; 217 } 218 return $cached_elem; 219 } 220 } 221 else { 222 # キャッシュ無効。 223 return $make_writer->(); 224 } 225 }->(); 226 if (defined $writer) { 227 $writer->reserve("$header $line\n"); 228 } else { 229 # XXX: do warn with properly frequency 230 #RunLoop->shared_loop->notify_warn("can't write to $concrete_fpath: ". 231 # "$header $line"); 232 } 233} 234 235sub flush_all_file_handles { 236 my $this = shift; 237 foreach my $cached_elem (values %{$this->{writer_cache}}) { 238 eval { 239 $cached_elem->flush; 240 }; 241 } 242} 243 244sub destruct { 245 my $this = shift; 246 # 開いている全てのLog::Writerを閉じて、キャッシュを空にする。 247 foreach my $cached_elem (values %{$this->{writer_cache}}) { 248 eval { 249 $cached_elem->flush; 250 $cached_elem->unregister; 251 }; 252 } 253 %{$this->{writer_cache}} = (); 254} 255 2561; 257 258=pod 259info: サーバとの生の通信を保存する 260default: off 261 262# Log系のモジュールでは、以下のように日付や時刻の置換が行なわれる。 263# %% : % 264# %Y : 年(4桁) 265# %m : 月(2桁) 266# %d : 日(2桁) 267# %H : 時間(2桁) 268# %M : 分(2桁) 269# %S : 秒(2桁) 270 271# ログを保存するディレクトリ。Tiarraが起動した位置からの相対パス。~指定は使えない。 272directory: rawlog 273 274# 各行のヘッダのフォーマット。省略されたら'%H:%M'。 275header: %H:%M:%S 276 277# ファイル名のフォーマット。省略されたら'%Y.%m.%d.txt' 278filename: %Y-%m-%d.txt 279 280# ログファイルのモード(8進数)。省略されたら600 281mode: 600 282 283# ログディレクトリのモード(8進数)。省略されたら700 284dir-mode: 700 285 286# 使っている文字コードがよくわからなかったときの文字コード。省略されたらutf8。 287# たぶんこの指定が生きることはないと思いますが……。 288charset: jis 289 290# NumericReply の名前を解決して表示する(ちゃんとした dump では無くなります) 291resolve-numeric: 1 292 293# ログを取るコマンドを表すマスク。省略されたら記録出来るだけのコマンドを記録する。 294command: *,-ping,-pong 295 296# 各ログファイルを開きっぱなしにするかどうか。 297# このオプションは多くの場合、ディスクアクセスを抑えて効率良くログを保存しますが 298# ログを記録すべき全てのファイルを開いたままにするので、50や100のチャンネルを 299# 別々のファイルにログを取るような場合には使うべきではありません。 300# 万一 fd があふれた場合、クライアントから(またはサーバへ)接続できない・ 301# 新たなモジュールをロードできない・ログが全然できないなどの症状が起こる可能性が 302# あります。limit の詳細については OS 等のドキュメントを参照してください。 303-keep-file-open: 1 304 305# keep-file-open 時に各行ごとに flush するかどうか。 306# open/close の負荷は気になるが、ログは失いたくない人向け。 307# keep-file-open が有効でないなら無視され(1になり)ます。 308-always-flush: 0 309 310# keep-file-openを有効にした場合、発言の度にログファイルに追記するのではなく 311# 一定の分量が溜まってから書き込まれる。そのため、ファイルを開いても 312# 最近の発言はまだ書き込まれていない可能性がある。 313# syncを設定すると、即座にログをディスクに書き込むためのコマンドが追加される。 314# 省略された場合はコマンドを追加しない。 315sync: sync 316 317# 各サーバの設定。サーバ名の部分はマスクである。 318# 記述された順序で検索されるので、全てのサーバにマッチする"*"などは最後に書かなければならない。 319# 指定されたディレクトリが存在しなかったら、勝手に作られる。 320# フォーマットは次の通り。 321# channel: <ディレクトリ名> <サーバ名マスク> 322# 例: 323# filename: %Y-%m-%d.txt 324# server: ircnet ircnet 325# server: others * 326# この例では、ircnetのログはircnet/%Y.%m.%d.txtに、 327# それ以外のログはothers/%Y.%m.%d.txtに保存される。 328server: ircnet ircnet 329server: others * 330=cut 331