#!/usr/bin/perl # =head1 NAME Memcached - A Plugin to monitor Memcached Servers (Multigraph) =head1 MUNIN CONFIGURATION [memcached_*] env.host 127.0.0.1 *default* env.port 11211 *default* env.timescale 3 *default* =head2 MUNIN ENVIRONMENT CONFIGURATION EXPLANATION host = host we are going to monitor port = port we are connecting to, in order to gather stats timescale = what time frame do we want to format our graphs too =head1 NODE CONFIGURATION Please make sure you can telnet to your memcache servers and issue the following commands: stats, stats settings, stats items and stats slabs. Available Graphs contained in this Plugin bytes => This graphs the current network traffic in and out commands => I This graphs the current commands being issued to the memcache machine. B conns => This graphs the current, max connections as well as avg conns per sec avg conns per sec is derived from total_conns / uptime. evictions => I This graphs the current evictions on the node. B items => I This graphs the current items and total items in the memcached node. B memory => I This graphs the current and max memory allocation B The following example holds true for all graphing options in this plugin. Example: ln -s /usr/share/munin/plugins/memcached_multi_ /etc/munin/plugins/memcached_multi_bytes =head1 ADDITIONAL INFORMATION You will find that some of the graphs have LEI on them. This was done in order to save room on space for text and stands for B. The B variable formats certain graphs based on the following guidelines. 1 => Seconds 2 => Minutes 3 => Hours B<*Default*> 4 => Days =head1 ACKNOWLEDGEMENTS The core of this plugin is based on the mysql_ plugin maintained by Kjell-Magne Ãierud Thanks to dormando as well for putting up with me ;) =head1 AUTHOR Matt West < https://code.google.com/p/memcached-munin-plugin/ > =head1 LICENSE GPLv2 =head1 MAGIC MARKERS #%# family=auto #%# capabilities=autoconf suggest =cut use strict; use IO::Socket; use Munin::Plugin; need_multigraph(); my $host = $ENV{host} || "127.0.0.1"; my $port = $ENV{port} || 11211; my %stats; # This hash contains the information contained in two memcache commands # stats and stats settings. my %items; # This gives us eviction rates and other hit stats per slab # We track this so we can see if something was evicted earlier than necessary my %chnks; # This gives us the memory size and usage per slab # We track this so we can see what slab is being used the most and has no free chunks # so we can re-tune memcached to allocate more pages for the specified chunk size my $timescale = $ENV{timescale} || 3; # This gives us the ability to control the timescale our graphs are displaying. # The default it set to divide by hours, if you want to get seconds set it to 1. # Options: 1 = seconds, 2 = minutes, 3 = hours, 4 = days # So I was trying to figure out how to build this up, and looking at some good examples # I decided to use the format, or for the most part, the format from the mysql_ munin plugin # for Innodb by Kjell-Magne Ãierud, it just spoke ease of flexibility especially with multigraphs # thanks btw ;) # # %graphs is a container for all of the graph definition information. In here is where you'll # find the configuration information for munin's graphing procedure. # Format: # # $graph{graph_name} => { # config => { # # You'll find keys and values stored here for graph manipulation # }, # datasrc => [ # # Name: name given to data value # # Attr: Attribute for given value # { name => 'Name', (Attr) }, # { ... }, # ], # } my %graphs; $graphs{items} = { config => { args => '--base 1000 --lower-limit 0', vlabel => 'Items in Memcached', category => 'memcached', title => 'Items', info => 'This graph shows the number of items in use by memcached', }, datasrc => [ { name => 'curr_items', label => 'Current Items', min => '0' }, { name => 'total_items', label => 'New Items', min => '0', type => 'DERIVE' }, ], }; $graphs{memory} = { config => { args => '--base 1024 --lower-limit 0', vlabel => 'Bytes Used', category => 'memcached', title => 'Memory Usage', info => 'This graph shows the memory consumption of memcached', }, datasrc => [ { name => 'limit_maxbytes', draw => 'AREA', label => 'Maximum Bytes Allocated', min => '0' }, { name => 'bytes', draw => 'AREA', label => 'Current Bytes Used', min => '0' }, ], }; $graphs{bytes} = { config => { args => '--base 1000', vlabel => 'bits in (-) / out (+)', title => 'Network Traffic', category => 'memcached', info => 'This graph shows the network traffic in (-) / out (+) of the machine', order => 'bytes_read bytes_written', }, datasrc => [ { name => 'bytes_read', type => 'DERIVE', label => 'Network Traffic coming in (-)', graph => 'no', cdef => 'bytes_read,8,*', min => '0' }, { name => 'bytes_written', type => 'DERIVE', label => 'Traffic in (-) / out (+)', negative => 'bytes_read', cdef => 'bytes_written,8,*', min => '0' }, ], }; $graphs{conns} = { config => { args => '--base 1000 --lower-limit 0', vlabel => 'Connections per ${graph_period}', category => 'memcached', title => 'Connections', info => 'This graph shows the number of connections being handled by memcached', order => 'max_conns curr_conns avg_conns', }, datasrc => [ { name => 'curr_conns', label => 'Current Connections', min => '0' }, { name => 'max_conns', label => 'Max Connections', min => '0' }, { name => 'avg_conns' , label => 'Avg Connections', min => '0' }, ], }; $graphs{commands} = { config => { args => '--base 1000 --lower-limit 0', vlabel => 'Commands per ${graph_period}', category => 'memcached', title => 'Commands', info => 'This graph shows the number of commands being handled by memcached', }, datasrc => [ { name => 'cmd_get', type => 'DERIVE', label => 'Gets', info => 'Cumulative number of retrieval reqs', min => '0' }, { name => 'cmd_set', type => 'DERIVE', label => 'Sets', info => 'Cumulative number of storage reqs', min => '0' }, { name => 'get_hits', type => 'DERIVE', label => 'Get Hits', info => 'Number of keys that were requested and found', min => '0' }, { name => 'get_misses', type => 'DERIVE', label => 'Get Misses', info => 'Number of keys there were requested and not found', min => '0' }, { name => 'delete_hits', type => 'DERIVE', label => 'Delete Hits', info => 'Number of delete requests that resulted in a deletion of a key', min => '0' }, { name => 'delete_misses', type => 'DERIVE', label => 'Delete Misses', info => 'Number of delete requests for missing key', min => '0' }, { name => 'incr_hits', type => 'DERIVE', label => 'Increment Hits', info => 'Number of successful increment requests', min => '0' }, { name => 'incr_misses', type => 'DERIVE', label => 'Increment Misses', info => 'Number of unsuccessful increment requests', min => '0' }, { name => 'decr_hits', type => 'DERIVE', label => 'Decrement Hits', info => 'Number of successful decrement requests', min => '0' }, { name => 'decr_misses', type => 'DERIVE', label => 'Decrement Misses', info => 'Number of unsuccessful decrement requests', min => '0' }, ], }; $graphs{evictions} = { config => { args => '--base 1000 --lower-limit 0', vlabel => 'Evictions per ${graph_period}', category => 'memcached', title => 'Evictions', info => 'This graph shows the number of evictions per second', }, datasrc => [ { name => 'evictions', label => 'Evictions', info => 'Cumulative Evictions Across All Slabs', type => 'DERIVE', min => '0' }, { name => 'evicted_nonzero', label => 'Evictions prior to Expire', info => 'Cumulative Evictions forced to expire prior to expiration', type => 'DERIVE', min => '0' }, { name => 'reclaimed', label => 'Reclaimed Items', info => 'Cumulative Reclaimed Item Entries Across All Slabs', type => 'DERIVE', min => '0' }, ], }; $graphs{slabchnks} = { config => { args => '--base 1000 --lower-limit 0', vlabel => 'Available Chunks for this Slab', category => 'memcached', title => 'Chunk Usage for Slab: ', info => 'This graph shows you the chunk usage for this memory slab.', }, datasrc => [ { name => 'total_chunks', label => 'Total Chunks Available', min => '0' }, { name => 'used_chunks', label => 'Total Chunks in Use', min => '0' }, { name => 'free_chunks', label => 'Total Chunks Not in Use (Free)', min => '0' }, ], }; $graphs{slabhits} = { config => { args => '--base 1000 --lower-limit 0', vlabel => 'Hits per Slab per ${graph_period}', category => 'memcached', title => 'Hits for Slab: ', info => 'This graph shows you the successful hit rate for this memory slab.', }, datasrc => [ { name => 'get_hits', label => 'Get Requests', type => 'DERIVE', min => '0' }, { name => 'cmd_set', label => 'Set Requests', type => 'DERIVE', min => '0' }, { name => 'delete_hits', label => 'Delete Requests', type => 'DERIVE', min => '0' }, { name => 'incr_hits', label => 'Increment Requests', type => 'DERIVE', min => '0' }, { name => 'decr_hits', label => 'Decrement Requests', type => 'DERIVE', min => '0' }, ], }; $graphs{slabevics} = { config => { args => '--base 1000 --lower-limit 0', vlabel => 'Evictions per Slab per ${graph_period}', category => 'memcached', title => 'Evictions for Slab: ', info => 'This graph shows you the eviction rate for this memory slab.', }, datasrc => [ { name => 'evicted', label => 'Total Evictions', type => 'DERIVE', min => '0' }, { name => 'evicted_nonzero', label => 'Evictions from LRU Prior to Expire', type => 'DERIVE', min => '0' }, { name => 'reclaimed', label => 'Reclaimed Expired Items', info => 'This is number of times items were stored in expired entry memory space', type => 'DERIVE', min => '0' }, ], }; $graphs{slabevictime} = { config => { args => '--base 1000 --lower-limit 0', vlabel => ' since Request for LEI', category => 'memcached', title => 'Eviction Request Time for Slab: ', info => 'This graph shows you the time since we requested the last evicted item', }, datasrc => [ { name => 'evicted_time', label => 'Eviction Time (LEI)', info => 'Time Since Request for Last Evicted Item', min => '0' }, ], }; $graphs{slabitems} = { config => { args => '--base 1000 --lower-limit 0', vlabel => 'Items per Slab', category => 'memcached', title => 'Items in Slab: ', info => 'This graph shows you the number of items and reclaimed items per slab.', }, datasrc => [ { name => 'number', label => 'Items', info => 'This is the amount of items stored in this slab', min => '0' }, ], }; $graphs{slabitemtime} = { config => { args => '--base 1000 --lower-limit 0', vlabel => ' since item was stored', category => 'memcached', title => 'Age of Eldest Item in Slab: ', info => 'This graph shows you the time of the eldest item in this slab', }, datasrc => [ { name => 'age', label => 'Eldest Item\'s Age', min => '0' }, ], }; ## #### Config Check #### ## if (defined $ARGV[0] && $ARGV[0] eq 'config') { $0 =~ /memcached_multi_(.+)*/; my $plugin = $1; die 'Unknown Plugin Specified: ' . ($plugin ? $plugin : '') unless $graphs{$plugin}; # We need to fetch the stats before we do any config, cause its needed for multigraph fetch_stats(); # Now lets go ahead and print out our config. do_config($plugin); exit 0; } ## #### Autoconf Check #### ## if (defined $ARGV[0] && $ARGV[0] eq 'autoconf') { my $s = IO::Socket::INET->new( Proto => "tcp", PeerAddr => $host, PeerPort => $port, ); if (defined($s)) { print "yes\n"; exit 0; } else { print "no (unable to connect to $host\[:$port\])\n"; exit 0; } } ## #### Suggest Check #### ## if (defined $ARGV[0] && $ARGV[0] eq 'suggest') { my $s = IO::Socket::INET->new( Proto => "tcp", PeerAddr => $host, PeerPort => $port, ); if (defined($s)) { my @rootplugins = ('bytes','conns','commands','evictions','items','memory'); foreach my $plugin (@rootplugins) { print "$plugin\n"; } exit 0; } else { print "no (unable to connect to $host\[:$port\])\n"; exit 0; } } ## #### Well We aren't running (auto)config/suggest so lets print some stats #### ## fetch_output(); ## #### Subroutines for printing info gathered from memcached #### ## ## #### This subroutine performs the bulk processing for printing statistics. ## sub fetch_output { $0 =~ /memcached_multi_(.+)*/; my $plugin = $1; die 'Unknown Plugin Specified: ' . ($plugin ? $plugin : '') unless $graphs{$plugin}; # Well we need to actually fetch the stats before we do anything to them. fetch_stats(); # Now lets go ahead and print out our output. my @subgraphs; if ($plugin eq 'memory') { @subgraphs = ('slabchnks'); foreach my $slabid(sort{$a <=> $b} keys %chnks) { print_submulti_output($slabid,$plugin,@subgraphs); } print_rootmulti_output($plugin); } elsif ($plugin eq 'commands') { @subgraphs = ('slabhits'); foreach my $slabid(sort{$a <=> $b} keys %chnks) { print_submulti_output($slabid,$plugin,@subgraphs); } print_rootmulti_output($plugin); } elsif ($plugin eq 'evictions') { @subgraphs = ('slabevics','slabevictime'); foreach my $slabid (sort{$a <=> $b} keys %items) { print_submulti_output($slabid,$plugin,@subgraphs); } print_rootmulti_output($plugin); } elsif ($plugin eq 'items') { @subgraphs = ('slabitems','slabitemtime'); foreach my $slabid (sort{$a <=> $b} keys %items) { print_submulti_output($slabid,$plugin,@subgraphs); } print_rootmulti_output($plugin); } else { print_root_output($plugin); } return; } ## #### This subroutine is for the root non-multigraph graphs which render on the main node page #### ## sub print_root_output { my ($plugin) = (@_); my $graph = $graphs{$plugin}; print "graph memcached_$plugin\n"; if ($plugin ne 'conns') { foreach my $dsrc (@{$graph->{datasrc}}) { my %datasrc = %$dsrc; while ( my ($key, $value) = each(%datasrc)) { next if ($key ne 'name'); my $output = $stats{$value}; print "$dsrc->{name}.value $output\n"; } } } else { my $output; foreach my $dsrc (@{$graph->{datasrc}}) { my %datasrc = %$dsrc; while ( my ($key, $value) = each(%datasrc)) { if ($value eq 'max_conns') { $output = $stats{maxconns}; } elsif ($value eq 'curr_conns') { $output = $stats{curr_connections}; } elsif ($value eq 'avg_conns') { $output = sprintf("%02d", $stats{total_connections} / $stats{uptime}); } else { next; } print "$dsrc->{name}.value $output\n"; } } } return; } ## #### This subroutine is for the root multigraph graphs which render on the main node page #### ## sub print_rootmulti_output { my ($plugin) = (@_); my $graph = $graphs{$plugin}; print "multigraph memcached_$plugin\n"; foreach my $dsrc (@{$graph->{datasrc}}) { my $output = 0; my %datasrc = %$dsrc; while ( my ($key, $value) = each(%datasrc)) { next if ($key ne 'name'); if (($plugin eq 'evictions') && ($value eq 'evicted_nonzero')) { foreach my $slabid (sort{$a <=> $b} keys %items) { $output += $items{$slabid}->{evicted_nonzero}; } } else { $output = $stats{$value}; } print "$dsrc->{name}.value $output\n"; } } return; } ## #### This subroutine is for the sub multigraph graphs created via the multigraph plugin #### ## sub print_submulti_output { my ($slabid,$plugin,@subgraphs) = (@_); my $currslab = undef; foreach my $sgraph (@subgraphs) { my $graph = $graphs{$sgraph}; print "multigraph memcached_$plugin.$sgraph\_$slabid\n"; if ($plugin eq 'evictions') { $currslab = $items{$slabid}; } elsif ($plugin eq 'memory') { $currslab = $chnks{$slabid}; } elsif ($plugin eq 'commands') { $currslab = $chnks{$slabid}; } elsif ($plugin eq 'items') { $currslab = $items{$slabid}; } else { return; } foreach my $dsrc (@{$graph->{datasrc}}) { my %datasrc = %$dsrc; while ( my ($key, $value) = each(%datasrc)) { next if ($key ne 'name'); my $output = $currslab->{$value}; if (($sgraph eq 'slabevictime') || ($sgraph eq 'slabitemtime')) { $output = time_scale('data',$output); ; } print "$dsrc->{name}.value $output\n"; } } } return; } ## #### Subroutines for printing out config information for graphs #### ## ## #### This subroutine does the bulk printing the config info per graph #### ## sub do_config { my ($plugin) = (@_); my @subgraphs; if ($plugin eq 'memory') { @subgraphs = ('slabchnks'); foreach my $slabid (sort{$a <=> $b} keys %chnks) { print_submulti_config($slabid,$plugin,@subgraphs); } print_rootmulti_config($plugin); } elsif ($plugin eq 'commands') { @subgraphs = ('slabhits'); foreach my $slabid (sort{$a <=> $b} keys %chnks) { print_submulti_config($slabid,$plugin,@subgraphs); } print_rootmulti_config($plugin); } elsif ($plugin eq 'evictions') { @subgraphs = ('slabevics','slabevictime'); foreach my $slabid (sort{$a <=> $b} keys %items) { print_submulti_config($slabid,$plugin,@subgraphs); } print_rootmulti_config($plugin); } elsif ($plugin eq 'items') { @subgraphs = ('slabitems','slabitemtime'); foreach my $slabid (sort{$a <=> $b} keys %items) { print_submulti_config($slabid,$plugin,@subgraphs); } print_rootmulti_config($plugin); } else { print_root_config($plugin); } return; } ## #### This subroutine is for the config info for sub multigraph graphs created via the multigraph plugin #### ## sub print_submulti_config { my ($slabid,$plugin,@subgraphs) = (@_); my ($slabitems,$slabchnks) = undef; foreach my $sgraph (@subgraphs) { my $graph = $graphs{$sgraph}; my %graphconf = %{$graph->{config}}; print "multigraph memcached_$plugin.$sgraph\_$slabid\n"; while ( my ($key, $value) = each(%graphconf)) { if ($key eq 'title') { print "graph_$key $value" . "$slabid" . " ($chnks{$slabid}->{chunk_size} Bytes)\n"; } elsif (($key eq 'vlabel') && (($sgraph eq 'slabevictime') || ($sgraph eq 'slabitemtime'))) { $value = time_scale('config',$value); print "graph_$key $value\n"; } else { print "graph_$key $value\n"; } } foreach my $dsrc (@{$graph->{datasrc}}) { my %datasrc = %$dsrc; while ( my ($key, $value) = each(%datasrc)) { next if ($key eq 'name'); print "$dsrc->{name}.$key $value\n"; } } } return; } ## #### This subroutine is for the config info for root multigraph graphs which render on the main node page #### ## sub print_rootmulti_config { my ($plugin) = (@_); die 'Unknown Plugin Specified: ' . ($plugin ? $plugin : '') unless $graphs{$plugin}; my $graph = $graphs{$plugin}; my %graphconf = %{$graph->{config}}; print "multigraph memcached_$plugin\n"; while ( my ($key, $value) = each(%graphconf)) { print "graph_$key $value\n"; } foreach my $dsrc (@{$graph->{datasrc}}) { my %datasrc = %$dsrc; while ( my ($key, $value) = each(%datasrc)) { next if ($key eq 'name'); print "$dsrc->{name}.$key $value\n"; } } return; } ## #### This subroutine is for the config info for non multigraph graphs which render on the main node page #### ## sub print_root_config { my ($plugin) = (@_); die 'Unknown Plugin Specified: ' . ($plugin ? $plugin : '') unless $graphs{$plugin}; my $graph = $graphs{$plugin}; my %graphconf = %{$graph->{config}}; print "graph memcached_$plugin\n"; while ( my ($key, $value) = each(%graphconf)) { print "graph_$key $value\n"; } foreach my $dsrc (@{$graph->{datasrc}}) { my %datasrc = %$dsrc; while ( my ($key, $value) = each(%datasrc)) { next if ($key eq 'name'); print "$dsrc->{name}.$key $value\n"; } } return; } ## #### This subroutine actually performs the data fetch for us #### #### These commands do not lock up Memcache at all #### ## sub fetch_stats { my $s = IO::Socket::INET->new( Proto => "tcp", PeerAddr => $host, PeerPort => $port, ); die "Error: Unable to Connect to $host\[:$port\]\n" unless $s; print $s "stats\r\n"; while (my $line = <$s>) { if ($line =~ /STAT\s(.+?)\s(\d+)/) { my ($skey,$svalue) = ($1,$2); $stats{$skey} = $svalue; } last if $line =~ /^END/; } print $s "stats settings\r\n"; while (my $line = <$s>) { if ($line =~ /STAT\s(.+?)\s(\d+)/) { my ($skey,$svalue) = ($1,$2); $stats{$skey} = $svalue; } last if $line =~ /^END/; } print $s "stats slabs\r\n"; while (my $line = <$s>) { if ($line =~ /STAT\s(\d+):(.+)\s(\d+)/) { my ($slabid,$slabkey,$slabvalue) = ($1,$2,$3); $chnks{$slabid}->{$slabkey} = $slabvalue; } last if $line =~ /^END/; } print $s "stats items\r\n"; while (my $line = <$s>) { if ($line =~ /STAT\sitems:(\d+):(.+?)\s(\d+)/) { my ($itemid,$itemkey,$itemvalue) = ($1,$2,$3); $items{$itemid}->{$itemkey} = $itemvalue; } last if $line =~ /^END/; } } ## #### This subroutine is to help manage the time_scale settings for the graph ## sub time_scale { my ($configopt,$origvalue) = (@_); my $value; if ($configopt eq 'config') { if ($timescale == 1) { $value = "Seconds" . $origvalue; } elsif ($timescale == 2) { $value = "Minutes" . $origvalue; } elsif (($timescale == 3) || ($timescale > 4) || (!defined($timescale))) { $value = "Hours" . $origvalue; } elsif ($timescale == 4) { $value = "Days" . $origvalue; } } elsif ($configopt eq 'data') { if ($timescale == 1) { $value = sprintf("%02.2f", $origvalue / 1); } elsif ($timescale == 2) { $value = sprintf("%02.2f", $origvalue / 60); } elsif (($timescale == 3) || ($timescale > 4) || (!defined($timescale))) { $value = sprintf("%02.2f", $origvalue / 3600); } elsif ($timescale == 4) { $value = sprintf("%02.2f", $origvalue / 86400); } } else { die "Unknown time_scale option given: either [config/data]\n"; } return $value; }