#! /usr/bin/perl # $Id: logdrake 238729 2008-03-06 15:15:47Z tv $ # Copyright (C) 2001-2008 Mandriva # Yves Duret # some code is Copyright: (C) 1999, Michael T. Babcock # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # # TODO: consider switching from TreeView to gtkhtml use strict; use lib qw(/usr/lib/libDrakX); use standalone; #- warning, standalone must be loaded very first, for 'explanations' use c; use common; use interactive; use do_pkgs; use mygtk3 qw(gtknew); #- do not import gtkadd which conflicts with ugtk3 version use ugtk3 qw(:create :dialogs :helpers :wrappers); $ugtk3::wm_icon = "/usr/share/mcc/themes/default/logdrake-mdk.png"; my $in = 'interactive'->vnew('su'); my ($isExplain, $Explain, $isFile, $File, $isWord, $Word); #- parse arguments list. foreach (@ARGV) { /^--explain=(.*)$/ and do { $isExplain = ($Explain) = $1; $isFile = 1; $File = "/var/log/explanations"; next }; /^--file=(.*)$/ and do { $isFile = ($File) = $1; next }; /^--word=(.*)$/ and do { $isWord = ($Word) = $1; next }; /^--alert$/ and do { alert_config(); quit() }; } if (!$::testing) { do_pkgs_standalone->new($in)->ensure_is_installed("syslog-daemon", "/etc/systemd/system/syslog.service") or exit(0); } my $isTail = $isFile; $| = 1 if $isTail; my $h = chomp_(`hostname -s`); $ugtk3::wm_icon = "logdrake"; my $explain_title = N("%s Tools Logs", N("Mageia")); my $my_win = ugtk3->new($isExplain ? $explain_title : N("Logs")); unless ($::isEmbedded) { $my_win->{window}->set_border_width(5); #$my_win->{window}->set_default_size(540,460); } $my_win->{window}->signal_connect(delete_event => \&quit); my $cal = gtkset_sensitive(Gtk3::Calendar->new, 0); my $mday = (localtime(time()))[3]; $cal->select_day($mday); my @months = qw(Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec); my $cal_mode = 0; my $cal_butt = gtksignal_connect(Gtk3::CheckButton->new(N("Show only for the selected day")), clicked => sub { $cal_mode = !$cal_mode; gtkset_sensitive($cal,$cal_mode) }); ### menus definition # the menus are not shown # but they provides shiny shortcut like C-q (Broken: FIXME) if ($::isEmbedded) { my $ui = gtknew('UIManager', actions => [ # [name, stock_id, value, label, accelerator, tooltip, callback] [ 'FileMenu', undef, N("_File") ], [ 'Save', undef, N("_Quit"), N("Q"), undef, \&save ], [ 'Quit', undef, N("_Quit"), N("Q"), undef, \&quit ], ], string => qq( )); $my_win->{rwindow}->add_accel_group($ui->get_accel_group); } ######### menus end ########## font and colors # Define global terms: # Define good notables: my @word_good = ("starting\n", "Freeing", "Detected", "starting.", "accepted.\n", "authenticated.\n", "Ready", "active", "reloading", "saved;", "restarting", "ONLINE\n"); my @word_warn = ("dangling", "closed.\n", "Assuming", "root", "root\n", "exiting\n", "missing", "Ignored", "adminalert:", "deleting", "OFFLINE\n"); my @word_bad = "bad"; my @word_note = ("LOGIN", "DHCP_OFFER", "optimized", "reset:", "unloaded", "disconnected", "connect", "Successful", "registered\n"); #my @line_good = ("up", "DHCP_ACK", "Cleaned", "Initializing", "Starting", "success", "successfully", "alive", "found", "ONLINE\n"); #my @line_warn = ("warning:", "WARNING:", "invalid", "obsolete", "bad", "Password", "detected", "timeout", "timeout:", "attackalert:", "wrong", "Lame", "FAILED", "failing", "unknown", "obsolete", "stopped.\n", "terminating.", "disabled\n", "disabled", "Lost"); #my @line_bad = ("DENY", "lost", "shutting", "dead", "DHCP_NAK", "failure;", "Unable", "inactive", "terminating", "refused", "rejected", "down", "OFFLINE\n", "error\n", "ERROR\n", "ERROR:", "error", "ERROR", "error:", "failed:"); # Now define what we want to use when: my $col_good = 'green4'; my $col_warn = 'yellow4'; my $col_bad = 'red'; my $col_note = 'purple'; my $col = 'darkcyan'; ######### font and colors end my %files = ( "auth" => { file => "/var/log/auth.log", desc => N("_:this is the auth.log log file\nAuthentication") }, "user" => { file => "/var/log/user.log", desc => N("_:this is the user.log log file\nUser") }, "messages" => { file => "/var/log/messages", desc => N("_:this is the /var/log/messages log file\nMessages") }, "syslog" => { file => "/var/log/syslog", desc => N("_:this is the /var/log/syslog log file\nSyslog") }, "explanations" => { file => "/var/log/explanations", desc => $explain_title } ); my $yy = gtkset_sensitive(gtksignal_connect(Gtk3::Button->new(N("search")) , clicked => \&search),0); my $log_text = gtktext_insert(Gtk3::TextView->new, [ [ '' ] ]); my $log_buf = $log_text->get_buffer; my $refcount_search; #### far from window my %toggle; gtkadd($my_win->{window}, gtkpack_(Gtk3::VBox->new(0,0), if_(!$::isEmbedded, 0, gtknew('Title1', label => N("A tool to monitor your logs"))), if_(!$isFile, 0, gtkpack__(Gtk3::VBox->new(0,2), gtknew('Title2', label => N("Settings")), # N("Show lines"), gtkpack__(Gtk3::HBox->new(0,0), " " . N("Matching") . " ", my $e_yes = Gtk3::Entry->new, " " . N("but not matching") . " ", my $e_no = Gtk3::Entry->new ), gtkpack_(Gtk3::HBox->new(0,0), 1, gtkadd(gtkset_border_width(Gtk3::Frame->new(N("Choose file")),2), gtkpack(gtkset_border_width(Gtk3::VBox->new(0,0),0), map { $toggle{$_} = gtksignal_connect(Gtk3::CheckButton->new($files{$_}{desc}), clicked => sub { $refcount_search++; gtkset_sensitive($yy, $refcount_search); }); $toggle{$_}->set_sensitive(0) if !-f $files{$_}{file}; $toggle{$_}; } sort keys %files, ) ), 0, gtkadd(gtkset_border_width(Gtk3::Frame->new(N("Calendar")),2), gtkpack__(gtkset_border_width(Gtk3::VBox->new(0,0),5), $cal_butt, $cal ) ) ), $yy, ) ), !$isExplain ? (0, gtknew('Title2', label => N("Content of the file")), 1, create_scrolled_window($log_text) ) : (1, create_scrolled_window($log_text)), 0, gtkadd(gtkset_border_width(gtkset_layout(Gtk3::HButtonBox->new, 'end'), 5), if_(!$isFile, gtksignal_connect(Gtk3::Button->new(N("Mail alert")), clicked => sub { eval { alert_config() }; my $err = $@; $::WizardWindow->destroy if defined $::WizardWindow; undef $::WizardWindow; if ($err && $err !~ /wizcancel/) { err_dialog(N("Error"), N("The alert wizard has failed unexpectedly:") . "\n\n" . $err); } })), gtksignal_connect(Gtk3::Button->new(N("Save")), clicked => \&save), gtksignal_connect(Gtk3::Button->new($::isEmbedded ? N("Cancel") : N("Quit")), clicked => \&quit) ) ) ); $isFile && !$::isEmbedded and gtkset_size_request($log_text, 400, 500); $my_win->{window}->show_all; search() if $isFile; $my_win->main; sub quit() { ugtk3->exit(0) } #------------------------------------------------------------- # search functions #------------------------------------------------------------- sub search() { my $window = $log_text->get_window; return if !$window; $window->freeze_updates; $log_buf->set_text(''); if ($isFile) { parse_file($File, $File); } else { foreach (keys %files) { parse_file($files{$_}{file}, $files{$_}{desc}) if $toggle{$_}->get_active; } } $window->thaw_updates; $log_text->show; gtkflush(); } my $timer; my @logs; my $F; sub parse_file { my ($file, $descr) = @_; $file =~ s/\.gz$//; my ($pbar, $win_pb); unless ($::isEmbedded && $isExplain) { local $::main_window = $my_win->{real_window}; gtkadd($win_pb = gtkset_modal(Gtk3::Window->new('toplevel'), 1), gtkpack(Gtk3::VBox->new(2,0), Gtk3::Label->new(" " . N("please wait, parsing file: %s", $descr) . " "), $pbar = Gtk3::ProgressBar->new ) ); $win_pb->set_transient_for($my_win->{real_window}); $win_pb->set_position('center'); $win_pb->realize; $win_pb->show_all; gtkflush(); } my $ey = $e_yes->get_chars(0, -1); my $en = $e_no->get_chars(0, -1); $ey =~ s/ OR /|/; $ey =~ s/^\*$//; $en =~ s/^\*$/.*/; $ey = $ey . $Word if $isWord; if ($cal_mode) { my (undef, $month, $day) = $cal->get_date; $ey = $months[$month] . "\\s{1,2}$day\\s.*$ey.*\n"; } my @all = -e $file ? catMaybeCompressed($file) : N("Sorry, log file isn't available!"); if ($isExplain) { my (@t, $t); while (@all) { $t = pop @all; next if $t =~ /logdrake/; last if $t !~ /$Explain/; push @t, $t; } @all = reverse @t; } my $taille = @all; my $i = 0; my $test; if ($en && !$ey) { $test = sub { $_[0] !~ /$en/ }; } elsif ($ey && !$en) { $test = sub { $_[0] =~ /$ey/ }; } else { $test = sub { $_[0] =~ /$ey/ && $_[0] !~ /$en/ }; } foreach (@all) { $i++; if ($pbar && $i % 10) { $pbar->set_fraction($i/$taille); $win_pb->get_window->process_updates(1); # no gtkflush() because we do not want to refresh the TextView } logcolorize($_) if $test->($_); } $win_pb->destroy if !$::isEmbedded || !$isExplain; if ($isTail) { close $F if $F; if (open $F, $file) { local $_; while (<$F>) {} #to prevent to output the file twice.. # $log_text->set_point($log_text->get_length()); $timer ||= Glib::Timeout->add(1000, sub { logcolorize($_) while <$F>; seek $F, 0, 1; }); } else { my $error = $!; my $string = chomp_(`LC_ALL=C date '+%b %d %T'`) . " " . N("Error while opening \"%s\" log file: %s\n", $file, $error); # `` return non utf8 and concat of non utf8 & utf8 is non utf8: c::set_tagged_utf8($string); logcolorize($string); } } insert_text_n_scroll(); } ########################################################################################## sub logcolorize { my ($string) = @_; # we get date & time if it is date & time (dmesg) $string =~ s/(\D{3} .. (\d\d:\d\d:\d\d ))//; my $timestamp = $isExplain ? $2 : $1; my @rec = split(/\s/, $string); @rec = map { c::set_tagged_utf8($_); $_ } @rec if utf8::is_utf8($string); log_output($timestamp, 'Bold', 'darkcyan'); # date & time if any... # BUG: $col hasn't yet be reseted $isExplain or log_output("$rec[0] ", 'Bold', $rec[0] eq $h ? 'blue' : $col); # hostname if ($rec[1] eq "last") { log_output(" last message repeated ", undef, 'green'); log_output($rec[4], 'Bold', 'green'); log_output(" times\n", undef, 'green'); return; } # Extract PID if present if ($rec[1] =~ /\[(\d+)\]:/) { my $pid = $1; $rec[1] =~ s/\[$1\]://; log_output($rec[1] . "[", undef, 'green'); log_output($pid, 'Bold', 'black'); log_output("]: ", undef, 'green'); } else { log_output($rec[1] . " ", undef, 'green'); } foreach my $therest (2 .. $#rec) { $col = 'darkcyan'; # Check for keywords to highlight foreach (@word_good) { $col = $col_good if $_ eq $rec[$therest] } foreach (@word_warn) { $col = $col_warn if $_ eq $rec[$therest] } foreach (@word_bad) { $col = $col_bad if $_ eq $rec[$therest] } foreach (@word_note) { $col = $col_note if $_ eq $rec[$therest] } # Watch for words that indicate entire lines should be highlighted #foreach (@line_good) { $col = $col_good if $_ eq $rec[$therest] } #foreach (@line_warn) { $col = $col_warn if $_ eq $rec[$therest] } #foreach (@line_bad) { $col = $col_bad if $_ eq $rec[$therest] } log_output("$rec[$therest] ", undef, $col); } log_output("\n", undef, 'black'); insert_text_n_scroll() if $isExplain; } # log_output (Gtk3::TextView, [ [ ... ] ]) sub log_output { my ($text, $font, $col) = @_; my $tag = join('', $font, $col); push @logs, [ $text, $tag ]; $log_buf->{tags}{$tag} ||= { foreground => $col }; # if_($font, font => $font), } sub insert_text_n_scroll() { ugtk3::gtktext_insert($log_text, \@logs, append => 1); $log_text->scroll_to_iter($log_buf->get_end_iter, 0, 1, 0.5, 0.5); undef @logs; } #------------------------------------------------------------- # mail/sms alert #------------------------------------------------------------- sub alert_config() { local $::isEmbedded = 0; undef $::WizardTable; undef $::WizardWindow; my $conffile = "/etc/sysconfig/mail_alert"; my %options = getVarsFromSh($conffile); $options{LOAD} ||= 3; $options{MAIL} ||= "root"; $options{SMTP} ||= "localhost"; my $service = { httpd => N("Apache World Wide Web Server"), bind => N("Domain Name Resolver"), ftp => N("Ftp Server"), postfix => N("Postfix Mail Server"), samba => N("Samba Server"), sshd => N("SSH Server"), webmin => N("Webmin Service"), xinetd => N("Xinetd Service") }; my @installed_d = grep { -e "/etc/init.d/$_" } sort keys %$service; my %services_to_check = map { $_ => 1 } split(':', $options{SERVICES}); $::isWizard = 1; my $mode; my $cron_file = "/etc/cron.hourly/logdrake_service"; my %modes = ( configure => N("Configure the mail alert system"), disable => N("Stop the mail alert system"), ); require wizards; my $wiz = wizards->new({ defaultimage => "logdrake.png", name => N("Mail alert"), pages => { welcome => { name => N("Mail alert configuration") . "\n\n" . N("Welcome to the mail configuration utility.\n\nHere, you'll be able to set up the alert system.\n"), no_back => 1, data => [ { val => \$mode, label => N("What do you want to do?"), list => [ keys %modes ], format => sub { $modes{$_[0]} }, }, ], post => sub { $mode eq 'configure' ? 'services' : 'stop' }, }, services => { name => N("Services settings") . "\n\n" . N("You will receive an alert if one of the selected services is no longer running"), data => [ map { { label => $_, val => \$services_to_check{$_}, type => "bool", text => $service->{$_} } } @installed_d ], next => "load", }, load => { #PO- Here "load" is a noun; that is load refers to the system/CPU) load name => N("Load setting") . "\n\n" . N("You will receive an alert if the load is higher than this value"), data => [ { label => N("_: load here is a noun, the load of the system\nLoad"), val => \$options{LOAD}, type => 'range', min => 1, max => 50 } ], next => "email", }, email => { name => N("Alert configuration") . "\n\n" . N("Please enter your email address below ") . "\n" . N("and enter the name (or the IP) of the SMTP server you wish to use"), data => [ { label => N("Email address"), val => \$options{MAIL} }, { label => N("Email server"), val => \$options{SMTP} }, ], complete => sub { if ($options{MAIL} !~ /[\w.-]*\@[\w.-]/ && !member($options{MAIL}, map { $_->[0] } list_passwd())) { err_dialog(N("Error"), N("\"%s\" neither is a valid email nor is an existing local user!", $options{MAIL})); return 1; } if (member($options{MAIL}, map { $_->[0] } list_passwd()) && $options{SMP} !~ /localhost/) { err_dialog(N("Error"), N("\"%s\" is a local user, but you did not select a local smtp, so you must use a complete email address!", $options{MAIL})); return 1; } }, next => "end", }, end => { name => N("Congratulations") . "\n\n" . N("The wizard successfully configured the mail alert."), end => 1, no_back => 1, }, stop => { pre => sub { eval { rm_rf($cron_file) } }, name => N("Congratulations") . "\n\n" . N("The wizard successfully disabled the mail alert."), end => 1, no_back => 1, }, }, }); $wiz->process($in); return if $mode eq 'disable'; $options{SERVICES} = join ':', grep { $services_to_check{$_} } sort keys %services_to_check; use Data::Dumper; output_with_perm $cron_file, 0755, q(#!/usr/bin/perl # generated by logdrake use MDK::Common; my $r; my %options = getVarsFromSh("/etc/sysconfig/mail_alert"); #- check services my ) . Data::Dumper->Dump([ $service ], [qw(*services)]) . q( foreach (split(':', $options{SERVICES})) { next unless $services{$_}; $r .= "Service $_ ($services{$_} is not running)\\n" unless -e "/var/lock/subsys/$_"; } #- load my ($load) = split ' ', first(cat_("/proc/loadavg")); $r .= "Load is huge: $load\n" if $load > $options{LOAD}; #- report it if ($r) { use Mail::Mailer; my $mailer = Mail::Mailer->new('smtp', Server => $options{SMTP}); $mailer->open({ From => 'root@localhost', To => $options{MAIL}, Subject => "DrakLog Mail Alert", }) or die "Cannot open: $!\n"; print $mailer $r; $mailer->close; } # EOF); setVarsInSh($conffile, \%options); if (defined $::WizardWindow) { $::WizardWindow->destroy; undef $::WizardWindow; } } #------------------------------------------------------------- # menu callback functions #------------------------------------------------------------- sub save() { $::isWizard = 0; my $y = $in->ask_filename({ title => N("Save as.."), directory => "/root", save => 1 }) or return; my $buf = $log_text->get_buffer; my ($start, $end) = $buf->get_bounds; output($y, $buf->get_text($start, $end, 0)); }