#!/usr/bin/perl -w # -*- perl -*- =head1 NAME if - Multigraph plugin to monitor network wired and wireless interfaces =head1 INTERPRETATION In the general graphs made key fields for each interface, a subsidiary graphs for each interface there are detailed This plugin displays the following charts: Traffic, bit Traffic, packets Average packet size Interface errors WiFi interface errors WiFi interface signal and noise WiFi interface link quality Interface utilisation Virtual interface names prefixes with '~' =head1 CONFIGURATION This plugin is configurable environment variables. env.exclude - Removing interfaces from graphs, default empty env.include - Includes interfaces into graphs, default empty env.if_max_bps - Maximum interface bps. Available suffixes: k, M, G, default empty env.protexct_peaks - Protect graph peaks, default 'no' env.min_packet_size - Minimal network packet size, default 20 Example: [if] env.exclude lo env.include wlan0 tun0 env.wlan0_max_bps 54M env.eth0_max_bps 1G env.protect_peaks yes Protect peak: 1. Protect wifi signal and noise values, all values > 0 print as NaN 2. protect all percent values. All values > 100 print as NaN 3. Protect bps values. env.if_max_bps must be set. All values > max_bps prints as 0 4. protect pps values. env.if_max_bps must be set. All values > max_bps/minimal packet size, prints as 0 =head1 AUTHOR Gorlow Maxim aka Sheridan (email and jabber) =head1 LICENSE GPLv2 =head1 MAGIC MARKERS #%# family=auto #%# capabilities=autoconf =cut use strict; use warnings; use IO::Dir; use Munin::Plugin; use Data::Dumper; # ------------------------------------------------------------- constants --------------------- my $exclude = $ENV{exclude} || ''; my $include = $ENV{include} || '-'; my $protect_peacks = $ENV{protect_peaks} || 'no'; my $min_packet_size = $ENV{min_packet_size} || 20; my $ifpath = '/sys/class/net'; # ----------------------------------- global ----------------- my $interfaces = {}; # ------------------------ available graphs ------------------------- my $graphs = { 'if_bit' => { 'munin' => { 'category' => 'network', 'args' => '--base 1000', 'title' => ':if: traffic, bit', 'vlabel' => 'Bit in (-) / out (+), avg. per second', 'info' => 'This graph shows the traffic in bit of the :if:, averaged value per second from last update' }, 'per_if_fields' => [qw(rx_bytes tx_bytes)], 'general_fields' => [qw(rx_bytes tx_bytes)] }, 'if_packets' => { 'munin' => { 'category' => 'network', 'args' => '--base 1000', 'title' => ':if: traffic, packets', 'vlabel' => 'Packets in (-) / out (+), avg. per second', 'info' => 'This graph shows the traffic in packets of the :if:, averaged value per second from last update' }, 'per_if_fields' => [qw(rx_packets tx_packets rx_compressed tx_compressed rx_dropped tx_dropped multicast)], 'general_fields' => [qw(rx_packets tx_packets)] }, 'if_errors' => { 'munin' => { 'category' => 'network', 'args' => '--base 1000', 'title' => ':if: errors', 'vlabel' => 'Errors RX (-) / TX (+)', 'info' => 'This graph shows the errors of the :if: from last update', 'scale' => 'no' }, 'per_if_fields' => [qw(rx_errors tx_errors rx_fifo_errors tx_fifo_errors rx_crc_errors rx_frame_errors rx_length_errors rx_missed_errors rx_over_errors collisions tx_aborted_errors tx_carrier_errors tx_heartbeat_errors tx_window_errors)], 'general_fields' => [qw(rx_errors tx_errors)] }, 'if_wifi_sino' => { 'munin' => { 'category' => 'wireless', 'args' => '--base 1000 -u 0', 'title' => ':if: signal and noise levels', 'vlabel' => 'dB', 'info' => 'This graph shows the WiFi signal and noise levels of the :if:', 'scale' => 'no' }, 'per_if_fields' => [qw(signal noise)], 'general_fields' => [qw(signal)] }, 'if_wifi_link_quality' => { 'munin' => { 'category' => 'wireless', 'args' => '--base 1000', 'title' => ':if: link quality', 'vlabel' => '%', 'info' => 'This graph shows the WiFi link quality of the :if:', 'scale' => 'no' }, 'per_if_fields' => [qw(link)], 'general_fields' => [qw(link)] }, 'if_wifi_errors' => { 'munin' => { 'category' => 'wireless', 'args' => '--base 1000', 'title' => ':if: errors', 'vlabel' => 'Errors RX (-) / TX (+)', 'info' => 'This graph shows the WiFi errors of the :if: from last update', 'scale' => 'no' }, 'per_if_fields' => [qw(nwid fragment crypt beacon retries misc)], 'general_fields' => [qw(rx_wifierr tx_wifierr)] }, 'if_utilisation' => { 'munin' => { 'category' => 'network', 'args' => '--base 1000', 'title' => ':if: utilisation', 'vlabel' => '%', 'info' => 'This graph shows utilisation of the :if:', 'scale' => 'no' }, 'per_if_fields' => [qw(rx_percent tx_percent)], 'general_fields' => [qw(rx_percent tx_percent)] }, 'if_avgpacketsize' => { 'munin' => { 'category' => 'network', 'args' => '--base 1024', 'title' => ':if: average packet size', 'vlabel' => 'bytes', 'info' => 'This graph shows average packet size of the :if:' }, 'per_if_fields' => [qw(rx_size tx_size)], 'general_fields' => [qw(rx_size tx_size)] } }; #-------------------------- available fields ------------------------- # info: # 'munin' => {} - just copy fields to munin config # 'source' => - field data source # { # 'type' => types: # 'file' - data just cat from file # 'location' => file location # 'calculated' => calculated data # { # 'type' - types: # 'percent', # 'full' => # { # 'source' => 'interface', # 'name' => 'bps' # }, # 'part' => # { # 'source' => 'field', # 'name' => 'tx_bytes' # } # 'division', # 'dividend' => # { # 'source' => 'field', # 'name' => 'rx_bytes' # }, # 'divider' => # { # 'source' => 'field', # 'name' => 'rx_packets' # } # 'sum', # 'sum' => [qw(nwid fragment crypt)] # # } # } # 'difference' => difference types: # count - just count from last update # per_secund - count from last update / time difference per last update # 'negative' => - if field under zero line # { # 'type' => types: # 'dummy' - dummy field, not draw # 'value' => '' - value for dummy field in update # 'field' - exists field, must be included in graph # 'name' => '' - field name # } # 'peack_protect' => protect peaks. Using munin field.max and field.min and truncate data to NaN # protect types # 'max_interface_bps' - maximum: max interface bps (if configured), minimum - zero # 'packet_size_range' - maximum: mtu, minimum: minimal packet size (may be configured) # 'max_interface_pps' - maximum: max interface bps/minimum packt size (if configured), minimum - zero # 'percents' - maximum: 100, minimum: 0 # 'min_number:max_number' - no comments :) my $fields = { 'collisions' => { 'munin' => { 'label' => 'Collisions' , 'info' => 'Transmit collisions', 'type' => 'GAUGE', 'draw' => 'LINE1' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/collisions' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'multicast' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Multicast packets', 'info' => 'Multicast packets received' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/multicast' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'rx_bytes' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX bit', 'info' => 'RX bit' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_bytes' }, 'cdef' => '8,*', 'peack_protect' => 'max_interface_bps', 'negative' => { 'type' => 'field', 'name' => 'tx_bytes' }, 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'rx_compressed' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX compressed packets', 'info' => 'Compressed packets', }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_compressed' }, 'peack_protect' => 'max_interface_pps', 'negative' => { 'type' => 'field', 'name' => 'tx_compressed' }, 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'rx_crc_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'CRC errors' , 'info' => 'CRC errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_crc_errors' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'rx_dropped' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX dropped packets', 'info' => 'Dropped frames' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_dropped' }, 'negative' => { 'type' => 'field', 'name' => 'tx_dropped' }, 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'rx_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX errors', 'info' => 'Bad packets' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_errors' }, 'negative' => { 'type' => 'field', 'name' => 'tx_errors' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'rx_fifo_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX FIFO errors', 'info' => 'FIFO overrun errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_fifo_errors' }, 'negative' => { 'type' => 'field', 'name' => 'tx_fifo_errors' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'rx_frame_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Frame format errors', 'info' => 'Frame format errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_frame_errors' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'rx_length_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Length errors', 'info' => 'Length errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_length_errors' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'rx_missed_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Missed packetss', 'info' => 'Missed packets' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_missed_errors' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'rx_over_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Overrun errors', 'info' => 'Overrun errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_over_errors' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'rx_packets' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX packets', 'info' => 'RX packets' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/rx_packets' }, 'peack_protect' => 'max_interface_pps', 'negative' => { 'type' => 'field', 'name' => 'tx_packets' }, 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'tx_aborted_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Aborted frames', 'info' => 'Aborted frames' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_aborted_errors' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'tx_bytes' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX bit', 'info' => 'TX bit' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_bytes' }, 'cdef' => '8,*', 'peack_protect' => 'max_interface_bps', 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'tx_carrier_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Carrier errors', 'info' => 'Carrier errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_carrier_errors' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'tx_compressed' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX compressed packets', 'info' => 'Compressed packets' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_compressed' }, 'peack_protect' => 'max_interface_pps', 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'tx_dropped' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX dropped packets', 'info' => 'Dropped frames' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_dropped' }, 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'tx_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX errors', 'info' => 'Transmit problems' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_errors' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'tx_fifo_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX FIFO errors', 'info' => 'FIFO errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_fifo_errors' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'tx_heartbeat_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Heartbeat errors', 'info' => 'Heartbeat errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_heartbeat_errors' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'tx_packets' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX packets', 'info' => 'TX packets' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_packets' }, 'peack_protect' => 'max_interface_pps', 'difference' => 'per_secund' }, # -------------------------------------------------------------------------- 'tx_window_errors' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Window errors', 'info' => 'Window errors' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/statistics/tx_window_errors' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'signal' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Signal level', 'info' => 'WiFi signal level' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/level', 'prepare' => ':data:=:data:-256' }, # 'cdef' => '-256,+', 'peack_protect' => '-256:0' }, # -------------------------------------------------------------------------- 'noise' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Noise level', 'info' => 'WiFi noise level' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/noise', 'prepare' => ':data:=:data:-256' }, # 'cdef' => '-256,+', 'peack_protect' => '-256:0' }, # -------------------------------------------------------------------------- 'link' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Signal quality', 'info' => 'WiFi signal quality' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/link' }, 'peack_protect' => 'percent' }, # -------------------------------------------------------------------------- 'rx_percent' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX Utilisation', 'info' => 'RX utilisation' }, 'source' => { 'type' => 'calculated', 'calculated' => { 'type' => 'percent', 'full' => { 'source' => 'interface', 'name' => 'bps' }, 'part' => { 'source' => 'field', 'name' => 'rx_bytes' } } }, 'peack_protect' => 'percent', 'negative' => { 'type' => 'field', 'name' => 'tx_percent' } }, # -------------------------------------------------------------------------- 'tx_percent' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX Utilisation', 'info' => 'TX utilisation' }, 'source' => { 'type' => 'calculated', 'calculated' => { 'type' => 'percent', 'full' => { 'source' => 'interface', 'name' => 'bps' }, 'part' => { 'source' => 'field', 'name' => 'tx_bytes' } } }, 'peack_protect' => 'percent' }, # -------------------------------------------------------------------------- 'rx_size' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX packet size', 'info' => 'Average RX packet size' }, 'source' => { 'type' => 'calculated', 'calculated' => { 'type' => 'division', 'dividend' => { 'source' => 'field', 'name' => 'rx_bytes' }, 'divider' => { 'source' => 'field', 'name' => 'rx_packets' } } }, 'peack_protect' => 'packet_size_range', 'negative' => { 'type' => 'field', 'name' => 'tx_size' } }, # -------------------------------------------------------------------------- 'tx_size' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX packet size', 'info' => 'Average TX packet size' }, 'source' => { 'type' => 'calculated', 'calculated' => { 'type' => 'division', 'dividend' => { 'source' => 'field', 'name' => 'tx_bytes' }, 'divider' => { 'source' => 'field', 'name' => 'tx_packets' } } }, 'peack_protect' => 'packet_size_range' }, # -------------------------------------------------------------------------- 'retries' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Max. retries reached', 'info' => 'Max MAC retries num reached' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/retries' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'nwid' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Wrong nwid/essid', 'info' => 'Wrong nwid/essid' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/nwid' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'misc' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Other', 'info' => 'Others cases' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/misc' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'fragment' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'MAC reassemby', 'info' => 'Can\'t perform MAC reassembly' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/fragment' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'beacon' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Missed beacons', 'info' => 'Missed beacons/superframe' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/beacon' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'crypt' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'Code/decode', 'info' => 'Unable to code/decode (WEP)' }, 'source' => { 'type' => 'file', 'location' => $ifpath.'/:if:/wireless/crypt' }, 'negative' => { 'type' => 'dummy', 'value' => 'NaN' }, 'difference' => 'count' }, # -------------------------------------------------------------------------- 'rx_wifierr' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'RX errors', 'info' => 'Total RX Wifi Errors' }, 'source' => { 'type' => 'calculated', 'calculated' => { 'type' => 'sum', 'sum' => [qw(nwid fragment crypt)] } }, 'negative' => { 'type' => 'field', 'name' => 'tx_wifierr' } }, # -------------------------------------------------------------------------- 'tx_wifierr' => { 'munin' => { 'type' => 'GAUGE', 'draw' => 'LINE1', 'label' => 'TX errors', 'info' => 'Total TX Wifi errors' }, 'source' => { 'type' => 'calculated', 'calculated' => { 'type' => 'sum', 'sum' => [qw(misc beacon retries)] } } } }; # ----------------- main ---------------- need_multigraph(); if (defined($ARGV[0]) and ($ARGV[0] eq 'autoconf')) { printf("%s\n", -e $ifpath ? "yes" : "no ($ifpath not exists)"); exit (0); } $interfaces = get_interfaces(); if (defined($ARGV[0]) and ($ARGV[0] eq 'config')) { print_config(); exit (0); } print_values(); exit(0); # ====================================== both config and values =========================== # --------------- read sysfs file (one file - one value) -------------- sub get_file_content { my $file = $_[0]; return 'NaN' if (-z $file); open (FH, '<', $file) or die "$! $file \n"; my $content = ; close (FH); chomp $content; #print "$content\n"; return trim($content); } # ------------------ build interface list and his options ------------------------- sub get_interfaces { my $interfaces; my $ifdir = IO::Dir->new($ifpath); if(defined $ifdir) { my $if; while (defined ($if = $ifdir->read)) { next unless -d "$ifpath/$if"; next if $if =~ m/\./; unless($if =~ m/$include/) { next unless get_file_content(sprintf("%s/%s/operstate", $ifpath, $if)) =~ m/(up|unknown)/; next if $exclude =~ m/$if/; } my $mtufile = sprintf("%s/%s/mtu", $ifpath, $if); if(-e $mtufile) { $interfaces->{$if}{'mtu'} = get_file_content($mtufile); } my $bps = $ENV{"${if}_max_bps"} || undef; if(defined($bps)) { my ($num, $suff) = $bps =~ /(\d+)(\w)/; if ($suff eq 'k') { $bps = $num * 1000 / 8; } elsif($suff eq 'M') { $bps = $num * 1000 * 1000 / 8; } elsif($suff eq 'G') { $bps = $num * 1000 * 1000 * 1000 / 8; } $interfaces->{$if}{'bps'} = $bps; } if (-e sprintf("/sys/devices/virtual/net/%s", $if)) { $interfaces->{$if}{'virtual'} = 1; } $interfaces->{$if}{'name'} = exists($interfaces->{$if}{'virtual'}) ? sprintf("~%s", $if) : $if; } my ($maxlen, $tl) = (0, 0); for (keys %{$interfaces}) { $tl = length($interfaces->{$_}{'name'}); $maxlen = $tl if $tl > $maxlen; } for (keys %{$interfaces}) { $interfaces->{$_}{'name'} = sprintf("[%${maxlen}s]", $interfaces->{$_}{'name'}); } } else { die "$ifpath not exists\n"; } return $interfaces; } # ----------------------- trim whitespace at begin and end of string ------------ sub trim { my($string)=@_; for ($string) { s/^\s+//; s/\s+$//; } return $string; } # ------------------------ replacing :if: from strings to need value ---------------------- sub replace_if_template { my ($string, $replacement) = @_[0..1]; $string =~ s/:if:/$replacement/g; return $string; } # --------------------------- calculating range values for peack_protect -------------------------- sub get_peak_range { my ($field, $if) = @_[0..1]; my $range = {}; return $range unless defined($fields->{$field}{'peack_protect'}); # percent if ($fields->{$field}{'peack_protect'} eq 'percent') { $range->{'max'} = 100; $range->{'min'} = 0; } # numbers elsif ($fields->{$field}{'peack_protect'} =~ m/[-\d]+:[-\d]+/) { my @r = split(/:/, $fields->{$field}{'peack_protect'}); $range->{'max'} = $r[1]; $range->{'min'} = $r[0]; } # bytes per sec elsif($fields->{$field}{'peack_protect'} eq 'max_interface_bps' and defined ($interfaces->{$if}{'bps'})) { $range->{'max'} = $interfaces->{$if}{'bps'}; $range->{'min'} = 0; } # packets per sec elsif($fields->{$field}{'peack_protect'} eq 'max_interface_pps' and defined ($interfaces->{$if}{'bps'})) { $range->{'max'} = $interfaces->{$if}{'bps'}/$min_packet_size; $range->{'min'} = 0; } # packets size range elsif($fields->{$field}{'peack_protect'} eq 'packet_size_range' and defined ($interfaces->{$if}{'mtu'})) { $range->{'max'} = $interfaces->{$if}{'mtu'}; $range->{'min'} = $min_packet_size; } return $range; } # ----------------------------- checking avialability of fields ------------------------- sub check_field_avialability { my ($if, $field) = @_[0..1]; unless(exists($fields->{$field}{'avialable'}{$if})) { # -------------------- file ---------------- if($fields->{$field}{'source'}{'type'} eq 'file') { my $file = $fields->{$field}{'source'}{'location'}; $file =~ s/:if:/$if/g; $fields->{$field}{'avialable'}{$if} = 1 if (-e $file); } #---------------------------- calculated ---------------- elsif ($fields->{$field}{'source'}{'type'} eq 'calculated') { #------------------------------ percent ------------------------ if($fields->{$field}{'source'}{'calculated'}{'type'} eq 'percent') { my %available; for my $a ('full', 'part') { if($fields->{$field}{'source'}{'calculated'}{$a}{'source'} eq 'interface') { $available{$a} = exists($interfaces->{$if}{$fields->{$field}{'source'}{'calculated'}{$a}{'name'}}); } elsif($fields->{$field}{'source'}{'calculated'}{$a}{'source'} eq 'field') { $available{$a} = check_field_avialability($if, $fields->{$field}{'source'}{'calculated'}{$a}{'name'}); } } $fields->{$field}{'available'}{$if} = ($available{'full'} and $available{'part'}); } #------------------------------ division ------------------------ elsif($fields->{$field}{'source'}{'calculated'}{'type'} eq 'division') { my %available; for my $a ('dividend', 'divider') { if($fields->{$field}{'source'}{'calculated'}{$a}{'source'} eq 'interface') { $available{$a} = exists($interfaces->{$if}{$fields->{$field}{'source'}{'calculated'}{$a}{'name'}}); } elsif($fields->{$field}{'source'}{'calculated'}{$a}{'source'} eq 'field') { $available{$a} = check_field_avialability($if, $fields->{$field}{'source'}{'calculated'}{$a}{'name'}); } } $fields->{$field}{'available'}{$if} = ($available{'dividend'} and $available{'divider'}); } #------------------------------ sum ------------------------ elsif($fields->{$field}{'source'}{'calculated'}{'type'} eq 'sum') { my $count = 0; for my $a (@{$fields->{$field}{'source'}{'calculated'}{'sum'}}) { $count++ if (check_field_avialability($if, $a)); } $fields->{$field}{'avialable'}{$if} = ($count == scalar(@{$fields->{$field}{'source'}{'calculated'}{'sum'}})); } } } return $fields->{$field}{'avialable'}{$if}; } # ================================== config-only ============================== # --------------- concatenate field names ------------------ sub concat_names { my ($f1, $f2, $if) = @_[0..2]; my $name = $f1; if ($f1 ne $f2) { my @a = split(' ', $f1); my @b = split(' ', $f2); my ($t, $ra, $rb) = ('','',''); for (my $i = scalar(@a) - 1; $i >= 0; $i--) { #printf("%s %s\n", $a[$i], $b[$i]); if ($a[$i] eq $b[$i]) { $t = sprintf("%s %s", $a[$i], $t); } else { $ra = sprintf("%s %s", $a[$i], $ra); $rb = sprintf("%s %s", $b[$i], $rb); } } $name = trim(sprintf("%s/%s %s", trim($ra), trim($rb), trim($t))); } if (exists($interfaces->{$if})) { $name = sprintf ("%s %s", $interfaces->{$if}{'name'}, $name); } return $name; } # --------------------------- generating graph field ---------------------- sub generate_field { my ($config, $graph_name, $field, $if, $is_general_graph) = @_[0..4]; return '' unless(check_field_avialability($if, $field)); my $field_graph_name = $is_general_graph ? sprintf("%s_%s", $if, $field) : $field; for my $option (keys %{$fields->{$field}{'munin'}}) { next if exists($config->{$graph_name}{'fields'}{$field_graph_name}{$option}); $config->{$graph_name}{'fields'}{$field_graph_name}{$option} = replace_if_template($fields->{$field}{'munin'}{$option}, $interfaces->{$if}{'name'}); } if(exists($fields->{$field}{'cdef'})) { $config->{$graph_name}{'fields'}{$field_graph_name}{'cdef'} = sprintf("%s,%s", $field_graph_name, $fields->{$field}{'cdef'}); } if(exists($fields->{$field}{'negative'})) { my ($up_field_name, $down_field_name) = ('', $field_graph_name); my ($up_field, $down_field) = ('', $field); if($fields->{$down_field}{'negative'}{'type'} eq 'field') { $up_field = $fields->{$down_field}{'negative'}{'name'}; $up_field_name = $is_general_graph ? sprintf("%s_%s", $if, $up_field) : $up_field; $config->{$graph_name}{'fields'}{$up_field_name}{'label'} = concat_names($fields->{$down_field}{'munin'}{'label'}, $fields->{$up_field}{'munin'}{'label'}, $is_general_graph ? $if : ''); } elsif($fields->{$down_field}{'negative'}{'type'} eq 'dummy') { $up_field_name = $is_general_graph ? sprintf("%s_%s_dummy", $if, $down_field) : sprintf("%s_dummy", $down_field); $config->{$graph_name}{'fields'}{$up_field_name}{'label'} = concat_names($fields->{$down_field}{'munin'}{'label'}, $fields->{$down_field}{'munin'}{'label'}, $is_general_graph ? $if : ''); $config->{$graph_name}{'fields'}{$up_field_name}{'info'} = $fields->{$down_field}{'munin'}{'info'}; } $config->{$graph_name}{'fields'}{$up_field_name}{'negative'} = $down_field_name; $config->{$graph_name}{'fields'}{$down_field_name}{'graph'} = 'no'; $config->{$graph_name}{'fields'}{$down_field_name}{'label'} = 'none'; } # Fix field label on general graphs if ($is_general_graph and not $config->{$graph_name}{'fields'}{$field_graph_name}{'label'} =~ m/$if/) { $config->{$graph_name}{'fields'}{$field_graph_name}{'label'} = sprintf ("%s %s", $interfaces->{$if}{'name'}, $fields->{$field}{'munin'}{'label'}); } # do peaks protect if($protect_peacks ne 'no' and exists($fields->{$field}{'peack_protect'})) { my $range = get_peak_range($field, $if); for my $a (qw(min max)) { if (exists($range->{$a})) { $config->{$graph_name}{'fields'}{$field_graph_name}{$a} = $range->{$a}; } } } return $field_graph_name; } # ------------------------------- generating graph ---------------------------- sub generate_graph { my ($config, $graph, $if, $is_general_graph) = @_[0..4]; my @order = (); my $graph_name = $is_general_graph ? $graph : sprintf("%s.%s", $graph, $if); if($is_general_graph) { for my $field (@{$graphs->{$graph}{'general_fields'}}) { for my $general_if (keys %{$interfaces}) { my $res_field = generate_field($config, $graph_name, $field, $general_if, 1); push(@order, $res_field) if $res_field ne ''; } } } else { for my $field (@{$graphs->{$graph}{'per_if_fields'}}) { my $res_field = generate_field($config, $graph_name, $field, $if, 0); push(@order, $res_field) if $res_field ne ''; } } if(scalar(@order) > 0) { for my $option (keys %{$graphs->{$graph}{'munin'}}) { $config->{$graph_name}{'graph'}{$option} = replace_if_template($graphs->{$graph}{'munin'}{$option}, $is_general_graph ? 'All interfaces' : $interfaces->{$if}{'name'}); } $config->{$graph_name}{'graph'}{'order'} = join(' ', @order); # if scalar(@order) > 1; unless($is_general_graph) { $config->{$graph_name}{'graph'}{'category'} = $if; } } } # ------------------------ generate general and per-interface graphs ------------------------------ sub generate_graphs { my ($config, $graph) = @_[0..1]; generate_graph($config, $graph, '', 1); for my $if (keys %{$interfaces}) { generate_graph($config, $graph, $if, 0); } } # ---------------------------------------------------------- config ------------------------------------------------------ sub print_config { my $config = {}; my $graph; for $graph (keys %{$graphs}) { generate_graphs($config, $graph); } #-------------------- print --------------- for $graph (sort keys %{$config}) { printf ("multigraph %s\n", $graph); for my $option (sort keys %{$config->{$graph}{'graph'}}) { printf ("graph_%s %s\n", $option, $config->{$graph}{'graph'}{$option}); } for my $field (sort keys %{$config->{$graph}{'fields'}}) { for my $type (sort keys %{$config->{$graph}{'fields'}{$field}}) { printf ("%s.%s %s\n", $field, $type, $config->{$graph}{'fields'}{$field}{$type}); } } print "\n"; } } # =========================================== values ========================================================== # ------------------------------- calculate percent -------------------------- sub percent { my ($full, $current) = @_[0..1]; return $current/($full/100); } # ----------------------------------- saving state data using munin -------------------- sub save_state_data { my $data = $_[0]; my $d = Data::Dumper->new([$data]); $d->Indent(0); save_state($d->Dump); } # -------------------------------- loading previous state data using munin ------------------- sub restore_state_data { my $VAR1; my $states = (restore_state())[0]; eval $states if defined $states; return $VAR1; } # -------------------- protect field data from under zero value (for example prev tx_bytes = 10000, interface reset, current tx_bytes = 100, 100-1000=-900) sub underzero_protect { my ($a, $b) = @_[0..1]; return $a > $b ? $b : $b - $a; } # ------------------- calculating difference from last stored data --------------------------------- sub calc_diff { my ($raw_data, $raw_prev_data, $if, $field) = @_[0..4]; return $raw_data->{$if}{$field} unless (exists($fields->{$field}{'difference'}) and defined($raw_prev_data)); if ($fields->{$field}{'difference'} eq 'count' ) { return underzero_protect($raw_prev_data->{$if}{$field}, $raw_data->{$if}{$field}); } elsif ($fields->{$field}{'difference'} eq 'per_secund') { return underzero_protect($raw_prev_data->{$if}{$field}, $raw_data->{$if}{$field}) / ($raw_data->{'timestamp'} - $raw_prev_data->{'timestamp'}); } } # ---------------------- protecting values from peaks ------------------------ sub protect_data_peak { my ($field, $if, $value) = @_[0..2]; my $range = get_peak_range($field, $if); return $value if ( $protect_peacks ne 'no' or ( $value ne 'NaN' and exists($range->{'max'}) and $value <= $range->{'max'} and $value >= $range->{'min'} ) ); return 'NaN'; } # --------------------------------- loading or calculating fields values ---------------------------- sub get_field_data { my ($data, $raw_data, $raw_prev_data, $if, $field) = @_[0..4]; unless (exists($data->{$if}{$field})) { # ---------------------------- file source ------------------------------------------------------------ if($fields->{$field}{'source'}{'type'} eq 'file' and not exists($raw_data->{$if}{$field})) { $raw_data->{$if}{$field} = get_file_content(replace_if_template($fields->{$field}{'source'}{'location'}, $if)); $data->{$if}{$field} = calc_diff($raw_data, $raw_prev_data, $if, $field); } # ---------------------------- calculated source ------------------------------------------------------------ elsif($fields->{$field}{'source'}{'type'} eq 'calculated') { # -------------------------------- percent --------------------------- if($fields->{$field}{'source'}{'calculated'}{'type'} eq 'percent') { my $percents = {}; for my $pf (qw(full part)) { if ($fields->{$field}{'source'}{'calculated'}{$pf}{'source'} eq 'interface') { $percents->{$pf} = $interfaces->{$if}{$fields->{$field}{'source'}{'calculated'}{$pf}{'name'}}; } elsif ($fields->{$field}{'source'}{'calculated'}{$pf}{'source'} eq 'field') { $percents->{$pf} = get_field_data($data, $raw_data, $raw_prev_data, $if, $fields->{$field}{'source'}{'calculated'}{$pf}{'name'}); } } $data->{$if}{$field} = percent($percents->{'full'}, $percents->{'part'}); } # -------------------------------- division --------------------------- if($fields->{$field}{'source'}{'calculated'}{'type'} eq 'division') { my $division = {}; for my $df (qw(dividend divider)) { if ($fields->{$field}{'source'}{'calculated'}{$df}{'source'} eq 'interface') { $division->{$df} = $interfaces->{$if}{$fields->{$field}{'source'}{'calculated'}{$df}{'name'}}; } elsif ($fields->{$field}{'source'}{'calculated'}{$df}{'source'} eq 'field') { $division->{$df} = get_field_data($data, $raw_data, $raw_prev_data, $if, $fields->{$field}{'source'}{'calculated'}{$df}{'name'}); } } $data->{$if}{$field} = $division->{'divider'} != 0 ? $division->{'dividend'}/$division->{'divider'} : 'NaN'; } # -------------------------------- sum --------------------------- if($fields->{$field}{'source'}{'calculated'}{'type'} eq 'sum') { my $sum = 0; for my $s (@{$fields->{$field}{'source'}{'calculated'}{'sum'}}) { $sum += get_field_data($data, $raw_data, $raw_prev_data, $if, $s); } $data->{$if}{$field} = $sum; } } if(exists($fields->{$field}{'source'}{'prepare'})) { my $eval = $fields->{$field}{'source'}{'prepare'}; $eval =~ s/:data:/\$data->{\$if}{\$field}/g; eval $eval; } $data->{$if}{$field} = protect_data_peak($field, $if, $data->{$if}{$field}); } return $data->{$if}{$field}; } # ------------------------- preparing value for print ---------------------------- sub prepare_value { my ($values, $field, $field_name, $graph_name, $if, $data, $raw_data, $raw_prev_data) = @_[0..7]; if(check_field_avialability($if, $field)) { $values->{$graph_name}{$field_name} = get_field_data($data, $raw_data, $raw_prev_data, $if, $field); if(exists($fields->{$field}{'negative'}) and $fields->{$field}{'negative'}{'type'} eq 'dummy') { $values->{$graph_name}{$field_name.'_dummy'} = $fields->{$field}{'negative'}{'value'}; } } } # --------------------------------- print field.value value for every graph ---------------------- sub print_values { my $data = {}; my $raw_data = {}; my $raw_prev_data = restore_state_data(); my $values = {}; $raw_data->{'timestamp'} = time(); for my $graph (keys %{$graphs}) { for my $field (@{$graphs->{$graph}{'general_fields'}}) { for my $if (keys %{$interfaces}) { prepare_value($values, $field, sprintf("%s_%s", $if, $field), $graph, $if, $data, $raw_data, $raw_prev_data); } } } for my $if (keys %{$interfaces}) { for my $graph (keys %{$graphs}) { my $graph_name = sprintf("%s.%s", $graph, $if); for my $field (@{$graphs->{$graph}{'per_if_fields'}}) { prepare_value($values, $field, $field, $graph_name, $if, $data, $raw_data, $raw_prev_data); } } } save_state_data($raw_data); exit (0) unless defined ($raw_prev_data); # first update need just for collect and save data # ------------------------ print ------------------------ for my $graph (sort (keys %{$values})) { printf ("multigraph %s\n", $graph); for my $field (sort keys %{$values->{$graph}}) { printf("%s.value %s\n", $field, $values->{$graph}{$field}); } print "\n"; } }