Inspired by ysth's Debugging with tied variables node, and motivated by a desire to dig deeper into understanding the mechanics of creating an object-oriented package, I set out to create Tie::Scalar::Logged. I am posting this as a Meditation, because I don't consider it a work-complete, but rather a stage in the process upon which I would like to reflect and possibly gather additional input.

The following code snippet allows you to tie a scalar to a logfile. You may choose which types of activities will be logged (default is ALL: TIESCALAR, STORE, FETCH, and DESTROY). A log file will be created (if it doesn't already exist), and any activity related to the tied scalar will be scribbled into the logfile for later review.

The scalar should behave just like any old normal scalar, but with the side effect that any use of the tied scalar will result in another line being written into the logfile.

From this starting framework, it shouldn't be too difficult to also create Tie::Hash::Logged, and Tie::Array::Logged.

Here is the code, along with a sample usage:


package Tie::Scalar::Logged; use IO::File; require Tie::Scalar; @ISA = qw/Tie::StdScalar/; use strict; use warnings; # TIESCALAR: Mostly self-explanatory, but just to be # thorough, $logfile is the filename of the output file. # $name is a single-quoted string naming the variable that # has been tied. This is for reporting purposes only. # $events is an optional array ref that should contain # the names of the methods you would like to track. Method # names will be case insensitive. They may include: # ALL, TIESCALAR, STORE, FETCH, DESTROY. sub TIESCALAR { my ( $class, $logfile, $name, $events ) = @_; my $self = {}; $self->{Name} = $name; $self->{LOG} = new IO::File ">> $logfile" or return undef; $self->{Value} = undef; if ( not defined ( $events ) or grep {/ALL/i} @$events ) { @$events = qw/TIESCALAR STORE FETCH DESTROY/; } $self->{uc($_)}=1 foreach @$events; print {$self->{LOG}} "$self->{Name} => TIESCALAR\tLogging to $logfile\n" if $self->{TIESCALAR}; bless $self, $class; } sub STORE { my ( $self, $value ) = @_; $self->{Value} = $value; print {$self->{LOG}} "$self->{Name} => STORE\t\tValue = $value\n" if $self->{STORE}; return $self->{Value}; } sub FETCH { my $self = shift; print {$self->{LOG}} "$self->{Name} => FETCH\t\tValue = $self->{Value}\n" if $self->{FETCH}; return $self->{Value}; } sub DESTROY { my $self = shift; print {$self->{LOG}} "$self->{Name} => DESTROY\t\tValue = $self->{Value}\n" if $self->{DESTROY}; $self->{LOG}->close; } 1; # ---------- Begin main ---------- package main; use strict; use warnings; # Simple test: Create a lexical variable and perform a few # operations on it. The logfile will then contain a log of # all activity affecting the variable. my $var; # When tieing a variable to Tie::Scalar::Logged, the parameter list # is as follows: Variable to tie, Package name, Logfile name, # Single-quoted string naming variable that is being tied, and # finally, an optional reference to an array containing one or # more of the following activities that can be logged: # ALL, TIESCALAR, FETCH, STORE, DESTROY. (Case/Order insensitive.) tie $var, "Tie::Scalar::Logged", 'log.txt', '$var', [qw/all/]; $var = 10; $var += 10; print $var, "\n";


I am interested in reading comments that might lead to improvement. One thing in particular is I would be interested in finding a way (if it is possible) of also logging the line-number where the activity being logged took place. Maybe if there's an interest in this module I can figure out what it takes to get it ready to be put on CPAN.

I should take a moment to thank those of you who, in the Chatterbox, helped me fumble my way through my first attempt at an OO module. I really appreciate the learning opportunities I gain from this site, and the teaching-moments I gain from so many of this site's regulars.

Update: Changed

$self->{LOG} = new IO::File ">> $logfile" or die "Can't open $logfile for append: $!\n";

to or return undef; for a more graceful reaction if the logfile is unable to be opened. Thanks duff and ysth for the suggestion.

Changed module name from Tie::Scalar::Log to Tie::Scalar::Logged per duff's recommendation.


Dave

Replies are listed 'Best First'.
Re: Scalars tied to logfiles
by Anonymous Monk on Dec 11, 2003 at 05:54 UTC

    To include location information just use Carp; in your module and change each logging statement to use Carp::shortmess:

    print {$self->{LOG}} Carp::shortmess "$self->{Name} => STORE\t\tValue = $value" if $self->{STORE};
      I like that suggestion, and in my tests it worked like a charm. But it does make for a pretty messy looking logfile. Perhaps I'll have to preprocess the output of Carp::shortmess before I write it to the logfile, to massage it into something a little cleaner looking.


      Dave

Re: Scalars tied to logfiles
by duff (Parson) on Dec 11, 2003 at 05:18 UTC

    Rather than dieing if unable to open the logfile for writing, I'd just return undef. Of course, I wouldn't use IO::File either, so maybe it's just me.

    To figure out where the changes happened to the scalar, have a look at caller

    Oh, and you'll probably want to add an UNTIE routine so that users can turn logging on and off without having to wait for object destruction.

      You don't need to define an UNTIE method for untie($var) to work. If you don't, it just un-sets the flag on the tied scalar, and reduces the reference count on the tie object.

      $h=$ENV{HOME};my@q=split/\n\n/,`cat $h/.quotes`;$s="$h/." ."signature";$t=`cat $s`;print$t,"\n",$q[rand($#q)],"\n";

        Aye, but he'll probably want to close the filehandle on untie.

Re: Scalars tied to logfiles
by Anonymous Monk on Dec 11, 2003 at 16:29 UTC

    Here's a different approach: adding wrappers to the standard tied routines instead of redefining them:

    # usage: tie $var, 'Tie::Scalar::Log', # init value (may be undef), # logfile (string), # varname (string), # events (array or undef); ## Tie::Scalar::Log.pm package Tie::Scalar::Log; use Hook::LexWrap; use IO::File; use Carp; require Tie::Scalar; our @ISA = qw/Tie::StdScalar/; my %LOG_IT; wrap Tie::StdScalar::TIESCALAR, post => sub { my $self = $_[-1]; my ($logfile,$name,$events) = @_; $logfile ||= ''; my $log = IO::File->new($logfile,'>>'); if ($log) { $LOG_IT{$self} = {name => $name, log => $log}; $events ||= []; $events = [qw/TIESCALAR FETCH STORE DESTROY/] if grep /all/i, @$events or !@$events; $LOG_IT{$self}{uc($_)} = 1 for @$events; $self->logit("TIESCALAR => $logfile") if $LOG_IT{$self}{TIESCALA +R}; } else { carp "logfile <$logfile> couldn't be created: variable not tied" +; $_[-1] = undef; } }, pre => sub { carp q|Wrong number of arguments to tie()| and $_[-1] = undef unless @_ == 6 }; wrap Tie::StdScalar::FETCH, post => sub {$_[0]->logit("FETCH => $_[-1]") if $LOG_IT{$_[0]}{FETCH +}}; wrap Tie::StdScalar::STORE, post => sub {$_[0]->logit("STORE => $_[-1]") if $LOG_IT{$_[0]}{STORE +}}; wrap Tie::StdScalar::DESTROY, post => sub {$_[0]->logit("DESTROY") if $LOG_IT{$_[0]}{DESTROY}}; sub logit { my ($self, $msg) = @_; my $name = $LOG_IT{$self}{name}; my ($pkg,$file,$line) = caller(1); print {$LOG_IT{$self}{log}} "$name => $msg ", "\t<at $file line $line>\n" } 1;
Re: Scalars tied to logfiles
by duff (Parson) on Dec 11, 2003 at 15:15 UTC

    You should probably change your sample usage to include the possiblity that the file couldn't be opened.

    tie $var, "Tie::Scalar::Logged", 'log.txt', '$var', [qw/all/] or die "Can't open log.txt - $!";
Re: Scalars tied to logfiles
by diotalevi (Canon) on Dec 12, 2003 at 18:41 UTC
    Change your initializer to accept a file handle or a filename. Not all of us want to use files that exist on disks.