line |
stmt |
bran |
cond |
sub |
pod |
time |
code |
1
|
|
|
|
|
|
|
#!/usr/bin/perl |
2
|
|
|
|
|
|
|
|
3
|
|
|
|
|
|
|
=head1 NAME |
4
|
|
|
|
|
|
|
|
5
|
|
|
|
|
|
|
Log::Message::JSON - structured messages that stringify to JSON |
6
|
|
|
|
|
|
|
|
7
|
|
|
|
|
|
|
=head1 SYNOPSIS |
8
|
|
|
|
|
|
|
|
9
|
|
|
|
|
|
|
package My::Application::Module; |
10
|
|
|
|
|
|
|
|
11
|
|
|
|
|
|
|
use Log::Log4perl; |
12
|
|
|
|
|
|
|
use Log::Message::JSON qw{logmsg}; |
13
|
|
|
|
|
|
|
|
14
|
|
|
|
|
|
|
sub do_something { |
15
|
|
|
|
|
|
|
my ($self, $foo, $bar, @rest) = @_; |
16
|
|
|
|
|
|
|
|
17
|
|
|
|
|
|
|
my $logger = Log::Log4perl->get_logger(); |
18
|
|
|
|
|
|
|
|
19
|
|
|
|
|
|
|
$logger->info(logmsg message => "do_something entered", |
20
|
|
|
|
|
|
|
foo => $foo, bar => $bar, rest => \@rest); |
21
|
|
|
|
|
|
|
|
22
|
|
|
|
|
|
|
# ... |
23
|
|
|
|
|
|
|
} |
24
|
|
|
|
|
|
|
|
25
|
|
|
|
|
|
|
# in flat-file logs entry would look like: |
26
|
|
|
|
|
|
|
# Dec 28 00:24:52 example.net My-Application[1587]: {"message":"do_something entered","foo":"value of foo","bar":"value of bar","rest":["value","of","rest"]} |
27
|
|
|
|
|
|
|
|
28
|
|
|
|
|
|
|
=head1 DESCRIPTION |
29
|
|
|
|
|
|
|
|
30
|
|
|
|
|
|
|
Good logging requires today a lot more than in Good Ol' Times[tm]. Each log |
31
|
|
|
|
|
|
|
entry should have a structure and be machine-parseable. On the other hand, |
32
|
|
|
|
|
|
|
there are lot of logging libraries that don't quite support structured logs |
33
|
|
|
|
|
|
|
and only process flat strings. |
34
|
|
|
|
|
|
|
|
35
|
|
|
|
|
|
|
L architecture allows both, flat strings and structured |
36
|
|
|
|
|
|
|
entries. It's up to appender module whether it accepts one or another form. |
37
|
|
|
|
|
|
|
Unfortunately, this makes application developer to decide in advance, which |
38
|
|
|
|
|
|
|
appenders could be in use and defeats much of Log::Log4perl's flexibility. |
39
|
|
|
|
|
|
|
|
40
|
|
|
|
|
|
|
Log::Message::JSON is an attempt to solve this problem. Developer can create |
41
|
|
|
|
|
|
|
a message that has an internal structure (i.e. is a hash(ref)), and at the |
42
|
|
|
|
|
|
|
same time it can be used as a simple string, instantly serializing to |
43
|
|
|
|
|
|
|
single-line JSON. This way the developer don't need to decide on appenders in |
44
|
|
|
|
|
|
|
advance. Moreover, flat string logfiles are easier to parse, especially if |
45
|
|
|
|
|
|
|
entries have this form. |
46
|
|
|
|
|
|
|
|
47
|
|
|
|
|
|
|
Of course, you don't need Log::Log4perl to use this module. It could be used |
48
|
|
|
|
|
|
|
wherever a hashref needs to be sensibly stringified while preserving its all |
49
|
|
|
|
|
|
|
hash-like features. |
50
|
|
|
|
|
|
|
|
51
|
|
|
|
|
|
|
=cut |
52
|
|
|
|
|
|
|
|
53
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
54
|
|
|
|
|
|
|
|
55
|
|
|
|
|
|
|
package Log::Message::JSON; |
56
|
|
|
|
|
|
|
|
57
|
7
|
|
|
7
|
|
308932
|
use warnings; |
|
7
|
|
|
|
|
17
|
|
|
7
|
|
|
|
|
268
|
|
58
|
7
|
|
|
7
|
|
357
|
use strict; |
|
7
|
|
|
|
|
16
|
|
|
7
|
|
|
|
|
384
|
|
59
|
|
|
|
|
|
|
|
60
|
7
|
|
|
7
|
|
42
|
use base qw{Exporter}; |
|
7
|
|
|
|
|
17
|
|
|
7
|
|
|
|
|
1302
|
|
61
|
|
|
|
|
|
|
our @EXPORT_OK = qw{&logmsg &logmess &msg &json}; |
62
|
|
|
|
|
|
|
|
63
|
7
|
|
|
7
|
|
18799
|
use overload ('""' => \&to_json); |
|
7
|
|
|
|
|
9889
|
|
|
7
|
|
|
|
|
67
|
|
64
|
|
|
|
|
|
|
|
65
|
7
|
|
|
7
|
|
7021
|
use Log::Message::JSON::Hash; |
|
7
|
|
|
|
|
27
|
|
|
7
|
|
|
|
|
306
|
|
66
|
7
|
|
|
7
|
|
52
|
use Carp; |
|
7
|
|
|
|
|
16
|
|
|
7
|
|
|
|
|
8240
|
|
67
|
|
|
|
|
|
|
|
68
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
69
|
|
|
|
|
|
|
|
70
|
|
|
|
|
|
|
our $VERSION = '0.30.01'; |
71
|
|
|
|
|
|
|
|
72
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
73
|
|
|
|
|
|
|
|
74
|
|
|
|
|
|
|
=head1 API |
75
|
|
|
|
|
|
|
|
76
|
|
|
|
|
|
|
The preferred way is the short way. Object-oriented API is described here |
77
|
|
|
|
|
|
|
mainly for reference. |
78
|
|
|
|
|
|
|
|
79
|
|
|
|
|
|
|
=head2 Short Way |
80
|
|
|
|
|
|
|
|
81
|
|
|
|
|
|
|
=over |
82
|
|
|
|
|
|
|
|
83
|
|
|
|
|
|
|
=cut |
84
|
|
|
|
|
|
|
|
85
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
86
|
|
|
|
|
|
|
|
87
|
|
|
|
|
|
|
=item C |
88
|
|
|
|
|
|
|
|
89
|
|
|
|
|
|
|
=item C |
90
|
|
|
|
|
|
|
|
91
|
|
|
|
|
|
|
=item C |
92
|
|
|
|
|
|
|
|
93
|
|
|
|
|
|
|
=item C |
94
|
|
|
|
|
|
|
|
95
|
|
|
|
|
|
|
These are plain functions. They all are exported (but none by default, you |
96
|
|
|
|
|
|
|
need to specifically ask for them), they all do the same and they all accept |
97
|
|
|
|
|
|
|
the same arguments. They are provided for your convenience. Choose the one |
98
|
|
|
|
|
|
|
that don't clash with your methods (but please, make your life easier in |
99
|
|
|
|
|
|
|
future and choose one for whole application). |
100
|
|
|
|
|
|
|
|
101
|
|
|
|
|
|
|
These functions accept either a reference to a hash or a list of |
102
|
|
|
|
|
|
|
C<< key => value >> pairs. The latter form preserves keys order, so I believe |
103
|
|
|
|
|
|
|
it's more useful. Also, in the latter form you may skip the first key name; |
104
|
|
|
|
|
|
|
the value will be stored under C key in such case. |
105
|
|
|
|
|
|
|
|
106
|
|
|
|
|
|
|
Returned value is an object created with C method (see |
107
|
|
|
|
|
|
|
L"Object-Oriented API">), so it's a reference to a hash (blessed, but still |
108
|
|
|
|
|
|
|
hashref, with all its consequences). |
109
|
|
|
|
|
|
|
|
110
|
|
|
|
|
|
|
Usage example: |
111
|
|
|
|
|
|
|
|
112
|
|
|
|
|
|
|
use Log::Message::JSON qw{logmsg}; |
113
|
|
|
|
|
|
|
use Log::Log4perl; |
114
|
|
|
|
|
|
|
|
115
|
|
|
|
|
|
|
my $msg1 = logmsg { key1 => 1, key2 => 2 }; |
116
|
|
|
|
|
|
|
my $msg2 = logmsg foo => 1, bar => 2, text => "some text"; |
117
|
|
|
|
|
|
|
my $msg3 = logmsg "my log message", host => hostname(); |
118
|
|
|
|
|
|
|
|
119
|
|
|
|
|
|
|
my $logger = Log::Log4perl->get_logger(); |
120
|
|
|
|
|
|
|
$logger->info($msg1); |
121
|
|
|
|
|
|
|
$logger->debug($msg2); |
122
|
|
|
|
|
|
|
$logger->warn($msg3); |
123
|
|
|
|
|
|
|
|
124
|
|
|
|
|
|
|
print $msg1; |
125
|
|
|
|
|
|
|
printf "%s => %s\n", $_, $msg2->{$_} for keys %$msg2; |
126
|
|
|
|
|
|
|
|
127
|
|
|
|
|
|
|
=cut |
128
|
|
|
|
|
|
|
|
129
|
|
|
|
|
|
|
sub json { |
130
|
1
|
|
|
1
|
1
|
1017
|
return __PACKAGE__->new(@_); |
131
|
|
|
|
|
|
|
} |
132
|
|
|
|
|
|
|
|
133
|
|
|
|
|
|
|
sub logmsg { |
134
|
1
|
|
|
1
|
1
|
643
|
return __PACKAGE__->new(@_); |
135
|
|
|
|
|
|
|
} |
136
|
|
|
|
|
|
|
|
137
|
|
|
|
|
|
|
sub logmess { |
138
|
1
|
|
|
1
|
1
|
660
|
return __PACKAGE__->new(@_); |
139
|
|
|
|
|
|
|
} |
140
|
|
|
|
|
|
|
|
141
|
|
|
|
|
|
|
sub msg { |
142
|
3
|
|
|
3
|
1
|
1480
|
return __PACKAGE__->new(@_); |
143
|
|
|
|
|
|
|
} |
144
|
|
|
|
|
|
|
|
145
|
|
|
|
|
|
|
=back |
146
|
|
|
|
|
|
|
|
147
|
|
|
|
|
|
|
=cut |
148
|
|
|
|
|
|
|
|
149
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
150
|
|
|
|
|
|
|
|
151
|
|
|
|
|
|
|
=head2 Object-Oriented API |
152
|
|
|
|
|
|
|
|
153
|
|
|
|
|
|
|
=over |
154
|
|
|
|
|
|
|
|
155
|
|
|
|
|
|
|
=cut |
156
|
|
|
|
|
|
|
|
157
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
158
|
|
|
|
|
|
|
|
159
|
|
|
|
|
|
|
=item C<< new(key => value, ...) >> |
160
|
|
|
|
|
|
|
|
161
|
|
|
|
|
|
|
=item C<< new({ key => value, ... }) >> |
162
|
|
|
|
|
|
|
|
163
|
|
|
|
|
|
|
Constructor. |
164
|
|
|
|
|
|
|
|
165
|
|
|
|
|
|
|
This method creates a new hash reference. The underlying hash is tied to |
166
|
|
|
|
|
|
|
L (actually, to a proxy class that uses L as |
167
|
|
|
|
|
|
|
a backend) and filled with arguments. Because of overloaded stringification |
168
|
|
|
|
|
|
|
operator, reference is blessed with Log::Message::JSON package. |
169
|
|
|
|
|
|
|
|
170
|
|
|
|
|
|
|
If the first call form (list of pairs) was used, the order of key/value pairs |
171
|
|
|
|
|
|
|
is preserved. If the number of elements is odd, the first element is believed |
172
|
|
|
|
|
|
|
to be value of C key. |
173
|
|
|
|
|
|
|
|
174
|
|
|
|
|
|
|
If the second call form (hashref) was used, key/value pairs are sorted using |
175
|
|
|
|
|
|
|
C operator, unless the referred hash was tied to L. |
176
|
|
|
|
|
|
|
|
177
|
|
|
|
|
|
|
=cut |
178
|
|
|
|
|
|
|
|
179
|
|
|
|
|
|
|
sub new { |
180
|
6
|
|
|
6
|
1
|
23
|
my ($class, @args) = @_; |
181
|
|
|
|
|
|
|
|
182
|
6
|
|
|
|
|
46
|
tie my %self, 'Log::Message::JSON::Hash'; |
183
|
|
|
|
|
|
|
|
184
|
6
|
50
|
33
|
|
|
40
|
if (@args == 1 && (ref $args[0] eq 'HASH' || eval {$args[0]->isa('HASH')})) { |
|
|
|
66
|
|
|
|
|
185
|
0
|
0
|
|
|
|
0
|
if (eval { tied(%{ $args[0] })->isa("Tie::IxHash") }) { |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
186
|
|
|
|
|
|
|
# no sort, hash probably tied to Tie::IxHash or something |
187
|
0
|
|
|
|
|
0
|
%self = %{ $args[0] }; |
|
0
|
|
|
|
|
0
|
|
188
|
|
|
|
|
|
|
} else { |
189
|
|
|
|
|
|
|
# sort keys from the hash |
190
|
0
|
|
|
|
|
0
|
%self = map { $_ => $args[0]{$_} } sort keys %{ $args[0] }; |
|
0
|
|
|
|
|
0
|
|
|
0
|
|
|
|
|
0
|
|
191
|
|
|
|
|
|
|
} |
192
|
|
|
|
|
|
|
} else { |
193
|
|
|
|
|
|
|
# keep the order |
194
|
6
|
100
|
|
|
|
20
|
if (@args % 2 == 1) { |
195
|
1
|
|
|
|
|
5
|
%self = ("message", @args); |
196
|
|
|
|
|
|
|
} else { |
197
|
5
|
|
|
|
|
29
|
%self = @args; |
198
|
|
|
|
|
|
|
} |
199
|
|
|
|
|
|
|
} |
200
|
|
|
|
|
|
|
|
201
|
6
|
|
|
|
|
123
|
return bless \%self, $class; |
202
|
|
|
|
|
|
|
} |
203
|
|
|
|
|
|
|
|
204
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
205
|
|
|
|
|
|
|
# |
206
|
|
|
|
|
|
|
# auxiliary functions |
207
|
|
|
|
|
|
|
# |
208
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
209
|
|
|
|
|
|
|
|
210
|
|
|
|
|
|
|
=begin Test::Pod::Coverage |
211
|
|
|
|
|
|
|
|
212
|
|
|
|
|
|
|
=item C |
213
|
|
|
|
|
|
|
|
214
|
|
|
|
|
|
|
Helper function for quoting strings in JSON. |
215
|
|
|
|
|
|
|
|
216
|
|
|
|
|
|
|
=end Test::Pod::Coverage |
217
|
|
|
|
|
|
|
|
218
|
|
|
|
|
|
|
=cut |
219
|
|
|
|
|
|
|
|
220
|
|
|
|
|
|
|
sub quote($) { |
221
|
9
|
|
|
9
|
1
|
12
|
my ($str) = @_; |
222
|
|
|
|
|
|
|
|
223
|
9
|
|
|
|
|
42
|
my %q = ( |
224
|
|
|
|
|
|
|
"\\" => "\\\\", |
225
|
|
|
|
|
|
|
'"' => '\"', |
226
|
|
|
|
|
|
|
"\n" => "\\n", |
227
|
|
|
|
|
|
|
"\r" => "\\r", |
228
|
|
|
|
|
|
|
"\t" => "\\t", |
229
|
|
|
|
|
|
|
); |
230
|
9
|
|
|
|
|
15
|
$str =~ s/([\\"\n\r\t])/$q{$1}/g; |
231
|
|
|
|
|
|
|
|
232
|
9
|
|
|
|
|
64
|
return $str; |
233
|
|
|
|
|
|
|
} |
234
|
|
|
|
|
|
|
|
235
|
|
|
|
|
|
|
=item C |
236
|
|
|
|
|
|
|
|
237
|
|
|
|
|
|
|
JSON encoding method. This method returns a JSON string that contains no tabs |
238
|
|
|
|
|
|
|
nor newlines. Just a single line of text. |
239
|
|
|
|
|
|
|
|
240
|
|
|
|
|
|
|
=cut |
241
|
|
|
|
|
|
|
|
242
|
|
|
|
|
|
|
# This is my own JSON encoder. This serves as two purposes: first, it relaxes |
243
|
|
|
|
|
|
|
# dependencies on external modules; second, it detects object of class |
244
|
|
|
|
|
|
|
# Log::Message::JSON (at root level, possibly) to preserve keys order when |
245
|
|
|
|
|
|
|
# JSON-infying. |
246
|
|
|
|
|
|
|
sub to_json($) { |
247
|
12
|
|
|
12
|
1
|
158
|
my ($value) = @_; |
248
|
|
|
|
|
|
|
|
249
|
12
|
100
|
|
|
|
99
|
if (ref $value eq __PACKAGE__) { # plain hash, tied |
|
|
100
|
|
|
|
|
|
|
|
100
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
|
|
50
|
|
|
|
|
|
250
|
1
|
|
|
|
|
2
|
my $tied = tied %$value; |
251
|
|
|
|
|
|
|
|
252
|
|
|
|
|
|
|
# store cache for this object if there was no cache for it |
253
|
1
|
50
|
|
|
|
5
|
if (not defined $tied->cache) { |
254
|
4
|
|
|
|
|
93
|
my @pairs = map { |
255
|
1
|
|
|
|
|
6
|
sprintf '%s:%s', to_json($_), to_json($value->{$_}) |
256
|
|
|
|
|
|
|
} keys %$value; |
257
|
1
|
|
|
|
|
8
|
$tied->cache(sprintf "{%s}", join ",", @pairs); |
258
|
|
|
|
|
|
|
} |
259
|
|
|
|
|
|
|
|
260
|
1
|
|
|
|
|
3
|
return $tied->cache; |
261
|
|
|
|
|
|
|
} elsif (ref $value eq "HASH") { # plain hash |
262
|
1
|
|
|
|
|
3
|
my @pairs = map { |
263
|
1
|
|
|
|
|
5
|
sprintf '%s:%s', to_json($_), to_json($value->{$_}) |
264
|
|
|
|
|
|
|
} sort keys %$value; |
265
|
1
|
|
|
|
|
7
|
return sprintf "{%s}", join ",", @pairs; |
266
|
|
|
|
|
|
|
} elsif (ref $value eq "ARRAY") { # plain array |
267
|
1
|
|
|
|
|
3
|
my @elems = map { to_json($_) } @$value; |
|
1
|
|
|
|
|
5
|
|
268
|
1
|
|
|
|
|
7
|
return sprintf "[%s]", join ",", @elems; |
269
|
|
|
|
|
|
|
} elsif (ref $value eq "SCALAR") { # plain scalar (reference) |
270
|
0
|
|
|
|
|
0
|
return sprintf '"%s"', quote($$value); |
271
|
|
|
|
|
|
|
} elsif (not defined $value) { # undef (null) |
272
|
0
|
|
|
|
|
0
|
return 'null'; |
273
|
|
|
|
|
|
|
} elsif (not ref $value) { # plain scalar |
274
|
9
|
|
|
|
|
18
|
return sprintf '"%s"', quote($value); |
275
|
|
|
|
|
|
|
} else { # compound object |
276
|
|
|
|
|
|
|
# TODO |
277
|
0
|
|
|
|
|
|
croak "Type @{[ref $value]} unsupported yet\n"; |
|
0
|
|
|
|
|
|
|
278
|
|
|
|
|
|
|
} |
279
|
|
|
|
|
|
|
} |
280
|
|
|
|
|
|
|
|
281
|
|
|
|
|
|
|
=back |
282
|
|
|
|
|
|
|
|
283
|
|
|
|
|
|
|
=cut |
284
|
|
|
|
|
|
|
|
285
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
286
|
|
|
|
|
|
|
|
287
|
|
|
|
|
|
|
=head1 C NOTES |
288
|
|
|
|
|
|
|
|
289
|
|
|
|
|
|
|
To use Log::Message::JSON as a reason for C, you need to assign it to |
290
|
|
|
|
|
|
|
C<$@> variable and call C without arguments (works for Perl 5.8+). |
291
|
|
|
|
|
|
|
|
292
|
|
|
|
|
|
|
unless (open my $f, "<", $file) { |
293
|
|
|
|
|
|
|
$@ = msg "error opening file", |
294
|
|
|
|
|
|
|
filename => $file, |
295
|
|
|
|
|
|
|
error => "$!", errno => $! + 0; |
296
|
|
|
|
|
|
|
die; |
297
|
|
|
|
|
|
|
} |
298
|
|
|
|
|
|
|
|
299
|
|
|
|
|
|
|
Of course just calling C will work as well, but it will result |
300
|
|
|
|
|
|
|
in a message without end-of-line character. |
301
|
|
|
|
|
|
|
|
302
|
|
|
|
|
|
|
=begin Test::Pod::Coverage |
303
|
|
|
|
|
|
|
|
304
|
|
|
|
|
|
|
=item C |
305
|
|
|
|
|
|
|
|
306
|
|
|
|
|
|
|
B: C |
307
|
|
|
|
|
|
|
|
308
|
|
|
|
|
|
|
=end Test::Pod::Coverage |
309
|
|
|
|
|
|
|
|
310
|
|
|
|
|
|
|
=cut |
311
|
|
|
|
|
|
|
|
312
|
|
|
|
|
|
|
|
313
|
|
|
|
|
|
|
sub PROPAGATE { |
314
|
0
|
|
|
0
|
1
|
|
my ($self, $file, $line) = @_; |
315
|
|
|
|
|
|
|
|
316
|
0
|
|
|
|
|
|
return sprintf "died at %s line %d, %s\n", $file, $line, $self; |
317
|
|
|
|
|
|
|
} |
318
|
|
|
|
|
|
|
|
319
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
320
|
|
|
|
|
|
|
|
321
|
|
|
|
|
|
|
=head1 C NOTES |
322
|
|
|
|
|
|
|
|
323
|
|
|
|
|
|
|
You might be tempted to use custom I to stringify the |
324
|
|
|
|
|
|
|
message. It would look like this: |
325
|
|
|
|
|
|
|
|
326
|
|
|
|
|
|
|
my $logger = Log::Log4perl->get_logger(); |
327
|
|
|
|
|
|
|
$logger->info({ filter => \&dumper, value => $mydata }); |
328
|
|
|
|
|
|
|
|
329
|
|
|
|
|
|
|
This won't work too well: the filter gets called before appender module, so |
330
|
|
|
|
|
|
|
the appender gets a string instead of a structured message. The better way |
331
|
|
|
|
|
|
|
would be: |
332
|
|
|
|
|
|
|
|
333
|
|
|
|
|
|
|
my $logger = Log::Log4perl->get_logger(); |
334
|
|
|
|
|
|
|
$logger->info(logmsg $mydata); |
335
|
|
|
|
|
|
|
|
336
|
|
|
|
|
|
|
Log::Log4perl only processes C and C when the object is |
337
|
|
|
|
|
|
|
a plain, unblessed hash, so you may safely use these two key names. |
338
|
|
|
|
|
|
|
|
339
|
|
|
|
|
|
|
=cut |
340
|
|
|
|
|
|
|
|
341
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
342
|
|
|
|
|
|
|
|
343
|
|
|
|
|
|
|
=head1 AUTHOR |
344
|
|
|
|
|
|
|
|
345
|
|
|
|
|
|
|
Stanislaw Klekot, C<< >> |
346
|
|
|
|
|
|
|
|
347
|
|
|
|
|
|
|
=head1 LICENSE AND COPYRIGHT |
348
|
|
|
|
|
|
|
|
349
|
|
|
|
|
|
|
Copyright 2012 Stanislaw Klekot. |
350
|
|
|
|
|
|
|
|
351
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify it |
352
|
|
|
|
|
|
|
under the terms of either: the GNU General Public License as published |
353
|
|
|
|
|
|
|
by the Free Software Foundation; or the Artistic License. |
354
|
|
|
|
|
|
|
|
355
|
|
|
|
|
|
|
See http://dev.perl.org/licenses/ for more information. |
356
|
|
|
|
|
|
|
|
357
|
|
|
|
|
|
|
=head1 SEE ALSO |
358
|
|
|
|
|
|
|
|
359
|
|
|
|
|
|
|
L, L, |
360
|
|
|
|
|
|
|
L, L, |
361
|
|
|
|
|
|
|
L. |
362
|
|
|
|
|
|
|
|
363
|
|
|
|
|
|
|
=cut |
364
|
|
|
|
|
|
|
|
365
|
|
|
|
|
|
|
#----------------------------------------------------------------------------- |
366
|
|
|
|
|
|
|
1; |
367
|
|
|
|
|
|
|
# vim:ft=perl |