/usr/local/CPAN/Stem/Stem/Log.pm


#  File: Stem/Log.pm

#  This file is part of Stem.
#  Copyright (C) 1999, 2000, 2001 Stem Systems, Inc.

#  Stem is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.

#  Stem is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.

#  You should have received a copy of the GNU General Public License
#  along with Stem; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

#  For a license to use the Stem under conditions other than those
#  described here, to purchase support for this software, or to purchase a
#  commercial warranty contract, please contact Stem Systems at:

#       Stem Systems, Inc.		781-643-7504
#  	79 Everett St.			info@stemsystems.com
#  	Arlington, MA 02474
#  	USA

use strict ;

use Stem::Log::Entry ;
use Stem::Log::File ;

my %logs ;

package Stem::Log ;

use Stem::Trace 'log' => 'stem_status', 'sub' => 'TraceStatus' ;
use Stem::Trace 'log' => 'stem_error' , 'sub' => 'TraceError' ;


use Data::Dumper ;

use Stem::Vars ;

Stem::Route::register_class( __PACKAGE__, 'log' ) ;

my $attr_spec = [

	{
		'name'		=> 'name',
		'required'	=> 1,
		'help'		=> <<HELP,
Name of this logical log.
HELP
	},
	{
		'name'		=> 'file',
		'class'		=> 'Stem::Log::File',
		'help'		=> <<HELP,
The Stem::Log::File object that will create and manage a physical log file.
HELP
	},
	{
		'name'		=> 'format',
		'default'	=> '%T',
		'help'		=> <<HELP,
Format to print entries for this logical log. See elsewhere in this
document for the details of the sprintf-like format'
HELP
	},
	{
		'name'		=> 'strftime',
		'default'	=> '%C',
		'help'		=> <<HELP,
Format passed to strftime to print the %f entry format.
HELP
	},
	{
		'name'		=> 'use_gmt',
		'default'	=> 1,
		'type'		=> 'boolean',
		'help'		=> <<HELP,
Make strftime use gmtime instead of localtime to break the log entry
timestamp into its parts.
HELP
	},
	{
		'name'		=> 'filters',
		'help'		=> <<HELP,
List of key/value pairs. The keys are either rules, actions or 'flag'.
The value is passed to the function for the key. Use a list for complex values.
HELP
	},

] ;


sub new {

	my( $class ) = shift ;

	my $self = Stem::Class::parse_args( $attr_spec, @_ ) ;
	return $self unless ref $self ;

	$logs{ $self->{'name'} } = $self ;

	return ;
}

# table to convert filter keys to code refs to execute
# these are all passed the $entry hash ref, the filter arg and the log object

my %filter_to_code = (

	'match_text'	=> sub { $_[0]->{'text'}  =~ /$_[1]/ },
	'match_label'	=> sub { $_[0]->{'label'} =~ /$_[1]/ },

	'eq_level'	=> sub { $_[0]->{'level'} == $_[1] },
	'lt_level'	=> sub { $_[0]->{'level'} <  $_[1] },
	'le_level'	=> sub { $_[0]->{'level'} <= $_[1] },
	'gt_level'	=> sub { $_[0]->{'level'} >  $_[1] },
	'ge_level'	=> sub { $_[0]->{'level'} >= $_[1] },

	'env_eq_level'	=> sub { $_[0]->{'level'} == ( $Env{ $_[1] } || 0 ) },
	'env_lt_level'	=> sub { $_[0]->{'level'} >  ( $Env{ $_[1] } || 0 ) },
	'env_le_level'	=> sub { $_[0]->{'level'} >= ( $Env{ $_[1] } || 0 ) },
	'env_gt_level'	=> sub { $_[0]->{'level'} <  ( $Env{ $_[1] } || 0 ) },
	'env_ge_level'	=> sub { $_[0]->{'level'} <= ( $Env{ $_[1] } || 0 ) },

	'file'		=> \&_action_file,
	'stdout'	=> \&_action_stdout,
	'stderr'	=> \&_action_stderr,
	'dev_tty'	=> \&_action_dev_tty,
	'console'	=> \&_action_console,
#	'msg'		=> \&_action_msg,
	'write'		=> \&_action_write,
	'wall'		=> \&_action_wall,
	'email'		=> \&_action_email,
	'page'		=> \&_action_page,
	'forward'	=> \&_action_forward,

	'custom'	=> \&_custom_filter,
) ;

my %flag_to_code = (

	'set'		=> sub { $_[0]->{'flag'} = 1 },
	'clear'		=> sub { $_[0]->{'flag'} = 0 },
	'invert'	=> sub { $_[0]->{'flag'} = ! $_[0]->{'flag'} },
	'inverted_test'	=> sub { $_[0]->{'invert_test'} = 1 },
	'normal_test'	=> sub { $_[0]->{'invert_test'} = 0 },
	'or'		=> sub { $_[0]->{'or'} = 1 },
	'and'		=> sub { $_[0]->{'or'} = 0 },
) ;

sub submit {

	my( $self, $entry ) = @_ ;

	$entry->{'format'} = $self->{'format'} ;
	$entry->{'strftime'} = $self->{'strftime'} ;
	$entry->{'use_gmt'} = $self->{'use_gmt'} ;

	my $filter_list = $self->{'filters'} ;

	unless ( $filter_list ) {

# no filter so the default is to log to the file

		_action_file( $entry, 0, $self ) ;

		return ;
	}

# start with all actions enabled

	$entry->{'flag'} = 1 ;

# scan the filter list by pairs

	for( my $i = 0 ; $i < @{$filter_list} ; $i += 2 ) {

		my ( $filter_key, $filter_arg ) =
				@{$filter_list}[$i, $i + 1] ;

# handle the flag operations first.

		if ( $filter_key eq 'flag' ) {

			if ( my $code = $flag_to_code{ $filter_arg } ) {

				$code->( $entry ) ;
			}

			next ;
		}

# skip this filter rule/action if the flag is false

		next unless $entry->{'flag'} && ! $entry->{'invert_test'} ;

# check for and remove a 'not_' prefix

		my $not = $filter_key =~ s/^not_(\w+)$/$1/ ;

#print "FILT $filter_key $filter_arg\n" ;

		my $code = $filter_to_code{ $filter_key } ;

		next unless $code ;

# execute the rule/action code

		my $flag_val = $code->( $entry, $filter_arg, $self ) ;

# don't mung the flag unless we get a boolean return

		next unless defined( $flag_val ) ;

# invert the returned flag value if needed

		$flag_val = ! $flag_val if $not ;

# do the right boolean op

		if ( $entry->{'or'} ) {

			$entry->{'flag'} ||= $flag_val ;
		}
		else {

			$entry->{'flag'} &&= $flag_val ;
		}
	}
}


sub _format_entry {

	my( $entry ) = @_ ;

	my $formatted = $entry->{'format'} ;

	$formatted =~ s/%(.)/_format_field( $entry, $1 )/seg ;

	return $formatted ;
}

my %letter_to_key = (

	'T'	=> 'text',
	't'	=> 'time',
	'L'	=> 'label',
	'l'	=> 'level',
	'H'	=> 'hub_name',
	'h'	=> 'host_name',
	'P'	=> 'program_name',
) ;

sub _format_field {

	my( $entry, $letter ) = @_ ;

	if ( my $key = $letter_to_key{ $letter } ) {

		return $entry->{$key} ;
	}

	if ( $letter eq 'f' ) {

		require POSIX ;

		$entry->{'formatted_time'} ||= do {

			my @times = ( $entry->{'use_gmt'} ) ?
					gmtime( $entry->{'time'} ) :
					localtime( $entry->{'time'} ) ;

			POSIX::strftime( $entry->{'strftime'}, @times ) ;
		} ;

		return $entry->{'formatted_time'} ;
	}

	return $letter ;
}

sub _action_file {

	my( $entry, $arg, $log_obj ) = @_ ;

	my $file = $log_obj->{'file'} ;

	$file or return ;

	$entry->{'formatted'} ||= _format_entry( $entry ) ;

	$file->write( $entry->{'formatted'} ) ;

	return ;
}

sub _action_stdout {

	my( $entry ) = shift ;

	$entry->{'formatted'} ||= _format_entry( $entry ) ;

	print STDOUT $entry->{'formatted'} ;

	return ;
}

sub _action_stderr {

	my( $entry ) = shift ;

	$entry->{'formatted'} ||= _format_entry( $entry ) ;

	print STDERR $entry->{'formatted'} ;

	return ;
}

sub _action_write {

	my( $entry, $arg ) = @_ ;

	$entry->{'formatted'} ||= _format_entry( $entry ) ;

	my @users = ref $arg ? @{$arg} : $arg ;

	foreach my $user ( @users ) {

		system <<SYS ;
/bin/echo '$entry->{'formatted'}' | write $user >/dev/null 2>&1 &
SYS
	}

	return ;
}

sub _action_wall {

	my( $entry ) = shift ;

	$entry->{'formatted'} ||= _format_entry( $entry ) ;


	system <<SYS ;
/bin/echo '$entry->{'formatted'}' | wall &
SYS

	return ;
}

# handle to write log entries to /dev/tty

my $tty_fh ;

sub _action_dev_tty {

	my( $entry ) = shift ;

	$tty_fh ||= IO::File->new( ">/dev/tty" ) ;

	unless( $tty_fh ) {

		warn "can't open log file /dev/tty $!" ;
		return ;
	}

	$entry->{'formatted'} ||= _format_entry( $entry ) ;

	print $tty_fh $entry->{'formatted'} ;

	return ;
}

sub _action_console {

	my( $entry ) = shift ;

	$entry->{'formatted'} ||= _format_entry( $entry ) ;

	return unless Stem::Console->can( 'write' ) ;

	Stem::Console->write( $entry->{'formatted'} ) ;

	return ;
}

sub _action_forward {

	my( $entry, $arg ) = @_ ;

	my @logs = ref $arg ? @{$arg} : $arg ;

	my $entry_obj = $entry->{'entry_obj'} ;

	$entry_obj->submit( @logs ) ;

	return ;
}

sub _action_email {

	my( $entry, $arg ) = @_ ;

	$entry->{'formatted'} ||= _format_entry( $entry ) ;

	my ( $email_addr, $subject ) = ( ref $arg ) ?
				@{$arg} : ( $arg, 'Stem::Log' ) ;

#print "EMAIL  $email_addr: $subject\n" ;

	require Mail::Send ;

	my $mail = Mail::Send->new(
			'To'	=> $email_addr,
			'Subject' => $subject
	) ;

	my $fh = $mail->open();

	$fh->print( $entry->{'formatted'} ) ;

	$fh->close;

	return ;
}

sub _custom_filter {

	my( $entry, $arg ) = @_ ;

#####
# do this
#####

	return ;
}

sub find_log {

	my ( $log_name ) = @_ ;

	return( $logs{ $log_name } ) ;
}

sub status_cmd {

	my $status_text .= sprintf( "%-20s%-40s%10s\n",
						"Logical Log",
	                                        "Physical File",
						"Size" ) ;
	$status_text .= sprintf "-" x 70 . "\n";

	foreach my $log_name ( sort keys %logs ) {

		my $ref = $logs{$log_name} ;

		$status_text .= sprintf "%-20s%-40s%10s\n",
                                                     $log_name,
		                                     $ref->{'file'}{'path'},
						     $ref->{'file'}{'size'} ;
	}

	$status_text .= "\n\n" ;

	return $status_text ;
}

1 ;