diff options
Diffstat (limited to 'Bugzilla')
-rw-r--r-- | Bugzilla/Config/Advanced.pm | 6 | ||||
-rw-r--r-- | Bugzilla/Constants.pm | 2 | ||||
-rw-r--r-- | Bugzilla/Error.pm | 111 | ||||
-rw-r--r-- | Bugzilla/Install/Filesystem.pm | 7 | ||||
-rw-r--r-- | Bugzilla/Logging.pm | 8 | ||||
-rw-r--r-- | Bugzilla/Sentry.pm | 374 |
6 files changed, 56 insertions, 452 deletions
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm index 2eec11dbe..9316d8e48 100644 --- a/Bugzilla/Config/Advanced.pm +++ b/Bugzilla/Config/Advanced.pm @@ -38,12 +38,6 @@ use constant get_param_list => ( }, { - name => 'sentry_uri', - type => 't', - default => '', - }, - - { name => 'metrics_enabled', type => 'b', default => 0 diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index 6e3a12736..80d9c4e0c 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -694,8 +694,6 @@ sub _bz_locations { 'webdotdir' => "$datadir/webdot", 'extensionsdir' => "$libpath/extensions", 'assetsdir' => "$datadir/assets", - # error_reports store error/warnings destined for sentry - 'error_reports' => "$libpath/error_reports", 'confdir' => $confdir, }; } diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index d67571848..ef57303e9 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -13,9 +13,10 @@ use warnings; use base qw(Exporter); -@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage); +## no critic (Modules::ProhibitAutomaticExportation) +our @EXPORT = qw( ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage); +## use critic -use Bugzilla::Sentry; use Bugzilla::Constants; use Bugzilla::WebService::Constants; use Bugzilla::Util; @@ -37,7 +38,7 @@ sub _in_eval { } sub _throw_error { - my ($name, $error, $vars) = @_; + my ($name, $error, $vars, $logfunc) = @_; $vars ||= {}; $vars->{error} = $error; @@ -47,38 +48,6 @@ sub _throw_error { my $dbh = eval { Bugzilla->dbh }; $dbh->bz_rollback_transaction() if ($dbh && $dbh->bz_in_transaction() && !_in_eval()); - my $datadir = bz_locations()->{'datadir'}; - # If a writable $datadir/errorlog exists, log error details there. - if (-w "$datadir/errorlog") { - require Data::Dumper; - my $mesg = ""; - for (1..75) { $mesg .= "-"; }; - $mesg .= "\n[$$] " . time2str("%D %H:%M:%S ", time()); - $mesg .= "$name $error "; - $mesg .= remote_ip(); - $mesg .= Bugzilla->user->login; - $mesg .= (' actually ' . Bugzilla->sudoer->login) if Bugzilla->sudoer; - $mesg .= "\n"; - my %params = Bugzilla->cgi->Vars; - $Data::Dumper::Useqq = 1; - for my $param (sort keys %params) { - my $val = $params{$param}; - # obscure passwords - $val = "*****" if $param =~ /password/i; - # limit line length - $val =~ s/^(.{512}).*$/$1\[CHOP\]/; - $mesg .= "[$$] " . Data::Dumper->Dump([$val],["param($param)"]); - } - for my $var (sort keys %ENV) { - my $val = $ENV{$var}; - $val = "*****" if $val =~ /password|http_pass/i; - $mesg .= "[$$] " . Data::Dumper->Dump([$val],["env($var)"]); - } - open(ERRORLOGFID, ">>", "$datadir/errorlog"); - print ERRORLOGFID "$mesg\n"; - close ERRORLOGFID; - } - my $template = Bugzilla->template; my $message; @@ -97,34 +66,24 @@ sub _throw_error { message => \$message }); if ($Bugzilla::Template::is_processing) { - $name =~ /^global\/(user|code)-error/; - my $type = $1 // 'unknown'; + my ($type) = $name =~ /^global\/(user|code)-error/; + $type //= 'unknown'; die Template::Exception->new("bugzilla.$type.$error", $vars); } if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { - if (sentry_should_notify($vars->{error})) { - $vars->{maintainers_notified} = 1; - $vars->{processed} = {}; - } else { - $vars->{maintainers_notified} = 0; - } - my $cgi = Bugzilla->cgi; $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); $template->process($name, $vars) || ThrowTemplateError($template->error()); print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; - - if ($vars->{maintainers_notified}) { - sentry_handle_error($vars->{error}, $vars->{processed}->{error_message}); - } + $logfunc->("webpage error: $error"); } elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { die Dumper($vars); } elsif (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("$message\n"); + die "$message\n"; } elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT || Bugzilla->error_mode == ERROR_MODE_JSON_RPC @@ -141,6 +100,7 @@ sub _throw_error { } if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { + $logfunc->("XMLRPC error: $error ($code)"); die SOAP::Fault->faultcode($code)->faultstring($message); } else { @@ -150,6 +110,11 @@ sub _throw_error { if (Bugzilla->error_mode == ERROR_MODE_REST) { my %status_code_map = %{ REST_STATUS_CODE_MAP() }; $status_code = $status_code_map{$code} || $status_code_map{'_default'}; + $logfunc->("REST error: $error (HTTP $status_code, internal code $code)"); + } + else { + my $fake_code = 100000 + $code; + $logfunc->("JSONRPC error: $error ($fake_code)"); } # Technically JSON-RPC isn't allowed to have error numbers # higher than 999, but we do this to avoid conflicts with @@ -170,22 +135,44 @@ sub _throw_error { exit; } + +sub _add_vars_to_logging_fields { + my ($vars) = @_; + + foreach my $key (keys %$vars) { + Bugzilla::Logging->fields->{"var_$key"} = $vars->{$key}; + } +} + +sub _make_logfunc { + my ($type) = @_; + my $logger = Log::Log4perl->get_logger("Bugzilla.Error.$type"); + return sub { + local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 3; + if ($type eq 'User') { + $logger->warn(@_); + } + else { + $logger->error(@_); + } + }; +} + + sub ThrowUserError { - _throw_error("global/user-error.html.tmpl", @_); + my ($error, $vars) = @_; + my $logfunc = _make_logfunc('User'); + _add_vars_to_logging_fields($vars); + + _throw_error( 'global/user-error.html.tmpl', $error, $vars, $logfunc); } sub ThrowCodeError { - my (undef, $vars) = @_; - - # Don't show function arguments, in case they contain - # confidential data. - local $Carp::MaxArgNums = -1; - # Don't show the error as coming from Bugzilla::Error, show it - # as coming from the caller. - local $Carp::CarpInternal{'Bugzilla::Error'} = 1; - $vars->{traceback} //= Carp::longmess(); + my ($error, $vars) = @_; + my $logfunc = _make_logfunc('User'); + _add_vars_to_logging_fields($vars); - _throw_error("global/code-error.html.tmpl", @_); + _throw_error( 'global/code-error.html.tmpl', $error, $vars, $logfunc ); } sub ThrowTemplateError { @@ -211,10 +198,12 @@ sub ThrowTemplateError { # we never want to display this to the user exit if $template_err =~ /\bModPerl::Util::exit\b/; + state $logger = Log::Log4perl->get_logger('Bugzilla.Error.Template'); + $logger->error($template_err); + $vars->{'template_error_msg'} = $template_err; $vars->{'error'} = "template_error"; - sentry_handle_error('error', $template_err); $vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//; my $template = Bugzilla->template; diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm index 71169345b..70b195090 100644 --- a/Bugzilla/Install/Filesystem.pm +++ b/Bugzilla/Install/Filesystem.pm @@ -186,7 +186,6 @@ sub FILESYSTEM { my $template_cache = bz_locations()->{'template_cache'}; my $graphsdir = bz_locations()->{'graphsdir'}; my $assetsdir = bz_locations()->{'assetsdir'}; - my $error_reports = bz_locations()->{'error_reports'}; # We want to set the permissions the same for all localconfig files # across all PROJECTs, so we do something special with $localconfig, @@ -221,7 +220,6 @@ sub FILESYSTEM { 'runtests.pl' => { perms => OWNER_EXECUTE }, 'jobqueue.pl' => { perms => OWNER_EXECUTE }, 'migrate.pl' => { perms => OWNER_EXECUTE }, - 'sentry.pl' => { perms => WS_EXECUTE }, 'metrics.pl' => { perms => WS_EXECUTE }, 'Makefile.PL' => { perms => OWNER_EXECUTE }, 'gen-cpanfile.pl' => { perms => OWNER_EXECUTE }, @@ -271,8 +269,6 @@ sub FILESYSTEM { # Writeable directories $template_cache => { files => CGI_READ, dirs => DIR_CGI_OVERWRITE }, - $error_reports => { files => CGI_READ, - dirs => DIR_CGI_WRITE }, $attachdir => { files => CGI_WRITE, dirs => DIR_CGI_WRITE }, $webdotdir => { files => WS_SERVE, @@ -365,7 +361,6 @@ sub FILESYSTEM { $webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, $assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, $template_cache => DIR_CGI_WRITE, - $error_reports => DIR_CGI_WRITE, # Directories that contain content served directly by the web server. "$skinsdir/custom" => DIR_WS_SERVE, "$skinsdir/contrib" => DIR_WS_SERVE, @@ -466,8 +461,6 @@ sub FILESYSTEM { contents => HT_DEFAULT_DENY }, "$datadir/.htaccess" => { perms => WS_SERVE, contents => HT_DEFAULT_DENY }, - "$error_reports/.htaccess" => { perms => WS_SERVE, - contents => HT_DEFAULT_DENY }, "$graphsdir/.htaccess" => { perms => WS_SERVE, contents => HT_GRAPHS_DIR }, "$webdotdir/.htaccess" => { perms => WS_SERVE, diff --git a/Bugzilla/Logging.pm b/Bugzilla/Logging.pm index 769485c86..4a7abcb21 100644 --- a/Bugzilla/Logging.pm +++ b/Bugzilla/Logging.pm @@ -10,7 +10,7 @@ use 5.10.1; use strict; use warnings; -use Log::Log4perl; +use Log::Log4perl qw(:easy); use Log::Log4perl::MDC; use File::Spec::Functions qw(rel2abs); use Bugzilla::Constants qw(bz_locations); @@ -20,11 +20,15 @@ sub is_interactive { return not exists $ENV{SERVER_SOFTWARE} } +sub fields { + return Log::Log4perl::MDC->get_context->{fields} //= {}; +} + BEGIN { my $file = $ENV{LOG4PERL_CONFIG_FILE} // 'log4perl-syslog.conf'; Log::Log4perl::Logger::create_custom_level('NOTICE', 'WARN', 5, 2); Log::Log4perl->init(rel2abs($file, bz_locations->{confdir})); - Log::Log4perl->get_logger(__PACKAGE__)->trace("logging enabled in $PROGRAM_NAME"); + TRACE("logging enabled in $PROGRAM_NAME"); } # this is copied from Log::Log4perl's :easy handling, diff --git a/Bugzilla/Sentry.pm b/Bugzilla/Sentry.pm deleted file mode 100644 index 0d7a9c980..000000000 --- a/Bugzilla/Sentry.pm +++ /dev/null @@ -1,374 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# This Source Code Form is "Incompatible With Secondary Licenses", as -# defined by the Mozilla Public License, v. 2.0. - -package Bugzilla::Sentry; - -use 5.10.1; -use strict; -use warnings; - -use base qw(Exporter); -our @EXPORT = qw( - sentry_handle_error - sentry_should_notify -); - -use Carp; -use DateTime; -use File::Temp; -use JSON (); -use List::MoreUtils qw( any ); -use LWP::UserAgent; -use Sys::Hostname; -use URI; -use URI::QueryParam; - -use Bugzilla::Constants; -use Bugzilla::RNG qw(irand); -use Bugzilla::Util; -use Bugzilla::WebService::Constants; - -use constant CONFIG => { - # 'codes' lists the code-errors which are sent to sentry - codes => [qw( - bug_error - chart_datafile_corrupt - chart_dir_nonexistent - chart_file_open_fail - illegal_content_type_method - jobqueue_insert_failed - ldap_bind_failed - mail_send_error - template_error - token_generation_error - param_must_be_numeric - )], - - # any error/warning messages matching these regex's will not be logged or - # sent to sentry - ignore => [ - qr/^compiled template :\s*$/, - qr/^Use of uninitialized value \$compiled in concatenation \(\.\) or string/, - ], - - # any error/warning messages matching these regex's will be logged but not - # sent to sentry - sentry_ignore => [ - qr/Software caused connection abort/, - qr/Could not check out .*\/cvsroot/, - qr/Unicode character \S+ is illegal/, - qr/Lost connection to MySQL server during query/, - qr/Call me again when you have some data to chart/, - qr/relative paths are not allowed/, - qr/Illegal mix of collations for operation/, - ], - - # (ab)use the logger to classify error/warning types - logger => [ - { - match => [ - qr/DBD::mysql/, - qr/Can't connect to the database/, - ], - logger => 'database_error', - }, - { - match => [ qr/PatchReader/ ], - logger => 'patchreader', - }, - { - match => [ qr/Use of uninitialized value/ ], - logger => 'uninitialized_warning', - }, - ], -}; - -sub sentry_generate_id { - return sprintf('%04x%04x%04x%04x%04x%04x%04x%04x', - irand(0xffff), irand(0xffff), - irand(0xffff), - irand(0x0fff) | 0x4000, - irand(0x3fff) | 0x8000, - irand(0xffff), irand(0xffff), irand(0xffff) - ); -} - -sub sentry_should_notify { - my $code_error = shift; - return grep { $_ eq $code_error } @{ CONFIG->{codes} }; -} - -sub sentry_handle_error { - my $level = shift; - my @message = split(/\n/, shift); - my $id = sentry_generate_id(); - - my $is_error = $level eq 'error'; - if ($level ne 'error' && $level ne 'warning') { - # it's a code-error - return 0 unless sentry_should_notify($level); - $is_error = 1; - $level = 'error'; - } - - # build traceback - my $traceback; - { - # for now don't show function arguments, in case they contain - # confidential data. waiting on bug 700683 - #local $Carp::MaxArgLen = 256; - #local $Carp::MaxArgNums = 0; - local $Carp::MaxArgNums = -1; - local $Carp::CarpInternal{'CGI::Carp'} = 1; - local $Carp::CarpInternal{'Bugzilla::Error'} = 1; - local $Carp::CarpInternal{'Bugzilla::Sentry'} = 1; - $traceback = trim(Carp::longmess()); - } - - # strip timestamp - foreach my $line (@message) { - $line =~ s/^\[[^\]]+\] //; - } - my $message = join(" ", map { trim($_) } grep { $_ ne '' } @message); - - # message content filtering - foreach my $re (@{ CONFIG->{ignore} }) { - return 0 if $message =~ $re; - } - - # determine logger - my $logger; - foreach my $config (@{ CONFIG->{logger} }) { - foreach my $re (@{ $config->{match} }) { - if ($message =~ $re) { - $logger = $config->{logger}; - last; - } - } - last if $logger; - } - $logger ||= $level; - - # don't send to sentry unless configured - my $send_to_sentry = Bugzilla->params->{sentry_uri} ? 1 : 0; - - # web service filtering - if ($send_to_sentry - && (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)) - { - my ($code) = $message =~ /^(-?\d+): /; - if ($code - && !($code == ERROR_UNKNOWN_FATAL || $code == ERROR_UNKNOWN_TRANSIENT)) - { - $send_to_sentry = 0; - } - } - - # message content filtering - if ($send_to_sentry) { - foreach my $re (@{ CONFIG->{sentry_ignore} }) { - if ($message =~ $re) { - $send_to_sentry = 0; - last; - } - } - } - - # invalid boolean search errors need special handling - if ($message =~ /selectcol_arrayref failed: syntax error/ - && $message =~ /IN BOOLEAN MODE/ - && $message =~ /Bugzilla\/Search\.pm/) - { - $send_to_sentry = 0; - } - - # for now, don't send patchreader errors to sentry - $send_to_sentry = 0 - if $logger eq 'patchreader'; - - # log to apache's error_log - if ($send_to_sentry) { - _write_to_error_log("$message [#$id]", $is_error); - } else { - $traceback =~ s/\n/ /g; - _write_to_error_log("$message $traceback", $is_error); - } - - return 0 unless $send_to_sentry; - - my $user_data = undef; - eval { - my $user = Bugzilla->user; - if ($user->id) { - $user_data = { - id => $user->login, - name => $user->name, - }; - } - }; - - my $uri = URI->new(Bugzilla->cgi->self_url); - $uri->query(undef); - - # sanitise - - # sanitise these query-string params - # names are checked as-is as well as prefixed by BUGZILLA_ - my @sanitise_params = qw( PASSWORD TOKEN API_KEY ); - - # remove these ENV vars - my @sanitise_vars = qw( HTTP_COOKIE HTTP_X_BUGZILLA_PASSWORD HTTP_X_BUGZILLA_API_KEY HTTP_X_BUGZILLA_TOKEN ); - - foreach my $var (qw( QUERY_STRING REDIRECT_QUERY_STRING )) { - next unless exists $ENV{$var}; - my @pairs = split('&', $ENV{$var}); - foreach my $pair (@pairs) { - next unless $pair =~ /^([^=]+)=(.+)$/; - my ($param, $value) = ($1, $2); - if (any { uc($param) eq $_ || uc($param) eq "BUGZILLA_$_" } @sanitise_params) { - $value = '*'; - } - $pair = $param . '=' . $value; - } - $ENV{$var} = join('&', @pairs); - } - foreach my $var (qw( REQUEST_URI HTTP_REFERER )) { - next unless exists $ENV{$var}; - my $uri = URI->new($ENV{$var}); - foreach my $param ($uri->query_param) { - if (any { uc($param) eq $_ || uc($param) eq "BUGZILLA_$_" } @sanitise_params) { - $uri->query_param($param, '*'); - } - } - $ENV{$var} = $uri->as_string; - } - foreach my $var (@sanitise_vars) { - delete $ENV{$var}; - } - - my $now = DateTime->now(); - my $data = { - event_id => $id, - message => $message, - timestamp => $now->iso8601(), - level => $level, - platform => 'Other', - logger => $logger, - server_name => hostname(), - 'sentry.interfaces.User' => $user_data, - 'sentry.interfaces.Http' => { - url => $uri->as_string, - method => $ENV{REQUEST_METHOD}, - query_string => $ENV{QUERY_STRING}, - env => \%ENV, - }, - extra => { - stacktrace => $traceback, - }, - }; - - my $fh = File::Temp->new( - DIR => bz_locations()->{error_reports}, - TEMPLATE => $now->ymd('') . $now->hms('') . '-XXXX', - SUFFIX => '.dump', - UNLINK => 0, - - ); - if (!$fh) { - warn "Failed to create dump file: $!\n"; - return; - } - print $fh JSON->new->utf8(1)->pretty(0)->allow_nonref(1)->encode($data); - close($fh); - return 1; -} - -sub _write_to_error_log { - my ($message, $is_error) = @_; - if ($ENV{MOD_PERL}) { - require Apache2::Log; - if ($is_error) { - Apache2::ServerRec::log_error($message); - } else { - Apache2::ServerRec::warn($message); - } - } else { - print STDERR $message, "\n"; - } -} - -# lifted from Bugzilla::Error -sub _in_eval { - my $in_eval = 0; - for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - last if $sub =~ /^ModPerl/; - last if $sub =~ /^Bugzilla::Template/; - $in_eval = 1 if $sub =~ /^\(eval\)/; - } - return $in_eval; -} - -sub _sentry_die_handler { - my $message = shift; - $message =~ s/^undef error - //; - - # avoid recursion, and check for CGI::Carp::die failures - my $in_cgi_carp_die = 0; - for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - return if $sub =~ /:_sentry_die_handler$/; - $in_cgi_carp_die = 1 if $sub =~ /CGI::Carp::die$/; - } - - return if $Bugzilla::Template::is_processing; - return if _in_eval(); - - # mod_perl overrides exit to call die with this string - exit if $message =~ /\bModPerl::Util::exit\b/; - - my $nested_error = ''; - my $is_compilation_failure = $message =~ /\bcompilation (aborted|failed)\b/i; - - # if we are called via CGI::Carp::die chances are something is seriously - # wrong, so skip trying to use ThrowTemplateError - if (!$in_cgi_carp_die && !$is_compilation_failure) { - eval { - my $cgi = Bugzilla->cgi; - $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); - Bugzilla::Error::ThrowTemplateError($message); - print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; - }; - $nested_error = $@ if $@; - } - - if ($is_compilation_failure || - $in_cgi_carp_die || - ($nested_error && $nested_error !~ /\bModPerl::Util::exit\b/) - ) { - sentry_handle_error('error', $message); - - # and call the normal error management - # (ISE for web pages, error response for web services, etc) - CORE::die($message); - } - exit; -} - -sub install_sentry_handler { - $SIG{__DIE__} = \&sentry_die_handler; - $SIG{__WARN__} = sub { - return if _in_eval(); - sentry_handle_error('warning', shift); - }; -} - -BEGIN { - if ($ENV{SCRIPT_NAME} || $ENV{MOD_PERL}) { - install_sentry_handler(); - } -} - -1; |