FU: Add super awesome and butt-ugly FU::debug_info web interface

This is so much more useful than embedding debugging info inside pages,
as I've been doing before.
This commit is contained in:
Yorhel 2025-02-27 09:10:37 +01:00
parent de36b90cde
commit b06cc24826
2 changed files with 430 additions and 23 deletions

100
FU.pm
View file

@ -130,21 +130,27 @@ sub init_db($info) {
}
my @before_request;
my @after_request;
sub before_request :prototype(&) ($f) { push @before_request, $f }
sub after_request :prototype(&) ($f) { unshift @after_request, $f }
sub _caller_info {
my($i, @c, @x) = (1);
$x[0] !~ /^FU(?:$|::)/ && push @c, [ @x[0..3] ] while (@x = caller $i++);
\@c
}
our @before_request;
our @after_request;
sub before_request :prototype(&) ($f) { push @before_request, [ $f, _caller_info ] }
sub after_request :prototype(&) ($f) { unshift @after_request, [ $f, _caller_info ] }
my %path_routes;
my %re_routes;
our %path_routes;
our %re_routes;
sub _add_route($path, $sub, $method) {
if (ref $path eq 'REGEXP' || ref $path eq 'Regexp') {
push $re_routes{$method}->@*, [ qr/^$path$/, $sub ];
push $re_routes{$method}->@*, [ qr/^$path$/, $sub, _caller_info ];
} elsif (!ref $path) {
confess("A route has already been registered for $method $path") if $path_routes{$method}{$path};
$path_routes{$method}{$path} = $sub;
$path_routes{$method}{$path} = [ $sub, _caller_info ];
} else {
confess('Path argument in route registration must be a string or regex');
}
@ -207,6 +213,12 @@ sub _monitor {
}
our $debug_info = [];
sub debug_info($path, $storage=undef, $history=100) {
$debug_info = { path => $path, storage => $storage, history => $history }
}
our $hdrname_re = qr/[!#\$\%&'\*\+-\.^_`\|~0-9a-zA-Z]{1,127}/;
our $method_re = qr/(?:HEAD|GET|POST|DELETE|OPTIONS|PUT|PATCH|QUERY)/;
@ -305,7 +317,7 @@ sub _log_err($e) {
}
sub _do_req($c) {
local $REQ = { hdr => {}, trace_start => time };
local $REQ = { hdr => {}, trace_start => time, trace_id => sprintf('%010x%08x%04x', int time, $$, int rand 1<<16) };
local $fu = bless {}, 'FU::obj';
$REQ->{ip} = $c->{client_sock} isa 'IO::Socket::INET' ? $c->{client_sock}->peerhost : '127.0.0.1';
@ -315,15 +327,27 @@ sub _do_req($c) {
_read_req $c;
$REQ->{trace_start} = time;
for my $h (@before_request) { $h->() }
my $path = fu->path;
my $method = fu->method eq 'HEAD' ? 'GET' : fu->method;
# Intercept requests for debug_info, ensuring no website hooks get called.
if (debug && $method eq 'GET' && $debug_info->{path} && $path eq $debug_info->{path}) {
require FU::DebugImpl;
FU::DebugImpl::render();
fu->_flush($c->{fcgi_obj} || $c->{client_sock});
fu->error(-1);
}
for my $h (@before_request) { $h->[0]->() }
my $r = $path_routes{$method}{$path};
if ($r) { $r->() }
else {
if ($r) {
$REQ->{trace_han} = [ $path, $r->[1] ];
$r->[0]->();
} else {
for $r ($re_routes{ fu->method }->@*) {
if($path =~ $r->[0]) {
$REQ->{trace_han} = [ $r->[0], $r->[2] ];
$r->[1]->(@{^CAPTURE});
fu->done;
}
@ -333,11 +357,12 @@ sub _do_req($c) {
1;
};
return if !$ok && ref $@ eq 'FU::err' && $@->[0] == -1;
$REQ->{trace_exn} = $ok ? undef : $@;
my $err = $ok || _is_done($@) ? undef : $@;
_log_err $err;
for my $h (@after_request) {
$ok = eval { $h->(); 1 };
$ok = eval { $h->[0]->(); 1 };
_log_err $@ if !$ok;
$err = $@ if !$err && !$ok && !_is_done($@);
}
@ -361,7 +386,12 @@ sub _do_req($c) {
$REQ->{trace_end} = time;
fu->_flush($c->{fcgi_obj} || $c->{client_sock});
my $proc_ms = (time - $REQ->{trace_start}) * 1000;
if (debug && $REQ->{trace_id} && $debug_info->{history} && $debug_info->{storage}) {
require FU::DebugImpl;
FU::DebugImpl::save();
}
my $proc_ms = ($REQ->{trace_end} - $REQ->{trace_start}) * 1000;
log_write(sprintf "%.0fms%s %s-%s %s-%d\n", $proc_ms,
$REQ->{trace_nsql} ?
sprintf ' (sql %.0f+%.0fms, %d/%d/%d)',
@ -639,11 +669,15 @@ sub formdata {
# Response generation methods
sub done { die bless [200,'Done'], 'FU::err' }
sub error($,$code,$msg=$code) { die bless [$code,$msg], 'FU::err' }
sub done { die bless [200,'Done',FU::_caller_info], 'FU::err' }
sub error($,$code,$msg=$code) { die bless [$code,$msg,FU::_caller_info], 'FU::err' }
sub status($, $code) { $FU::REQ->{status} = $code }
sub set_body($, $data) { $FU::REQ->{resbody} = $data }
sub set_body($, $data) {
confess "Invalid undef body" if !defined $data;
confess "Invalid attempt to set body to $data" if ref $data;
$FU::REQ->{resbody} = $data;
}
sub reset {
fu->status(200);
@ -950,9 +984,29 @@ handling and performance tracing.
Enable or disable debug mode. Returns the current mode when no argument is
given.
Debug mode currently only enables more verbose logging, but it may influence
other features in the future as well. You're of course free to use the debug
setting to enable or disable debugging features in your own code.
Debug mode currently enables more verbose logging and the C<debug_info>
interface below. It may influence other features in the future as well. You're
of course free to use the debug setting to enable or disable debugging features
in your own code.
=item FU::debug_info($path, $storage, $history)
Enable the built-in web interface for inspecting debug info. The interface is
accessible from your browser at the given C<$path>, which is matched against
C<< fu->path >>.
When the optional C<$storage> argument is given and set to an existing
directory, detailed request data is logged and stored in that directory, which
is then made available through the web interface. The C<$history> argument sets
the number of requests to keep, which defaults to 100.
Request logging and the web interface are only available when C<FU::debug> mode
is enabled.
B<WARNING:> This interface exposes internal and potentially sensitive
information. When this option is configured, make sure to B<ABSOLUTELY NEVER>
enable debug mode in production! Or at least set an absolutely impossible to
guess C<$path>.
=item FU::log_slow_reqs($ms)
@ -1271,7 +1325,7 @@ though.
This method loads the entire file contents in memory and does not support range
requests, so DO NOT use it to send large files. Actual web servers are much
more efficient at sending static files.
more efficient at serving static files.
The content-type header is determined from the file extension in C<$path>,
using the configured C<FU::mime_types>. As fallback, files that look like they
@ -1424,7 +1478,7 @@ external process manager.
=back
When C<--monitor> or C<--max-reqs> are set or C<<--proc>> is larger than 1, FU
When C<--monitor> or C<--max-reqs> are set or C<--proc> is larger than 1, FU
starts a supervisor process to ensure the requested number of worker processes
are running and that they are restarted when necessary. When FU has been loaded
with the C<-spawn> flag, this supervisor process runs directly from the context