diff --git a/lib/Core/HTTPD.pm b/lib/Core/HTTPD.pm new file mode 100644 index 0000000..2b92db4 --- /dev/null +++ b/lib/Core/HTTPD.pm @@ -0,0 +1,196 @@ +package Core::HTTPD; + +use strict; +use warnings; + +use base 'Resmon::Module'; + +use LWP::UserAgent; + +=pod + +=head1 NAME + +Core::HTTPD - monitor HTTPD stats via mod_status + +=head1 SYNOPSIS + + Core::HTTPD { + local : url => http://server.example.com/path/to/mod_status/ + } + + Core::HTTPD { + local : url => http://server.example.com/path/to/mod_status/, username => username, password => password + } + +=head1 DESCRIPTION + +This module monitors HTTPD statistics via HTTP/HTTPS requests to mod_status. + +=head1 CONFIGURATION + +=over + +=item check_name + +Arbitrary name of the check. + +=item url + +The mod_status URL to connect to. + +=item username + +The basic authentication username to send when requesting statistics (required if password is specified). + +=item password + +The basic authentication password to send when requesting statistics (required if username is specified). + +=back + +=head1 METRICS + +=over + +=item total_hits + +Total number of requests + +=item total_bytes + +Total number of bytes transfered + +=item cpu_load + +Current CPU load from the HTTPD processes. + +=item busy_workers + +Current count of busy workers/servers. + +=item idle_workers + +Current count of idle workers/servers. + +=item threads_waiting + +Current count of threads waiting for a connection. + +=item threads_starting + +Current count of threads starting up.. + +=item threads_reading + +Current count of threads reading a request. + +=item threads_writing + +Current count of threads writing a response. + +=item threads_keep_alive + +Current count of threads idle in keep alive. + +=item threads_dns_lookup + +Current count of threads performing a DNS lookup. + +=item threads_closing + +Current count of threads closing a connection. + +=item threads_logging + +Current count of threads logging. + +=item threads_stopping + +Current count of threads gracefully finishing. + +=item threads_idle + +Current count of threads idle. + +=item threads_available + +Current count of thread slots with no current process. + +=back + +=cut + + +sub new { + my ($class, $check_name, $config) = @_; + my $self = $class->SUPER::new($check_name, $config); + + $self->{'user_agent'} = LWP::UserAgent->new(); + $self->{'user_agent'}->agent('Resmon'); + $self->{'user_agent'}->timeout($config->{'check_timeout'} || 10); + + $self->{'scoreboard_keys'} = { + '_' => 'threads_waiting', + 'S' => 'threads_starting', + 'R' => 'threads_reading', + 'W' => 'threads_writing', + 'K' => 'threads_keep_alive', + 'D' => 'threads_dns_lookup', + 'C' => 'threads_closing', + 'L' => 'threads_logging', + 'G' => 'threads_stopping', + 'I' => 'threads_idle', + '.' => 'threads_available', + }; + + bless($self, $class); + return $self; +} + +sub handler { + my $self = shift; + my $config = $self->{'config'}; + my $url = $config->{'url'} || die "URL is required.\n"; + my $username = $config->{'username'}; + my $password = $config->{'password'}; + + my $user_agent = $self->{'user_agent'}; + my $scoreboard_keys = $self->{'scoreboard_keys'}; + + my $request = HTTP::Request->new(GET => "$url?auto"); + if($username && $password) { + $request->authorization_basic($username, $password); + } + + my $response = $user_agent->request($request); + $response->is_success || die "HTTP GET failed to $url: " . $response->status_line . "\n"; + + my $content = $response->decoded_content(); + + my $result = { map { $_ => 0 } values %$scoreboard_keys }; + foreach my $line (split(/\n/, $content)) { + my ($key, $val) = split(/: /, $line); + + if ($key eq 'Total Accesses') { + $result->{'total_hits'} = [$val, 'n']; + } elsif ($key eq 'Total kBytes') { + $result->{'total_bytes'} = [$val * 1024, 'n']; + } elsif ($key eq 'CPULoad') { + $result->{'cpu_load'} = [$val, 'n']; + } elsif ($key =~ /^Busy/) { + $result->{'busy_workers'} = [$val, 'n']; + } elsif ($key =~ /^Idle/) { + $result->{'idle_workers'} = [$val, 'n']; + } elsif ($key eq 'Scoreboard') { + my @chars = split(//, $val); + foreach my $state (split(//, $val)) { + $result->{$scoreboard_keys->{$state}}++; + } + } + } + + return $result; +} + +1; \ No newline at end of file diff --git a/lib/Core/JMX.pm b/lib/Core/JMX.pm new file mode 100644 index 0000000..33ae692 --- /dev/null +++ b/lib/Core/JMX.pm @@ -0,0 +1,189 @@ +package Core::JMX; + +use strict; +use warnings; + +use base 'Resmon::Module'; + +use JMX::Jmx4Perl; + +=pod + +=head1 NAME + +Core::JMX - monitor JMX stats via Jmx4Perl/Jolokia + +=head1 SYNOPSIS + + Core::JMX { + memorypool : query => java.lang:type=MemoryPool\,name=*, attributes => Usage, metric_pattern => /^.*?=(.+?)\,.*_(.*)$/$1_$2/ + } + + Core::JMX { + memory : query => java.lang:type=Memory, attributes => HeapMemoryUsage,NonHeapMemoryUsage + } + +=head1 DESCRIPTION + +This module monitors arbitrary JMX statistics usage using jmx4perl/jolokia. + +=head1 CONFIGURATION + +=over + +=item check_name + +Arbitrary name of the check. + +=item url + +The Jolokia URL to connect to (optional; defaults to "http://localhost:8778/jolokia/"). + +=item username + +The basic authentication username to send when requesting JMX statistics (required if password is specified). + +=item password + +The basic authentication password to send when requesting JMX statistics (required if username is specified). + +=item query + +The JMX query to perform (required if alias is not specified). + +=item attributes + +The JMX attributes to return, comma-separated (optional when query is specified, otherwise all attributes for query are returned). + +=item alias + +The name of a Jmx4Perl alias (required if query is not specified). + +=item product + +The name of a Jmx4Perl product (optional but recommended when using an alias; defaults to "unknown"). + +=item metric_pattern + +A Perl regular expression used to transform the returned JMX names into friendlier metric names (recommended for most queries). + +=back + +=head1 METRICS + +=over + +=item * + +Metrics as reported by JMX, and transformed by metric_pattern. + +=back + +=cut + +sub new { + my ($class, $check_name, $config) = @_; + my $self = $class->SUPER::new($check_name, $config); + + my $url = $config->{'url'} || 'http://localhost:8778/jolokia/'; + my $username = $config->{'username'}; + my $password = $config->{'password'}; + my $product = $config->{'product'} || 'unknown'; + + if($username && $password) { + $self->{'jmx'} = new JMX::Jmx4Perl(url => $url, product => $product, user => $username, password => $password); + } else { + $self->{'jmx'} = new JMX::Jmx4Perl(url => $url, product => $product); + } + + my $metric_pattern = $config->{'metric_pattern'}; + if ($metric_pattern) { + $metric_pattern = trim_slashes($metric_pattern); + my ($metric_pattern_search, $metric_pattern_replace) = split(/(?{'metric_pattern_search'} = $metric_pattern_search; + $self->{'metric_pattern_replace'} = '"' . $metric_pattern_replace . '"'; + } + + bless($self, $class); + return $self; +} + +sub trim_slashes { + my $string = shift; + $string =~ s/^\/?(.*?)\/?$/$1/; + return $string; +} + +sub flatten_recursive { + my ($prefix, $in, $out) = @_; + for my $key (keys %$in) { + my $value = $in->{$key}; + my $new_prefix = $prefix ? $prefix . '_' . $key : $key; + $new_prefix =~ s/\s+/_/g; + $new_prefix = lc($new_prefix); + + if ( defined $value && ref $value eq 'HASH' ) { + flatten_recursive($new_prefix, $value, $out); + } + else { + $out->{$new_prefix} = [$value, 'n']; + } + } +} + +sub handler { + my $self = shift; + + my $jmx = $self->{'jmx'}; + my $metric_pattern_search = $self->{'metric_pattern_search'}; + my $metric_pattern_replace = $self->{'metric_pattern_replace'}; + my $config = $self->{'config'}; + + my $alias = $config->{'alias'}; + my $query = $config->{'query'}; + my $attributes = $config->{'attributes'}; + + my $response; + if (defined $alias) { + + # Use a pre-defined jmx4perl alias + $response = $jmx->get_attribute($alias); + + } elsif (defined $query) { + if (defined $attributes) + { + # Get comma-separated list of attributes + my @array = split(/,/, $attributes); + $response = $jmx->get_attribute($query, \@array); + + } else { + + # Get all attributes + $response = $jmx->get_attribute($query, undef); + + } + } else { + + die "Either alias or query is required."; + + } + + my $flattened = {}; + flatten_recursive('', $response, $flattened); + + my $result; + if($metric_pattern_search && $metric_pattern_replace) { + my $transformed = {}; + while( my ($key, $val) = each %$flattened ) { + $key =~ s/$metric_pattern_search/$metric_pattern_replace/ee; + $transformed->{$key} = $val; + } + $result = $transformed; + } else { + $result = $flattened; + } + + return $result; +}; + +1; diff --git a/lib/Core/KStat.pm b/lib/Core/KStat.pm new file mode 100644 index 0000000..3ad6fc1 --- /dev/null +++ b/lib/Core/KStat.pm @@ -0,0 +1,200 @@ +package Core::KStat; + +use strict; +use warnings; + +use base 'Resmon::Module'; +use List::Util qw(sum); +use Sun::Solaris::Kstat; + +=pod + +=head1 NAME + +Core::KStat - Get Solaris kernel metrics + +=head1 SYNOPSIS + + Core::KStat { + run_queue_length : mode => ratio, numerator => unix::sysinfo:runque, denominator => unix::sysinfo:updates + } + + Core::KStat { + page_in : mode => sum, kstats => cpu::vm:pgpgin + page_out : mode => sum, kstats => cpu::vm:pgpgout + } + +=head1 DESCRIPTION + +This module retrieves metrics from Solaris's KStat subsystem, and performs one of several mathematical operations on it. + +=head1 CONFIGURATION + +=over + +=item check_name + +Arbitrary name of the check. + +=item mode + +Mathematical operation to perform on the results, one of value, sum, average, ratio, percent (optional, defaults to value). + +=item kstat_numerator + +KStat query to use as numerator, returning a single result (required if mode is ratio or percent). + +=item kstat_denominator + +KStat query to use as denominator, returning a single result (required if mode is ratio or percent). + +=item kstat + +KStat query, returning a single result (required if mode is value). + +=item kstats + +KStats queries, space separated, returning one or more values (required if mode is sum or average). + +=back + +=head1 METRICS + +=over + +=item result + +The result of performing the operation on the KStat(s). + +=back + +=cut + +sub get_kstat_values { + my $kstat_query = shift; + my %kstat_hash = @_; + my ($module_part, $instance_part, $name_part, $statistic_part) = split_kstat_query($kstat_query); + + my @values = (); + foreach my $module (get_hash_keys($module_part, %kstat_hash)) { + my %module_hash = %{$kstat_hash{$module}}; + foreach my $instance (get_hash_keys($instance_part, %module_hash)) { + my %instance_hash = %{$module_hash{$instance}}; + foreach my $name (get_hash_keys($name_part, %instance_hash)) { + my %statistic_hash = %{$instance_hash{$name}}; + foreach my $statistic (get_hash_keys($statistic_part, %statistic_hash)) { + my $value = $statistic_hash{$statistic}; + push(@values, $value); + } + } + } + } + + if (@values == 0) { + die "kstat query \"$kstat_query\": Statistic not found\n"; + } + + return @values; +} + +sub get_kstat_value { + my $kstat_query = shift; + my %kstat_hash = @_; + my @values = get_kstat_values($kstat_query, %kstat_hash); + + if (@values > 1) + { + die "kstat query \"$kstat_query\": Matched multiple statistics\n"; + } + return $values[0]; +} + +sub get_hash_keys { + my $key = shift; + my %hash = @_; + + if(! defined($key) || $key eq '') { + my @keys = keys(%hash); + return @keys; + } + elsif(exists $hash{$key}) { + return ($key); + } + else { + return (); + } +} + +sub split_kstat_query { + my ($query) = @_; + my @parts = split(/:/, $query, 4); + if (@parts != 4 || $parts[3] eq '') { + die "kstat_query must match format: [module]:[instance]:[name]:statistic\n"; + } + return @parts; +} + +sub new { + my ($class, $check_name, $config) = @_; + my $self = $class->SUPER::new($check_name, $config); + + $self->{'kstat'} = Sun::Solaris::Kstat->new(); + + bless($self, $class); + return $self; +} + +sub handler { + my $self = shift; + my $config = $self->{'config'}; + my $mode = $config->{'mode'} || 'value'; + + my $kstat = $self->{'kstat'}; + $kstat->update(); + + my $result; + if($mode eq 'value') { + + my $kstat_query = $config->{'kstat'} || die "KStat query is required.\n"; + + $result = get_kstat_value($kstat_query, %$kstat); + + } elsif($mode eq 'ratio' || $mode eq 'percent') { + + my $kstat_numerator = $config->{'kstat_numerator'} || die "KStat numerator is required.\n"; + my $kstat_denominator = $config->{'kstat_denominator'} || die "KStat denominator is required.\n"; + + my $numerator = get_kstat_value($kstat_numerator, %$kstat); + my $denominator = get_kstat_value($kstat_denominator, %$kstat); + + if($mode eq 'ratio') { + $result = ($numerator / $denominator); + } elsif ($mode eq 'percent') { + $result = (100 * $numerator / ($numerator + $denominator) ); + } + + } elsif($mode eq 'sum' || $mode eq 'average') { + + my $kstat_queries = $config->{'kstats'} || die "One or more KStat queries are required.\n"; + my @kstat_values = split(/\s+/, $config->{'kstats'}); + + my @values = (); + foreach my $kstat_query (@kstat_values) { + push(@values, get_kstat_values($kstat_query, %$kstat)); + } + $result = sum(@values); + + if("average" eq $mode) { + $result /= scalar(@values); + } + + } else { + die "Unsupported mode: $mode\n"; + } + + return { + 'result' => [$result, 'n'] + }; +}; + +1; diff --git a/lib/Core/LDAP.pm b/lib/Core/LDAP.pm new file mode 100644 index 0000000..1476f88 --- /dev/null +++ b/lib/Core/LDAP.pm @@ -0,0 +1,152 @@ +package Core::LDAP; + +use strict; +use warnings; +use Switch; + +use base 'Resmon::Module'; + +use Net::LDAP; + +=pod + +=head1 NAME + +Core::LDAP - monitor LDAP stats via Net::LDAP + +=head1 SYNOPSIS + + Core::LDAP { + stats : baseDN => cn=snmp\,cn=monitor, attributes => bytessent,bytesrecv + } + + Core::LDAP { + email_users : uri => ldap://localhost:1389, baseDN => dc=example\,dc=com, filter => (objectClass=mailRecipient) + } + +=head1 DESCRIPTION + +This module monitors arbitrary LDAP attributes or search counts via Net::LDAP. + +=head1 CONFIGURATION + +=over + +=item check_name + +Arbitrary name of the check. + +=item uri + +The LDAP URI to connect to (optional; defaults to "ldap://localhost"). + +=item base_dn + +The Base DN under which to search (required). + +=item bind_dn + +DN to use for bind authentication (required if bind_password is specified). + +=item bind_password + +Password to use for bind authentication (required if bind_dn is specified). + +=item attributes + +One or more comma-separated LDAP attributes to return directly as metrics (required unless filter is specified). + +=item filter + +An LDAP filter query to perform, and report the number of entries found (required unless attributes are specified). + +=back + +=head1 METRICS + +=over + +=item count + +The count of query results if filter is specified. + +=item * + +Attribute values if attributes are specified. + +=back + +=cut + +sub appendAttributes { + my ($out, $entry, $prefix) = @_; + foreach my $attribute ($entry->attributes()) { + my $key = $attribute; + $key =~ s/;/-/g; + if($prefix) { + $key = "$prefix-$key"; + } + my $value = $entry->get_value($attribute); + $out->{$key} = [$value, 'n']; + } +} + +sub handler { + my $self = shift; + my $config = $self->{'config'}; + my $uri = $config->{'uri'} || 'ldap://localhost'; + my $base_dn = $config->{'base_dn'} || die "base_dn is required\n"; + my $bind_dn = $config->{'bind_dn'}; + my $bind_password = $config->{'bind_password'}; + my $attributes = $config->{'attributes'}; + my $filter = $config->{'filter'}; + + if(!$attributes && !$filter) + { + die "Either filter or attributes is required\n"; + } + + my $ldap = Net::LDAP->new($uri) or die "$@"; + + my $response; + if($bind_dn && $bind_password) { + $response = $ldap->bind($bind_dn, password => $bind_password); + } else { + $response = $ldap->bind(); + } + $response->code && die $response->error; + + my $result = {}; + if ($attributes) { + my @array = split(/,/, $attributes); + $response = $ldap->search( + base => $baseDN, + scope => 'base', + attrs => \@array, + filter => '(objectclass=*)'); + $response->code && die $response->error; + + if(1 == $response->count()) { + my $entry = $response->entry(0); + appendAttributes($result, $entry); + } else { + foreach my $entry ($response->entries) { + appendAttributes($result, $entry, $entry->DN()); + } + } + } elsif ($filter) { + $response = $ldap->search( + base => $baseDN, + scope => 'sub', + attrs => ['1.1'], + filter => $filter); + $response->code && die $response->error; + $result->{'count'} = [$response->count(), 'n']; + } + + $ldap->unbind(); + + return $result; +} + +1; \ No newline at end of file diff --git a/lib/Core/MetricsCommand.pm b/lib/Core/MetricsCommand.pm new file mode 100644 index 0000000..7475a64 --- /dev/null +++ b/lib/Core/MetricsCommand.pm @@ -0,0 +1,148 @@ +package Core::MetricsCommand; + +use strict; +use warnings; + +use base 'Resmon::Module'; + +use Resmon::ExtComm qw(run_command); + +=pod + +=head1 NAME + +Core::MetricsCommand - retrieve metrics from an executable + +=head1 SYNOPSIS + + Core::MetricsCommand { + local : cmd => /path/to/executable -arguments + } + + Core::MetricsCommand { + local : cmd => /path/to/executable -arguments, metric_separator_pattern => / /, key_value_separator_pattern => /:/ + } + + Core::MetricsCommand { + * : cmd => /path/to/executable -arguments, check_name_key => id + } + +=head1 DESCRIPTION + +Retrieve metrics by running an executable. + +=head1 CONFIGURATION + +=over + +=item check_name + +Arbitrary name of the check, or * if executable returns multiple checks. If * is +specified, check_name_key must also be specified, and any values in the output +for this key will be used as check names. + +=item cmd + +The command and any arguments to run (required). + +=item metric_separator_pattern + +A regular expression that defines how multiple metrics are separated (defaults to / /) + +=item key_value_separator_pattern + +A regular expression that defines how metric keys are are separated from values (defaults to /:/) + +=item check_name_key + +If the executable returns multiple checks, the key that's value will be used as the check name (valid and required only if * specified as check_name) + +=back + +=head1 METRICS + +=over + +=item * + +Metrics depend on results of the executable. + +=back + +=cut + +sub new { + my ($class, $check_name, $config) = @_; + my $self = $class->SUPER::new($check_name, $config); + + my $metric_separator_pattern = $config->{'metric_separator_pattern'} || '/ /'; + $self->{'metric_separator_pattern'} = trim_slashes($metric_separator_pattern); + + my $key_value_separator_pattern = $config->{'key_value_separator_pattern'} || '/:/'; + $self->{'key_value_separator_pattern'} = trim_slashes($key_value_separator_pattern); + + bless($self, $class); + return $self; +} + +sub trim_slashes { + my $string = shift; + $string =~ s/^\/?(.*?)\/?$/$1/; + return $string; +} + +sub content_to_hash { + my ($self, $content) = @_; + + my $result = {}; + + my @metrics_array = split(/$self->{'metric_separator_pattern'}/, $content); + foreach my $metric (@metrics_array) { + chomp($metric); + my ($key, $val) = split(/$self->{'key_value_separator_pattern'}/, $metric); + $result->{$key} = [$val, 'n']; + } + + return $result; +} + +sub handler { + my $self = shift; + my $config = $self->{'config'}; + my $cmd = $config->{'cmd'} || die "Command is required.\n"; + + my $output = run_command($cmd); + + return $self->content_to_hash($output); +} + +sub wildcard_handler { + my $self = shift; + my $config = $self->{'config'}; + my $cmd = $config->{'cmd'} || die "Command is required.\n"; + my $check_name_key= $config->{'check_name_key'} || die "Check name key is required.\n"; + + my $output = run_command($cmd); + + my $result = {}; + my $line_counter = 0; + foreach my $line (split(/\n/, $output)) { + my $line_result = $self->content_to_hash($line); + + my $check_name = delete($line_result->{$check_name_key}); + if(defined($check_name)) { + if (ref($check_name) eq 'ARRAY') { + $check_name = ${$check_name}[0]; + } + } else { + $check_name = "unknown_$line_counter"; + } + + $result->{$check_name} = $line_result; + $line_counter++; + } + + return $result; +} + +1; \ No newline at end of file diff --git a/lib/Core/MetricsFile.pm b/lib/Core/MetricsFile.pm new file mode 100644 index 0000000..0629a05 --- /dev/null +++ b/lib/Core/MetricsFile.pm @@ -0,0 +1,151 @@ +package Core::MetricsFile; + +use strict; +use warnings; + +use base 'Resmon::Module'; + +=pod + +=head1 NAME + +Core::MetricsFile - retrieve metrics from a flat file + +=head1 SYNOPSIS + + Core::MetricsFile { + local : file => /path/to/metrics/file + } + + Core::MetricsFile { + local : file => /path/to/metrics/file, metric_separator_pattern => / /, key_value_separator_pattern => /:/ + } + + Core::MetricsFile { + * : file => /path/to/metrics/multi-line-file, check_name_key => id + } + +=head1 DESCRIPTION + +Retrieve metrics from existing flat file. + +=head1 CONFIGURATION + +=over + +=item check_name + +Arbitrary name of the check, or * if file contains multiple checks. If * is +specified, check_name_key must also be specified, and any values in the file +for this key will be used as check names. + +=item file + +The full path to the file containing metrics data (required). + +=item metric_separator_pattern + +A regular expression that defines how multiple metrics are separated (defaults to / /) + +=item key_value_separator_pattern + +A regular expression that defines how metric keys are are separated from values (defaults to /:/) + +=item check_name_key + +If the file contains multiple checks, the key that's value will be used as the check name (valid and required only if * specified as check_name) + +=back + +=head1 METRICS + +=over + +=item * + +Metrics depend on contents of file. + +=back + +=cut + +sub new { + my ($class, $check_name, $config) = @_; + my $self = $class->SUPER::new($check_name, $config); + + my $metric_separator_pattern = $config->{'metric_separator_pattern'} || '/ /'; + $self->{'metric_separator_pattern'} = trim_slashes($metric_separator_pattern); + + my $key_value_separator_pattern = $config->{'key_value_separator_pattern'} || '/:/'; + $self->{'key_value_separator_pattern'} = trim_slashes($key_value_separator_pattern); + + bless($self, $class); + return $self; +} + +sub trim_slashes { + my $string = shift; + $string =~ s/^\/?(.*?)\/?$/$1/; + return $string; +} + +sub content_to_hash { + my ($self, $content) = @_; + + my $result = {}; + + my @metrics_array = split(/$self->{'metric_separator_pattern'}/, $content); + foreach my $metric (@metrics_array) { + chomp($metric); + my ($key, $val) = split(/$self->{'key_value_separator_pattern'}/, $metric); + $result->{$key} = [$val, 'n']; + } + + return $result; +} + +sub handler { + my $self = shift; + my $config = $self->{'config'}; + my $file = $config->{'file'} || die "File is required.\n"; + + local $/=undef; + open(my $FH, "<", $file) or die "Couldn't open file $file: $!\n"; + my $output = <$FH>; + close $FH; + + return $self->content_to_hash($output); +} + +sub wildcard_handler { + my $self = shift; + my $config = $self->{'config'}; + my $file = $config->{'file'} || die "File is required.\n"; + my $check_name_key= $config->{'check_name_key'} || die "Check name key is required.\n"; + + open(my $FH, "<", $file) or die "Couldn't open file $file: $!\n"; + my @output = <$FH>; + close $FH; + + my $result = {}; + my $line_counter = 0; + foreach my $line (@output) { + my $line_result = $self->content_to_hash($line); + + my $check_name = delete($line_result->{$check_name_key}); + if(defined($check_name)) { + if (ref($check_name) eq 'ARRAY') { + $check_name = ${$check_name}[0]; + } + } else { + $check_name = "unknown_$line_counter"; + } + + $result->{$check_name} = $line_result; + $line_counter++; + } + + return $result; +} + +1; \ No newline at end of file diff --git a/lib/Core/SNMP.pm b/lib/Core/SNMP.pm new file mode 100644 index 0000000..733d018 --- /dev/null +++ b/lib/Core/SNMP.pm @@ -0,0 +1,233 @@ +package Core::SNMP; + +use strict; +use warnings; +use base 'Resmon::Module'; +use Net::SNMP; + +=pod + +=head1 NAME + +Core::SNMP - Get values via SNMP, either individual metrics or parts of tables + +=head1 SYNOPSIS + + Core::SNMP { + ProcessCount : oid => 1.3.6.1.2.1.25.1.6.0 + } + + Core::SNMP { + NetInPackets : mode => table, oid_instance_name => 1.3.6.1.2.1.2.2.1.2, oid_names_values => packets_in 1.3.6.1.2.1.2.2.1.10 packets_out 1.3.6.1.2.1.2.2.1.16 + } + + Core::SNMP { + NetInPackets : mode => table, oid_instance_name => 1.3.6.1.2.1.2.2.1.2, oid_names_values => packets_in 1.3.6.1.2.1.2.2.1.10 packets_out 1.3.6.1.2.1.2.2.1.16, oid_filter => 1.3.6.1.2.1.2.2.1.2, filter_values => nge0 nge1 + } + + +=head1 DESCRIPTION + +This module retrieves metric values from an SNMP server, with two different modes of operation + +In "single" mode, a single value is retrieved for the specified oid. + +In "table" mode, an SNMP table is queried, using oid_instance_name as the metric name, and oid_value as the metric value. + + +=head1 CONFIGURATION + +=over + +=item check_name + +Arbitrary name of the check. + +=item mode + +Mode of operation, either "single" or "table (defaults to "single"). + +=item oid + +SNMP OID to query for a value in "single" mode (required if mode="single"). + +=item oid_instance_name + +SNMP OID to query for the metric name of each item in a table (required if mode="table"). + +=item oid_names_values + +List of names/SNMP OID pairs to query for the metric values of each item in a table, space-separated (required if mode="table"). + +=item oid_filter + +SNMP OID to query and apply as a filter in conjunction with filter_values to each item in a table (required if filter_values is specified). + +=item filter_values + +Values from oid_filter to apply as a filter in conjunction with oid_filter to each item in a table (required if oid_filter is specified). + +=item community + +SNMP community string to use (defaults to "public"). + +=item hostname + +SNMP hostname to use to (defaults to "127.0.0.1"). + +=item port + +SNMP port to use to (defaults to 161). + +=item name_pattern + +A Perl regular expression used to transform the returned names into friendlier metric names (optional if mode=""table"", defaults to no pattern). + +=back + +=cut + +sub new { + my ($class, $check_name, $config) = @_; + my $self = $class->SUPER::new($check_name, $config); + + my $mode = $config->{'mode'} || 'table'; + my $name_pattern = $config->{'name_pattern'}; + if ('table' eq $mode && defined($name_pattern)) { + $name_pattern = trim_slashes($name_pattern); + my ($name_pattern_search, $name_pattern_replace) = split(/(?{'name_pattern_search'} = $name_pattern_search; + $self->{'name_pattern_replace'} = '"' . $name_pattern_replace . '"'; + } + + bless($self, $class); + return $self; +} + + +sub get_snmp_value { + my ($session, $oid) = @_; + + my $hash = $session->get_request($oid); + + if (!defined($hash)) { + print STDERR "SNMP Failure getting OID $oid\n"; + print STDERR $session->error() . "\n"; + } + + return $hash->{$oid}; +} + +sub get_snmp_values { + my ($session, $oid) = @_; + + my $hash = $session->get_table($oid); + + if (!defined($hash)) { + print STDERR "SNMP Failure getting table OID $oid\n"; + print STDERR $session->error() . "\n"; + } + + return $hash; +} + +sub get_last_oid { + my ($oid) = @_; + $oid =~ s/^[0-9.]*\.([0-9])/$1/; + return $oid; +} + +sub trim_slashes { + my $string = shift; + $string =~ s/^\/?(.*?)\/?$/$1/; + return $string; +} + +sub handler { + + my $self = shift; + my $config = $self->{'config'}; + + my $mode = $config->{'mode'} || 'single'; + my $community = $config->{'community'} || 'public'; + my $hostname = $config->{'hostname'} || '127.0.0.1'; + my $port = $config->{'port'} || 161; + my $version = $config->{'version'} || 2; + + # Start SNMP session + my ($session, $error) = Net::SNMP->session( + -hostname => $hostname, + -community => $community, + -timeout => "10", + -port => $port, + -version => $version); + + if (!defined($session)) { + printf STDERR "ERR: $error\n"; + return; + } + + my $result = {}; + if ('single' eq $mode) { + + my $oid = $config->{'oid'} || die "Parameter oid is required"; + + my $name = $self->{'check_name'}; + my $value = get_snmp_value($session, $oid); + $result->{$name} = $value; + + } elsif ('table' eq $mode) { + + my $oid_instance_name = $config->{'oid_instance_name'} || die "Paramter oid_instance_name is required"; + my $oid_names_values = $config->{'oid_names_values'} || die "Parameter oid_names_values is required"; + my $oid_filter = $config->{'oid_filter'}; + my $filter_values = $config->{'filter_values'}; + my $name_pattern_search = $self->{'name_pattern_search'}; + my $name_pattern_replace = $self->{'name_pattern_replace'}; + + + my $instance_names_by_oid = get_snmp_values($session, $oid_instance_name); + my $instance_names_by_index = { map { get_last_oid($_) => $instance_names_by_oid->{$_} } keys %$instance_names_by_oid }; + + if($oid_filter && $filter_values) { + my $filter_values_hash = { map { $_ => 1 } split(/\s+/, $filter_values) }; + + my $filter_values_by_oid = get_snmp_values($session, $oid_filter); + my $filter_values_by_index = { map { get_last_oid($_) => $filter_values_by_oid->{$_} } keys %$filter_values_by_oid }; + + $instance_names_by_index = { map { + exists $filter_values_by_index->{$_} && exists $filter_values_hash->{$filter_values_by_index->{$_}} + ? ($_ => $instance_names_by_index->{$_}) + : () } keys %$instance_names_by_index }; + } + + my %oid_name_value_hash = split(/\s+/, $oid_names_values); + while( my ($name, $oid_value) = each %oid_name_value_hash ) { + my $values_by_oid = get_snmp_values($session, $oid_value); + while( my ($oid, $value) = each %$values_by_oid ) { + my $instance_name = $instance_names_by_index->{get_last_oid($oid)}; + if($instance_name) { + $result->{"$instance_name-$name"} = $value; + } + } + } + + if(defined($name_pattern_search) && defined($name_pattern_replace)) { + my $transformed = {}; + while( my ($key, $val) = each %$result ) { + $key =~ s/$name_pattern_search/$name_pattern_replace/gee; + $transformed->{$key} = $val; + } + $result = $transformed; + } + + } else { + + die "Unsupported mode $mode"; + + } + $session->close(); + + return $result; +}; +1;