File Coverage

blib/lib/OSLV/Monitor/Backends/FreeBSD.pm
Criterion Covered Total %
statement 26 231 11.2
branch 0 114 0.0
condition 0 51 0.0
subroutine 9 13 69.2
pod 3 4 75.0
total 38 413 9.2


line stmt bran cond sub pod time code
1             package OSLV::Monitor::Backends::FreeBSD;
2              
3 1     1   85623 use 5.006;
  1         3  
4 1     1   5 use strict;
  1         11  
  1         21  
5 1     1   3 use warnings;
  1         2  
  1         48  
6 1     1   682 use JSON qw(decode_json encode_json);
  1         11784  
  1         5  
7 1     1   546 use Clone qw(clone);
  1         424  
  1         62  
8 1     1   507 use File::Slurp qw(read_file write_file);
  1         28894  
  1         66  
9 1     1   7 use List::Util qw( uniq );
  1         1  
  1         46  
10 1     1   7 use Scalar::Util qw(looks_like_number);
  1         2  
  1         29  
11 1     1   3 use Time::HiRes qw(gettimeofday);
  1         1  
  1         7  
12              
13             =head1 NAME
14              
15             OSLV::Monitor::Backends::FreeBSD - backend for FreeBSD jails
16              
17             =head1 VERSION
18              
19             Version 0.0.5
20              
21             =cut
22              
23             our $VERSION = '0.0.5';
24              
25             =head1 SYNOPSIS
26              
27             use OSLV::Monitor::Backends::FreeBSD;
28              
29             my $backend = OSLV::Monitor::Backends::FreeBSD->new;
30              
31             my $usable=$backend->usable;
32             if ( $usable ){
33             $return_hash_ref=$backend->run;
34             }
35              
36             The stats names match those produced by "ps --libxo json".
37              
38             =head2 METHODS
39              
40             =head2 new
41              
42             Initiates the backend object.
43              
44             my $backend=OSLV::MOnitor::Backend::FreeBSD->new(
45             base_dir => $base_dir,
46             );
47              
48             The following arguments are usable.
49              
50             - base_dir :: Path to use for the base dir, where the proc
51             cache, freebsd_proc_cache.json, is is created.
52             Default :: /var/cache/oslv_monitor
53              
54             - obj :: The OSLVM::Monitor object.
55              
56             =cut
57              
58             sub new {
59 0     0 1   my ( $blank, %opts ) = @_;
60              
61 0 0         if ( !defined( $opts{base_dir} ) ) {
62 0           $opts{base_dir} = '/var/cache/oslv_monitor';
63             }
64              
65 0 0         if ( !defined( $opts{obj} ) ) {
    0          
66 0           die('$opts{obj} is undef');
67             } elsif ( ref( $opts{obj} ) ne 'OSLV::Monitor' ) {
68 0           die('ref $opts{obj} is not OSLV::Monitor');
69             }
70              
71 0           my $self = { version => 1, proc_cache => $opts{base_dir} . '/freebsd_proc_cache.json', obj => $opts{obj} };
72 0           bless $self;
73              
74 0           return $self;
75             } ## end sub new
76              
77             =head2 run
78              
79             $return_hash_ref=$backend->run;
80              
81             =cut
82              
83             sub run {
84 0     0 1   my $self = $_[0];
85              
86 0           my $data = {
87             errors => [],
88             cache_failure => 0,
89             oslvms => {},
90             has => {
91             'linux_mem_stats' => 0,
92             'rwdops' => 0,
93             'rwdbytes' => 0,
94             'rwdblocks' => 1,
95             'signals-taken' => 1,
96             'recv_sent_msgs' => 1,
97             'cows' => 1,
98             'stack-size' => 1,
99             'swaps' => 1,
100             'sock' => 0,
101             },
102             totals => {
103             'copy-on-write-faults' => 0,
104             'cpu-time' => 0,
105             'data-size' => 0,
106             'elapsed-times' => 0,
107             'involuntary-context-switches' => 0,
108             'major-faults' => 0,
109             'minor-faults' => 0,
110             'percent-cpu' => 0,
111             'percent-memory' => 0,
112             'read-blocks' => 0,
113             'received-messages' => 0,
114             'rss' => 0,
115             'sent-messages' => 0,
116             'stack-size' => 0,
117             'swaps' => 0,
118             'system-time' => 0,
119             'text-size' => 0,
120             'user-time' => 0,
121             'virtual-size' => 0,
122             'voluntary-context-switches' => 0,
123             'written-blocks' => 0,
124             'procs' => 0,
125             'signals-taken' => 0,
126             },
127             };
128              
129 0           my $proc_cache;
130 0           my $new_proc_cache = {};
131 0           my $cache_is_new = 0;
132 0 0         if ( -f $self->{proc_cache} ) {
133 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
134             warn( 'DEBUG, '
135             . join( '.', gettimeofday )
136             . ': backend run loading proc cache "'
137             . $self->{proc_cache}
138 0           . '"' );
139             }
140 0           eval {
141 0           my $raw_cache = read_file( $self->{proc_cache} );
142 0           $proc_cache = decode_json($raw_cache);
143             };
144 0 0         if ($@) {
145 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
146             warn( 'DEBUG, '
147             . join( '.', gettimeofday )
148             . ': backend run proc cache "'
149             . $self->{proc_cache}
150 0           . '" could not be loaded... '
151             . $@ );
152             }
153             push(
154 0           @{ $data->{errors} },
155 0           'reading proc cache "' . $self->{proc_cache} . '" failed... using a empty one...' . $@
156             );
157 0           $data->{cache_failure} = 1;
158 0           $proc_cache = {};
159 0           return $data;
160             } ## end if ($@)
161             } else {
162 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
163 0           warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': backend run proc cache is new' );
164             }
165 0           $cache_is_new = 1;
166             }
167              
168 0           my $base_stats = {
169             'copy-on-write-faults' => 0,
170             'cpu-time' => 0,
171             'data-size' => 0,
172             'elapsed-times' => 0,
173             'involuntary-context-switches' => 0,
174             'major-faults' => 0,
175             'minor-faults' => 0,
176             'percent-cpu' => 0,
177             'percent-memory' => 0,
178             'read-blocks' => 0,
179             'received-messages' => 0,
180             'rss' => 0,
181             'sent-messages' => 0,
182             'stack-size' => 0,
183             'swaps' => 0,
184             'system-time' => 0,
185             'text-size' => 0,
186             'user-time' => 0,
187             'virtual-size' => 0,
188             'voluntary-context-switches' => 0,
189             'written-blocks' => 0,
190             'procs' => 0,
191             'signals-taken' => 0,
192             'ip' => [],
193             'path' => [],
194             };
195              
196             # get a list of jails for jid to name mapping
197 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
198 0           warn( 'DEBUG, '
199             . join( '.', gettimeofday )
200             . ': backend run calling "/usr/sbin/jls -h --libxo json 2> /dev/null"' );
201             }
202 0           my $output = `/usr/sbin/jls -h --libxo json 2> /dev/null`;
203 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
204 0           warn( 'DEBUG, '
205             . join( '.', gettimeofday )
206             . ': backend run "/usr/sbin/jls -h --libxo json 2> /dev/null" finished' );
207             }
208 0           my $jls;
209 0           my @IP_keys = ( 'ip4.addr', 'ip6.addr' );
210 0           eval { $jls = decode_json($output) };
  0            
211 0 0         if ($@) {
212 0           push( @{ $data->{errors} }, 'decoding output from "jls -h --libxo json 2> /dev/null" failed... ' . $@ );
  0            
213 0           return $data;
214             }
215 0 0 0       if ( defined($jls)
      0        
      0        
      0        
      0        
216             && ref($jls) eq 'HASH'
217             && defined( $jls->{'jail-information'} )
218             && ref( $jls->{'jail-information'} ) eq 'HASH'
219             && defined( $jls->{'jail-information'}{jail} )
220             && ref( $jls->{'jail-information'}{jail} ) eq 'ARRAY' )
221             {
222 0           foreach my $jls_jail ( @{ $jls->{'jail-information'}{jail} } ) {
  0            
223 0 0 0       if ( defined( $jls_jail->{name} ) && defined( $jls_jail->{jid} ) ) {
224 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
225             warn( 'DEBUG, '
226             . join( '.', gettimeofday )
227             . ': backend run processing jname="'
228             . $jls_jail->{name}
229             . '" jid="'
230             . $jls_jail->{jid}
231 0           . '"' );
232             }
233              
234 0           my $jname = $jls_jail->{name};
235 0           my $include_jail = $self->{'obj'}->include($jname);
236              
237 0 0         if ($include_jail) {
238 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
239 0           warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': backend run calling clone($base_stats)' );
240             }
241              
242 0           $data->{oslvms}{$jname} = clone($base_stats);
243              
244 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
245 0           warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': backend run done clone($base_stats) done' );
246             }
247              
248             # finds each ip ifconfig shows in a jail
249 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
250 0           warn( 'DEBUG, '
251             . join( '.', gettimeofday )
252             . ': backend run calling "ifconfig -j '
253             . $jname
254             . ' 2> /dev/null"' );
255             }
256 0           my $output = `ifconfig -j $jname 2> /dev/null`;
257 0           my %found_IPv4;
258             my %found_IPv6;
259 0 0         if ( $? eq 0 ) {
    0          
260 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
261 0           warn( 'DEBUG, '
262             . join( '.', gettimeofday )
263             . ': backend run processing ifconfig info for jail' );
264             }
265 0           my @output_split = split( /\n/, $output );
266 0           my $interface;
267 0           foreach my $line (@output_split) {
268 0 0 0       if ( $line =~ /^[a-zA-Z].*\:[\ \t]+flags\=/ ) {
    0 0        
    0          
269 0           $interface = $line;
270 0           $interface =~ s/\:[\ \t]+flags.*//;
271             } elsif ( $line =~ /^[\ \t]+inet6 /
272             && defined($interface) )
273             {
274 0           $line =~ s/^[\ \t]+inet6 //;
275 0           $line =~ s/\ .*$//;
276 0           $line =~ s/\%.*$//;
277 0           $found_IPv6{$line} = $interface;
278             } elsif ( $line =~ /^[\ \t]+inet /
279             && defined($interface) )
280             {
281 0           $line =~ s/^[\ \t]+inet //;
282 0           $line =~ s/ .*$//;
283 0           $found_IPv4{$line} = $interface;
284             }
285             } ## end foreach my $line (@output_split)
286             } elsif ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
287 0           warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': backend run ifconfig exited non-zero' );
288             }
289             ## end if ( $? eq 0 )
290              
291 0           foreach my $ip_key (@IP_keys) {
292 0           my @current_IPs;
293              
294 0 0         if ( $ip_key eq 'ip4.addr' ) {
295 0           @current_IPs = keys(%found_IPv4);
296             } else {
297 0           @current_IPs = keys(%found_IPv6);
298             }
299              
300 0 0 0       if ( defined( $jls_jail->{$ip_key} )
      0        
301             && ref( $jls_jail->{$ip_key} ) eq 'ARRAY'
302             && defined( $jls_jail->{$ip_key}[0] ) )
303             {
304 0           foreach my $ip ( @{ $jls_jail->{$ip_key} } ) {
  0            
305 0 0 0       if ( ref($ip) eq '' && !defined( $found_IPv4{$ip} ) && !defined( $found_IPv6{$ip} ) ) {
      0        
306 0 0 0       if ( $ip =~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/
307             || $ip =~ /^[\:0-9a-fA-F]+$/ )
308             {
309 0           push( @current_IPs, $ip );
310             }
311             }
312             }
313             } ## end if ( defined( $jls_jail->{$ip_key} ) && ref...)
314 0           foreach my $ip (@current_IPs) {
315 0           my $ip_if;
316             my $ip_gw;
317 0           my $ip_gw_if;
318              
319 0 0 0       if ( $ip_key eq 'ip4.addr'
    0 0        
320             && defined( $found_IPv4{$ip} ) )
321             {
322 0           $ip_if = $found_IPv4{$ip};
323             } elsif ( $ip_key eq 'ip6.addr'
324             && defined( $found_IPv6{$ip} ) )
325             {
326 0           $ip_if = $found_IPv6{$ip};
327             }
328             # set the ip type flag for netstat
329 0           my $ip_flag = '-6';
330 0 0         if ( $ip_key eq 'ip4.addr' ) {
331 0           $ip_flag = '-4';
332             }
333              
334             # fetch route info for the jail
335 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
336 0           warn( 'DEBUG, '
337             . join( '.', gettimeofday )
338             . ': backend run calling "route -n -j '
339             . $jname . ' '
340             . $ip_flag
341             . ' show default 2> /dev/null"' );
342             }
343 0           my $output = `route -n -j $jname $ip_flag show default 2> /dev/null`;
344 0 0         if ( $? eq 0 ) {
    0          
345 0           my @output_split = split( /\n/, $output );
346 0           foreach my $line (@output_split) {
347 0 0         if ( $line =~ /gateway\:[\ \t]/ ) {
    0          
348 0           $line =~ s/.*gateway\:[\ \t]+//;
349 0           $line =~ s/[\ \t]*$//;
350 0           $ip_gw = $line;
351             } elsif ( $line =~ /interface:[\ \t]/ ) {
352 0           $line =~ s/.*interface\:[\ \t]+//;
353 0           $line =~ s/[\ \t]*$//;
354 0           $ip_gw_if = $line;
355             }
356             } ## end foreach my $line (@output_split)
357             } elsif ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
358 0           warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': backend run route exited non-zero' );
359             }
360              
361             push(
362 0           @{ $data->{oslvms}{$jname}{ip} },
  0            
363             {
364             ip => $ip,
365             if => $ip_if,
366             gw => $ip_gw,
367             gw_if => $ip_gw_if,
368             }
369             );
370             } ## end foreach my $ip (@current_IPs)
371             } ## end foreach my $ip_key (@IP_keys)
372             } ## end if ($include_jail)
373             } ## end if ( defined( $jls_jail->{name} ) && defined...)
374             } ## end foreach my $jls_jail ( @{ $jls->{'jail-information'...}})
375             } ## end if ( defined($jls) && ref($jls) eq 'HASH' ...)
376              
377             # remove possible dup paths
378 0           my @found_jails = keys( %{ $data->{oslvms} } );
  0            
379 0           foreach my $jail (@found_jails) {
380 0           my @uniq_paths = uniq( @{ $data->{oslvms}{$jail}{path} } );
  0            
381 0           $data->{oslvms}{$jail}{path} = \@uniq_paths;
382             }
383              
384 0           my @stats = (
385             'copy-on-write-faults', 'cpu-time',
386             'data-size', 'elapsed-times',
387             'involuntary-context-switches', 'major-faults',
388             'minor-faults', 'percent-cpu',
389             'percent-memory', 'read-blocks',
390             'received-messages', 'rss',
391             'sent-messages', 'stack-size',
392             'swaps', 'system-time',
393             'text-size', 'user-time',
394             'virtual-size', 'voluntary-context-switches',
395             'written-blocks', 'signals-taken',
396             );
397              
398             # values that are time stats that require additional processing
399 0           my $times = { 'cpu-time' => 1, 'system-time' => 1, 'user-time' => 1, };
400             # these are counters and differences needed computed for them
401 0           my $counters = {
402             'cpu-time' => 1,
403             'system-time' => 1,
404             'user-time' => 1,
405             'read-blocks' => 1,
406             'major-faults' => 1,
407             'involuntary-context-switches' => 1,
408             'minor-faults' => 1,
409             'received-messages' => 1,
410             'sent-messages' => 1,
411             'swaps' => 1,
412             'voluntary-context-switches' => 1,
413             'written-blocks' => 1,
414             'copy-on-write-faults' => 1,
415             'signals-taken' => 1,
416             };
417              
418 0           foreach my $jail (@found_jails) {
419 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
420 0           warn( 'DEBUG, '
421             . join( '.', gettimeofday )
422             . ': backend run calling "/bin/ps ax --libxo json -o %cpu,%mem,pid,acflag,cow,dsiz,etimes,inblk,jail,majflt,minflt,msgrcv,msgsnd,nivcsw,nswap,nvcsw,oublk,rss,ssiz,systime,time,tsiz,usertime,vsz,pid,gid,uid,command,nsigs -J '
423             . $jail
424             . ' 2> /dev/null"' );
425             }
426             $output
427 0           = `/bin/ps ax --libxo json -o %cpu,%mem,pid,acflag,cow,dsiz,etimes,inblk,jail,majflt,minflt,msgrcv,msgsnd,nivcsw,nswap,nvcsw,oublk,rss,ssiz,systime,time,tsiz,usertime,vsz,pid,gid,uid,command,nsigs -J $jail 2> /dev/null`;
428 0           my $ps;
429 0           eval { $ps = decode_json($output); };
  0            
430 0 0         if ( !$@ ) {
431 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
432 0           warn( 'DEBUG, '
433             . join( '.', gettimeofday )
434             . ': backend run processing proc info for jail '
435             . $jail
436             . '' );
437             }
438 0           foreach my $proc ( @{ $ps->{'process-information'}{process} } ) {
  0            
439 0 0         if ( $proc->{'elapsed-times'} ne '-' ) {
440             my $cache_name
441 0           = $proc->{pid} . '-' . $proc->{uid} . '-' . $proc->{gid} . '-' . $jail . '-' . $proc->{command};
442              
443 0           foreach my $stat (@stats) {
444 0           my $stat_value = $proc->{$stat};
445             # pre-process the stat if it is a time value that requires it
446 0 0         if ( $times->{$stat} ) {
447             # [days-][hours:]minutes:seconds
448 0           my $seconds = 0;
449 0           my $time = $stat_value;
450              
451 0 0         if ( $time =~ /-/ ) {
452 0           my $days = $time;
453 0           $days =~ s/\-.*$//;
454 0           $time =~ s/^.*\-//;
455 0           $seconds = $seconds + ( $days * 86400 );
456             }
457 0           my @time_split = split( /\:/, $time );
458 0 0         if ( defined( $time_split[2] ) ) {
459 0           $seconds
460             = $seconds + ( 3600 * $time_split[0] ) + ( 60 * $time_split[1] ) + $time_split[2];
461             } else {
462 0           $seconds = $seconds + ( 60 * $time_split[1] ) + $time_split[1];
463             }
464 0           $stat_value = $seconds;
465 0           $proc->{$stat} = $stat_value;
466             } ## end if ( $times->{$stat} )
467              
468 0 0         if ( looks_like_number($stat_value) ) {
469 0 0         if ( $counters->{$stat} ) {
470 0 0 0       if ( defined( $proc_cache->{$cache_name} )
471             && defined( $proc_cache->{$cache_name}{$stat} ) )
472             {
473 0           $stat_value = ( $stat_value - $proc_cache->{$cache_name}{$stat} ) / 300;
474             } else {
475 0           $stat_value = $stat_value / 300;
476             }
477             $data->{oslvms}{$jail}{$stat}
478 0           = $data->{oslvms}{$jail}{$stat} + $stat_value;
479 0           $data->{totals}{$stat} = $data->{totals}{$stat} + $stat_value;
480             } else {
481             $data->{oslvms}{$jail}{$stat}
482 0           = $data->{oslvms}{$jail}{$stat} + $stat_value;
483 0           $data->{totals}{$stat} = $data->{totals}{$stat} + $stat_value;
484             }
485             } else {
486 0           warn( '"' . $stat_value . '" for ' . $stat . ' does not appear numeric' );
487             }
488             } ## end foreach my $stat (@stats)
489              
490 0           $data->{oslvms}{$jail}{procs}++;
491 0           $data->{totals}{procs}++;
492              
493 0           $new_proc_cache->{$cache_name} = $proc;
494             } ## end if ( $proc->{'elapsed-times'} ne '-' )
495             } ## end foreach my $proc ( @{ $ps->{'process-information'...}})
496             } ## end if ( !$@ )
497             } ## end foreach my $jail (@found_jails)
498              
499             # save the proc cache for next run
500 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
501             warn( 'DEBUG, '
502             . join( '.', gettimeofday )
503             . ': backend run writing proc cache to "'
504             . $self->{proc_cache}
505 0           . '"' );
506             }
507 0           eval { write_file( $self->{proc_cache}, encode_json($new_proc_cache) ); };
  0            
508 0 0         if ($@) {
509 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
510             warn( 'DEBUG, '
511             . join( '.', gettimeofday )
512             . ': backend run errored writing proc cache to "'
513 0           . $self->{proc_cache} . '"... '
514             . $@ );
515             }
516 0           push( @{ $data->{errors} }, 'saving proc cache failed, "' . $self->{proc_cache} . '"... ' . $@ );
  0            
517             } ## end if ($@)
518              
519 0 0         if ($cache_is_new) {
520 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
521 0           warn( 'DEBUG, '
522             . join( '.', gettimeofday )
523             . ': backend run cache is new so not returning any oslvm info and zeroing totals' );
524             }
525 0           delete( $data->{oslvms} );
526 0           $data->{oslvms} = {};
527 0           my @total_keys = keys( %{ $data->{totals} } );
  0            
528 0           foreach my $total_key (@total_keys) {
529 0 0         if ( ref( $data->{totals}{$total_key} ) eq '' ) {
530 0           $data->{totals}{$total_key} = 0;
531             }
532             }
533             } ## end if ($cache_is_new)
534              
535 0 0         if ( $ENV{'OSLV_MONITOR_DEBUG'} ) {
536 0           warn( 'DEBUG, ' . join( '.', gettimeofday ) . ': backend run done' );
537             }
538              
539 0           return $data;
540             } ## end sub run
541              
542             =head2 usable
543              
544             Dies if not usable.
545              
546             eval{ $backend->usable; };
547             if ( $@ ){
548             print 'Not usable because... '.$@."\n";
549             }
550              
551             =cut
552              
553             sub usable {
554 0     0 1   my $self = $_[0];
555              
556             # make sure it is freebsd
557 0 0         if ( $^O !~ 'freebsd' ) {
558 0           die '$^O is "' . $^O . '" and not "freebsd"';
559             }
560              
561             # make sure we can locate jls
562 0           my $cmd_bin = `/bin/sh -c 'which jls 2> /dev/null'`;
563 0 0         if ( $? != 0 ) {
564 0           die 'The command "jls" is not in the path... ' . $ENV{PATH};
565             }
566              
567 0           return 1;
568             } ## end sub usable
569              
570             sub ip_to_if {
571 0     0 0   my $self = $_[0];
572 0           my $ip = $_[1];
573              
574 0 0 0       if ( !defined($ip) || ref($ip) ne '' ) {
575 0           return undef;
576             }
577              
578 0           my $if = IO::Interface::Simple->new_from_address($ip);
579              
580 0 0         if ( !defined($if) ) {
581 0           return undef;
582             }
583              
584 0           return $if->name;
585             } ## end sub ip_to_if
586              
587             =head1 AUTHOR
588              
589             Zane C. Bowers-Hadley, C<< >>
590              
591             =head1 BUGS
592              
593             Please report any bugs or feature requests to C, or through
594             the web interface at L. I will be notified, and then you'll
595             automatically be notified of progress on your bug as I make changes.
596              
597              
598              
599              
600             =head1 SUPPORT
601              
602             You can find documentation for this module with the perldoc command.
603              
604             perldoc OSLV::Monitor
605              
606              
607             You can also look for information at:
608              
609             =over 4
610              
611             =item * RT: CPAN's request tracker (report bugs here)
612              
613             L
614              
615             =item * CPAN Ratings
616              
617             L
618              
619             =item * Search CPAN
620              
621             L
622              
623             =back
624              
625              
626             =head1 ACKNOWLEDGEMENTS
627              
628              
629             =head1 LICENSE AND COPYRIGHT
630              
631             This software is Copyright (c) 2024 by Zane C. Bowers-Hadley.
632              
633             This is free software, licensed under:
634              
635             The Artistic License 2.0 (GPL Compatible)
636              
637              
638             =cut
639              
640             1; # End of OSLV::Monitor