#!/usr/bin/env perl #------------------------------------------------------------------------------ # # pgBadger - Advanced PostgreSQL log analyzer # # This program is open source, licensed under the PostgreSQL Licence. # For license terms, see the LICENSE file. #------------------------------------------------------------------------------ # # Settings in postgresql.conf # # You should enable SQL query logging with log_min_duration_statement >= 0 # With stderr output # Log line prefix should be: log_line_prefix = '%t [%p]: ' # Log line prefix should be: log_line_prefix = '%t [%p]: user=%u,db=%d ' # Log line prefix should be: log_line_prefix = '%t [%p]: db=%d,user=%u ' # If you need report per client Ip adresses you can add client=%h or remote=%h # pgbadger will also recognized the following form: # log_line_prefix = '%t [%p]: db=%d,user=%u,client=%h ' # or # log_line_prefix = '%t [%p]: user=%u,db=%d,remote=%h ' # With syslog output # Log line prefix should be: log_line_prefix = 'db=%d,user=%u ' # # Additional information that could be collected and reported # log_checkpoints = on # log_connections = on # log_disconnections = on # log_lock_waits = on # log_temp_files = 0 # log_autovacuum_min_duration = 0 #------------------------------------------------------------------------------ use vars qw($VERSION); use strict qw(vars subs); use Getopt::Long qw(:config no_ignore_case bundling); use IO::File; use Benchmark; use File::Basename; use Storable qw(store_fd fd_retrieve); use Time::Local qw(timegm_nocheck timelocal_nocheck timegm timelocal); use POSIX qw(locale_h sys_wait_h _exit strftime); setlocale(LC_NUMERIC, ''); setlocale(LC_ALL, 'C'); use File::Spec; use File::Temp qw/ tempfile /; use IO::Handle; use IO::Pipe; use FileHandle; use Socket; use constant EBCDIC => "\t" ne "\011"; use Encode qw(encode decode); $VERSION = '13.2'; $SIG{'CHLD'} = 'DEFAULT'; my $TMP_DIR = File::Spec->tmpdir() || '/tmp'; my %RUNNING_PIDS = (); my @tempfiles = (); my $parent_pid = $$; my $interrupt = 0; my $tmp_last_parsed = ''; my $tmp_dblist = ''; my @SQL_ACTION = ('SELECT', 'INSERT', 'UPDATE', 'DELETE', 'COPY FROM', 'COPY TO', 'CTE', 'DDL', 'TCL', 'CURSOR'); my @LATENCY_PERCENTILE = sort {$a <=> $b} (99,95,90); my $graphid = 1; my $NODATA = '
NO DATASET
'; my $MAX_QUERY_LENGTH = 25000; my $terminate = 0; my %CACHE_DNS = (); my $DNSLookupTimeout = 1; # (in seconds) my $EXPLAIN_URL = 'https://explain.depesz.com/'; my $EXPLAIN_POST = qq{
}; my $PID_DIR = $TMP_DIR; my $PID_FILE = undef; my %DBLIST = (); my $DBALL = 'postgres'; my $LOG_EOL_TYPE = 'LF'; # Factor used to estimate the total size of compressed file # when real size can not be obtained (bz2 or remote files) my $BZ_FACTOR = 30; my $GZ_FACTOR = 15; my $XZ_FACTOR = 18; my @E2A = ( 0, 1, 2, 3,156, 9,134,127,151,141,142, 11, 12, 13, 14, 15, 16, 17, 18, 19,157, 10, 8,135, 24, 25,146,143, 28, 29, 30, 31, 128,129,130,131,132,133, 23, 27,136,137,138,139,140, 5, 6, 7, 144,145, 22,147,148,149,150, 4,152,153,154,155, 20, 21,158, 26, 32,160,226,228,224,225,227,229,231,241,162, 46, 60, 40, 43,124, 38,233,234,235,232,237,238,239,236,223, 33, 36, 42, 41, 59, 94, 45, 47,194,196,192,193,195,197,199,209,166, 44, 37, 95, 62, 63, 248,201,202,203,200,205,206,207,204, 96, 58, 35, 64, 39, 61, 34, 216, 97, 98, 99,100,101,102,103,104,105,171,187,240,253,254,177, 176,106,107,108,109,110,111,112,113,114,170,186,230,184,198,164, 181,126,115,116,117,118,119,120,121,122,161,191,208, 91,222,174, 172,163,165,183,169,167,182,188,189,190,221,168,175, 93,180,215, 123, 65, 66, 67, 68, 69, 70, 71, 72, 73,173,244,246,242,243,245, 125, 74, 75, 76, 77, 78, 79, 80, 81, 82,185,251,252,249,250,255, 92,247, 83, 84, 85, 86, 87, 88, 89, 90,178,212,214,210,211,213, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,179,219,220,217,218,159 ); if (EBCDIC && ord('^') == 106) { # as in the BS2000 posix-bc coded character set $E2A[74] = 96; $E2A[95] = 159; $E2A[106] = 94; $E2A[121] = 168; $E2A[161] = 175; $E2A[173] = 221; $E2A[176] = 162; $E2A[186] = 172; $E2A[187] = 91; $E2A[188] = 92; $E2A[192] = 249; $E2A[208] = 166; $E2A[221] = 219; $E2A[224] = 217; $E2A[251] = 123; $E2A[253] = 125; $E2A[255] = 126; } elsif (EBCDIC && ord('^') == 176) { # as in codepage 037 on os400 $E2A[21] = 133; $E2A[37] = 10; $E2A[95] = 172; $E2A[173] = 221; $E2A[176] = 94; $E2A[186] = 91; $E2A[187] = 93; $E2A[189] = 168; } my $pgbadger_logo = ''; my $pgbadger_ico = 'data:image/x-icon;base64, AAABAAEAIyMQAAEABAA8BAAAFgAAACgAAAAjAAAARgAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAA AAAAAAAAAgAAGRsZACgqKQA2OTcASEpJAFpdWwBoa2kAeHt5AImMigCeoZ8AsLOxAMTHxQDR1NIA 5enmAPv+/AAAAAAA///////////////////////wAAD///////////H///////////AAAP////// //9Fq7Yv////////8AAA////////8V7u7qD////////wAAD///////8B7qWN5AL///////AAAP// ///y8Avrc3rtMCH/////8AAA/////xABvbAAAJ6kAA/////wAAD////wAG5tQAAADp6RAP////AA AP//MQBd7C2lRESOWe5xAD//8AAA//8APO7iC+7e7u4A3uxwBf/wAAD/9Aju7iAAvu7u0QAN7ukA 7/AAAP/wCe7kAAAF7ugAAAHO6xD/8AAA//AK7CAAAAHO1AAAABnrEP/wAAD/8ArAAAAAAc7kAAAA AIwQ//AAAP/wCjAAAAAC3uQAAAAAHBCf8AAA//AIEBVnIATu5gAXZhAFEP/wAAD/8AIAqxdwBu7p AFoX0QIQ//AAAP/wAAPsBCAL7u4QBwfmAAD/8AAA//AAA8owAC7u7lAAKbYAAJ/wAAD/8AAAAAAA fu7uwAAAAAAA//AAAP/wAAAAAADu7u7jAAAAAAD/8AAA//AAAAAABe7u7uoAAAAAAP/wAAD/8AAA AAAL7u7u7QAAAAAAn/AAAP/wAAAAAB3u7u7uYAAAAAD/8AAA//MAAAAATu7u7u6QAAAAAP/wAAD/ /wAAAAAM7u7u7TAAAAAD//AAAP//IQAAAAKu7u7UAAAAAB//8AAA////IAAAAAju7BAAAAAP///w AAD////2AAA1je7ulUAAA/////AAAP/////xEAnO7u7pIAH/////8AAA//////9CABju6iACP/// ///wAAD////////wAAggAP////////AAAP////////8wAAA/////////8AAA///////////w//// ///////wAAD///////////////////////AAAP/////gAAAA//+//+AAAAD//Af/4AAAAP/4A//g AAAA//AA/+AAAAD/oAA/4AAAAP8AAB/gAAAA/gAAD+AAAADwAAAB4AAAAPAAAADgAAAA4AAAAGAA AADgAAAA4AAAAOAAAADgAAAA4AAAAOAAAADgAAAAYAAAAOAAAADgAAAA4AAAAOAAAADgAAAA4AAA AOAAAABgAAAA4AAAAOAAAADgAAAA4AAAAOAAAADgAAAA4AAAAGAAAADgAAAA4AAAAOAAAADgAAAA 8AAAAOAAAADwAAAB4AAAAPwAAAfgAAAA/gAAD+AAAAD/gAA/4AAAAP/AAH/gAAAA//gD/+AAAAD/ /Af/4AAAAP//v//gAAAA/////+AAAAA '; my %CLASS_ERROR_CODE = ( '00' => 'Successful Completion', '01' => 'Warning', '02' => 'No Data (this is also a warning class per the SQL standard)', '03' => 'SQL Statement Not Yet Complete', '08' => 'Connection Exception', '09' => 'Triggered Action Exception', '0A' => 'Feature Not Supported', '0B' => 'Invalid Transaction Initiation', '0F' => 'Locator Exception', '0L' => 'Invalid Grantor', '0P' => 'Invalid Role Specification', '0Z' => 'Diagnostics Exception', '20' => 'Case Not Found', '21' => 'Cardinality Violation', '22' => 'Data Exception', '23' => 'Integrity Constraint Violation', '24' => 'Invalid Cursor State', '25' => 'Invalid Transaction State', '26' => 'Invalid SQL Statement Name', '27' => 'Triggered Data Change Violation', '28' => 'Invalid Authorization Specification', '2B' => 'Dependent Privilege Descriptors Still Exist', '2D' => 'Invalid Transaction Termination', '2F' => 'SQL Routine Exception', '34' => 'Invalid Cursor Name', '38' => 'External Routine Exception', '39' => 'External Routine Invocation Exception', '3B' => 'Savepoint Exception', '3D' => 'Invalid Catalog Name', '3F' => 'Invalid Schema Name', '40' => 'Transaction Rollback', '42' => 'Syntax Error or Access Rule Violation', '44' => 'WITH CHECK OPTION Violation', '53' => 'Insufficient Resources', '54' => 'Program Limit Exceeded', '55' => 'Object Not In Prerequisite State', '57' => 'Operator Intervention', '58' => 'System Error (errors external to PostgreSQL itself)', '72' => 'Snapshot Failure', 'F0' => 'Configuration File Error', 'HV' => 'Foreign Data Wrapper Error (SQL/MED)', 'P0' => 'PL/pgSQL Error', 'XX' => 'Internal Error', ); #### # method used to fork as many child as wanted ## sub spawn { my $coderef = shift; unless (@_ == 0 && $coderef && ref($coderef) eq 'CODE') { print "usage: spawn CODEREF"; exit 0; } my $pid; if (!defined($pid = fork)) { print STDERR "ERROR: cannot fork: $!\n"; return; } elsif ($pid) { $RUNNING_PIDS{$pid} = $pid; return; # the parent } # the child -- go spawn $< = $>; $( = $); # suid progs only exit &$coderef(); } # Command line options my $journalctl_cmd = ''; my $zcat_cmd = 'gunzip -c'; my $zcat = $zcat_cmd; my $bzcat = 'bunzip2 -c'; my $lz4cat = 'lz4cat'; my $ucat = 'unzip -p'; my $xzcat = 'xzcat'; my $zstdcat = 'zstdcat'; my $gzip_uncompress_size = "gunzip -l \"%f\" | grep -E '^\\s*[0-9]+' | awk '{print \$2}'"; # lz4 archive can only contain one file. # Original size can be retrieved only if --content-size has been used for compression # it seems lz4 send output to stderr so redirect to stdout my $lz4_uncompress_size = " lz4 -v -c --list %f 2>&1 |tail -n 2|head -n1 | awk '{print \$6}'"; my $zip_uncompress_size = "unzip -l %f | awk '{if (NR==4) print \$1}'"; my $xz_uncompress_size = "xz --robot -l %f | grep totals | awk '{print \$5}'"; my $zstd_uncompress_size = "zstd -v -l %f |grep Decompressed | awk -F\"[ (]*\" '{print \$5}'"; my $format = ''; my @outfiles = (); my $outdir = ''; my $incremental = ''; my $extra_files = 0; my $help = ''; my $ver = ''; my @dbname = (); my @dbuser = (); my @dbclient = (); my @dbappname = (); my @exclude_user = (); my @exclude_appname = (); my @exclude_db = (); my @exclude_client = (); my @exclude_line = (); my $ident = ''; my $top = 0; my $sample = 3; my $extension = ''; my $maxlength = 100000; my $graph = 1; my $nograph = 0; my $debug = 0; my $noprettify = 0; my $from = ''; my $to = ''; my $from_hour = ''; my $to_hour = ''; my $quiet = 0; my $progress = 1; my $error_only = 0; my @exclude_query = (); my @exclude_queryid = (); my @exclude_time = (); my @include_time = (); my $exclude_file = ''; my @include_query = (); my $include_file = ''; my $disable_error = 0; my $disable_hourly = 0; my $disable_type = 0; my $disable_query = 0; my $disable_session = 0; my $disable_connection = 0; my $disable_lock = 0; my $disable_temporary = 0; my $disable_checkpoint = 0; my $disable_autovacuum = 0; my $avg_minutes = 5; my $histo_avg_minutes = 60; my $last_parsed = ''; my $report_title = ''; my $log_line_prefix = ''; my $compiled_prefix = ''; my $project_url = 'http://pgbadger.darold.net/'; my $t_min = 0; my $t_max = 0; my $remove_comment = 0; my $select_only = 0; my $queue_size = 0; my $job_per_file = 0; my $charset = 'utf-8'; my $csv_sep_char = ','; my %current_sessions = (); my %pgb_current_sessions = (); my $incr_date = ''; my $last_incr_date = ''; my $anonymize = 0; my $noclean = 0; my $retention = 0; my $dns_resolv = 0; my $nomultiline = 0; my $noreport = 0; my $log_duration = 0; my $logfile_list = ''; my $enable_checksum = 0; my $timezone = 0; my $opt_timezone = 0; my $pgbouncer_only = 0; my $rebuild = 0; my $week_start_monday = 0; my $iso_week_number = 0; my $use_sessionid_as_pid = 0; my $dump_normalized_only = 0; my $log_timezone = 0; my $opt_log_timezone = 0; my $json_prettify = 0; my $report_per_database = 0; my $html_outdir = ''; my $param_size_limit = 24; my $month_report = 0; my $day_report = 0; my $noexplain = 0; my $log_command = ''; my $wide_char = 0; my $noweekreport = 0; my $query_numbering = 0; my $keep_comments = 0; my $no_progessbar = 0; my $NUMPROGRESS = 10; my @DIMENSIONS = (800, 300); my $RESRC_URL = ''; my $img_format = 'png'; my @log_files = (); my %prefix_vars = (); my $q_prefix = ''; my @prefix_q_params = (); my %last_execute_stmt = (); my $disable_process_title = 0; my $dump_all_queries = 0; my $dump_raw_csv = 0; my $header_done = 0; my @include_pid = (); my @include_session = (); my $compress_extensions = qr/\.(zip|gz|xz|bz2|lz4|zst)$/i; my $remote_host = ''; my $ssh_command = ''; my $ssh_bin = 'ssh'; my $ssh_port = 22; my $ssh_identity = ''; my $ssh_user = ''; my $ssh_timeout = 10; my $ssh_options = "-o ConnectTimeout=$ssh_timeout -o PreferredAuthentications=hostbased,publickey"; my $ssh_sudo = 0; my $force_sample = 0; my $nofork = 0; my $curl_command = 'curl -k -s '; my $sql_prettified = pgFormatter::Beautify->new('colorize' => 1, 'format' => 'html', 'uc_keywords' => 0); # Flag for logs using UTC, in this case we don't autodetect the timezone my $isUTC = 0; # Do not display data in pie where percentage is lower than this value # to avoid label overlapping. my $pie_percentage_limit = 2; # Get the decimal separator my $n = 5 / 2; my $num_sep = ','; $num_sep = ' ' if ($n =~ /,/); # Set iso datetime pattern my $time_pattern = qr/(\d{4})-(\d{2})-(\d{2})[\sT](\d{2}):(\d{2}):(\d{2})/; # Inform the parent that it should stop iterate on parsing other files sub stop_parsing { &logmsg('DEBUG', "Received interrupt signal"); $interrupt = 1; } # With multiprocess we need to wait for all children sub wait_child { my $sig = shift; $interrupt = 2; print STDERR "Received terminating signal ($sig).\n"; 1 while wait != -1; $SIG{INT} = \&wait_child; $SIG{TERM} = \&wait_child; foreach my $f (@tempfiles) { unlink("$f->[1]") if (-e "$f->[1]"); } if ($report_per_database) { unlink("$tmp_dblist"); } if ($last_parsed && -e "$tmp_last_parsed") { unlink("$tmp_last_parsed"); } if ($last_parsed && -e "$last_parsed.tmp") { unlink("$last_parsed.tmp"); } if (-e "$PID_FILE") { unlink("$PID_FILE"); } _exit(2); } $SIG{INT} = \&wait_child; $SIG{TERM} = \&wait_child; if ($^O !~ /MSWin32|dos/i) { $SIG{USR2} = \&stop_parsing; } else { $nofork = 1; } $| = 1; my $histogram_query = ''; my $histogram_session = ''; # get the command line parameters my $result = GetOptions( "a|average=i" => \$avg_minutes, "A|histo-average=i" => \$histo_avg_minutes, "b|begin=s" => \$from, "c|dbclient=s" => \@dbclient, "C|nocomment!" => \$remove_comment, "d|dbname=s" => \@dbname, "D|dns-resolv!" => \$dns_resolv, "e|end=s" => \$to, "E|explode!" => \$report_per_database, "f|format=s" => \$format, "G|nograph!" => \$nograph, "h|help!" => \$help, "H|html-outdir=s" => \$html_outdir, "i|ident=s" => \$ident, "I|incremental!" => \$incremental, "j|jobs=i" => \$queue_size, "J|Jobs=i" => \$job_per_file, "l|last-parsed=s" => \$last_parsed, "L|logfile-list=s" => \$logfile_list, "m|maxlength=i" => \$maxlength, "M|no-multiline!" => \$nomultiline, "N|appname=s" => \@dbappname, "o|outfile=s" => \@outfiles, "O|outdir=s" => \$outdir, "p|prefix=s" => \$log_line_prefix, "P|no-prettify!" => \$noprettify, "q|quiet!" => \$quiet, "Q|query-numbering!" => \$query_numbering, "r|remote-host=s" => \$remote_host, 'R|retention=i' => \$retention, "s|sample=i" => \$sample, "S|select-only!" => \$select_only, "t|top=i" => \$top, "T|title=s" => \$report_title, "u|dbuser=s" => \@dbuser, "U|exclude-user=s" => \@exclude_user, "v|verbose!" => \$debug, "V|version!" => \$ver, "w|watch-mode!" => \$error_only, "W|wide-char!" => \$wide_char, "x|extension=s" => \$extension, "X|extra-files!" => \$extra_files, "z|zcat=s" => \$zcat, "Z|timezone=f" => \$opt_timezone, "pie-limit=i" => \$pie_percentage_limit, "image-format=s" => \$img_format, "exclude-query=s" => \@exclude_query, "exclude-queryid=s" => \@exclude_queryid, "exclude-file=s" => \$exclude_file, "exclude-db=s" => \@exclude_db, "exclude-client=s" => \@exclude_client, "exclude-appname=s" => \@exclude_appname, "include-query=s" => \@include_query, "exclude-line=s" => \@exclude_line, "include-file=s" => \$include_file, "disable-error!" => \$disable_error, "disable-hourly!" => \$disable_hourly, "disable-type!" => \$disable_type, "disable-query!" => \$disable_query, "disable-session!" => \$disable_session, "disable-connection!" => \$disable_connection, "disable-lock!" => \$disable_lock, "disable-temporary!" => \$disable_temporary, "disable-checkpoint!" => \$disable_checkpoint, "disable-autovacuum!" => \$disable_autovacuum, "charset=s" => \$charset, "csv-separator=s" => \$csv_sep_char, "include-time=s" => \@include_time, "exclude-time=s" => \@exclude_time, 'ssh-command=s' => \$ssh_command, 'ssh-program=s' => \$ssh_bin, 'ssh-port=i' => \$ssh_port, 'ssh-identity=s' => \$ssh_identity, 'ssh-option=s' => \$ssh_options, 'ssh-sudo!' => \$ssh_sudo, 'ssh-user=s' => \$ssh_user, 'ssh-timeout=i' => \$ssh_timeout, 'anonymize!' => \$anonymize, 'noclean!' => \$noclean, 'noreport!' => \$noreport, 'log-duration!' => \$log_duration, 'enable-checksum!' => \$enable_checksum, 'journalctl=s' => \$journalctl_cmd, 'pid-dir=s' => \$PID_DIR, 'pid-file=s' => \$PID_FILE, 'rebuild!' => \$rebuild, 'pgbouncer-only!' => \$pgbouncer_only, 'start-monday!' => \$week_start_monday, 'iso-week-number!' => \$iso_week_number, 'normalized-only!' => \$dump_normalized_only, 'log-timezone=f' => \$opt_log_timezone, 'prettify-json!' => \$json_prettify, 'month-report=s' => \$month_report, 'day-report=s' => \$day_report, 'noexplain!' => \$noexplain, 'command=s' => \$log_command, 'no-week!' => \$noweekreport, 'explain-url=s' => \$EXPLAIN_URL, 'tempdir=s' => \$TMP_DIR, 'no-process-info!' => \$disable_process_title, 'dump-all-queries!' => \$dump_all_queries, 'keep-comments!' => \$keep_comments, 'no-progressbar!' => \$no_progessbar, 'dump-raw-csv!' => \$dump_raw_csv, 'include-pid=i' => \@include_pid, 'include-session=s' => \@include_session, 'histogram-query=s' => \$histogram_query, 'histogram-session=s' => \$histogram_session, 'no-fork' => \$nofork, ); die "FATAL: use pgbadger --help\n" if (not $result); # Force rebuild mode when a month report is asked $rebuild = 1 if ($month_report); $rebuild = 2 if ($day_report); # Set report title $report_title = &escape_html($report_title) if $report_title; # Show version and exit if asked if ($ver) { print "pgBadger version $VERSION\n"; exit 0; } &usage() if ($help); # Create temporary file directory if not exists mkdir("$TMP_DIR") if (!-d "$TMP_DIR"); if (!-d "$TMP_DIR") { die("Can not use temporary directory $TMP_DIR.\n"); } # Try to load Digest::MD5 when asked if ($enable_checksum) { if (eval {require Digest::MD5;1} ne 1) { die("Can not load Perl module Digest::MD5.\n"); } else { Digest::MD5->import('md5_hex'); } } # Check if another process is already running unless ($PID_FILE) { $PID_FILE = $PID_DIR . '/pgbadger.pid'; } if (-e "$PID_FILE") { my $is_running = 2; if ($^O !~ /MSWin32|dos/i) { eval { $is_running = `ps auwx | grep pgbadger | grep -v grep | wc -l`; chomp($is_running); }; } if (!$@ && ($is_running <= 1)) { unlink("$PID_FILE"); } else { print "FATAL: another process is already started or remove the file, see $PID_FILE\n"; exit 3; } } # Create pid file if (open(my $out, '>', $PID_FILE)) { print $out $$; close($out); } else { print "FATAL: can't create pid file $PID_FILE, $!\n"; exit 3; } # Rewrite some command line arguments as lists &compute_arg_list(); # If pgBadger must parse remote files set the ssh command # If no user defined ssh command have been set my $remote_command = ''; if ($remote_host && !$ssh_command) { $remote_command = &set_ssh_command($ssh_command, $remote_host); } # Add journalctl command to the file list if not already found if ($journalctl_cmd) { if (!grep(/^\Q$journalctl_cmd\E$/, @ARGV)) { $journalctl_cmd .= " --output='short-iso'"; push(@ARGV, $journalctl_cmd); } } # Add custom command to file list if ($log_command) { if (!grep(/^\Q$log_command\E$/, @ARGV)) { push(@ARGV, $log_command); } } # Log files to be parsed are passed as command line arguments my $empty_files = 1; if ($#ARGV >= 0) { if (!$month_report) { foreach my $file (@ARGV) { push(@log_files, &set_file_list($file)); } } elsif (!$outdir) { $outdir = $ARGV[0]; } } if (!$incremental && $html_outdir) { localdie("FATAL: parameter -H, --html-outdir can only be used with incremental mode.\n"); } # Read list of log file to parse from a file if ($logfile_list) { if (!-e $logfile_list) { localdie("FATAL: logfile list $logfile_list must exist!\n"); } my $in = undef; if (not open($in, "<", $logfile_list)) { localdie("FATAL: can not read logfile list $logfile_list, $!.\n"); } my @files = <$in>; close($in); foreach my $file (@files) { chomp($file); $file =~ s/\r//; if ($file eq '-') { localdie("FATAL: stdin input - can not be used with logfile list.\n"); } push(@log_files, &set_file_list($file)); } } # Do not warn if all log files are empty if (!$rebuild && $empty_files) { &logmsg('DEBUG', "All log files are empty, exiting..."); unlink("$PID_FILE"); exit 0; } # Logfile is a mandatory parameter when journalctl command is not set. if ( !$rebuild && ($#log_files < 0) && !$journalctl_cmd && !$log_command) { if (!$quiet) { localdie("FATAL: you must give a log file at command line parameter.\n\n", 4); } else { unlink("$PID_FILE"); exit 4; } } # Check that binary format is not mixed with other formats in multiple output mode if ($#outfiles >= 1) { my @has_binary = grep { /\.bin$/i } @outfiles; if (@has_binary && @outfiles > 1) { localdie("FATAL: binary format (.bin) cannot be used with multiple output formats.\n\n"); } if ($dump_normalized_only || $dump_all_queries) { localdie("FATAL: dump of normalized queries cannot be used with multiple output.\n\n"); } } # Remove follow option from journalctl command to prevent infinit loop if ($journalctl_cmd) { $journalctl_cmd =~ s/(-f|--follow)\b//; } # Quiet mode is forced with progress bar $progress = 0 if ($quiet || $no_progessbar); # Set the default number minutes for queries and connections average $avg_minutes ||= 5; $avg_minutes = 60 if ($avg_minutes > 60); $avg_minutes = 1 if ($avg_minutes < 1); $histo_avg_minutes ||= 60; $histo_avg_minutes = 60 if ($histo_avg_minutes > 60); $histo_avg_minutes = 1 if ($histo_avg_minutes < 1); my @avgs = (); for (my $i = 0 ; $i < 60 ; $i += $avg_minutes) { push(@avgs, sprintf("%02d", $i)); } my @histo_avgs = (); for (my $i = 0 ; $i < 60 ; $i += $histo_avg_minutes) { push(@histo_avgs, sprintf("%02d", $i)); } # Set the URL to submit explain plan $EXPLAIN_POST = sprintf($EXPLAIN_POST, $EXPLAIN_URL); # Set error like log level regex my $parse_regex = qr/^(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|HINT|STATEMENT|CONTEXT|LOCATION)/; my $full_error_regex = qr/^(WARNING|ERROR|FATAL|PANIC|DETAIL|HINT|STATEMENT|CONTEXT)/; my $main_error_regex = qr/^(WARNING|ERROR|FATAL|PANIC)/; my $main_log_regex = qr/^(LOG|WARNING|ERROR|FATAL|PANIC)/; # Set syslog prefix regex my $other_syslog_line = ''; my $pgbouncer_log_format = ''; my $pgbouncer_log_parse1 = ''; my $pgbouncer_log_parse2 = ''; my $pgbouncer_log_parse3 = ''; # Variable to store parsed data following the line prefix my @prefix_params = (); my @pgb_prefix_params = (); my @pgb_prefix_parse1 = (); my @pgb_prefix_parse2 = (); my @pgb_prefix_parse3 = (); # Force incremental mode when rebuild mode is used if ($rebuild && !$incremental) { print STDERR "WARNING: --rebuild require incremental mode, activating it.\n" if (!$month_report || !$day_report); $incremental = 1; } &logmsg('DEBUG', "pgBadger version $VERSION." ); # set timezone to use &set_timezone(1); # Set default top query $top ||= 20; # Set output file my $outfile = ''; $outfile = $outfiles[0] if ($#outfiles >= 0); if (($dump_normalized_only || $dump_all_queries) && $outfile && $outfile !~ /\.txt$/){ localdie("FATAL: dump of normalized queries can be done in text output format, please use .txt extension.\n\n"); } # With multiple output format we must use a temporary binary file my $dft_extens = ''; if ($#outfiles >= 1) { # In non-incremental mode, use a temporary binary file. # In incremental mode, each format will be generated from the binary data. if (!$incremental) { # Set temporary binary file for non-incremental mode. $outfile = $TMP_DIR . "/pgbadger_tmp_$$.bin"; # Remove the default output format for the moment # otherwise all dump will have the same output $dft_extens = $extension; $extension = ''; } } elsif ($#outfiles == -1) { $extension = 'txt' if ($dump_normalized_only || $dump_all_queries); if ($extension) { push(@outfiles, 'out.' . $extension); } elsif ($incremental) { push(@outfiles, 'index.html'); } else { push(@outfiles, 'out.html'); } map { s/\.text/.txt/; } @outfiles; } # Set the default extension and output format, load JSON Perl module if required # Force text output with normalized query list only and disable incremental report # Set default filename of the output file my ($current_out_file, $extens) = &set_output_extension($outdir, $outfile, $extension); # Set default syslog ident name $ident ||= 'postgres'; # Set default pie percentage limit or fix value $pie_percentage_limit = 0 if ($pie_percentage_limit < 0); $pie_percentage_limit = 2 if ($pie_percentage_limit eq ''); $pie_percentage_limit = 100 if ($pie_percentage_limit > 100); # Set default download image format $img_format = lc($img_format); $img_format = 'jpeg' if ($img_format eq 'jpg'); $img_format = 'png' if ($img_format ne 'jpeg'); # Extract the output directory from outfile so that graphs will # be created in the same directory if ($current_out_file ne '-') { if (!$html_outdir && !$outdir) { my @infs = fileparse($current_out_file); if ($infs[0] ne '') { $outdir = $infs[1]; } else { # maybe a confusion between -O and -o localdie("FATAL: output file $current_out_file is a directory, should be a file\nor maybe you want to use -O | --outdir option instead.\n"); } } elsif ($outdir && !-d "$outdir") { # An output directory has been passed as command line parameter localdie("FATAL: $outdir is not a directory or doesn't exist.\n"); } elsif ($html_outdir && !-d "$html_outdir") { # An HTML output directory has been passed as command line parameter localdie("FATAL: $html_outdir is not a directory or doesn't exist.\n"); } $current_out_file = basename($current_out_file); $current_out_file = ($html_outdir || $outdir) . '/' . $current_out_file; $current_out_file =~ s#//#/#g; } # Remove graph support if output is not html $graph = 0 unless ($extens eq 'html' or $extens eq 'binary' or $extens eq 'json'); $graph = 0 if ($nograph); # Set some default values my $end_top = $top - 1; $queue_size ||= 1; $job_per_file ||= 1; if ($^O =~ /MSWin32|dos/i) { if ( ($queue_size > 1) || ($job_per_file > 1) ) { print STDERR "WARNING: parallel processing is not supported on this platform.\n"; } $queue_size = 1; $job_per_file = 1; } # Test file creation before going to parse log my $tmpfh = new IO::File ">$current_out_file"; if (not defined $tmpfh) { localdie("FATAL: can't write to $current_out_file, $!\n"); } $tmpfh->close(); unlink($current_out_file) if (-e $current_out_file); # -w and --disable-error can't go together if ($error_only && $disable_error) { localdie("FATAL: please choose between no event report and reporting events only.\n"); } # Set default search pattern for database, user name, application name and host in log_line_prefix my $regex_prefix_dbname = qr/(?:db|database)=([^,]*)/; my $regex_prefix_dbuser = qr/(?:user|usr)=([^,]*)/; my $regex_prefix_dbclient = qr/(?:client|remote|ip|host|connection_source)=([^,\(]*)/; my $regex_prefix_dbappname = qr/(?:app|application|application_name)=([^,]*)/; my $regex_prefix_queryid = qr/(?:queryid)=([^,]*)/; my $regex_prefix_sqlstate = qr/(?:error_code|state|state_code)=([^,]*)/; my $regex_prefix_backendtype = qr/(?:backend_type|btype)=([^,]*)/; # Set pattern to look for query type my $action_regex = qr/^[\s\(]*(DELETE|INSERT|UPDATE|SELECT|COPY|WITH|CREATE|DROP|ALTER|TRUNCATE|BEGIN|COMMIT|ROLLBACK|START|END|SAVEPOINT|DECLARE|CLOSE|FETCH|MOVE)/is; # Loading excluded query from file if any if ($exclude_file) { open(my $in, '<', $exclude_file) or localdie("FATAL: can't read file $exclude_file: $!\n"); my @exclq = <$in>; close($in); chomp(@exclq); foreach my $r (@exclq) { $r =~ s/\r//; &check_regex($r, '--exclude-file'); } push(@exclude_query, @exclq); } # Testing regex syntax if ($#exclude_query >= 0) { foreach my $r (@exclude_query) { &check_regex($r, '--exclude-query'); } } # Testing regex syntax if ($#exclude_time >= 0) { foreach my $r (@exclude_time) { &check_regex($r, '--exclude-time'); } } # # Testing regex syntax if ($#include_time >= 0) { foreach my $r (@include_time) { &check_regex($r, '--include-time'); } } # Loading included query from file if any if ($include_file) { open(my $in, '<', $include_file) or localdie("FATAL: can't read file $include_file: $!\n"); my @exclq = <$in>; close($in); chomp(@exclq); foreach my $r (@exclq) { $r =~ s/\r//; &check_regex($r, '--include-file'); } push(@include_query, @exclq); } # Testing regex syntax if ($#include_query >= 0) { foreach my $r (@include_query) { &check_regex($r, '--include-query'); } } # Check start/end date time if ($from) { if ( $from =~ /^(\d{2}):(\d{2}):(\d{2})([.]\d+([+-]\d+)?)?$/) { # only time, trick around the date part my $fractional_seconds = $4 || "0"; $from_hour = "$1:$2:$3.$fractional_seconds"; &logmsg('DEBUG', "Setting begin time to [$from_hour]" ); } elsif( $from =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})([.]\d+([+-]\d+)?)?$/ ) { my $fractional_seconds = $7 || "0"; $from = "$1-$2-$3 $4:$5:$6.$fractional_seconds"; &logmsg('DEBUG', "Setting begin datetime to [$from]" ); } else { localdie("FATAL: bad format for begin datetime/time, should be yyyy-mm-dd hh:mm:ss.l+tz or hh:mm:ss.l+tz\n"); } } if ($to) { if ( $to =~ /^(\d{2}):(\d{2}):(\d{2})([.]\d+([+-]\d+)?)?$/) { # only time, trick around the date part my $fractional_seconds = $4 || "0"; $to_hour = "$1:$2:$3.$fractional_seconds"; &logmsg('DEBUG', "Setting end time to [$to_hour]" ); } elsif( $to =~ /^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})([.]\d+([+-]\d+)?)?$/) { my $fractional_seconds = $7 || "0"; $to = "$1-$2-$3 $4:$5:$6.$fractional_seconds"; &logmsg('DEBUG', "Setting end time to [$to]" ); } else { localdie("FATAL: bad format for ending datetime, should be yyyy-mm-dd hh:mm:ss.l+tz or hh:mm:ss.l+tz\n"); } } if ($from && $to && ($from gt $to)) { localdie("FATAL: begin date is after end date!\n") ; } # Stores the last parsed line from log file to allow incremental parsing my $LAST_LINE = ''; # Set the level of the data aggregator, can be minute, hour or day follow the # size of the log file. my $LEVEL = 'hour'; # Month names my %month_abbr = ( 'Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06', 'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12' ); my %abbr_month = ( '01' => 'Jan', '02' => 'Feb', '03' => 'Mar', '04' => 'Apr', '05' => 'May', '06' => 'Jun', '07' => 'Jul', '08' => 'Aug', '09' => 'Sep', '10' => 'Oct', '11' => 'Nov', '12' => 'Dec' ); # Inbounds of query times histogram my @histogram_query_time = (0, 1, 5, 10, 25, 50, 100, 500, 1000, 10000); # Inbounds of session times histogram my @histogram_session_time = (0, 500, 1000, 30000, 60000, 600000, 1800000, 3600000, 28800000); # Check histogram values user redefinition if ($histogram_query) { if ($histogram_query =~ /[^0-9\s,]+/) { die("Incorrect value for option --histogram_query\n"); } @histogram_query_time = split(/\s*,\s*/, $histogram_query); } if ($histogram_session) { if ($histogram_session =~ /[^0-9\s,]+/) { die("Incorrect value for option --histogram_session\n"); } @histogram_session_time = split(/\s*,\s*/, $histogram_session); } # Where statistics are stored my %overall_stat = (); my %pgb_overall_stat = (); my %overall_checkpoint = (); my %top_slowest = (); my %normalyzed_info = (); my %error_info = (); my %pgb_error_info = (); my %pgb_pool_info = (); my %logs_type = (); my %errors_code = (); my %per_minute_info = (); my %pgb_per_minute_info = (); my %lock_info = (); my %tempfile_info = (); my %cancelled_info = (); my %connection_info = (); my %pgb_connection_info = (); my %database_info = (); my %application_info = (); my %user_info = (); my %host_info = (); my %session_info = (); my %pgb_session_info = (); my %conn_received = (); my %checkpoint_info = (); my %autovacuum_info = (); my %autoanalyze_info = (); my @graph_values = (); my %cur_info = (); my %cur_temp_info = (); my %cur_plan_info = (); my %cur_cancel_info = (); my %cur_lock_info = (); my $nlines = 0; my %last_line = (); my %pgb_last_line = (); our %saved_last_line = (); our %pgb_saved_last_line= (); my %top_locked_info = (); my %top_tempfile_info = (); my %top_cancelled_info = (); my %drawn_graphs = (); my %cur_bind_info = (); my %prepare_info = (); my %bind_info = (); # Global output filehandle my $fh = undef; my $t0 = Benchmark->new; # Write resources files from __DATA__ section if they have not been already copied # and return the HTML links to that files. If --extra-file is not used returns the # CSS and JS code to be embeded in HTML files my @jscode = &write_resources(); # Automatically set parameters with incremental mode if ($incremental) { # In incremental mode an output directory must be set if (!$html_outdir && !$outdir) { localdie("FATAL: you must specify an output directory with incremental mode, see -O or --outdir.\n") } # Ensure this is not a relative path if ($outdir && dirname($outdir) eq '.') { localdie("FATAL3: output directory ($outdir) is not an absolute path.\n"); } if ($html_outdir && dirname($html_outdir) eq '.') { localdie("FATAL: output HTML directory ($html_outdir) is not an absolute path.\n"); } # Ensure that the directory already exists if ($outdir && !-d $outdir) { localdie("FATAL: output directory $outdir does not exists.\n"); } # Verify that the HTML outdir exixts when specified if ($html_outdir && !-d $html_outdir) { localdie("FATAL: output HTML directory $html_outdir does not exists.\n"); } # Set default last parsed file in incremental mode if (!$last_parsed && $incremental) { $last_parsed = $outdir . '/LAST_PARSED'; } $current_out_file = 'index.html'; # Set default output format $extens = 'binary'; if ($rebuild && !$month_report && !$day_report) { # Look for directory where report must be generated again my @build_directories = (); # Find directories that shoud be rebuilt unless(opendir(DIR, "$outdir")) { localdie("FATAL: can't opendir $outdir: $!\n"); } my @dyears = grep { $_ =~ /^\d+$/ } readdir(DIR); closedir DIR; foreach my $y (sort { $a <=> $b } @dyears) { unless(opendir(DIR, "$outdir/$y")) { localdie("FATAL: can't opendir $outdir/$y: $!\n"); } my @dmonths = grep { $_ =~ /^\d+$/ } readdir(DIR); closedir DIR; foreach my $m (sort { $a <=> $b } @dmonths) { unless(opendir(DIR, "$outdir/$y/$m")) { localdie("FATAL: can't opendir $outdir/$y/$m: $!\n"); } my @ddays = grep { $_ =~ /^\d+$/ } readdir(DIR); closedir DIR; foreach my $d (sort { $a <=> $b } @ddays) { unless(opendir(DIR, "$outdir/$y/$m/$d")) { localdie("FATAL: can't opendir $outdir/$y/$m/$d: $!\n"); } my @binfiles = grep { $_ =~ /\.bin$/ } readdir(DIR); closedir DIR; push(@build_directories, "$y-$m-$d") if ($#binfiles >= 0); } } } &build_incremental_reports(@build_directories); my $t2 = Benchmark->new; my $td = timediff($t2, $t0); &logmsg('DEBUG', "rebuilding reports took: " . timestr($td)); # Remove pidfile unlink("$PID_FILE"); exit 0; } elsif ($month_report) { # Look for directory where cumulative report must be generated my @build_directories = (); # Get year+month as a path $month_report =~ s#/#-#g; my $month_path = $month_report; $month_path =~ s#-#/#g; if ($month_path !~ m#^\d{4}/\d{2}$#) { localdie("FATAL: invalid format YYYY-MM for --month-report option: $month_report"); } &logmsg('DEBUG', "building month report into $outdir/$month_path"); # Find days directories that shoud be used to build the monthly report unless(opendir(DIR, "$outdir/$month_path")) { localdie("FATAL: can't opendir $outdir/$month_path: $!\n"); } my @ddays = grep { $_ =~ /^\d+$/ } readdir(DIR); closedir DIR; foreach my $d (sort { $a <=> $b } @ddays) { unless(opendir(DIR, "$outdir/$month_path/$d")) { localdie("FATAL: can't opendir $outdir/$month_path/$d: $!\n"); } my @binfiles = grep { $_ =~ /\.bin$/ } readdir(DIR); closedir DIR; push(@build_directories, "$month_report-$d") if ($#binfiles >= 0); } &build_month_reports($month_path, @build_directories); my $t2 = Benchmark->new; my $td = timediff($t2, $t0); &logmsg('DEBUG', "building month report took: " . timestr($td)); # Remove pidfile unlink("$PID_FILE"); exit 0; } elsif ($day_report) { # Look for directory where cumulative report must be generated my @build_directories = (); # Get year+month as a path $day_report =~ s#/#-#g; my $day_path = $day_report; $day_path =~ s#-#/#g; if ($day_path !~ m#^\d{4}/\d{2}\/\d{2}$#) { localdie("FATAL: invalid format YYYY-MM-DD for --day-report option: $day_report"); } &logmsg('DEBUG', "building day report into $outdir/$day_path"); # Find days directories that shoud be used to build the monthly report unless(opendir(DIR, "$outdir/$day_path")) { localdie("FATAL: can't opendir $outdir/$day_path: $!\n"); } my @binfiles = grep { $_ =~ /\.bin$/ } readdir(DIR); closedir DIR; push(@build_directories, "$day_report") if ($#binfiles >= 0); &build_day_reports($day_path, @build_directories); my $t2 = Benchmark->new; my $td = timediff($t2, $t0); &logmsg('DEBUG', "building day report took: " . timestr($td)); # Remove pidfile unlink("$PID_FILE"); exit 0; } } else { # Extra files for resources are not allowed without incremental mode $extra_files = 0; } # Reading last line parsed if ($last_parsed && -e $last_parsed) { if (open(my $in, '<', $last_parsed)) { my @content = <$in>; close($in); foreach my $line (@content) { chomp($line); next if (!$line); my ($datetime, $current_pos, $orig, @others) = split(/\t/, $line); # Last parsed line with pgbouncer log starts with this keyword if ($datetime eq 'pgbouncer') { $pgb_saved_last_line{datetime} = $current_pos; $pgb_saved_last_line{current_pos} = $orig; $pgb_saved_last_line{orig} = join("\t", @others); } else { $saved_last_line{datetime} = $datetime; $saved_last_line{current_pos} = $current_pos; $saved_last_line{orig} = $orig; } &logmsg('DEBUG', "Found log offset " . ($saved_last_line{current_pos} || $pgb_saved_last_line{current_pos}) . " in file $last_parsed"); } # Those two log format must be read from start of the file if ( ($format eq 'binary') || ($format eq 'csv') ) { $saved_last_line{current_pos} = 0; $pgb_saved_last_line{current_pos} = 0 if ($format eq 'binary'); } } else { localdie("FATAL: can't read last parsed line from $last_parsed, $!\n"); } } $tmp_last_parsed = 'tmp_' . basename($last_parsed) if ($last_parsed); $tmp_last_parsed = "$TMP_DIR/$tmp_last_parsed"; $tmp_dblist = "$TMP_DIR/dblist.tmp"; ### ### Explanation: ### ### Logic for the BINARY storage ($outdir) SHOULD be: ### If ('noclean') ### do nothing (keep everything) ### If (NO 'noclean') and (NO 'retention'): ### use an arbitrary retention duration of: 5 weeks ### remove BINARY files older than LAST_PARSED_MONTH-retention ### DO NOT CHECK for HTML file existence as OLD HTML files may be deleted by external tools ### which may lead to BINARY files NEVER deleted ### If (NO 'noclean') and ('retention'): ### remove BINARY files older than LAST_PARSED_MONTH-retention ### DO NOT CHECK for HTML file existence as OLD HTML files may be deleted by external tools ### which may lead to BINARY files NEVER deleted ### ### Logic for the HTML storage ($html_outdir || $outdir) SHOULD be: ### DO NOT check 'noclean' as this flag is dedicated to BINARY files ### If (NO 'retention'): ### do nothing (keep everything) ### If ('retention'): ### remove HTML folders/files older than LAST_PARSED_MONTH-retention (= y/m/d): ### days older than d in y/m/d ### months older than m in y/m/d ### years older than y in y/m/d ### # Clear BIN/HTML storages in incremental mode my @all_outdir = (); push(@all_outdir, $outdir) if ($outdir); push(@all_outdir, $html_outdir) if ($html_outdir); ### $retention_bin = 5 if (!$noclean && !$retention); ### $retention_bin = 0 if ($noclean && !$retention); ### $retention_bin = $retention if (!$noclean && $retention); ### $retention_bin = 0 if ($noclean && $retention); ### equivalent to: my $retention_bin = $retention; $retention_bin = 0 if ($noclean && $retention); $retention_bin = 5 if (!$noclean && !$retention); ### $retention_html = 0 if (!$noclean && !$retention); ### $retention_html = 0 if ($noclean && !$retention); ### $retention_html = $retention if (!$noclean && $retention); ### $retention_html = $retention if ($noclean && $retention); ### equivalent to: my $retention_html = $retention; ### We will handle noclean/!noclean and retention_xxx/!retention_xxx below ### Note: !retention_xxx equivalent to retention_xxx = 0 &logmsg('DEBUG', "BIN/HTML Retention cleanup: Initial cleanup flags - noclean=[$noclean] - retention_bin=[$retention_bin] - retention_html=[$retention_html] - saved_last_line{datetime}=[$saved_last_line{datetime}] - pgb_saved_last_line{datetime}=[$pgb_saved_last_line{datetime}] - all_outdir=[@all_outdir]"); if ( scalar(@all_outdir) && ($saved_last_line{datetime} || $pgb_saved_last_line{datetime}) ) { foreach my $ret_dir (@all_outdir) { if (($saved_last_line{datetime} =~ /^(\d+)\-(\d+)\-(\d+) /) || ($pgb_saved_last_line{datetime} =~ /^(\d+)\-(\d+)\-(\d+) /)) { # Search the current week following the last parse date my $limit_yw_bin = $1; my $limit_yw_html = $1; my $wn = &get_week_number($1, $2, $3); # BIN: Case of year overlap if (($wn - $retention_bin) < 1) { # Rewind to previous year $limit_yw_bin--; # Get number of last week of previous year, can be 52 or 53 my $prevwn = &get_week_number($limit_yw_bin, 12, 31); # Add week number including retention_bin to the previous year $limit_yw_bin .= sprintf("%02d", $prevwn - abs($wn - $retention_bin)); } else { $limit_yw_bin .= sprintf("%02d", $wn - $retention_bin); } &logmsg('DEBUG', "BIN Retention cleanup: YearWeek Limit computation - YearWeek=<$limit_yw_bin> - This will help later removal"); # HTML: Case of year overlap if (($wn - $retention_html) < 1) { # Rewind to previous year $limit_yw_html--; # Get number of last week of previous year, can be 52 or 53 my $prevwn = &get_week_number($limit_yw_html, 12, 31); # Add week number including retention_html to the previous year $limit_yw_html .= sprintf("%02d", $prevwn - abs($wn - $retention_html)); } else { $limit_yw_html .= sprintf("%02d", $wn - $retention_html); } &logmsg('DEBUG', "HTML Retention cleanup: YearWeek Limit computation - YearWeek=<$limit_yw_html> - This will help later removal"); my @dyears = (); if ( opendir(DIR, "$ret_dir") ) { @dyears = grep { $_ =~ /^\d+$/ } readdir(DIR); closedir DIR; } else { &logmsg('ERROR', "BIN/HTML Retention cleanup: can't opendir $ret_dir: $!"); } # Find obsolete weeks dir that shoud be cleaned foreach my $y (sort { $a <=> $b } @dyears) { my @weeks = (); if ( opendir(DIR, "$ret_dir/$y") ) { @weeks = grep { $_ =~ /^week-\d+$/ } readdir(DIR); closedir DIR; } else { &logmsg('ERROR', "BIN/HTML Retention cleanup: can't opendir $ret_dir/$y: $!"); } foreach my $w (sort { $a <=> $b } @weeks) { $w =~ /^week-(\d+)$/; if ( (!$noclean) && $retention_bin ) { if ("$y$1" lt $limit_yw_bin) { &logmsg('DEBUG', "BIN Retention cleanup: Removing obsolete week directory $ret_dir/$y/week-$1"); &cleanup_directory_bin("$ret_dir/$y/week-$1", 1); } } if ( $retention_html ) { if ("$y$1" lt $limit_yw_html) { &logmsg('DEBUG', "HTML Retention cleanup: Removing obsolete week directory $ret_dir/$y/week-$1"); &cleanup_directory_html("$ret_dir/$y/week-$1", 1); } } } } # Find obsolete months and days foreach my $y (sort { $a <=> $b } @dyears) { my @dmonths = (); if ( opendir(DIR, "$ret_dir/$y") ) { @dmonths = grep { $_ =~ /^\d+$/ } readdir(DIR); closedir DIR; } else { &logmsg('ERROR', "BIN/HTML Retention cleanup: can't opendir $ret_dir/$y: $!"); } # Now remove the HTML monthly reports if ( $retention_html ) { foreach my $m (sort { $a <=> $b } @dmonths) { my $diff_day = $retention_html * 7 * 86400; my $lastday = 0; if (($saved_last_line{datetime} =~ /^(\d+)\-(\d+)\-(\d+) /) || ($pgb_saved_last_line{datetime} =~ /^(\d+)\-(\d+)\-(\d+) /)) { $lastday = timelocal(1,1,1,$3,$2-1,$1-1900); } if ( $lastday ) { my $lastday_minus_retention = $lastday - $diff_day; my $lastday_minus_retention_Y = strftime('%Y', localtime($lastday_minus_retention)); my $lastday_minus_retention_M = strftime('%m', localtime($lastday_minus_retention)); my $lastday_minus_retention_prev_month_Y = $lastday_minus_retention_Y; my $lastday_minus_retention_prev_month_M = $lastday_minus_retention_M - 1; if ( $lastday_minus_retention_prev_month_M < 1 ) { $lastday_minus_retention_prev_month_Y -= 1; $lastday_minus_retention_prev_month_M = 12; } $lastday_minus_retention_prev_month_Y = sprintf("%04d", $lastday_minus_retention_prev_month_Y); $lastday_minus_retention_prev_month_M = sprintf("%02d", $lastday_minus_retention_prev_month_M); if ("$y$m" lt "$lastday_minus_retention_Y$lastday_minus_retention_M") { &logmsg('DEBUG', "HTML Retention cleanup: Removing obsolete month directory $ret_dir/$y/$m"); &cleanup_directory_html("$ret_dir/$y/$m", 1); } } } } # Now remove the corresponding days foreach my $m (sort { $a <=> $b } @dmonths) { my @ddays = (); if ( opendir(DIR, "$ret_dir/$y/$m") ) { @ddays = grep { $_ =~ /^\d+$/ } readdir(DIR); closedir DIR; } else { &logmsg('ERROR', "BIN/HTML Retention cleanup: can't opendir $ret_dir/$y/$m: $!"); } foreach my $d (sort { $a <=> $b } @ddays) { if ( (!$noclean) && $retention_bin ) { # Remove obsolete days when we are in binary mode # with noreport - there's no week-N directory my $diff_day = $retention_bin * 7 * 86400; my $oldday = timelocal(1,1,1,$d,$m-1,$y-1900); my $lastday = $oldday; if (($saved_last_line{datetime} =~ /^(\d+)\-(\d+)\-(\d+) /) || ($pgb_saved_last_line{datetime} =~ /^(\d+)\-(\d+)\-(\d+) /)) { $lastday = timelocal(1,1,1,$3,$2-1,$1-1900); } if (($lastday - $oldday) > $diff_day) { &logmsg('DEBUG', "BIN Retention cleanup: Removing obsolete day directory $ret_dir/$y/$m/$d"); &cleanup_directory_bin("$ret_dir/$y/$m/$d", 1); } } if ( $retention_html ) { # Remove obsolete days when we are in binary mode # with noreport - there's no week-N directory my $diff_day = $retention_html * 7 * 86400; my $oldday = timelocal(1,1,1,$d,$m-1,$y-1900); my $lastday = $oldday; if (($saved_last_line{datetime} =~ /^(\d+)\-(\d+)\-(\d+) /) || ($pgb_saved_last_line{datetime} =~ /^(\d+)\-(\d+)\-(\d+) /)) { $lastday = timelocal(1,1,1,$3,$2-1,$1-1900); } if (($lastday - $oldday) > $diff_day) { &logmsg('DEBUG', "HTML Retention cleanup: Removing obsolete day directory $ret_dir/$y/$m/$d"); &cleanup_directory_html("$ret_dir/$y/$m/$d", 1); } } } if ( rmdir("$ret_dir/$y/$m") ) { &logmsg('DEBUG', "BIN/HTML Retention cleanup: Removing obsolete empty directory $ret_dir/$y/$m"); } } if ( rmdir("$ret_dir/$y") ) { &logmsg('DEBUG', "BIN/HTML Retention cleanup: Removing obsolete empty directory $ret_dir/$y"); } } } } } # Main loop reading log files my $global_totalsize = 0; my @given_log_files = ( @log_files ); chomp(@given_log_files); # Store globaly total size for each log files my %file_size = (); foreach my $logfile ( @given_log_files ) { $file_size{$logfile} = &get_file_size($logfile); $global_totalsize += $file_size{$logfile} if ($file_size{$logfile} > 0); } # Verify that the file has not changed for incremental move if (($incremental || $last_parsed) && !$remote_host) { my @tmpfilelist = (); # Removed files that have already been parsed during previous runs foreach my $f (@given_log_files) { if ($f eq '-') { &logmsg('DEBUG', "waiting for log entries from stdin."); $saved_last_line{current_pos} = 0; push(@tmpfilelist, $f); } elsif ($f =~ /\.bin$/) { &logmsg('DEBUG', "binary file \"$f\" as input, there is no log to parse."); $saved_last_line{current_pos} = 0; push(@tmpfilelist, $f); } elsif ( $journalctl_cmd && ($f eq $journalctl_cmd) ) { my $since = ''; if ( ($journalctl_cmd !~ /--since|-S/) && ($saved_last_line{datetime} =~ /^(\d+)-(\d+)-(\d+).(\d+):(\d+):(\d+)/) ) { $since = " --since='$1-$2-$3 $4:$5:$6'"; } &logmsg('DEBUG', "journalctl call will start since: $saved_last_line{datetime}"); my $new_journalctl_cmd = "$f$since"; push(@tmpfilelist, $new_journalctl_cmd); $file_size{$new_journalctl_cmd} = $file_size{$f}; } elsif ( $log_command && ($f eq $log_command) ) { &logmsg('DEBUG', "custom command waiting for log entries from stdin."); $saved_last_line{current_pos} = 0; push(@tmpfilelist, $f); } else { # Auto detect log format for proper parsing my $fmt = $format || 'stderr'; $fmt = autodetect_format($f, $file_size{$f}); $fmt ||= $format; # Set regex to parse the log file $fmt = set_parser_regex($fmt); if (($fmt ne 'pgbouncer') && ($saved_last_line{current_pos} > 0)) { my ($retcode, $msg) = &check_file_changed($f, $file_size{$f}, $fmt, $saved_last_line{datetime}, $saved_last_line{current_pos}); if (!$retcode) { &logmsg('DEBUG', "this file has already been parsed: $f, $msg"); } else { push(@tmpfilelist, $f); } } elsif (($fmt eq 'pgbouncer') && ($pgb_saved_last_line{current_pos} > 0)) { my ($retcode, $msg) = &check_file_changed($f, $file_size{$f}, $fmt, $pgb_saved_last_line{datetime}, $pgb_saved_last_line{current_pos}); if (!$retcode) { &logmsg('DEBUG', "this file has already been parsed: $f, $msg"); } else { push(@tmpfilelist, $f); } } else { push(@tmpfilelist, $f); } } } @given_log_files = (); push(@given_log_files, @tmpfilelist); } # Pipe used for progress bar in multiprocess my $pipe = undef; # Seeking to an old log position is not possible outside incremental mode if (!$last_parsed || !exists $saved_last_line{current_pos}) { $saved_last_line{current_pos} = 0; $pgb_saved_last_line{current_pos} = 0; } if ($dump_all_queries) { $fh = new IO::File; $fh->open($outfiles[0], '>') or localdie("FATAL: can't open output file $outfiles[0], $!\n"); } #### # Start parsing all log files #### # Number of running process my $child_count = 0; # Set max number of parallel process my $parallel_process = 0; # Open a pipe for interprocess communication my $reader = new IO::Handle; my $writer = new IO::Handle; # Fork the logger process if (!$nofork && $progress) { $pipe = IO::Pipe->new($reader, $writer); $writer->autoflush(1); spawn sub { &multiprocess_progressbar($global_totalsize); }; } # Initialise the list of reports to produce with the default report # if $report_per_database is enabled there will be a report for each # database. Information not related to a database (checkpoint, pgbouncer # statistics, etc.) will be included in the default report which should # be the postgres database to be read by the DBA of the PostgreSQL cluster. $DBLIST{$DBALL} = 1; # Prevent parallelism perl file to be higher than the number of files $job_per_file = ($#given_log_files+1) if ( ($job_per_file > 1) && ($job_per_file > ($#given_log_files+1)) ); # Parse each log file following the multiprocess mode chosen (-j or -J) my $current_log_file = ''; foreach my $logfile ( @given_log_files ) { $current_log_file = $logfile if ($#given_log_files > 0); # If we just want to build incremental reports from binary files # just build the list of input directories with binary files if ($incremental && $html_outdir && !$outdir) { my $incr_date = ''; my $binpath = ''; if ($logfile =~ /^(.*\/)(\d+)\/(\d+)\/(\d+)\/[^\/]+\.bin$/) { $binpath = $1; $incr_date = "$2-$3-$4"; } # Mark the directory as needing index update if (open(my $out, '>>', "$last_parsed.tmp")) { print $out "$binpath$incr_date\n"; close($out); } else { &logmsg('ERROR', "can't save last parsed line into $last_parsed.tmp, $!"); } next; } # Confirm if we can use multiprocess for this file my $pstatus = confirm_multiprocess($logfile); if ($pstatus >= 0) { if ($pstatus = 1 && $job_per_file > 1) { $parallel_process = $job_per_file; } else { $parallel_process = $queue_size; } } else { &logmsg('DEBUG', "parallel processing will not be used."); $parallel_process = 1; } # Wait until a child dies if max parallel processes is reach while ($child_count >= $parallel_process) { my $kid = waitpid(-1, WNOHANG); if ($kid > 0) { $child_count--; delete $RUNNING_PIDS{$kid}; } sleep(1); } # Get log format of the current file my $fmt = $format || 'stderr'; my $logfile_orig = $logfile; if ($logfile ne '-' && !$journalctl_cmd && !$log_command) { $fmt = &autodetect_format($logfile, $file_size{$logfile}); $fmt ||= $format; # Remove log format from filename if any $logfile =~ s/:(stderr|csv|syslog|pgbouncer|jsonlog|logplex|rds|redshift)\d*$//i; &logmsg('DEBUG', "pgBadger will use log format $fmt to parse $logfile."); } else { &logmsg('DEBUG', "Can not autodetect log format, assuming $fmt."); } # Set the timezone to use &set_timezone(); # Set the regex to parse the log file following the format $fmt = set_parser_regex($fmt); # Do not use split method with remote and compressed files, stdin, custom or journalctl command if ( ($parallel_process > 1) && ($queue_size > 1) && ($logfile !~ $compress_extensions) && ($logfile !~ /\.bin$/i) && ($logfile ne '-') && ($logfile !~ /^(http[s]*|ftp[s]*|ssh):/i) && (!$journalctl_cmd || ($logfile !~ /\Q$journalctl_cmd\E/)) && (!$log_command || ($logfile !~ /\Q$log_command\E/)) ) { # Create multiple processes to parse one log file by chunks of data my @chunks = split_logfile($logfile, $file_size{$logfile_orig}, ($fmt eq 'pgbouncer') ? $pgb_saved_last_line{current_pos} : $saved_last_line{current_pos}); &logmsg('DEBUG', "The following boundaries will be used to parse file $logfile, " . join('|', @chunks)); for (my $i = 0; $i < $#chunks; $i++) { while ($child_count >= $parallel_process) { my $kid = waitpid(-1, WNOHANG); if ($kid > 0) { $child_count--; delete $RUNNING_PIDS{$kid}; } sleep(1); } localdie("FATAL: Abort signal received when processing to next chunk\n") if ($interrupt == 2); last if ($interrupt); push(@tempfiles, [ tempfile('tmp_pgbadgerXXXX', SUFFIX => '.bin', DIR => $TMP_DIR, O_TEMPORARY => 1, UNLINK => 1 ) ]); spawn sub { &process_file($logfile, $file_size{$logfile_orig}, $fmt, $tempfiles[-1]->[0], $chunks[$i], $chunks[$i+1], $i); }; $child_count++; } } else { # Start parsing one file per parallel process if (!$nofork) { push(@tempfiles, [ tempfile('tmp_pgbadgerXXXX', SUFFIX => '.bin', DIR => $TMP_DIR, UNLINK => 1 ) ]); spawn sub { &process_file($logfile, $file_size{$logfile_orig}, $fmt, $tempfiles[-1]->[0], ($fmt eq 'pgbouncer') ? $pgb_saved_last_line{current_pos} : $saved_last_line{current_pos}); }; $child_count++; } else { &process_file($logfile, $file_size{$logfile_orig}, $fmt, undef, ($fmt eq 'pgbouncer') ? $pgb_saved_last_line{current_pos} : $saved_last_line{current_pos}); } } localdie("FATAL: Abort signal received when processing next file\n") if ($interrupt == 2); last if ($interrupt); } # Wait for all child processes to localdie except for the logger # On Windows OS $progress is disabled so we don't go here while (scalar keys %RUNNING_PIDS > $progress) { my $kid = waitpid(-1, WNOHANG); if ($kid > 0) { delete $RUNNING_PIDS{$kid}; } sleep(1); } # Terminate the process logger if (!$nofork) { foreach my $k (keys %RUNNING_PIDS) { kill('USR1', $k); %RUNNING_PIDS = (); } # Clear previous statistics &init_stats_vars(); } # Load all data gathered by all the different processes if (!$nofork) { foreach my $f (@tempfiles) { next if (!-e "$f->[1]" || -z "$f->[1]"); my $fht = new IO::File; $fht->open("< $f->[1]") or localdie("FATAL: can't open temp file $f->[1], $!\n"); load_stats($fht); $fht->close(); } } # Get last line parsed from all process if ($last_parsed) { &logmsg('DEBUG', "Reading temporary last parsed line from $tmp_last_parsed"); if (open(my $in, '<', $tmp_last_parsed) ) { while (my $line = <$in>) { chomp($line); $line =~ s/\r//; my ($d, $p, $l, @o) = split(/\t/, $line); if ($d ne 'pgbouncer') { if (!$last_line{datetime} || ($d gt $last_line{datetime})) { $last_line{datetime} = $d; $last_line{orig} = $l; $last_line{current_pos} = $p; } } else { $d = $p; $p = $l; $l = join("\t", @o); if (!$pgb_last_line{datetime} || ($d gt $pgb_last_line{datetime})) { $pgb_last_line{datetime} = $d; $pgb_last_line{orig} = $l; $pgb_last_line{current_pos} = $p; } } } close($in); } unlink("$tmp_last_parsed"); } # Save last line parsed if ($last_parsed && ($last_line{datetime} || $pgb_last_line{datetime}) && ($last_line{orig} || $pgb_last_line{orig}) ) { &logmsg('DEBUG', "Saving last parsed line into $last_parsed"); if (open(my $out, '>', $last_parsed)) { if ($last_line{datetime}) { $last_line{current_pos} ||= 0; print $out "$last_line{datetime}\t$last_line{current_pos}\t$last_line{orig}\n"; } elsif ($saved_last_line{datetime}) { $saved_last_line{current_pos} ||= 0; print $out "$saved_last_line{datetime}\t$saved_last_line{current_pos}\t$saved_last_line{orig}\n"; } if ($pgb_last_line{datetime}) { $pgb_last_line{current_pos} ||= 0; print $out "pgbouncer\t$pgb_last_line{datetime}\t$pgb_last_line{current_pos}\t$pgb_last_line{orig}\n"; } elsif ($pgb_saved_last_line{datetime}) { $pgb_saved_last_line{current_pos} ||= 0; print $out "pgbouncer\t$pgb_saved_last_line{datetime}\t$pgb_saved_last_line{current_pos}\t$pgb_saved_last_line{orig}\n"; } close($out); } else { &logmsg('ERROR', "can't save last parsed line into $last_parsed, $!"); } } if ($terminate) { unlink("$PID_FILE"); exit 2; } #### # Generates statistics output #### my $t1 = Benchmark->new; my $td = timediff($t1, $t0); &logmsg('DEBUG', "the log statistics gathering took:" . timestr($td)); if ($dump_all_queries) { $fh->close(); # Remove pidfile and temporary file unlink($tmp_dblist) if ($tmp_dblist); unlink("$PID_FILE"); unlink("$last_parsed.tmp") if (-e "$last_parsed.tmp"); unlink($TMP_DIR . "/pgbadger_tmp_$$.bin") if ($#outfiles >= 1); exit 0; } # Read the list of database we have proceeded in all child process if ($report_per_database) { %DBLIST = (); if (open(my $out, '<', "$tmp_dblist")) { my @data = <$out>; foreach my $tmp (@data) { chomp($tmp); my %dblist = split(/;/, $tmp); foreach my $d (keys %dblist) { next if ($#dbname >= 0 and !grep(/^$d$/i, @dbname)); $DBLIST{$d} = 1; $overall_stat{nlines}{$d} += $dblist{$d}; } } close($out); &logmsg('DEBUG', "looking for list of database retrieved from log: " . join(',', keys %DBLIST)); } else { &logmsg('ERROR', "can't read list of database from file $tmp_dblist, $!"); } } if ( !$incremental && ($#given_log_files >= 0) ) { # If we have a temporary binary file (multiple output formats), load it before generating reports if ($#outfiles >= 1 && -e $TMP_DIR . "/pgbadger_tmp_$$.bin") { &init_stats_vars(); if (open(my $bin_fh, '<', $TMP_DIR . "/pgbadger_tmp_$$.bin")) { my %stats = %{ fd_retrieve($bin_fh) }; close($bin_fh); # Restore all statistics from binary file %overall_stat = %{ $stats{overall_stat} } if (exists $stats{overall_stat}); %pgb_overall_stat = %{ $stats{pgb_overall_stat} } if (exists $stats{pgb_overall_stat}); %overall_checkpoint = %{ $stats{overall_checkpoint} } if (exists $stats{overall_checkpoint}); %normalyzed_info = %{ $stats{normalyzed_info} } if (exists $stats{normalyzed_info}); %error_info = %{ $stats{error_info} } if (exists $stats{error_info}); %pgb_error_info = %{ $stats{pgb_error_info} } if (exists $stats{pgb_error_info}); %pgb_pool_info = %{ $stats{pgb_pool_info} } if (exists $stats{pgb_pool_info}); %connection_info = %{ $stats{connection_info} } if (exists $stats{connection_info}); %pgb_connection_info = %{ $stats{pgb_connection_info} } if (exists $stats{pgb_connection_info}); %database_info = %{ $stats{database_info} } if (exists $stats{database_info}); %application_info = %{ $stats{application_info} } if (exists $stats{application_info}); %user_info = %{ $stats{user_info} } if (exists $stats{user_info}); %host_info = %{ $stats{host_info} } if (exists $stats{host_info}); %checkpoint_info = %{ $stats{checkpoint_info} } if (exists $stats{checkpoint_info}); %session_info = %{ $stats{session_info} } if (exists $stats{session_info}); %pgb_session_info = %{ $stats{pgb_session_info} } if (exists $stats{pgb_session_info}); %tempfile_info = %{ $stats{tempfile_info} } if (exists $stats{tempfile_info}); %logs_type = %{ $stats{logs_type} } if (exists $stats{logs_type}); %lock_info = %{ $stats{lock_info} } if (exists $stats{lock_info}); %per_minute_info = %{ $stats{per_minute_info} } if (exists $stats{per_minute_info}); %pgb_per_minute_info = %{ $stats{pgb_per_minute_info} } if (exists $stats{pgb_per_minute_info}); %top_slowest = %{ $stats{top_slowest} } if (exists $stats{top_slowest}); @log_files = @{ $stats{log_files} } if (exists $stats{log_files}); %autovacuum_info = %{ $stats{autovacuum_info} } if (exists $stats{autovacuum_info}); %autoanalyze_info = %{ $stats{autoanalyze_info} } if (exists $stats{autoanalyze_info}); %top_tempfile_info = %{ $stats{top_tempfile_info} } if (exists $stats{top_tempfile_info}); %top_locked_info = %{ $stats{top_locked_info} } if (exists $stats{top_locked_info}); %prepare_info = %{ $stats{prepare_info} } if (exists $stats{prepare_info}); %bind_info = %{ $stats{bind_info} } if (exists $stats{bind_info}); $nlines = $stats{nlines} if (exists $stats{nlines}); &logmsg('DEBUG', "binary statistics loaded from temporary file."); } else { &logmsg('WARNING', "could not load temporary binary file for multiple output generation."); } } my $chld_running = 0; foreach my $db (sort keys %DBLIST) { next if (!$db); if ($nofork || $parallel_process <= 1) { &gen_database_report($db); } else { while ($chld_running >= $parallel_process) { my $kid = waitpid(-1, WNOHANG); if ($kid > 0) { $chld_running--; delete $RUNNING_PIDS{$kid}; } sleep(1); } spawn sub { &gen_database_report($db); }; $chld_running++; } } if (!$nofork && $parallel_process > 1) { while (scalar keys %RUNNING_PIDS > $progress) { my $kid = waitpid(-1, WNOHANG); if ($kid > 0) { delete $RUNNING_PIDS{$kid}; } sleep(1); } } } elsif (!$incremental || !$noreport) { # Look for directory where report must be generated my @build_directories = (); if (-e "$last_parsed.tmp") { if (open(my $in, '<', "$last_parsed.tmp")) { while (my $l = <$in>) { chomp($l); $l =~ s/\r//; push(@build_directories, $l) if (!grep(/^$l$/, @build_directories)); } close($in); unlink("$last_parsed.tmp"); } else { &logmsg('ERROR', "can't read file $last_parsed.tmp, $!"); } &build_incremental_reports(@build_directories); } else { &logmsg('DEBUG', "no new entries in your log(s) since last run."); } } my $t2 = Benchmark->new; $td = timediff($t2, $t1); &logmsg('DEBUG', "building reports took: " . timestr($td)); $td = timediff($t2, $t0); &logmsg('DEBUG', "the total execution time took: " . timestr($td)); # Remove pidfile and temporary file unlink($tmp_dblist); unlink("$PID_FILE"); unlink("$last_parsed.tmp") if (-e "$last_parsed.tmp"); unlink($TMP_DIR . "/pgbadger_tmp_$$.bin") if ($#outfiles >= 1); exit 0; #------------------------------------------------------------------------------- # Show pgBadger command line usage sub usage { print qq{ Usage: pgbadger [options] logfile [...] PostgreSQL log analyzer with fully detailed reports and graphs. Arguments: logfile can be a single log file, a list of files, or a shell command returning a list of files. If you want to pass log content from stdin use - as filename. Note that input from stdin will not work with csvlog. Options: -a | --average minutes : number of minutes to build the average graphs of queries and connections. Default 5 minutes. -A | --histo-average min: number of minutes to build the histogram graphs of queries. Default 60 minutes. -b | --begin datetime : start date/time for the data to be parsed in log (either a timestamp or a time) -c | --dbclient host : only report on entries for the given client host. -C | --nocomment : remove comments like /* ... */ from queries. -d | --dbname database : only report on entries for the given database. -D | --dns-resolv : client ip addresses are replaced by their DNS name. Be warned that this can really slow down pgBadger. -e | --end datetime : end date/time for the data to be parsed in log (either a timestamp or a time) -E | --explode : explode the main report by generating one report per database. Global information not related to a database is added to the postgres database report. -f | --format logtype : possible values: syslog, syslog2, stderr, jsonlog, csv, pgbouncer, logplex, rds and redshift. Use this option when pgBadger is not able to detect the log format. -G | --nograph : disable graphs on HTML output. Enabled by default. -h | --help : show this message and exit. -H | --html-outdir path: path to directory where HTML report must be written in incremental mode, binary files stay on directory defined with -O, --outdir option. -i | --ident name : programname used as syslog ident. Default: postgres -I | --incremental : use incremental mode, reports will be generated by days in a separate directory, --outdir must be set. -j | --jobs number : number of jobs to run at same time for a single log file. Run as single by default or when working with csvlog format. -J | --Jobs number : number of log files to parse in parallel. Process one file at a time by default. -l | --last-parsed file: allow incremental log parsing by registering the last datetime and line parsed. Useful if you want to watch errors since last run or if you want one report per day with a log rotated each week. -L | --logfile-list file:file containing a list of log files to parse. -m | --maxlength size : maximum length of a query, it will be restricted to the given size. Default truncate size is $maxlength. -M | --no-multiline : do not collect multiline statements to avoid garbage especially on errors that generate a huge report. -N | --appname name : only report on entries for given application name -o | --outfile filename: define the filename for the output. Default depends on the output format: out.html, out.txt, out.bin, or out.json. This option can be used multiple times to output several formats. To use json output, the Perl module JSON::XS must be installed, to dump output to stdout, use - as filename. -O | --outdir path : directory where out files must be saved. -p | --prefix string : the value of your custom log_line_prefix as defined in your postgresql.conf. Only use it if you aren't using one of the standard prefixes specified in the pgBadger documentation, such as if your prefix includes additional variables like client IP or application name. MUST contain escape sequences for time (%t, %m or %n) and processes (%p or %c). See examples below. -P | --no-prettify : disable SQL queries prettify formatter. -q | --quiet : don't print anything to stdout, not even a progress bar. -Q | --query-numbering : add numbering of queries to the output when using options --dump-all-queries or --normalized-only. -r | --remote-host ip : set the host where to execute the cat command on remote log file to parse the file locally. -R | --retention N : number of weeks to keep in incremental mode. Defaults to 0, disabled. Used to set the number of weeks to keep in output directory. Older weeks and days directories are automatically removed. -s | --sample number : number of query samples to store. Default: 3. -S | --select-only : only report SELECT queries. -t | --top number : number of queries to store/display. Default: 20. -T | --title string : change title of the HTML page report. -u | --dbuser username : only report on entries for the given user. -U | --exclude-user username : exclude entries for the specified user from report. Can be used multiple time. -v | --verbose : enable verbose or debug mode. Disabled by default. -V | --version : show pgBadger version and exit. -w | --watch-mode : only report errors just like logwatch could do. -W | --wide-char : encode html output of queries into UTF8 to avoid Perl message "Wide character in print". -x | --extension : output format. Values: text, html, bin or json. Default: html -X | --extra-files : in incremental mode allow pgBadger to write CSS and JS files in the output directory as separate files. -z | --zcat exec_path : set the full path to the zcat program. Use it if zcat, bzcat or unzip is not in your path. -Z | --timezone +/-XX : Set the number of hours from GMT of the timezone. Use this to adjust date/time in JavaScript graphs. The value can be an integer, ex.: 2, or a float, ex.: 2.5. --anonymize : obscure all literals in queries, useful to hide --charset : used to set the HTML charset to be used. Default: utf-8. --command CMD : command to execute to retrieve log entries on stdin. pgBadger will open a pipe to the command and parse log entries generated by the command. --csv-separator : used to set the CSV field separator, default: , --day-report YYYY-MM-DD: create an HTML report over the specified day. Requires incremental output directories and the presence of all necessary binary data files --disable-autovacuum : do not generate autovacuum report. confidential data. --disable-checkpoint : do not generate checkpoint/restartpoint report. --disable-connection : do not generate connection report. --disable-error : do not generate error report. --disable-hourly : do not generate hourly report. --disable-lock : do not generate lock report. --disable-query : do not generate query reports (slowest, most frequent, queries by users, by database, ...). --disable-session : do not generate session report. --disable-temporary : do not generate temporary report. --disable-type : do not generate report of queries by type, database or user. --dump-all-queries : dump all queries found in the log file replacing bind parameters included in the queries at their respective placeholders positions. --dump-raw-csv : parse the log and dump the information into CSV format. No further processing is done, no report. --enable-checksum : used to add an md5 sum under each query report. --exclude-appname name : exclude entries for the specified application name from report. Example: "pg_dump". Can be used multiple times. --exclude-client name : exclude log entries for the specified client ip. Can be used multiple times. --exclude-db name : exclude entries for the specified database from report. Example: "postgres". Can be used multiple times. --exclude-file filename: path of the file that contains each regex to use to exclude queries from the report. One regex per line. --exclude-line regex : exclude any log entry that will match the given regex. Can be used multiple times. --exclude-query regex : any query matching the given regex will be excluded from the report. For example: "^(VACUUM|COMMIT)" You can use this option multiple times. --exclude-time regex : any timestamp matching the given regex will be excluded from the report. Example: "2013-04-12 .*" You can use this option multiple times. --explain-url URL : use it to override the url of the graphical explain tool. Default: $EXPLAIN_URL --histogram-query VAL : use custom inbound for query times histogram. Default inbound in milliseconds: 0,1,5,10,25,50,100,500,1000,10000 --histogram-session VAL: use custom inbound for session times histogram. Default inbound in milliseconds: 0,500,1000,30000,60000,600000,1800000,3600000,28800000 --include-file filename: path of the file that contains each regex to the queries to include from the report. One regex per line. --include-query regex : any query that does not match the given regex will be excluded from the report. You can use this option multiple times. For example: "(tbl1|tbl2)". --include-pid PID : only report events related to the session pid (\%p). Can be used multiple time. --include-session ID : only report events related to the session id (\%c). Can be used multiple time. --include-time regex : only timestamps matching the given regex will be included in the report. Example: "2013-04-12 .*" You can use this option multiple times. --iso-week-number : in incremental mode, calendar weeks start on Monday and respect the ISO 8601 week number, range 01 to 53, where week 1 is the first week that has at least 4 days in the new year. --keep-comments : do not remove comments from normalized queries. It can be useful if you want to distinguish between same normalized queries. --journalctl command : command to use to replace PostgreSQL logfile by a call to journalctl. Basically it might be: journalctl -u postgresql-9.5 --log-duration : force pgBadger to associate log entries generated by both log_duration = on and log_statement = 'all' --log-timezone +/-XX : Set the number of hours from GMT of the timezone that must be used to adjust date/time read from log file before beeing parsed. Using this option makes log search with a date/time more difficult. The value can be an integer, ex.: 2, or a float, ex.: 2.5. --month-report YYYY-MM : create a cumulative HTML report over the specified month. Requires incremental output directories and the presence of all necessary binary data files --noexplain : do not process lines generated by auto_explain. --no-fork : do not fork any process, for debugging purpose. --no-process-info : disable changing process title to help identify pgbadger process, some system do not support it. --no-progressbar : disable progressbar. --noreport : no reports will be created in incremental mode. --no-week : inform pgbadger to not build weekly reports in incremental mode. Useful if it takes too much time. --normalized-only : only dump all normalized queries to out.txt --pgbouncer-only : only show PgBouncer-related menus in the header. --pid-dir path : set the path where the pid file must be stored. Default /tmp --pid-file file : set the name of the pid file to manage concurrent execution of pgBadger. Default: pgbadger.pid --pie-limit num : pie data lower than num% will show a sum instead. --prettify-json : use it if you want json output to be prettified. --rebuild : used to rebuild all html reports in incremental output directories where there's binary data files. --start-monday : in incremental mode, calendar weeks start on Sunday. Use this option to start on a Monday. --tempdir DIR : set directory where temporary files will be written Default: File::Spec->tmpdir() || '/tmp' pgBadger is able to parse a remote log file using a passwordless ssh connection. Use -r or --remote-host to set the host IP address or hostname. There are also some additional options to fully control the ssh connection. --ssh-identity file path to the identity file to use. --ssh-option options list of -o options to use for the ssh connection. Options always used: -o ConnectTimeout=\$ssh_timeout -o PreferredAuthentications=hostbased,publickey --ssh-port port ssh port to use for the connection. Default: 22. --ssh-program ssh path to the ssh program to use. Default: ssh. --ssh-timeout second timeout to ssh connection failure. Default: 10 sec. --ssh-user username connection login name. Defaults to running user. Log file to parse can also be specified using an URI, supported protocols are http[s] and [s]ftp. The curl command will be used to download the file, and the file will be parsed during download. The ssh protocol is also supported and will use the ssh command like with the remote host use. See examples bellow. Return codes: 0: on success 1: die on error 2: if it has been interrupted using ctr+c for example 3: the pid file already exists or can not be created 4: no log file was given at command line Examples: pgbadger /var/log/postgresql.log pgbadger /var/log/postgres.log.2.gz /var/log/postgres.log.1.gz /var/log/postgres.log pgbadger /var/log/postgresql/postgresql-2012-05-* pgbadger --exclude-query="^(COPY|COMMIT)" /var/log/postgresql.log pgbadger -b "2012-06-25 10:56:11" -e "2012-06-25 10:59:11" /var/log/postgresql.log cat /var/log/postgres.log | pgbadger - # Log line prefix with stderr log output pgbadger --prefix '%t [%p]: user=%u,db=%d,client=%h' /pglog/postgresql-2012-08-21* pgbadger --prefix '%m %u@%d %p %r %a : ' /pglog/postgresql.log # Log line prefix with syslog log output pgbadger --prefix 'user=%u,db=%d,client=%h,appname=%a' /pglog/postgresql-2012-08-21* # Use my 8 CPUs to parse my 10GB file faster, much faster pgbadger -j 8 /pglog/postgresql-10.1-main.log Use URI notation for remote log file: pgbadger http://172.12.110.1//var/log/postgresql/postgresql-10.1-main.log pgbadger ftp://username\@172.12.110.14/postgresql-10.1-main.log pgbadger ssh://username\@172.12.110.14:2222//var/log/postgresql/postgresql-10.1-main.log* You can use together a local PostgreSQL log and a remote pgbouncer log file to parse: pgbadger /var/log/postgresql/postgresql-10.1-main.log ssh://username\@172.12.110.14/pgbouncer.log Reporting errors every week by cron job: 30 23 * * 1 /usr/bin/pgbadger -q -w /var/log/postgresql.log -o /var/reports/pg_errors.html Generate report every week using incremental behavior: 0 4 * * 1 /usr/bin/pgbadger -q `find /var/log/ -mtime -7 -name "postgresql.log*"` -o /var/reports/pg_errors-`date +\\%F`.html -l /var/reports/pgbadger_incremental_file.dat This supposes that your log file and HTML report are also rotated every week. Or better, use the auto-generated incremental reports: 0 4 * * * /usr/bin/pgbadger -I -q /var/log/postgresql/postgresql.log.1 -O /var/www/pg_reports/ will generate a report per day and per week. In incremental mode, you can also specify the number of weeks to keep in the reports: /usr/bin/pgbadger --retention 2 -I -q /var/log/postgresql/postgresql.log.1 -O /var/www/pg_reports/ If you have a pg_dump at 23:00 and 13:00 each day during half an hour, you can use pgBadger as follow to exclude these periods from the report: pgbadger --exclude-time "2013-09-.* (23|13):.*" postgresql.log This will help avoid having COPY statements, as generated by pg_dump, on top of the list of slowest queries. You can also use --exclude-appname "pg_dump" to solve this problem in a simpler way. You can also parse journalctl output just as if it was a log file: pgbadger --journalctl 'journalctl -u postgresql-9.5' or worst, call it from a remote host: pgbadger -r 192.168.1.159 --journalctl 'journalctl -u postgresql-9.5' you don't need to specify any log file at command line, but if you have other PostgreSQL log files to parse, you can add them as usual. To rebuild all incremental html reports after, proceed as follow: rm /path/to/reports/*.js rm /path/to/reports/*.css pgbadger -X -I -O /path/to/reports/ --rebuild it will also update all resource files (JS and CSS). Use -E or --explode if the reports were built using this option. pgBadger also supports Heroku PostgreSQL logs using logplex format: heroku logs -p postgres | pgbadger -f logplex -o heroku.html - this will stream Heroku PostgreSQL log to pgbadger through stdin. pgBadger can auto detect RDS and cloudwatch PostgreSQL logs using rds format: pgbadger -f rds -o rds_out.html rds.log Each CloudSQL Postgresql log is a fairly normal PostgreSQL log, but encapsulated in JSON format. It is autodetected by pgBadger but in case you need to force the log format use `jsonlog`: pgbadger -f jsonlog -o cloudsql_out.html cloudsql.log This is the same as with the jsonlog extension, the json format is different but pgBadger can parse both formats. pgBadger also supports logs produced by CloudNativePG Postgres operator for Kubernetes: pgbadger -f jsonlog -o cnpg_out.html cnpg.log To create a cumulative report over a month use command: pgbadger --month-report 2919-05 /path/to/incremental/reports/ this will add a link to the month name into the calendar view in incremental reports to look at report for month 2019 May. Use -E or --explode if the reports were built using this option. }; # Note that usage must be terminated by an extra newline # to not break POD documentation at make time. exit 0; } sub gen_database_report { my $db = shift; foreach $outfile (@outfiles) { ($current_out_file, $extens) = &set_output_extension($outdir, $outfile, $extension, $db); $extens = $dft_extens if ($current_out_file eq '-' && $dft_extens); if ($report_per_database) { &logmsg('LOG', "Ok, generating $extens report for database $db..."); } else { &logmsg('LOG', "Ok, generating $extens report..."); } $fh = new IO::File ">$current_out_file"; if (not defined $fh) { localdie("FATAL: can't write to $current_out_file, $!\n"); } if (($extens eq 'text') || ($extens eq 'txt')) { if ($error_only) { &dump_error_as_text($db); } else { &dump_as_text($db); } } elsif ($extens eq 'json') { if ($error_only) { &dump_error_as_json($db); } else { &dump_as_json($db); } } elsif ($extens eq 'binary') { &dump_as_binary($fh, $db); } else { &dump_as_html('.', $db); } $fh->close; } } #### # Function used to validate the possibility to use process on the given # file. Returns 1 when all multiprocess can be used, 0 when we can not # use multiprocess on a single file (remore file) and -1 when parallel # process can not be used too (binary mode). #### sub confirm_multiprocess { my $file = shift; if (!$nofork && $progress) { # Not supported on Windows if ($queue_size > 1) { &logmsg('DEBUG', "parallel processing is not supported on this plateform."); } return 0; } if ($remote_host || $file =~ /^(http[s]*|ftp[s]*|ssh):/) { # Disable multi process when using ssh to parse remote log if ($queue_size > 1) { &logmsg('DEBUG', "parallel processing is not supported with remote files."); } return 0; } # Disable parallel processing in binary mode if ($format eq 'binary') { if (($queue_size > 1) || ($job_per_file > 1)) { &logmsg('DEBUG', "parallel processing is not supported with binary format.") if (!$quiet); } return -1; } return 1; } sub set_ssh_command { my ($ssh_cmd, $rhost) = @_; #http://www.domain.com:8080/file.log:format #ftp://www.domain.com/file.log:format #ssh:root@domain.com:file.log:format # Extract format part my $fmt = ''; if ($rhost =~ s/\|([a-z2]+)$//) { $fmt = $1; } $ssh_cmd = $ssh_bin || 'ssh'; $ssh_cmd .= " -p $ssh_port" if ($ssh_port); $ssh_cmd .= " -i $ssh_identity" if ($ssh_identity); $ssh_cmd .= " $ssh_options" if ($ssh_options); if ($ssh_user && $rhost !~ /\@/) { $ssh_cmd .= " $ssh_user\@$rhost"; } else { $ssh_cmd .= " $rhost"; } if (wantarray()) { return ($ssh_cmd, $fmt); } else { return $ssh_cmd; } } sub set_file_list { my $file = shift; my @lfiles = (); my $file_orig = $file; my $fmt = ''; # Remove log format from log file if any if ($file =~ s/(:(?:stderr|csv|syslog|pgbouncer|jsonlog|logplex|rds|redshift)\d*)$//i) { $fmt = $1; } # Store the journalctl command as is we will create a pipe from this command if ( $journalctl_cmd && ($file =~ m/\Q$journalctl_cmd\E/) ) { push(@lfiles, $file_orig); $empty_files = 0; } # Store the journalctl command as is we will create a pipe from this command elsif ( $log_command && ($file =~ m/\Q$log_command\E/) ) { push(@lfiles, $file_orig); $empty_files = 0; } # Input from stdin elsif ($file eq '-') { if ($logfile_list) { localdie("FATAL: stdin input - can not be used with logfile list (-L).\n"); } push(@lfiles, $file_orig); $empty_files = 0; } # For input from other sources than stdin else { # if it is not a remote file store the file if it is not an empty file if (!$remote_host && $file !~ /^(http[s]*|[s]*ftp|ssh):/i) { localdie("FATAL: logfile \"$file\" must exist!\n") if (not -f $file); if (-z $file) { print "WARNING: file $file is empty\n" if (!$quiet); next; } push(@lfiles, $file_orig); $empty_files = 0; } # if this is a remote file extract the list of files using a ssh command elsif ($file !~ /^(http[s]*|[s]*ftp):/i) { # Get files from remote host if ($file !~ /^ssh:/) { my($filename, $dirs, $suffix) = fileparse($file); my $ssh_command_ls = "\""; $ssh_command_ls .= "sudo " if ($ssh_sudo); $ssh_command_ls .= "ls '$dirs'$filename\""; &logmsg('DEBUG', "Looking for remote filename using command: $remote_command $ssh_command_ls"); my @rfiles = `$remote_command "ls '$dirs'$filename"`; foreach my $f (@rfiles) { push(@lfiles, "$f$fmt"); } } elsif ($file =~ m#^ssh://([^\/]+)/(.*)#) { my $host_info = $1; my $file = $2; my $ssh = $ssh_command || 'ssh'; if ($host_info =~ s/:(\d+)$//) { $host_info = "-p $1 $host_info"; } $ssh .= " -i $ssh_identity" if ($ssh_identity); $ssh .= " $ssh_options" if ($ssh_options); my($filename, $dirs, $suffix) = fileparse($file); my $ssh_command_ls = "\""; $ssh_command_ls .= "sudo " if ($ssh_sudo); $ssh_command_ls .= "ls '$dirs'$filename\""; &logmsg('DEBUG', "Looking for remote filename using command: $ssh $host_info $ssh_command_ls"); my @rfiles = `$ssh $host_info $ssh_command_ls`; $dirs = '' if ( $filename ne '' ); #ls returns relative paths for an directory but absolute ones for a file or filename pattern foreach my $f (@rfiles) { $host_info =~ s/-p (\d+) (.*)/$2:$1/; push(@lfiles, "ssh://$host_info/$dirs$f$fmt"); } } $empty_files = 0; } # this is remote file extracted using http/ftp protocol, store the uri else { push(@lfiles, $file_orig); $empty_files = 0; } } return @lfiles; } # Get inbounds of query times histogram sub get_hist_inbound { my ($duration, @histogram) = @_; for (my $i = 0; $i <= $#histogram; $i++) { return $histogram[$i-1] if ($histogram[$i] > $duration); } return -1; } # Compile custom log line prefix prefix sub set_parser_regex { my $fmt = shift; @prefix_params = (); @prefix_q_params = (); if ($fmt eq 'pgbouncer') { $pgbouncer_log_format = qr/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+(?: [A-Z\+\-\d]{3,6})? (\d+) ([^\s]+) (.\-0x[0-9a-f\.]*): ([0-9a-zA-Z\_\[\]\-\.]*)\/([0-9a-zA-Z\_\[\]\-\.]*)\@([a-zA-Z0-9\-\.]+|\[local\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)(?:\(\d+\))??[:\d]* (.*)/; @pgb_prefix_params = ('t_timestamp', 't_pid', 't_loglevel', 't_session_id', 't_dbname', 't_dbuser', 't_client', 't_query'); $pgbouncer_log_parse1 = qr/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+(?: [A-Z\+\-\d]{3,6})? (\d+) ([^\s]+) (.\-0x[0-9a-f\.]+|[Ss]tats): (.*)/; @pgb_prefix_parse1 = ('t_timestamp', 't_pid', 't_loglevel', 't_session_id', 't_query'); $pgbouncer_log_parse2 = qr/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+(?: [A-Z\+\-\d]{3,6})? \d+ [^\s]+ .\-0x[0-9a-f\.]*: ([0-9a-zA-Z\_\[\]\-\.]*)\/([0-9a-zA-Z\_\[\]\-\.]*)\@([a-zA-Z0-9\-\.]+|\[local\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)?(?:\(\d+\))?(?:\(\d+\))?[:\d]* (.*)/; @pgb_prefix_parse2 = ('t_dbname', 't_dbuser', 't_client', 't_query'); $pgbouncer_log_parse3 = qr/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+(?: [A-Z\+\-\d]{3,6})? (\d+) ([^\s]+) ([^:]+: .*)/; @pgb_prefix_parse3 = ('t_timestamp', 't_pid', 't_loglevel', 't_query'); } elsif ($fmt eq 'pgbouncer1') { $pgbouncer_log_format = qr/^(...)\s+(\d+)\s(\d+):(\d+):(\d+)(?:\s[^\s]+)?\s([^\s]+)\s([^\s\[]+)\[(\d+)\]: (.\-0x[0-9a-f\.]*): ([0-9a-zA-Z\_\[\]\-\.]*)\/([0-9a-zA-Z\_\[\]\-\.]*)\@([a-zA-Z0-9\-\.]+|\[local\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)?(?:\(\d+\))?[:\d]* (.*)/; @pgb_prefix_params = ('t_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_host', 't_ident', 't_pid', 't_session_id', 't_dbname', 't_dbuser', 't_client', 't_query'); $pgbouncer_log_parse1 = qr/^(...)\s+(\d+)\s(\d+):(\d+):(\d+)(?:\s[^\s]+)?\s([^\s]+)\s([^\s\[]+)\[(\d+)\]: (.\-0x[0-9a-f\.]+|[Ss]tats): (.*)/; @pgb_prefix_parse1 = ('t_month', 't_day', 't_hour', 't_min', 't_sec', 't_host', 't_ident', 't_pid', 't_session_id', 't_query'); $pgbouncer_log_parse2 = qr/^...\s+\d+\s\d+:\d+:\d+(?:\s[^\s]+)?\s[^\s]+\s[^\s\[]+\[\d+\]: .\-0x[0-9a-f\.]*: ([0-9a-zA-Z\_\[\]\-\.]*)\/([0-9a-zA-Z\_\[\]\-\.]*)\@([a-zA-Z0-9\-\.]+|\[local\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)?(?:\(\d+\))?[:\d]* (.*)/; @pgb_prefix_parse2 = ('t_dbname', 't_dbuser', 't_client', 't_query'); $pgbouncer_log_parse3 = qr/^(...)\s+(\d+)\s(\d+):(\d+):(\d+)(?:\s[^\s]+)?\s([^\s]+)\s([^\s\[]+)\[(\d+)\]: ([^:]+: .*)/; @pgb_prefix_parse3 = ('t_month', 't_day', 't_hour', 't_min', 't_sec', 't_host', 't_ident', 't_pid', 't_query'); } elsif ($fmt eq 'pgbouncer2') { $pgbouncer_log_format = qr/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:.[^\s]+)?\s([^\s]+)\s(?:[^\s]+\s)?(?:[^\s]+\s)?([^\s\[]+)\[(\d+)\]: (.\-0x[0-9a-f\.]*): ([0-9a-zA-Z\_\[\]\-\.]*)\/([0-9a-zA-Z\_\[\]\-\.]*)\@([a-zA-Z0-9\-\.]+|\[local\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)?(?:\(\d+\))?[:\d]* (.*)/; @pgb_prefix_params = ('t_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_host', 't_ident', 't_pid', 't_session_id', 't_dbname', 't_dbuser', 't_client', 't_query'); $pgbouncer_log_parse1 = qr/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:.[^\s]+)?\s([^\s]+)\s(?:[^\s]+\s)?(?:[^\s]+\s)?([^\s\[]+)\[(\d+)\]: (.\-0x[0-9a-f\.]+|[Ss]tats): (.*)/; @pgb_prefix_parse1 = ('t_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_host', 't_ident', 't_pid', 't_session_id', 't_query'); $pgbouncer_log_parse2 = qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:.[^\s]+)?\s[^\s]+\s(?:[^\s]+\s)?(?:[^\s]+\s)?[^\s\[]+\[\d+\]: .\-0x[0-9a-f\.]*: ([0-9a-zA-Z\_\[\]\-\.]*)\/([0-9a-zA-Z\_\[\]\-\.]*)\@([a-zA-Z0-9\-\.]+|\[local\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)?(?:\(\d+\))?[:\d]* (.*)/; @pgb_prefix_parse2 = ('t_dbname', 't_dbuser', 't_client', 't_query'); $pgbouncer_log_parse3 = qr/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:.[^\s]+)?\s([^\s]+)\s(?:[^\s]+\s)?(?:[^\s]+\s)?([^\s\[]+)\[(\d+)\]: ([^:]+: .*)/; @pgb_prefix_parse3 = ('t_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_host', 't_ident', 't_pid', 't_query'); } elsif ($fmt eq 'pgbouncer3') { $pgbouncer_log_format = qr/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+(?: [A-Z\+\-\d]{3,6})? \[(\d+)\] ([^\s]+) (.\-0x[0-9a-f\.]*): ([0-9a-zA-Z\_\[\]\-\.]*)\/([0-9a-zA-Z\_\[\]\-\.]*)\@([a-zA-Z0-9\-\.]+|\[local\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)(?:\(\d+\))??[:\d]* (.*)/; @pgb_prefix_params = ('t_timestamp', 't_pid', 't_loglevel', 't_session_id', 't_dbname', 't_dbuser', 't_client', 't_query'); $pgbouncer_log_parse1 = qr/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+(?: [A-Z\+\-\d]{3,6})? \[(\d+)\] ([^\s]+) (.\-0x[0-9a-f\.]+|[Ss]tats): (.*)/; @pgb_prefix_parse1 = ('t_timestamp', 't_pid', 't_loglevel', 't_session_id', 't_query'); $pgbouncer_log_parse2 = qr/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d+(?: [A-Z\+\-\d]{3,6})? \[\d+\] [^\s]+ .\-0x[0-9a-f\.]*: ([0-9a-zA-Z\_\[\]\-\.]*)\/([0-9a-zA-Z\_\[\]\-\.]*)\@([a-zA-Z0-9\-\.]+|\[local\]|\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|[0-9a-fA-F:]+)?(?:\(\d+\))?(?:\(\d+\))?[:\d]* (.*)/; @pgb_prefix_parse2 = ('t_dbname', 't_dbuser', 't_client', 't_query'); $pgbouncer_log_parse3 = qr/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\.\d+(?: [A-Z\+\-\d]{3,6})? \[(\d+)\] ([^\s]+) ([^:]+: .*)/; @pgb_prefix_parse3 = ('t_timestamp', 't_pid', 't_loglevel', 't_query'); } elsif ($log_line_prefix) { # Build parameters name that will be extracted from the prefix regexp my %res = &build_log_line_prefix_regex($log_line_prefix); my $llp = $res{'llp'}; @prefix_params = @{ $res{'param_list'} }; $q_prefix = $res{'q_prefix'}; @prefix_q_params = @{ $res{'q_param_list'} }; if ($fmt eq 'syslog') { $llp = '^(...)\s+(\d+)\s(\d+):(\d+):(\d+)(\.\d+)?(?:\s[^\s]+)?\s([^\s]+)\s([^\s\[]+)\[(\d+)\]:(?:\s\[[^\]]+\])?\s\[(\d+)(?:\-\d+)?\]\s*' . $llp . '\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(?:[0-9A-Z]{5}:\s+)?(.*)'; $compiled_prefix = qr/$llp/; unshift(@prefix_params, 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_ms', 't_host', 't_ident', 't_pid', 't_session_line'); push(@prefix_params, 't_loglevel', 't_query'); $other_syslog_line = qr/^(...)\s+(\d+)\s(\d+):(\d+):(\d+)(?:\s[^\s]+)?\s([^\s]+)\s([^\s\[]+)\[(\d+)\]:(?:\s\[[^\]]+\])?\s\[(\d+)(?:\-\d+)?\]\s*(.*)/; } elsif ($fmt eq 'syslog2') { $fmt = 'syslog'; $llp = '^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(?:.[^\s]+)?\s([^\s]+)\s(?:[^\s]+\s)?(?:[^\s]+\s)?([^\s\[]+)\[(\d+)\]:(?:\s\[[^\]]+\])?(?:\s\[(\d+)(?:\-\d+)?\])?\s*' . $llp . '\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(?:[0-9A-Z]{5}:\s+)?(.*)'; $compiled_prefix = qr/$llp/; unshift(@prefix_params, 't_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_host', 't_ident', 't_pid', 't_session_line'); push(@prefix_params, 't_loglevel', 't_query'); $other_syslog_line = qr/^(\d+-\d+)-(\d+)T(\d+):(\d+):(\d+)(?:.[^\s]+)?\s([^\s]+)\s(?:[^\s]+\s)?(?:[^\s]+\s)?([^\s\[]+)\[(\d+)\]:(?:\s\[[^\]]+\])?(?:\s\[(\d+)(?:\-\d+)?\])?\s*(.*)/; } elsif ($fmt eq 'logplex') { # The output format of the heroku pg logs is as follows: timestamp app[dyno]: message $llp = '^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?[+\-]\d{2}:\d{2}\s+(?:[^\s]+)?\s*app\[postgres\.(\d+)\][:]?\s+\[([^\]]+)\]\s+\[\d+\-\d+\]\s+' . $llp . '\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(.*)'; $compiled_prefix = qr/$llp/; unshift(@prefix_params, 't_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_ms', 't_pid', 't_dbname'); push(@prefix_params, 't_loglevel', 't_query'); $other_syslog_line = qr/^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(?:\.\d+)?[+\-]\d{2}:\d{2}\s+(?:[^\s]+)?\s*app\[postgres\.\d+\][:]?\s+\[([^\]]+)\]\s+\[(\d+)\-(\d+)\]\s*(.*)/; } elsif ($fmt =~ /^rds$/) { # The output format of the RDS pg logs is as follows: %t:%r:%u@%d:[%p]: message # With Cloudwatch it is prefixed with another timestamp $llp = '^' . $llp . '(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(.*)'; $compiled_prefix = qr/$llp/; @prefix_params = ('t_timestamp', 't_client', 't_dbuser', 't_dbname', 't_pid', 't_loglevel', 't_query'); } elsif ($fmt =~ /^redshift$/) { # Look at format of the AWS redshift pg logs, for example: # '2020-03-07T16:09:43Z UTC [ db=dev user=rdsdb pid=16929 userid=1 xid=7382 ]' $llp = '^' . $llp . '(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(.*)'; $compiled_prefix = qr/$llp/; @prefix_params = ('t_timestamp', 't_dbname', 't_dbuser', 't_pid', 't_loglevel', 't_query'); } elsif ($fmt eq 'stderr' || $fmt eq 'default' || $fmt eq 'jsonlog') { $fmt = 'stderr' if ($fmt ne 'jsonlog'); $llp = '^' . $llp . '\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(?:[0-9A-Z]{5}:\s+)?(.*)'; $compiled_prefix = qr/$llp/; push(@prefix_params, 't_loglevel', 't_query'); } } elsif ($fmt eq 'syslog') { $compiled_prefix = qr/^(...)\s+(\d+)\s(\d+):(\d+):(\d+)(\.\d+)?(?:\s[^\s]+)?\s([^\s]+)\s([^\s\[]+)\[(\d+)\]:(?:\s\[[^\]]+\])?\s\[(\d+)(?:\-\d+)?\]\s*(.*?)\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(?:[0-9A-Z]{5}:\s+)?(.*)/; push(@prefix_params, 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_ms', 't_host', 't_ident', 't_pid', 't_session_line', 't_logprefix', 't_loglevel', 't_query'); $other_syslog_line = qr/^(...)\s+(\d+)\s(\d+):(\d+):(\d+)(?:\.\d+)?(?:\s[^\s]+)?\s([^\s]+)\s([^\s\[]+)\[(\d+)\]:(?:\s\[[^\]]+\])?\s\[(\d+)(?:\-\d+)?\]\s*(.*)/; } elsif ($fmt eq 'syslog2') { $fmt = 'syslog'; $compiled_prefix = qr/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:.[^\s]+)?\s([^\s]+)\s(?:[^\s]+\s)?(?:[^\s]+\s)?([^\s\[]+)\[(\d+)\]:(?:\s\[[^\]]+\])?(?:\s\[(\d+)(?:\-\d+)?\])?\s*(.*?)\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(?:[0-9A-Z]{5}:\s+)?(.*)/; push(@prefix_params, 't_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_host', 't_ident', 't_pid', 't_session_line', 't_logprefix', 't_loglevel', 't_query'); $other_syslog_line = qr/^(\d+-\d+)-(\d+)T(\d+):(\d+):(\d+)(?:.[^\s]+)?\s([^\s]+)\s(?:[^\s]+\s)?(?:[^\s]+\s)?([^\s\[]+)\[(\d+)\]:(?:\s\[[^\]]+\])?(?:\s\[(\d+)(?:\-\d+)?\])?\s*(.*)/; } elsif ($fmt eq 'logplex') { # The output format of the heroku pg logs is as follows: timestamp app[dyno]: message $compiled_prefix = qr/^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(\.\d+)?[+\-]\d{2}:\d{2}\s+(?:[^\s]+)?\s*app\[postgres\.(\d+)\][:]?\s+\[([^\]]+)\]\s+\[\d+\-\d+\]\s+(.*?)\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(.*)/; unshift(@prefix_params, 't_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_ms', 't_pid', 't_dbname'); push(@prefix_params, 't_logprefix', 't_loglevel', 't_query'); $other_syslog_line = qr/^(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)(?:\.\d+)?[+\-]\d{2}:\d{2}\s+(?:[^\s]+)?\s*app\[(postgres)\.(\d+)\][:]?\s+\[([^\]]+)\]\s+\[\d+\-\d+\]\s*(.*)/; } elsif ($fmt eq 'rds') { # The output format of the RDS pg logs is as follows: %t:%r:%u@%d:[%p]: message # With Cloudwatch it is prefixed with another timestamp $compiled_prefix = qr/^(?:\d+-\d+-\d+T\d+:\d+:\d+\.\d+Z)?\s*(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)\s*[^:]*:([^:]*):([^\@]*)\@([^:]*):\[(\d+)\]:(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(.*)/; unshift(@prefix_params, 't_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_client', 't_dbuser', 't_dbname', 't_pid', 't_loglevel', 't_query'); } elsif ($fmt eq 'redshift') { # Look at format of the AWS redshift pg logs, for example: # '2020-03-07T16:09:43Z UTC [ db=dev user=rdsdb pid=16929 userid=1 xid=7382 ]' $compiled_prefix = qr/^'(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)Z [^\s]+ \[ db=(.*?) user=(.*?) pid=(\d+) userid=\d+ xid=(?:.*?) \]' (LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(.*)/; unshift(@prefix_params, 't_year', 't_month', 't_day', 't_hour', 't_min', 't_sec', 't_dbname', 't_dbuser', 't_pid', 't_loglevel', 't_query'); } elsif ($fmt eq 'stderr') { $compiled_prefix = qr/^(\d{10}\.\d{3}|\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})(\.\d+)?(?: [A-Z\+\-\d]{3,6})?\s\[([0-9a-f\.]+)\][:]*\s(?:\[\d+\-\d+\])?\s*(.*?)\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(?:[0-9A-Z]{5}:\s+)?(.*)/; push(@prefix_params, 't_timestamp', 't_ms', 't_pid', 't_logprefix', 't_loglevel', 't_query'); } elsif ($fmt eq 'default') { $fmt = 'stderr'; $compiled_prefix = qr/^(\d{10}\.\d{3}|\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})(\.\d+)?(?: [A-Z\+\-\d]{3,6})?\s\[([0-9a-f\.]+)\][:]*\s(.*?)\s*(LOG|WARNING|ERROR|FATAL|PANIC|DETAIL|STATEMENT|HINT|CONTEXT|LOCATION):\s+(?:[0-9A-Z]{5}:\s+)?(.*)/; push(@prefix_params, 't_timestamp', 't_ms', 't_pid', 't_logprefix', 't_loglevel', 't_query'); } return $fmt; } sub check_regex { my ($pattern, $varname) = @_; eval {m/$pattern/i;}; if ($@) { localdie("FATAL: '$varname' invalid regex '$pattern', $!\n"); } } sub build_incremental_reports { my @build_directories = @_; my $destdir = $html_outdir || $outdir; my %weeks_directories = (); foreach my $bpath (sort @build_directories) { my $binpath = ''; $binpath = $1 if ($bpath =~ s/^(.*\/)(\d+\-\d+\-\d+)$/$2/); &logmsg('DEBUG', "Building incremental report for " . $bpath); $incr_date = $bpath; $last_incr_date = $bpath; # Set the path to binary files $bpath =~ s/\-/\//g; # Get the week number following the date $incr_date =~ /^(\d+)-(\d+)\-(\d+)$/; my $wn = &get_week_number($1, $2, $3); if (!$noweekreport) { if ($rebuild || !exists $weeks_directories{$wn}) { $weeks_directories{$wn}{dir} = "$1-$2"; $weeks_directories{$wn}{prefix} = $binpath if ($binpath); } } # First clear previous stored statistics &init_stats_vars(); # Load all data gathered by all the different processes $destdir = $binpath || $outdir; if (opendir(DIR, "$destdir/$bpath")) { my @mfiles = grep { !/^\./ && ($_ =~ /\.bin$/) } readdir(DIR); closedir DIR; foreach my $f (@mfiles) { my $fht = new IO::File; $fht->open("< $destdir/$bpath/$f") or localdie("FATAL: can't open file $destdir/$bpath/$f, $!\n"); load_stats($fht); $fht->close(); } } $destdir = $html_outdir || $outdir; foreach my $db (sort keys %DBLIST) { #next if ($#dbname >= 0 and !grep(/^$db$/i, @dbname)); my $tmp_dir = "$destdir/$db"; $tmp_dir = $destdir if (!$report_per_database); # Generate reports for each specified output format foreach $outfile (@outfiles) { my ($out_filename, $out_ext) = &set_output_extension('', $outfile, '', $db); $extens = $out_ext; $current_out_file = $out_filename; &logmsg('LOG', "Ok, generating $extens daily report into $tmp_dir/$bpath/..."); # set path and create subdirectories mkdir("$tmp_dir") if (!-d "$tmp_dir"); if ($bpath =~ m#^(\d+)/(\d+)/(\d+)#) { mkdir("$tmp_dir/$1") if (!-d "$tmp_dir/$1"); mkdir("$tmp_dir/$1/$2") if (!-d "$tmp_dir/$1/$2"); mkdir("$tmp_dir/$1/$2/$3") if (!-d "$tmp_dir/$1/$2/$3"); } else { &logmsg('ERROR', "invalid path: $bpath, can not create subdirectories."); } $fh = new IO::File ">$tmp_dir/$bpath/$current_out_file"; if (not defined $fh) { localdie("FATAL: can't write to $tmp_dir/$bpath/$current_out_file, $!\n"); } if (($extens eq 'text') || ($extens eq 'txt')) { if ($error_only) { &dump_error_as_text($db); } else { &dump_as_text($db); } } elsif ($extens eq 'json') { if ($error_only) { &dump_error_as_json($db); } else { &dump_as_json($db); } } else { &dump_as_html('../../..', $db); } $fh->close; } } } # Build a report per week foreach my $wn (sort { $a <=> $b } keys %weeks_directories) { &init_stats_vars(); # Get all days of the current week my $getwnb = $wn; $getwnb-- if (!$iso_week_number); my @wdays = &get_wdays_per_month($getwnb, $weeks_directories{$wn}{dir}); my $binpath = ''; $binpath = $weeks_directories{$wn}{prefix} if (defined $weeks_directories{$wn}{prefix}); my $wdir = ''; # Load data per day foreach my $bpath (@wdays) { $incr_date = $bpath; $bpath =~ s/\-/\//g; $incr_date =~ /^(\d+)\-(\d+)\-(\d+)$/; $wdir = "$1/week-$wn"; $destdir = $binpath || $outdir; # Load all data gathered by all the differents processes if (-e "$destdir/$bpath") { unless(opendir(DIR, "$destdir/$bpath")) { localdie("FATAL: can't opendir $destdir/$bpath: $!\n"); } my @mfiles = grep { !/^\./ && ($_ =~ /\.bin$/) } readdir(DIR); closedir DIR; foreach my $f (@mfiles) { my $fht = new IO::File; $fht->open("< $destdir/$bpath/$f") or localdie("FATAL: can't open file $destdir/$bpath/$f, $!\n"); load_stats($fht); $fht->close(); } } } $destdir = $html_outdir || $outdir; foreach my $db (sort keys %DBLIST) { #next if ($#dbname >= 0 and !grep(/^$db$/i, @dbname)); my $tmp_dir = "$destdir/$db"; $tmp_dir = $destdir if (!$report_per_database); # Generate reports for each specified output format foreach $outfile (@outfiles) { my ($out_filename, $out_ext) = &set_output_extension('', $outfile, '', $db); $extens = $out_ext; $current_out_file = $out_filename; &logmsg('LOG', "Ok, generating $extens weekly report into $tmp_dir/$wdir/..."); mkdir("$tmp_dir") if (!-d "$tmp_dir"); my $path = $tmp_dir; foreach my $d (split('/', $wdir)) { mkdir("$path/$d") if (!-d "$path/$d"); $path .= "/$d"; } $fh = new IO::File ">$tmp_dir/$wdir/$current_out_file"; if (not defined $fh) { localdie("FATAL: can't write to $tmp_dir/$wdir/$current_out_file, $!\n"); } if (($extens eq 'text') || ($extens eq 'txt')) { if ($error_only) { &dump_error_as_text($db); } else { &dump_as_text($db); } } elsif ($extens eq 'json') { if ($error_only) { &dump_error_as_json($db); } else { &dump_as_json($db); } } else { &dump_as_html('../..', $db); } $fh->close; } } } # Generate global index to access incremental reports &build_global_index(); } sub build_month_reports { my ($month_path, @build_directories) = @_; # First clear previous stored statistics &init_stats_vars(); foreach my $bpath (sort @build_directories) { $incr_date = $bpath; $last_incr_date = $bpath; # Set the path to binary files $bpath =~ s/\-/\//g; # Get the week number following the date $incr_date =~ /^(\d+)-(\d+)\-(\d+)$/; &logmsg('DEBUG', "reading month statistics from $outdir/$bpath"); # Load all data gathered by all the different processes unless(opendir(DIR, "$outdir/$bpath")) { localdie("FATAL: can't opendir $outdir/$bpath: $!\n"); } my @mfiles = grep { !/^\./ && ($_ =~ /\.bin$/) } readdir(DIR); closedir DIR; foreach my $f (@mfiles) { my $fht = new IO::File; $fht->open("< $outdir/$bpath/$f") or localdie("FATAL: can't open file $outdir/$bpath/$f, $!\n"); load_stats($fht); $fht->close(); } } my $dest_dir = $html_outdir || $outdir; foreach my $db (sort keys %DBLIST) { my $tmp_dir = "$dest_dir/$db"; $tmp_dir = $dest_dir if (!$report_per_database); # Generate reports for each specified output format foreach $outfile (@outfiles) { my ($out_filename, $out_ext) = &set_output_extension('', $outfile, '', $db); $extens = $out_ext; $current_out_file = $out_filename; &logmsg('LOG', "Ok, generating $extens monthly report into $tmp_dir/$month_path/$current_out_file"); mkdir("$tmp_dir") if (!-d "$tmp_dir"); my $path = $tmp_dir; foreach my $d (split('/', $month_path)) { mkdir("$path/$d") if (!-d "$path/$d"); $path .= "/$d"; } $fh = new IO::File ">$tmp_dir/$month_path/$current_out_file"; if (not defined $fh) { localdie("FATAL: can't write to $tmp_dir/$month_path/$current_out_file, $!\n"); } if (($extens eq 'text') || ($extens eq 'txt')) { if ($error_only) { &dump_error_as_text($db); } else { &dump_as_text($db); } } elsif ($extens eq 'json') { if ($error_only) { &dump_error_as_json($db); } else { &dump_as_json($db); } } else { &dump_as_html('../..', $db); } $fh->close; } } # Generate global index to access incremental reports &build_global_index(); } sub build_day_reports { my ($day_path, @build_directories) = @_; # First clear previous stored statistics &init_stats_vars(); foreach my $bpath (sort @build_directories) { $incr_date = $bpath; $last_incr_date = $bpath; # Set the path to binary files $bpath =~ s/\-/\//g; # Get the week number following the date $incr_date =~ /^(\d+)-(\d+)\-(\d+)$/; &logmsg('DEBUG', "reading month statistics from $outdir/$bpath"); # Load all data gathered by all the different processes unless(opendir(DIR, "$outdir/$bpath")) { localdie("FATAL: can't opendir $outdir/$bpath: $!\n"); } my @mfiles = grep { !/^\./ && ($_ =~ /\.bin$/) } readdir(DIR); closedir DIR; foreach my $f (@mfiles) { my $fht = new IO::File; $fht->open("< $outdir/$bpath/$f") or localdie("FATAL: can't open file $outdir/$bpath/$f, $!\n"); load_stats($fht); $fht->close(); } } my $dest_dir = $html_outdir || $outdir; foreach my $db (sort keys %DBLIST) { my $tmp_dir = "$dest_dir/$db"; $tmp_dir = $dest_dir if (!$report_per_database); # Generate reports for each specified output format foreach $outfile (@outfiles) { my ($out_filename, $out_ext) = &set_output_extension('', $outfile, '', $db); $extens = $out_ext; $current_out_file = $out_filename; &logmsg('LOG', "Ok, generating $extens daily report into $tmp_dir/$day_path/$current_out_file"); mkdir("$tmp_dir") if (!-d "$tmp_dir"); my $path = $tmp_dir; foreach my $d (split('/', $day_path)) { mkdir("$path/$d") if (!-d "$path/$d"); $path .= "/$d"; } $fh = new IO::File ">$tmp_dir/$day_path/$current_out_file"; if (not defined $fh) { localdie("FATAL: can't write to $tmp_dir/$day_path/$current_out_file, $!\n"); } if (($extens eq 'text') || ($extens eq 'txt')) { if ($error_only) { &dump_error_as_text($db); } else { &dump_as_text($db); } } elsif ($extens eq 'json') { if ($error_only) { &dump_error_as_json($db); } else { &dump_as_json($db); } } else { &dump_as_html('../..', $db); } $fh->close; } } # Generate global index to access incremental reports &build_global_index(); } sub build_global_index { &logmsg('LOG', "Ok, generating global index to access incremental reports..."); my $dest_dir = $html_outdir || $outdir; # Get database directories unless(opendir(DIR, "$dest_dir")) { localdie("FATAL: can't opendir $dest_dir: $!\n"); } my @dbs = grep { !/^\./ && !/^\d{4}$/ && -d "$dest_dir/$_" } readdir(DIR); closedir DIR; @dbs = ($DBALL) if (!$report_per_database); foreach my $db (@dbs) { #next if ($#dbname >= 0 and !grep(/^$db$/i, @dbname)); my $tmp_dir = "$dest_dir/$db"; $tmp_dir = $dest_dir if (!$report_per_database); &logmsg('DEBUG', "writing global index into $tmp_dir/index.html"); $fh = new IO::File ">$tmp_dir/index.html"; if (not defined $fh) { localdie("FATAL: can't write to $tmp_dir/index.html, $!\n"); } my $date = localtime(time); my @tmpjscode = @jscode; my $path_prefix = '.'; $path_prefix = '..' if ($report_per_database); for (my $i = 0; $i <= $#tmpjscode; $i++) { $tmpjscode[$i] =~ s/EDIT_URI/$path_prefix/; } my $local_title = 'Global Index on incremental reports'; if ($report_title) { $local_title = 'Global Index - ' . $report_title; } print $fh qq{ pgBadger :: $local_title @tmpjscode
}; # get year directories unless(opendir(DIR, "$tmp_dir")) { localdie("FATAL: can't opendir $tmp_dir: $!\n"); } my @dyears = grep { !/^\./ && /^\d{4}$/ } readdir(DIR); closedir DIR; foreach my $y (sort { $b <=> $a } @dyears) { print $fh qq{

Year $y

}; # foreach year directory look for week directories unless(opendir(DIR, "$tmp_dir/$y")) { localdie("FATAL: can't opendir $tmp_dir/$y: $!\n"); } my @ymonths = grep { /^\d{2}$/ } readdir(DIR); closedir DIR; my $i = 1; foreach my $m (sort {$a <=> $b } @ymonths) { print $fh "\n"; print $fh "\n\n" if ( ($i%4) == 0 ); $i++; } print $fh qq{
", &get_calendar($db, $y, $m), "
}; } print $fh qq{
 ^ 
}; $fh->close; } } sub cleanup_directory { my ($dir, $remove_dir) = @_; unless(opendir(DIR, "$dir")) { localdie("FATAL: can't opendir $dir: $!\n"); } my @todel = grep { !/^\./ } readdir(DIR); closedir DIR; map { unlink("$dir/$_"); } @todel; rmdir("$dir") if ($remove_dir); } sub cleanup_directory_bin { my ($dir, $remove_dir) = @_; if (opendir(DIR, "$dir")) { my @todel = grep { $_ =~ /\.bin$/i } readdir(DIR); closedir DIR; map { unlink("$dir/$_"); } @todel; rmdir("$dir") if ($remove_dir); } } sub cleanup_directory_html { my ($dir, $remove_dir) = @_; if (opendir(DIR, "$dir")) { my @todel = grep { $_ =~ /\.(html|txt|tsung|json)$/i } readdir(DIR); closedir DIR; map { unlink("$dir/$_"); } @todel; rmdir("$dir") if ($remove_dir); } } sub write_resources { # Write resource file to report directory or return resources in and array of lines my $rscfh; my @contents = (); my $endfile = ''; my $file = ''; my $major_version = $VERSION; $major_version =~ s/\..*//; my $rscdir = $html_outdir || $outdir; while (my $l = ) { last if ($l =~ /^__END__$/); if ($l =~ /^WRFILE: ([^\s]+)/) { $file = $1; if (!$extra_files) { if ($#contents > 0) { push(@contents, $endfile); } if ($file =~ /\.css$/i) { push(@contents, ""; } elsif ($file =~ /\.js$/i) { push(@contents, ""; } next; } $rscfh->close() if (defined $rscfh); if ($file =~ /\.css$/i) { push(@contents, "\n"); } elsif ($file =~ /\.js$/i) { push(@contents, "\n"); } if ($extra_files) { if (!-e "$rscdir/$major_version") { mkdir("$rscdir/$major_version"); } if (!-e "$rscdir/$major_version/$file") { $rscfh = new IO::File ">$rscdir/$major_version/$file"; localdie("FATAL: can't write file $rscdir/$major_version/$file\n") if (not defined $rscfh); } } next; } if (!$extra_files) { push(@contents, $l); } else { $rscfh->print($l) if (defined $rscfh); } } $rscfh->close() if (defined $rscfh); # Return __DATA__ content if --extra-files is not used # or HTML links to resources files if (!$extra_files) { push(@contents, $endfile); } return @contents; } sub sort_by_week { my $curr = shift; my $next = shift; $a =~ /week\-(\d+)/; $curr = $1; $b =~ /week\-(\d+)/; $next = $1; return $next <=> $curr; } sub init_stats_vars { # Empty where statistics are stored %overall_stat = (); %pgb_overall_stat = (); %overall_checkpoint = (); %top_slowest = (); %top_tempfile_info = (); %top_cancelled_info = (); %top_locked_info = (); %normalyzed_info = (); %error_info = (); %pgb_error_info = (); %pgb_pool_info = (); %logs_type = (); %errors_code = (); %per_minute_info = (); %pgb_per_minute_info = (); %lock_info = (); %tempfile_info = (); %cancelled_info = (); %connection_info = (); %pgb_connection_info = (); %database_info = (); %application_info = (); %session_info = (); %pgb_session_info = (); %conn_received = (); %checkpoint_info = (); %autovacuum_info = (); %autoanalyze_info = (); @graph_values = (); %cur_info = (); $nlines = 0; } #### # Main function called per each parser process #### sub multiprocess_progressbar { my $totalsize = shift; &logmsg('DEBUG', "Starting progressbar writer process"); $0 = 'pgbadger logger' if (!$disable_process_title); # Terminate the process when we haven't read the complete file but must exit local $SIG{USR1} = sub { print STDERR "\n"; exit 1; }; my $timeout = 3; my $cursize = 0; my $nqueries = 0; my $nerrors = 0; my $fmt = 'stderr'; $pipe->reader(); while (my $r = <$pipe>) { chomp($r); my @infos = split(/\s+/, $r); last if ($infos[0] eq 'QUIT'); $cursize += $infos[0]; $nqueries += $infos[1]; $nerrors += $infos[2]; $fmt = $infos[3] if ($#infos == 3); $cursize = $totalsize if ($totalsize > 0 && $cursize > $totalsize); print STDERR &progress_bar($cursize, $totalsize, 25, '=', $nqueries, $nerrors, $fmt); } print STDERR "\n"; exit 0; } sub update_progress_bar { my ($tmpoutfile, $nlines, $stop_offset, $totalsize, $cursize, $old_queries_count, $old_errors_count, $fmt) = @_; return if (!$progress); if (!$tmpoutfile || not defined $pipe) { if ($progress && (($nlines % $NUMPROGRESS) == 0)) { print STDERR &progress_bar($$cursize, $stop_offset || $totalsize, 25, '=', undef, undef, $fmt); $NUMPROGRESS *= 10 if ($NUMPROGRESS < 10000); } } else { if ($progress && ($nlines % $NUMPROGRESS) == 0) { $pipe->print("$$cursize " . ($overall_stat{'queries_number'} + $pgb_overall_stat{'errors_number'} - $$old_queries_count) . " " . ($overall_stat{'errors_number'} + $pgb_overall_stat{'errors_number'} - $$old_errors_count) . " $fmt\n"); $$old_queries_count = $overall_stat{'queries_number'} + $pgb_overall_stat{'queries_number'}; $$old_errors_count = $overall_stat{'errors_number'} + $pgb_overall_stat{'errors_number'}; $$cursize = 0; $NUMPROGRESS *= 10 if ($NUMPROGRESS < 10000); } } } sub is_pgbouncer_error { if ($_[0] =~ /(login|connect) failed /) { return 'FATAL'; } return 'LOG'; } sub set_current_db { my $dbn = shift; # Don't collect stats on database if it is excluded return $dbn if (grep(/^$dbn$/i, @exclude_db)); if (!$report_per_database || !$dbn || $dbn eq '[unknown]') { $overall_stat{nlines}{$DBALL}++; return $DBALL } $DBLIST{$dbn} = 1; $overall_stat{nlines}{$dbn}++; return $dbn; } #### # Main function called per each parser process #### sub process_file { my ($logfile, $totalsize, $fmt, $tmpoutfile, $start_offset, $stop_offset, $chunk_pos) = @_; my $old_queries_count = 0; my $old_errors_count = 0; my $getout = 0; my $http_download = ($logfile =~ /^(http[s]*:|[s]*ftp:)/i) ? 1 : 0; $start_offset ||= 0; $0 = 'pgbadger parser' if (!$disable_process_title); &init_stats_vars() if ($tmpoutfile); if (!$remote_host) { &logmsg('DEBUG', "Processing log file: $logfile"); } else { &logmsg('DEBUG', "Processing remote log file: $remote_host:$logfile"); } local $SIG{INT} = sub { print STDERR "Received SIGINT abort parsing...\n"; unlink("$PID_FILE"); $terminate = 1; }; local $SIG{TERM} = sub { print STDERR "Received SIGTERM abort parsing...\n"; unlink("$PID_FILE"); $terminate = 1; }; my $curdate = localtime(time); # Syslog does not have year information, so take care of year overlapping my ($gsec, $gmin, $ghour, $gmday, $gmon, $gyear, $gwday, $gyday, $gisdst) = localtime(time); $gyear += 1900; my $CURRENT_DATE = $gyear . sprintf("%02d", $gmon + 1) . sprintf("%02d", $gmday); my $cursize = 0; # Get a filehandle to the log file my $lfile = &get_log_file($logfile, $totalsize); if (!defined $lfile) { &logmsg('ERROR', "cannot read file $logfile, no handle."); return; } $pipe->writer() if (defined $pipe); if ($logfile ne '-') { if ($progress && ($getout != 1)) { if (!$tmpoutfile || not defined $pipe) { print STDERR &progress_bar( $cursize, $stop_offset || $totalsize, 25, '=', ($overall_stat{'queries_number'} + $pgb_overall_stat{'queries_number'}), ($overall_stat{'errors_number'}+$pgb_overall_stat{'errors_number'}), $fmt ); } else { $pipe->print("$cursize " . ($overall_stat{'queries_number'} + $pgb_overall_stat{'queries_number'} - $old_queries_count) . " " . ($overall_stat{'errors_number'} + $pgb_overall_stat{'errors_number'} - $old_errors_count) . " $fmt\n"); } } if (!$totalsize && $tmpoutfile) { &dump_as_binary($tmpoutfile); $tmpoutfile->close(); } } # Reset the start position if file is smaller that the current start offset if ($totalsize > -1 && $start_offset > $totalsize) { &logmsg('DEBUG', "Starting offset $start_offset is greater than total size $totalsize for file $logfile"); &logmsg('DEBUG', "Reverting start offset $start_offset to 0 for file $logfile, stoppping offset is " . ($stop_offset || $totalsize)); $start_offset = 0 ; } # Check if the first date in the log are after the last date saved if (($logfile ne '-') && ($fmt ne 'binary') && ($fmt ne 'csv') && !$http_download) { if ($start_offset && !$chunk_pos) { my ($retcode, $msg) = check_file_changed($logfile, $file_size{$logfile}, $fmt, ($fmt =~ /pgbouncer/) ? $pgb_saved_last_line{datetime} : $saved_last_line{datetime}, $start_offset, 0); if ($retcode != 1) { &logmsg('DEBUG', "This file should be parsed from the beginning: $logfile, $msg"); &logmsg('DEBUG', "Reverting start offset $start_offset to 0 for file $logfile, stopping offset is " . ($stop_offset || $totalsize)); $start_offset = 0; } $cursize = $start_offset; } } else { $start_offset = 0; $stop_offset = 0; } # Set some boolean to gain speed my $is_json_log = 0; $is_json_log = 1 if ($fmt =~ /jsonlog/); my $is_syslog = 0; $is_syslog = 1 if ($fmt =~ /syslog|logplex/); if ($stop_offset > 0) { $totalsize = $stop_offset - $start_offset; } my $current_offset = $start_offset || 0; if (!$remote_host) { &logmsg('DEBUG', "Starting reading file \"$logfile\"..."); } else { &logmsg('DEBUG', "Starting reading file \"$remote_host:$logfile\"..."); } # Parse pgbouncer logfile if ($fmt =~ /pgbouncer/) { my $cur_pid = ''; my @matches = (); my $has_exclusion = 0; if ($#exclude_line >= 0) { $has_exclusion = 1; } &logmsg('DEBUG', "Start parsing pgbouncer log at offset $start_offset of " . ( ($logfile ne '-') ? " file $logfile" : "stdin" ) . " to " . ($stop_offset || ( ($totalsize > 0) ? $totalsize : 'end' ) )); if ($start_offset) { # Move to the starting offset position in file $lfile->seek($start_offset, 0); } # pgbouncer reports are forced in the postgres report. # There is no per database pgbouncer statitiscs collected my $curdb = set_current_db(); while (my $line = <$lfile>) { # We received a signal last if ($terminate); # Get current size/offset in the log file $cursize += length($line) + (&get_eol_length() - 1); $current_offset += length($line) + (&get_eol_length() - 1); # Replace CR/LF by LF $line =~ s/\r//; # Start to exclude from parsing any desired lines if ($has_exclusion >= 0) { # Log line matches the excluded regex map { next if ($line =~ /$_/is); } @exclude_line; } chomp($line); $nlines++; next if (!$line); &update_progress_bar($tmpoutfile, $nlines, $stop_offset, $totalsize, \$cursize, \$old_queries_count, \$old_errors_count, $fmt); %prefix_vars = (); my $special_format = 0; @matches = ($line =~ $pgbouncer_log_parse1); if ($#matches == -1) { @matches = ($line =~ $pgbouncer_log_parse3); $special_format = 1 if ($#matches >= 0); } if ($#matches >= 0) { # Get all relevant fields extracted through the regexp if (!$special_format) { for (my $i = 0 ; $i <= $#pgb_prefix_parse1 ; $i++) { $prefix_vars{$pgb_prefix_parse1[$i]} = $matches[$i]; } } else { for (my $i = 0 ; $i <= $#pgb_prefix_parse3 ; $i++) { $prefix_vars{$pgb_prefix_parse3[$i]} = $matches[$i]; } } # Get detailled information from timestamp if (!$prefix_vars{'t_month'}) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = ($prefix_vars{'t_timestamp'} =~ $time_pattern); $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; } else { # Standard syslog format does not have year information, months are # three letters and days are not always with 2 digits. if ($prefix_vars{'t_month'} !~ /\d/) { $prefix_vars{'t_year'} = $gyear; $prefix_vars{'t_day'} = sprintf("%02d", $prefix_vars{'t_day'}); $prefix_vars{'t_month'} = $month_abbr{$prefix_vars{'t_month'}}; # Take care of year overlapping if ("$prefix_vars{'t_year'}$prefix_vars{'t_month'}$prefix_vars{'t_day'}" > $CURRENT_DATE) { $prefix_vars{'t_year'} = substr($CURRENT_DATE, 0, 4) - 1; } } $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; } # Change log level for some relevant messages if ($prefix_vars{'t_loglevel'} !~ $main_error_regex) { $prefix_vars{'t_loglevel'} = is_pgbouncer_error($prefix_vars{'t_query'}); } if ($prefix_vars{'t_session_id'} eq 'Stats') { $prefix_vars{'t_loglevel'} = 'STATS'; $prefix_vars{'t_session_id'} = ''; $prefix_vars{'t_query'} = 'Stats: ' . $prefix_vars{'t_query'}; } # Skip unwanted lines my $res = &skip_unwanted_line(); next if ($res == 1); if ($res == -1) { &update_progress_bar($tmpoutfile, $nlines, $stop_offset, $totalsize, \$cursize, \$old_queries_count, \$old_errors_count, $fmt); $getout = 2; last; } # Jump to the last line parsed if required next if (($incremental || $last_parsed) && !&check_incremental_position($fmt, $prefix_vars{'t_timestamp'}, $line)); # Store the current timestamp of the log line &store_current_timestamp($prefix_vars{'t_timestamp'}, $prefix_vars{'t_pid'}); # Override timestamp when we have to adjust datetime to the log timezone if ($log_timezone) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = change_timezone($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}); $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; } # Extract other information from the line @matches = ($line =~ $pgbouncer_log_parse2); if ($#matches >= 0) { for (my $i = 0 ; $i <= $#pgb_prefix_parse2 ; $i++) { $prefix_vars{$pgb_prefix_parse2[$i]} = $matches[$i]; } $prefix_vars{'t_client'} = _gethostbyaddr($prefix_vars{'t_client'}) if ($dns_resolv && $prefix_vars{'t_client'}); } else { # pgBouncer Statistics appear each minute in the log if ($prefix_vars{'t_query'} =~ /(\d+) req\/s, in (\d+) b\/s, out (\d+) b\/s,query (\d+) us, wait(?: time)? (\d+) us/) { $prefix_vars{'t_loglevel'} = 'STATS'; $prefix_vars{'t_req/s'} = $1; $prefix_vars{'t_inbytes/s'} = $2; $prefix_vars{'t_outbytes/s'} = $3; $prefix_vars{'t_avgduration'} = $4; $prefix_vars{'t_avgwaiting'} = $5; } elsif ($prefix_vars{'t_query'} =~ /(\d+) xacts\/s, (\d+) queries\/s, in (\d+) B\/s, out (\d+) B\/s, xact (\d+) us, query (\d+) us, wait(?: time)? (\d+) us/) { $prefix_vars{'t_loglevel'} = 'STATS'; $prefix_vars{'t_xact/s'} = $1; $prefix_vars{'t_req/s'} = $2; $prefix_vars{'t_inbytes/s'} = $3; $prefix_vars{'t_outbytes/s'} = $4; $prefix_vars{'t_avgtxduration'} = $5; $prefix_vars{'t_avgduration'} = $6; $prefix_vars{'t_avgwaiting'} = $7; } # pgbouncer 1.21 elsif ($prefix_vars{'t_query'} =~ /(\d+) xacts\/s, (\d+) queries\/s, (\d+) client parses\/s, (\d+) server parses\/s, (\d+) binds\/s, in (\d+) B\/s, out (\d+) B\/s, xact (\d+) us, query (\d+) us, wait (\d+) us/) { $prefix_vars{'t_loglevel'} = 'STATS'; $prefix_vars{'t_xact/s'} = $1; $prefix_vars{'t_req/s'} = $2; $prefix_vars{'t_client_parses/s'} = $3; $prefix_vars{'t_server_parses/s'} = $4; $prefix_vars{'t_binds/s'} = $5; $prefix_vars{'t_inbytes/s'} = $6; $prefix_vars{'t_outbytes/s'} = $7; $prefix_vars{'t_avgtxduration'} = $8; $prefix_vars{'t_avgduration'} = $9; $prefix_vars{'t_avgwaiting'} = $10; } } # Check if the log line should be excluded from the report if (&validate_log_line($prefix_vars{'t_pid'})) { $prefix_vars{'t_host'} = 'stderr'; # this unused variable is used to store format information when log format is not syslog # Process the log line &parse_pgbouncer($fmt); } } else { # unknown format &logmsg('DEBUG', "Unknown pgbouncer line format: $line"); } last if (($stop_offset > 0) && ($current_offset >= $stop_offset)); } if ($last_parsed) { $pgb_last_line{current_pos} = $current_offset; } } # Parse PostgreSQL log file with CSV format elsif ($fmt eq 'csv') { if ($queue_size > 1 || $job_per_file > 1) { &logmsg('WARNING', "parallel processing is disabled with csv format."); } require Text::CSV_XS; my $csv = Text::CSV_XS->new( { binary => 1, eol => $/, sep_char => $csv_sep_char, allow_loose_quotes => 1, } ); # Parse csvlog lines CSVLOOP: while (!$csv->eof()) { # CSV columns information: # ------------------------ # timestamp with milliseconds # username # database name # Process id # Remote host and port # session id # Line number # PS display # session start timestamp # Virtual transaction id # Transaction id # Error severity # SQL state code # errmessage # errdetail or errdetail_log # errhint # internal query # internal pos # errcontext # user query # file error location # application name # backend type # leader PID # query id while (my $row = $csv->getline($lfile)) { $row =~ s/\r//; # We received a signal last CSVLOOP if ($terminate); # Number of columns in csvlog (21 before 9.0, 22 before 13.0, 25 in 14.0) next if ( ($#{$row} < 21) && ($#{$row} > 24) ); # Set progress statistics $cursize += length(join(',', @$row)); $nlines++; &update_progress_bar($tmpoutfile, $nlines, $stop_offset, $totalsize, \$cursize, \$old_queries_count, \$old_errors_count, $fmt); # Reset state extracted from the previous csvlog row. %prefix_vars = (); next if ( ($row->[11] !~ $parse_regex) || ($row->[11] eq 'LOCATION')); # Extract the date if ($row->[0] =~ m/^(\d+)-(\d+)-(\d+)\s+(\d+):(\d+):(\d+)\.(\d+)/) { $prefix_vars{'t_year'} = $1; $prefix_vars{'t_month'} = $2; $prefix_vars{'t_day'} = $3; $prefix_vars{'t_hour'} = $4; $prefix_vars{'t_min'} = $5; $prefix_vars{'t_sec'} = $6; my $milli = $7 || 0; $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; # Remove newline characters from queries but not explain plan if (!grep(/Query Text:/, @$row)) { for (my $i = 0; $i <= $#$row; $i++) { $row->[$i] =~ s/[\r\n]+/ /gs; } } # Skip unwanted lines my $res = &skip_unwanted_line(); next if ($res == 1); if ($res == -1) { &update_progress_bar($tmpoutfile, $nlines, $stop_offset, $totalsize, \$cursize, \$old_queries_count, \$old_errors_count, $fmt); $getout = 2; last CSVLOOP; } # Jump to the last line parsed if required next if (($incremental || $last_parsed) && !&check_incremental_position($fmt, $prefix_vars{'t_timestamp'}, join(',', @$row))); # Set query parameters as global variables $prefix_vars{'t_dbuser'} = $row->[1] || ''; $prefix_vars{'t_dbname'} = $row->[2] || ''; $prefix_vars{'t_appname'} = $row->[22] || ''; $prefix_vars{'t_appname'} =~ s/[<>]/'/g; $prefix_vars{'t_client'} = $row->[4] || ''; $prefix_vars{'t_client'} =~ s/:.*//; $prefix_vars{'t_client'} = _gethostbyaddr($prefix_vars{'t_client'}) if ($dns_resolv); $prefix_vars{'t_host'} = 'csv'; # this unused variable is used to store format information when log format is not syslog $prefix_vars{'t_pid'} = $row->[3]; $prefix_vars{'t_session_line'} = $row->[5]; $prefix_vars{'t_session_line'} =~ s/\..*//; $prefix_vars{'t_loglevel'} = $row->[11]; $prefix_vars{'t_query'} = $row->[13]; # Set ERROR additional information $prefix_vars{'t_detail'} = $row->[14]; $prefix_vars{'t_hint'} = $row->[15]; $prefix_vars{'t_context'} = $row->[18]; $prefix_vars{'t_statement'} = $row->[19]; $prefix_vars{'t_queryid'} = $row->[24] if ($#{$row} >= 24); # Store the current timestamp of the log line &store_current_timestamp($prefix_vars{'t_timestamp'}, $prefix_vars{'t_pid'}, $prefix_vars{'t_dbname'}); # Update current timestamp with the timezone wanted if ($log_timezone) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = change_timezone($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}); $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; } # Check if the log line should be excluded from the report if (&validate_log_line($prefix_vars{'t_pid'})) { # Parse the query now &parse_query($fmt); # The information can be saved immediately with csvlog if (exists $cur_info{$prefix_vars{'t_pid'}}) { &store_queries($prefix_vars{'t_pid'}); delete $cur_info{$prefix_vars{'t_pid'}}; } } } } if (!$csv->eof()) { warn "WARNING: cannot use CSV on $logfile, " . $csv->error_diag() . " at line " . ($nlines+1), "\n"; print STDERR "DETAIL: " . $csv->error_input(), "\n" if ($csv->error_input()); print STDERR "reset CSV parser\n"; $csv->SetDiag(0); } else { $cursize = $totalsize; } } } elsif ($fmt eq 'binary') { return $getout if (!load_stats($lfile)); $pipe->print("$totalsize 0 0 $fmt\n") if (defined $pipe); } # Format is not CSV and in incremental mode we are not at end of the file else { my $cur_pid = ''; my @matches = (); my $goon = ($incremental) ? 1 : 0; my $has_exclusion = 0; if ($#exclude_line >= 0) { $has_exclusion = 1; } &logmsg('DEBUG', "Start parsing postgresql log at offset $start_offset of file \"$logfile\" to " . ($stop_offset || $totalsize)); if (!$journalctl_cmd && !$log_command) { if ($start_offset) { # Move to the starting offset position in file $lfile->seek($start_offset, 0); } else { $lfile->seek(0, 0); } } while (my $line = <$lfile>) { # We received a signal last if ($terminate); # Get current size/offset in the log file $cursize += length($line) + (&get_eol_length() - 1); $current_offset += length($line) + (&get_eol_length() - 1); # Skip INFO line generated by other software next if ($line =~ /\bINFO: /); # Replace CR/LF by LF $line =~ s/\r//; # Start to exclude from parsing any desired lines if ($has_exclusion >= 0) { # Log line matches the excluded regex map { next if ($line =~ /$_/is); } @exclude_line; } chomp($line); $nlines++; next if (!$line); &update_progress_bar($tmpoutfile, $nlines, $stop_offset, $totalsize, \$cursize, \$old_queries_count, \$old_errors_count, $fmt); %prefix_vars = (); # Parse jsonlog lines if ($is_json_log) { %prefix_vars = parse_jsonlog_input($line); if (exists $prefix_vars{'t_textPayload'}) { @matches = ($prefix_vars{'t_textPayload'} =~ $compiled_prefix); my $q_match = 0; if ($#matches < 0 && $q_prefix) { @matches = ($prefix_vars{'t_textPayload'} =~ $q_prefix); $q_match = 1; } if ($#matches >= 0) { if (!$q_match) { for (my $i = 0 ; $i <= $#prefix_params ; $i++) { $prefix_vars{$prefix_params[$i]} = $matches[$i]; } } else { for (my $i = 0 ; $i <= $#prefix_q_params ; $i++) { $prefix_vars{$prefix_q_params[$i]} = $matches[$i]; } } } delete $prefix_vars{'t_textPayload'}; $prefix_vars{'t_query'} = unescape_jsonlog($prefix_vars{'t_query'}); $prefix_vars{'t_pid'} = $prefix_vars{'t_session_id'} if ($use_sessionid_as_pid); # Skip location information next if ($prefix_vars{'t_loglevel'} eq 'LOCATION'); if ($prefix_vars{'t_mtimestamp'} && $prefix_vars{'t_mtimestamp'} =~ s/(\.\d+)$//) { $prefix_vars{'t_ms'} = $1; } if (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_mtimestamp'}) { $prefix_vars{'t_timestamp'} = $prefix_vars{'t_mtimestamp'}; } elsif (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_session_timestamp'}) { $prefix_vars{'t_timestamp'} = $prefix_vars{'t_session_timestamp'}; } elsif (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_epoch'}) { $prefix_vars{'t_timestamp'} = strftime("%Y-%m-%d %H:%M:%S", CORE::localtime($prefix_vars{'t_epoch'})); if ($prefix_vars{'t_epoch'} =~ /^\d{10}(\.\d{3})$/) { $prefix_vars{'t_timestamp'} .= $1; } } elsif ($prefix_vars{'t_timestamp'} =~ /^\d{10}(\.\d{3})$/) { my $ms = $1; $prefix_vars{'t_epoch'} = $prefix_vars{'t_timestamp'}; $prefix_vars{'t_timestamp'} = strftime("%Y-%m-%d %H:%M:%S", CORE::localtime($prefix_vars{'t_timestamp'})); $prefix_vars{'t_timestamp'} .= $ms; } if ($prefix_vars{'t_timestamp'}) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = ($prefix_vars{'t_timestamp'} =~ $time_pattern); } elsif ($prefix_vars{'t_year'}) { $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; } $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; # Skip this line if there is no timestamp next if (!$prefix_vars{'t_timestamp'} || $prefix_vars{'t_timestamp'} eq '-- ::'); if ($prefix_vars{'t_hostport'} && !$prefix_vars{'t_client'}) { $prefix_vars{'t_client'} = $prefix_vars{'t_hostport'}; # Remove the port part $prefix_vars{'t_client'} =~ s/\(.*//; } } $cur_pid = $prefix_vars{'t_pid'} if ($prefix_vars{'t_pid'}); $prefix_vars{'t_query'} =~ s/^\\t//; # Collect orphaned lines of multiline queries if (!$prefix_vars{'t_loglevel'} && $cur_pid) { # Some log line may be written by applications next if ($line =~ /\bLOG: /); # Parse orphan lines to append information to the right place &parse_orphan_line($cur_pid, $prefix_vars{'t_query'}); } elsif ($cur_pid) { # Skip unwanted lines my $res = &skip_unwanted_line(); next if ($res == 1); if ($res == -1) { &update_progress_bar($tmpoutfile, $nlines, $stop_offset, $totalsize, \$cursize, \$old_queries_count, \$old_errors_count, $fmt); $getout = 2; last; } # Jump to the last line parsed if required next if (($incremental || $last_parsed) && !&check_incremental_position($fmt, $prefix_vars{'t_timestamp'}, $line)); # We have reach previous incremental position (or we not in increment mode) $goon = 1; # Store the current timestamp of the log line &store_current_timestamp($prefix_vars{'t_timestamp'}, $prefix_vars{'t_pid'}, $prefix_vars{'t_dbname'}); # Update current timestamp with the timezone wanted if ($log_timezone) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = change_timezone($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}); $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; } # Check if the log line should be excluded from the report if (&validate_log_line($prefix_vars{'t_pid'})) { # The information can be saved when we are switching to a new main message if ($cur_pid && exists $cur_info{$cur_pid} && ($prefix_vars{'t_loglevel'} =~ /^(LOG|ERROR|FATAL|PANIC|WARNING)$/)) { &store_queries($cur_pid); delete $cur_info{$cur_pid}; } # Parse the query now &parse_query($fmt); } } elsif ($fmt ne 'jsonlog' && $line !~ /textpayload/i) { &logmsg('DEBUG', "Unknown $fmt line format: $line"); } } # Parse syslog lines elsif ($is_syslog) { @matches = ($line =~ $compiled_prefix); my $q_match = 0; if ($#matches < 0 && $q_prefix) { @matches = ($line =~ $q_prefix); $q_match = 1; } if ($#matches >= 0) { if (!$q_match) { for (my $i = 0 ; $i <= $#prefix_params ; $i++) { $prefix_vars{$prefix_params[$i]} = $matches[$i]; } } else { for (my $i = 0 ; $i <= $#prefix_q_params ; $i++) { $prefix_vars{$prefix_q_params[$i]} = $matches[$i]; } } # skip non postgresql lines next if (exists $prefix_vars{'t_ident'} && $prefix_vars{'t_ident'} ne $ident); # Skip location information next if ($prefix_vars{'t_loglevel'} eq 'LOCATION'); # Standard syslog format does not have year information, months are # three letters and days are not always with 2 digits. if ($prefix_vars{'t_month'} !~ /\d/) { $prefix_vars{'t_year'} = $gyear; $prefix_vars{'t_day'} = sprintf("%02d", $prefix_vars{'t_day'}); $prefix_vars{'t_month'} = $month_abbr{$prefix_vars{'t_month'}}; # Take care of year overlapping if ("$prefix_vars{'t_year'}$prefix_vars{'t_month'}$prefix_vars{'t_day'}" > $CURRENT_DATE) { $prefix_vars{'t_year'} = substr($CURRENT_DATE, 0, 4) - 1; } } $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; if ($prefix_vars{'t_hostport'} && !$prefix_vars{'t_client'}) { $prefix_vars{'t_client'} = $prefix_vars{'t_hostport'}; # Remove the port part $prefix_vars{'t_client'} =~ s/\(.*//; } # Skip unwanted lines my $res = &skip_unwanted_line(); next if ($res == 1); if ($res == -1) { &update_progress_bar($tmpoutfile, $nlines, $stop_offset, $totalsize, \$cursize, \$old_queries_count, \$old_errors_count, $fmt); $getout = 2; last; } # Jump to the last line parsed if required next if (($incremental || $last_parsed) && !&check_incremental_position($fmt, $prefix_vars{'t_timestamp'}, $line)); # We have reach previous incremental position (or we not in increment mode) $goon = 1; $prefix_vars{'t_client'} = _gethostbyaddr($prefix_vars{'t_client'}) if ($dns_resolv); # Store the current timestamp of the log line &store_current_timestamp($prefix_vars{'t_timestamp'}, $prefix_vars{'t_pid'}, $prefix_vars{'t_dbname'}); # Update current timestamp with the timezone wanted if ($log_timezone) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = change_timezone($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}); $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; } # Extract information from log line prefix if (!$log_line_prefix) { &parse_log_prefix($prefix_vars{'t_logprefix'}); } # Check if the log line should be excluded from the report if (&validate_log_line($prefix_vars{'t_pid'})) { # The information can be saved when we are switching to a new main message if ($cur_pid && exists $cur_info{$cur_pid} && ($prefix_vars{'t_loglevel'} =~ /^(LOG|ERROR|FATAL|PANIC|WARNING)$/)) { &store_queries($cur_pid); delete $cur_info{$cur_pid} if (!$log_duration || ($cur_info{$cur_pid}{duration} ne '' && $cur_info{$cur_pid}{query} ne '')); } # Process the log line &parse_query($fmt); $cur_pid = $prefix_vars{'t_pid'}; } else { $cur_pid = ''; } } elsif ($goon && ($line =~ $other_syslog_line)) { $cur_pid = $8; my $t_query = $10; if ($fmt eq 'logplex' && not exists $cur_info{$cur_pid}{cur_db}) { $cur_info{$cur_pid}{cur_db} = $9; } $t_query =~ s/#011/\t/g; next if ($t_query eq "\t"); # Some log line may be written by applications next if ($t_query =~ /\bLOG: /); # Parse orphan lines to append information to the right place &parse_orphan_line($cur_pid, $t_query); } # Collect orphaned lines of multiline queries elsif ($cur_pid) { if ($line =~ $other_syslog_line) { $cur_pid = $8; my $t_query = $10; if ($fmt eq 'logplex' && not exists $cur_info{$cur_pid}{cur_db}) { $cur_info{$cur_pid}{cur_db} = $9; } $t_query =~ s/#011/\t/g; next if ($t_query eq "\t"); # Some log line may be written by applications next if ($t_query =~ /\bLOG: /); # Parse orphan lines to append information to the right place &parse_orphan_line($cur_pid, $t_query); } else { # Some log line may be written by applications next if ($line =~ /\bLOG: /); # Parse orphan lines to append information to the right place &parse_orphan_line($cur_pid, $line); } } else { &logmsg('DEBUG', "Unknown $fmt line format: $line"); } } elsif ($fmt eq 'stderr' or $fmt eq 'rds' or $fmt eq 'redshift') { @matches = ($line =~ $compiled_prefix); my $q_match = 0; if ($#matches < 0 && $q_prefix) { @matches = ($line =~ $q_prefix); $q_match = 1; } if ($#matches >= 0) { if (!$q_match) { for (my $i = 0 ; $i <= $#prefix_params ; $i++) { $prefix_vars{$prefix_params[$i]} = $matches[$i]; } } else { for (my $i = 0 ; $i <= $#prefix_q_params ; $i++) { $prefix_vars{$prefix_q_params[$i]} = $matches[$i]; } } $prefix_vars{'t_client'} =~ s/\(.*// if ($fmt eq 'rds'); $prefix_vars{'t_pid'} = $prefix_vars{'t_session_id'} if ($use_sessionid_as_pid); # Skip location information next if ($prefix_vars{'t_loglevel'} eq 'LOCATION'); if ($prefix_vars{'t_mtimestamp'} && $prefix_vars{'t_mtimestamp'} =~ s/(\.\d+)$//) { $prefix_vars{'t_ms'} = $1; } if (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_mtimestamp'}) { $prefix_vars{'t_timestamp'} = $prefix_vars{'t_mtimestamp'}; } elsif (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_session_timestamp'}) { $prefix_vars{'t_timestamp'} = $prefix_vars{'t_session_timestamp'}; } elsif (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_epoch'}) { $prefix_vars{'t_timestamp'} = strftime("%Y-%m-%d %H:%M:%S", CORE::localtime($prefix_vars{'t_epoch'})); if ($prefix_vars{'t_epoch'} =~ /^\d{10}(\.\d{3})$/) { $prefix_vars{'t_timestamp'} .= $1; } } elsif ($prefix_vars{'t_timestamp'} =~ /^\d{10}(\.\d{3})$/) { my $ms = $1; $prefix_vars{'t_epoch'} = $prefix_vars{'t_timestamp'}; $prefix_vars{'t_timestamp'} = strftime("%Y-%m-%d %H:%M:%S", CORE::localtime($prefix_vars{'t_timestamp'})); $prefix_vars{'t_timestamp'} .= $ms; } if ($prefix_vars{'t_timestamp'}) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = ($prefix_vars{'t_timestamp'} =~ $time_pattern); } elsif ($prefix_vars{'t_year'}) { $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; } $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; # Skip this line if there is no timestamp next if (!$prefix_vars{'t_timestamp'} || $prefix_vars{'t_timestamp'} eq '-- ::'); if ($prefix_vars{'t_hostport'} && !$prefix_vars{'t_client'}) { $prefix_vars{'t_client'} = $prefix_vars{'t_hostport'}; # Remove the port part $prefix_vars{'t_client'} =~ s/\(.*//; } $force_sample = 1 if ($fmt eq 'redshift' && $prefix_vars{'t_loglevel'} eq 'LOG' && $prefix_vars{'t_client'} !~ /duration: /); # Skip unwanted lines my $res = &skip_unwanted_line(); next if ($res == 1); if ($res == -1) { &update_progress_bar($tmpoutfile, $nlines, $stop_offset, $totalsize, \$cursize, \$old_queries_count, \$old_errors_count, $fmt); $getout = 2; last; } # Jump to the last line parsed if required next if (($incremental || $last_parsed) && !&check_incremental_position($fmt, $prefix_vars{'t_timestamp'}, $line)); # We have reach previous incremental position (or we not in increment mode) $goon = 1; $prefix_vars{'t_client'} = _gethostbyaddr($prefix_vars{'t_client'}) if ($dns_resolv); # Store the current timestamp of the log line &store_current_timestamp($prefix_vars{'t_timestamp'}, $prefix_vars{'t_pid'}, $prefix_vars{'t_dbname'}); # Update current timestamp with the timezone wanted if ($log_timezone) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = change_timezone($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}); $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; } # Extract information from log line prefix if (!$log_line_prefix) { &parse_log_prefix($prefix_vars{'t_logprefix'}); } # Check if the log line should be excluded from the report if (&validate_log_line($prefix_vars{'t_pid'})) { # this unused variable is used to store format information # when log format is not syslog $prefix_vars{'t_host'} = 'stderr'; # The information from previous loop can be saved # when we are switching to a new main message if ($cur_pid && exists $cur_info{$cur_pid} && $prefix_vars{'t_loglevel'} =~ /^(LOG|ERROR|FATAL|PANIC|WARNING)$/) { &store_queries($cur_pid); delete $cur_info{$cur_pid} if (!$log_duration || ($cur_info{$cur_pid}{duration} ne '' && $cur_info{$cur_pid}{query} ne '')); } # Process the log line &parse_query($fmt); $cur_pid = $prefix_vars{'t_pid'}; } else { $cur_pid = ''; } } # Collect additional query information elsif ($goon && $cur_pid) { # Some log line may be written by applications next if ($line =~ /\bLOG: /); # Parse orphan lines to append information to the right place &parse_orphan_line($cur_pid, $line); } elsif ($goon) { # unknown format &logmsg('DEBUG', "Unknown $fmt log line format: $line"); } } last if (($stop_offset > 0) && ($current_offset >= $stop_offset)); } if ($goon && $last_parsed) { &logmsg('DEBUG', "setting current position in log to $current_offset"); $last_line{current_pos} = $current_offset; } } close $lfile; # Inform the parent that it should stop parsing other files if (!$nofork && $terminate) { if ($^O !~ /MSWin32|dos/i) { kill('USR2', $parent_pid); } else { kill('TERM', $parent_pid); } return $terminate; } # Get stats from all pending temporary storage foreach my $pid (sort {$cur_info{$a}{date} <=> $cur_info{$b}{date}} keys %cur_info) { # Stores last query information &store_queries($pid, 1); } # Stores last temporary files and lock information foreach my $pid (keys %cur_temp_info) { &store_temporary_and_lock_infos($pid); } # Stores last cancelled queries information foreach my $pid (keys %cur_cancel_info) { &store_temporary_and_lock_infos($pid); } # Stores last temporary files and lock information foreach my $pid (keys %cur_lock_info) { &store_temporary_and_lock_infos($pid); } if ($progress && ($getout != 1)) { # Bzip2 and remote download compressed files has an # estimated size. Force 100% at end of log parsing if (($http_download && $logfile =~ $compress_extensions ) || $logfile =~ /\.bz2$/i) { $cursize = $totalsize; } if (!$tmpoutfile || not defined $pipe) { print STDERR &progress_bar($cursize, $stop_offset || $totalsize, 25, '=', ($overall_stat{'queries_number'} + $pgb_overall_stat{'queries_number'}), ($overall_stat{'errors_number'} + $pgb_overall_stat{'errors_number'}), $fmt ); print STDERR "\n"; } else { $pipe->print("$cursize " . ($overall_stat{'queries_number'} + $pgb_overall_stat{'queries_number'} - $old_queries_count) . " " . ($overall_stat{'errors_number'} + $pgb_overall_stat{'errors_number'} - $old_errors_count) . " $fmt\n"); } } # Case where we build reports from binary only with no new log entries. if ($incremental && $html_outdir && !$outdir) { if ($logfile =~ /\/(\d+)\/(\d+)\/(\d+)\/[^\/]+\.bin$/) { $last_line{datetime} = "$1-$2-$3"; } } %cur_info = (); # In incremental mode data are saved to disk per day if ($incremental && ($last_line{datetime} || (($fmt =~ /pgbouncer/) && $pgb_last_line{datetime}))) { $incr_date = ($fmt =~ /pgbouncer/) ? $pgb_last_line{datetime} : $last_line{datetime}; $incr_date =~ s/\s.*$//; # set path and create subdirectories if ($incr_date =~ /^(\d+)-(\d+)-(\d+)/) { mkdir("$outdir/$1") if (!-d "$outdir/$1"); mkdir("$outdir/$1/$2") if (!-d "$outdir/$1/$2"); mkdir("$outdir/$1/$2/$3") if (!-d "$outdir/$1/$2/$3"); } else { &logmsg('ERROR', "invalid incremental date: $incr_date, can not create subdirectories."); } my $bpath = $incr_date; $bpath =~ s/\-/\//g; # Mark the directory as needing index update if (open(my $out, '>>', "$last_parsed.tmp")) { flock($out, 2) || return $getout; print $out "$incr_date\n"; close($out); } else { &logmsg('ERROR', "can't save last parsed line into $last_parsed.tmp, $!"); } # Save binary data if ($outdir) { my $filenum = $$; $filenum++ while (-e "$outdir/$bpath/$incr_date-$filenum.bin"); my $fhb = new IO::File ">$outdir/$bpath/$incr_date-$filenum.bin"; if (not defined $fhb) { localdie("FATAL: can't write to $outdir/$bpath/$incr_date-$filenum.bin, $!\n"); } &dump_as_binary($fhb); $fhb->close; } } elsif (fileno($tmpoutfile)) { &dump_as_binary($tmpoutfile); $tmpoutfile->close(); } # Save last line into temporary file if ($last_parsed && (scalar keys %last_line || scalar keys %pgb_last_line)) { if (open(my $out, '>>', "$tmp_last_parsed")) { flock($out, 2) || return $getout; if ($fmt =~ /pgbouncer/) { $pgb_last_line{current_pos} ||= 0; &logmsg('DEBUG', "Saving pgbouncer last parsed line into $tmp_last_parsed ($pgb_last_line{datetime}\t$pgb_last_line{current_pos})"); print $out "pgbouncer\t$pgb_last_line{datetime}\t$pgb_last_line{current_pos}\t$pgb_last_line{orig}\n"; } else { $last_line{current_pos} ||= 0; &logmsg('DEBUG', "Saving last parsed line into $tmp_last_parsed ($last_line{datetime}\t$last_line{current_pos})"); print $out "$last_line{datetime}\t$last_line{current_pos}\t$last_line{orig}\n"; } close($out); } else { &logmsg('ERROR', "can't save last parsed line into $tmp_last_parsed, $!"); } } # Inform the parent that it should stop parsing other files if (!$nofork && $getout) { if ($^O !~ /MSWin32|dos/i) { kill('USR2', $parent_pid); } else { kill('TERM', $parent_pid); } } # Write the list of database we have proceeded in this process if ($report_per_database) { if (open(my $out, '>>', "$tmp_dblist")) { flock($out, 2) || return $getout; print $out join(';', %{ $overall_stat{nlines} }), "\n"; close($out); } else { &logmsg('ERROR', "can't save last parsed line into $tmp_dblist, $!"); } } &init_stats_vars() if ($tmpoutfile); return $getout; } sub unescape_jsonlog { my $str = shift; while ($str =~ s/([^\\])\\"/$1"/g) {}; while ($str =~ s/([^\\])\\t/$1\t/g) {}; while ($str =~ s/\\r\\n/\n/gs) {}; while ($str =~ s/([^\\])\\r/$1\n/gs) {}; while ($str =~ s/([^\\])\\n/$1\n/gs) {}; return $str; } sub parse_jsonlog_input { my $str = shift; # This is Azure json log input return parse_jsonazure_input($str) if ($str =~ /"record_application_name_s"/); # This is CloudNativePG json log input return parse_jsoncnpg_input($str) if ($str =~ /"logging_pod"/); my %infos = (); # json columns information from jsonlog extension: # ------------------------------------------------- # timestamp with milliseconds # username # database name # Process id # Remote host and port # session id # Line number* # PS display * # session start timestamp * # Virtual transaction id # Transaction id # Error severity # SQL state code # errdetail or errdetail_log # errhint # internal query # internal pos * # errcontext # user query # file error location # application name # errmessage # backend type * # leader PID * # query id * # # (*) information not available for the moment # Extract the date if ($str =~ m/[\{,]"timestamp":\s*"(\d+)-(\d+)-(\d+).(\d+):(\d+):(\d+)/) { $infos{'t_year'} = $1; $infos{'t_month'} = $2; $infos{'t_day'} = $3; $infos{'t_hour'} = $4; $infos{'t_min'} = $5; $infos{'t_sec'} = $6; $infos{'t_timestamp'} = "$infos{'t_year'}-$infos{'t_month'}-$infos{'t_day'} $infos{'t_hour'}:$infos{'t_min'}:$infos{'t_sec'}"; $infos{'t_time'} = "$infos{'t_hour'}:$infos{'t_min'}:$infos{'t_sec'}"; } # Set query parameters as global variables if ($str =~ m/"user":\s*"(.*?)"(?:,"|\})/) { $infos{'t_dbuser'} = $1; } elsif ($str =~ m/,user=([^\s]*) /) { $infos{'t_dbuser'} = $1; } if ($str =~ m/"dbname":\s*"(.*?)"(?:,"|\})/) { $infos{'t_dbname'} = $1; } elsif ($str =~ m/ db=([^,]*),/) { $infos{'t_dbname'} = $1; } if ($str =~ m/"application_name":\s*"(.*?)"(?:,"|\})/) { $infos{'t_appname'} = $1; } elsif ($str =~ m/"project_id":\s*"([^"])"/) { $infos{'t_appname'} = $1; } if ($str =~ m/"remote_host":\s*"(.*?)"(?:,"|\})/) { $infos{'t_client'} = $1; $infos{'t_client'} =~ s/:.*//; $infos{'t_client'} = _gethostbyaddr($infos{'t_client'}) if ($dns_resolv); } $infos{'t_host'} = 'jsonlog'; # this unused variable is used to store format information when log format is not syslog # Try to extract the pid information if ($str =~ m/"pid":\s*"(.*?)"(?:,"|\})/) { $infos{'t_pid'} = $1; } elsif ($str =~ m/"pid":\s*([0-9]+)/) { $infos{'t_pid'} = $1; } elsif ($str =~ m/"textPayload":\s*"\[(\d+)\]:/) { $infos{'t_pid'} = $1; } elsif ($str =~ m/"textPayload":\s*"(\d+-\d+-\d+[T\s]\d+:\d+:\d+\.\d+\s[^\s]*\s+\[(\d+)\]:.*)",(?:"timestamp":.*)?$/) { $infos{'t_pid'} = $2; $infos{'t_textPayload'} = $1; } elsif ($str =~ m/"textPayload":\s*"(.*)","timestamp":"/) { $infos{'t_query'} = unescape_jsonlog($1); } if ($str =~ m/"error_severity":\s*"(.*?)"(?:,"|\})/) { $infos{'t_loglevel'} = $1; } elsif ($str =~ m/user=[^\s]* ([^:]+):\s{1,2}(.*)","timestamp":"/) { $infos{'t_loglevel'} = $1; $infos{'t_query'} = unescape_jsonlog($2); } if ($str =~ m/"state_code":\s*"(.*?)"(?:,"|\})/) { $infos{'t_sqlstate'} = $1; } if ($str =~ m/"message":\s*"(.*?)"(?:,"|\})/) { $infos{'t_query'} = unescape_jsonlog($1); } elsif ($str =~ m/"statement":\s*"(.*?)"(?:,"|\})/) { $infos{'t_query'} = unescape_jsonlog($1); } # Set ERROR additional information if ($str =~ m/"(?:detail_log|detail)":\s*"(.*?)"(?:,"|\})/) { $infos{'t_detail'} = unescape_jsonlog($1); } if ($str =~ m/"hint":\s*"(.*?)"(?:,"|\})/) { $infos{'t_hint'} = unescape_jsonlog($1); } if ($str =~ m/"context":\s*"(.*?)"(?:,"|\})/) { $infos{'t_context'} = unescape_jsonlog($1); } if ($str =~ m/"(?:statement|internal_query)":\s*"(.*?)"(?:,"|\})/) { $infos{'t_statement'} = unescape_jsonlog($1); } # Backend type information if ($str =~ m/"backend_type":\s*"(.*?)"(?:,"|\})/) { $infos{'t_backend_type'} = $1; } return %infos; } sub parse_jsonazure_input { my $str = shift; my %infos = (); # json columns information from Azure Log Analytics Workspaces # ------------------------------------------------------------ # TableName # record_application_name_s # record_backend_type_s # record_command_tag_s # record_connection_from_s # record_context_s # record_database_name_s # record_detail_s # record_error_severity_s # record_hint_s # record_log_time_s # record_message_s # record_process_id_s # record_query_id_s # record_query_pos_s # record_query_s # record_session_id_s # record_session_line_num_s # record_session_start_time_s # record_sql_state_code_s # record_transaction_id_s # record_user_name_s # record_virtual_transaction_id_s # Extract the date if ($str =~ m/"record_log_time_s":\s*"(\d+)-(\d+)-(\d+).(\d+):(\d+):(\d+)/) { $infos{'t_year'} = $1; $infos{'t_month'} = $2; $infos{'t_day'} = $3; $infos{'t_hour'} = $4; $infos{'t_min'} = $5; $infos{'t_sec'} = $6; $infos{'t_timestamp'} = "$infos{'t_year'}-$infos{'t_month'}-$infos{'t_day'} $infos{'t_hour'}:$infos{'t_min'}:$infos{'t_sec'}"; $infos{'t_time'} = "$infos{'t_hour'}:$infos{'t_min'}:$infos{'t_sec'}"; } if ($str =~ m/"record_user_name_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_dbuser'} = $1; } if ($str =~ m/"record_database_name_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_dbname'} = $1; } if ($str =~ m/"record_application_name_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_appname'} = $1; } if ($str =~ m/"record_connection_from_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_client'} = $1; $infos{'t_client'} =~ s/:.*//; $infos{'t_client'} = _gethostbyaddr($infos{'t_client'}) if ($dns_resolv && $infos{'t_client'} ne '[local]'); } $infos{'t_host'} = 'jsonlog'; # this unused variable is used to store format information when log format is not syslog if ($str =~ m/"record_process_id_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_pid'} = $1; } if ($str =~ m/"record_error_severity_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_loglevel'} = $1; } if ($str =~ m/"record_sql_state_code_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_sqlstate'} = $1; } if ($str =~ m/"record_query_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_query'} = unescape_jsonlog($1); } # Set ERROR additional information if ($str =~ m/"record_detail_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_detail'} = unescape_jsonlog($1); } if ($str =~ m/"record_hint_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_hint'} = unescape_jsonlog($1); } if ($str =~ m/"record_context_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_context'} = unescape_jsonlog($1); } # Backend type information if ($str =~ m/"record_backend_type_s":\s*"(.*?)"(?:,\s*"|\})/) { $infos{'t_backend_type'} = $1; } return %infos; } sub parse_jsoncnpg_input { my $str = shift; my %infos = (); # json log line from CloudNativePG Operator: # ------------------------------------------------- # { # "level": "info", # "ts": 1619781249.7188137, # "logger": "postgres", # "msg": "record", # "record": { # "log_time": "2021-04-30 11:14:09.718 UTC", # "user_name": "", # "database_name": "", # "process_id": "25", # "connection_from": "", # "session_id": "608be681.19", # "session_line_num": "1", # "command_tag": "", # "session_start_time": "2021-04-30 11:14:09 UTC", # "virtual_transaction_id": "", # "transaction_id": "0", # "error_severity": "LOG", # "sql_state_code": "00000", # "message": "database system was interrupted; last known up at 2021-04-30 11:14:07 UTC", # "detail": "", # "hint": "", # "internal_query": "", # "internal_query_pos": "", # "context": "", # "query": "", # "query_pos": "", # "location": "", # "application_name": "", # "backend_type": "startup" # }, # "logging_pod": "cluster-example-1", # } # Extract the date if ($str =~ m/[\{,]"log_time":\s*"(\d+)-(\d+)-(\d+).(\d+):(\d+):(\d+)/) { $infos{'t_year'} = $1; $infos{'t_month'} = $2; $infos{'t_day'} = $3; $infos{'t_hour'} = $4; $infos{'t_min'} = $5; $infos{'t_sec'} = $6; $infos{'t_timestamp'} = "$infos{'t_year'}-$infos{'t_month'}-$infos{'t_day'} $infos{'t_hour'}:$infos{'t_min'}:$infos{'t_sec'}"; $infos{'t_time'} = "$infos{'t_hour'}:$infos{'t_min'}:$infos{'t_sec'}"; } # Set query parameters as global variables if ($str =~ m/"user_name":\s*"(.*?)"(?:,"|\})/) { $infos{'t_dbuser'} = $1; } if ($str =~ m/"database_name":\s*"(.*?)"(?:,"|\})/) { $infos{'t_dbname'} = $1; } if ($str =~ m/"application_name":\s*"(.*?)"(?:,"|\})/) { $infos{'t_appname'} = $1; } if ($str =~ m/"connection_from":\s*"(.*?)"(?:,"|\})/) { $infos{'t_client'} = $1; $infos{'t_client'} =~ s/:.*//; $infos{'t_client'} = _gethostbyaddr($infos{'t_client'}) if ($dns_resolv); } $infos{'t_host'} = 'jsonlog'; # this unused variable is used to store format information when log format is not syslog if ($str =~ m/"process_id":\s*"(.*?)"(?:,"|\})/) { $infos{'t_pid'} = $1; } if ($str =~ m/"error_severity":\s*"(.*?)"(?:,"|\})/) { $infos{'t_loglevel'} = $1; } if ($str =~ m/"sql_state_code":\s*"(.*?)"(?:,"|\})/) { $infos{'t_sqlstate'} = $1; } if ($str =~ m/"message":\s*"(.*?)"(?:,"|\})/) { $infos{'t_query'} = unescape_jsonlog($1); } # Set ERROR additional information if ($str =~ m/"detail":\s*"(.*?)"(?:,"|\})/) { $infos{'t_detail'} = unescape_jsonlog($1); } if ($str =~ m/"hint":\s*"(.*?)"(?:,"|\})/) { $infos{'t_hint'} = unescape_jsonlog($1); } if ($str =~ m/"context":\s*"(.*?)"(?:,"|\})/) { $infos{'t_context'} = unescape_jsonlog($1); } if ($str =~ m/"internal_query":\s*"(.*?)"(?:,"|\})/) { $infos{'t_statement'} = unescape_jsonlog($1); } # Backend type information if ($str =~ m/"backend_type":\s*"(.*?)"(?:,"|\})/) { $infos{'t_backend_type'} = $1; } return %infos; } sub parse_orphan_line { my ($cur_pid, $line, $t_dbname) = @_; my $date_part = "$cur_info{$cur_pid}{year}$cur_info{$cur_pid}{month}$cur_info{$cur_pid}{day}"; my $curdb = undef; if (!exists $cur_info{$cur_pid} || !exists $cur_info{$cur_pid}{cur_db} || !$cur_info{$cur_pid}{cur_db}) { $curdb = set_current_db($t_dbname); } else { $curdb = $cur_info{$cur_pid}{cur_db}; } if (!$report_per_database) { $curdb = $DBALL; } # Store vacuum related information if ($cur_info{$cur_pid}{vacuum} && ($line =~ /^[\s\t]*(pages|tuples|buffer usage|avg read rate|system usage|WAL usage|I\/O timings|frozen):/)) { if ($line =~ /(pages|tuples): (\d+) removed, (\d+) remain/) { $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{$1}{removed} += $2; $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{$1}{remain} += $3; } if ($line =~ /(\d+) are dead but not yet removable/) { $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{tuples}{notremovable} += $1; } if ($line =~ /timings: read: ([\d\.]+) ms, write: ([\d\.]+) ms/) { $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{read} += $1; $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{write} += $2; } if ($line =~ /frozen: (\d+) pages from table .* had (\d+) tuples frozen/) { $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{pages}{frozen} += $1; $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{tuples}{frozen} += $2; } if ($line =~ /^[\s\t]*system usage: CPU: user: (.*) (?:sec|s,) system: (.*) (?:sec|s,) elapsed: ([^\s]+)/) { $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{elapsed} += $3; # Add vacuum counts and duration to the $per_minute_info. # $per_minute_info{$curdb}{$date_part}{$cur_info{$cur_pid}{hour}}{$cur_info{$cur_pid}{min}}{autovacuum}{count}++; # Duplicate to parse_query() -> Store autovacuum information $per_minute_info{$curdb}{$date_part}{$cur_info{$cur_pid}{hour}}{$cur_info{$cur_pid}{min}}{autovacuum}{second}{$cur_info{$cur_pid}{second}} += $3; if ($3 > $autovacuum_info{$curdb}{peak}{system_usage}{elapsed}) { $autovacuum_info{$curdb}{peak}{system_usage}{elapsed} = $3; $autovacuum_info{$curdb}{peak}{system_usage}{table} = $cur_info{$cur_pid}{vacuum}; $autovacuum_info{$curdb}{peak}{system_usage}{date} = "$cur_info{$cur_pid}{year}-$cur_info{$cur_pid}{month}-$cur_info{$cur_pid}{day} " . "$cur_info{$cur_pid}{hour}:$cur_info{$cur_pid}{min}:$cur_info{$cur_pid}{sec}"; } } if ($line =~ /, (\d+) skipped due to pins, (\d+) skipped frozen/) { $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{skip_pins} += $1; $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{skip_frozen} += $2; } if ($line =~ /buffer usage: (\d+) hits, (\d+) misses, (\d+) dirtied/) { $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{hits} += $1; $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{misses} += $2; $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{dirtied} += $3; } if ($line =~ /WAL usage: (\d+) records, (\d+) full page images, (\d+) bytes/) { $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{wal_record} += $1; $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{wal_full_page} += $2; $autovacuum_info{$curdb}{tables}{$cur_info{$cur_pid}{vacuum}}{wal_bytes} += $3; } } # stores bind parameters if parameter syntax is detected elsif ( $cur_info{$cur_pid}{parameters} || $cur_bind_info{$cur_pid}) { if (!$error_only) { if ($cur_info{$cur_pid}{parameters}) { if ($cur_info{$cur_pid}{parameters} =~ /=$/) { $cur_info{$cur_pid}{parameters} .= ' '; $cur_bind_info{$cur_pid}{'bind'}{'parameters'} .= ' ' if (exists $cur_bind_info{$cur_pid}{'bind'}{'parameters'}); } $line =~ s/^\t+/\\n/; $cur_bind_info{$cur_pid}{'bind'}{'parameters'} .= $line if (exists $cur_bind_info{$cur_pid}{'bind'}{'parameters'}); $cur_info{$cur_pid}{parameters} .= $line; } elsif (exists $cur_bind_info{$cur_pid}{bind}{'query'} && !exists $cur_info{$cur_pid}{query}) { $cur_bind_info{$cur_pid}{bind}{'query'} .= $line; } else { $cur_info{$cur_pid}{query} .= $line; } } } # stores explain plan lines elsif (exists $cur_plan_info{$cur_pid}{plan}) { $cur_plan_info{$cur_pid}{plan} .= "\n" . $line; } # If we have previously stored a temporary file query, append to that query elsif (exists $cur_temp_info{$cur_pid}{size}) { $cur_temp_info{$cur_pid}{query} .= "\n" . $line; } # If we have previously stored a query that generates locks, append to that query elsif (exists $cur_lock_info{$cur_pid}{query}) { $cur_lock_info{$cur_pid}{query} .= "\n" . $line; } # If we have previously stored a cancelled query, append to that query elsif (exists $cur_cancel_info{$cur_pid}{query}) { $cur_cancel_info{$cur_pid}{query} .= "\n" . $line; } # Otherwise append the orphan line to the corresponding part of the query else { # Append to the error statement if one is defined if (exists $cur_info{$cur_pid}{statement}) { $cur_info{$cur_pid}{statement} .= "\n" . $line if (!$nomultiline); # Append to the bind parameters if one is defined } elsif (exists $cur_info{$cur_pid}{parameters}) { $cur_info{$cur_pid}{parameters} .= $line if (!$error_only); # Append to the error context if one is defined } elsif (exists $cur_info{$cur_pid}{context}) { $cur_info{$cur_pid}{context} .= "\n" . $line; # Append to the query detail if one is defined } elsif (exists $cur_info{$cur_pid}{detail}) { $cur_info{$cur_pid}{detail} .= "\n" . $line; # After all append to the query if one is defined } elsif (exists $cur_info{$cur_pid}{query}) { $cur_info{$cur_pid}{query} .= "\n" . $line if (!$nomultiline && !$error_only); # Associate to bind|prepare query now that we collect it too } elsif (exists $cur_bind_info{$cur_pid}{'bind'}) { $cur_bind_info{$cur_pid}{'bind'}{'query'} .= "\n" . $line; } elsif (exists $cur_bind_info{$cur_pid}{'prepare'}) { $cur_bind_info{$cur_pid}{'prepare'}{'query'} .= "\n" . $line; } } } # Store the current timestamp of the log line sub store_current_timestamp { my ($t_timestamp, $t_pid, $t_dbname) = @_; # Store current report name and list of database my $curdb = undef; if (!exists $cur_info{$t_pid} || !exists $cur_info{$t_pid}{cur_db}) { $curdb = set_current_db($t_dbname); } else { $curdb = $cur_info{$t_pid}{cur_db}; } if (!$overall_stat{$curdb}{'first_log_ts'} || ($overall_stat{$curdb}{'first_log_ts'} gt $t_timestamp)) { $overall_stat{$curdb}{'first_log_ts'} = $t_timestamp; } if (!$overall_stat{$curdb}{'last_log_ts'} || ($overall_stat{$curdb}{'last_log_ts'} lt $t_timestamp)) { $overall_stat{$curdb}{'last_log_ts'} = $t_timestamp; } } sub detect_new_log_line { my ($lfile, $fmt, $current_date, $gyear, $saved_date, $startoffset) = @_; my $more_lines = 0; my $num_line_checked = 0; while (my $line = <$lfile>) { $line =~ s/\r//; chomp($line); next if (!$line); $num_line_checked++; if ($fmt =~ /syslog|logplex/) { my @matches = ($line =~ $compiled_prefix); if ($#matches >= 0) { for (my $i = 0 ; $i <= $#prefix_params ; $i++) { $prefix_vars{$prefix_params[$i]} = $matches[$i]; } # Standard syslog format does not have year information, months are # three letters and days are not always with 2 digits. if ($prefix_vars{'t_month'} !~ /\d/) { $prefix_vars{'t_year'} = $gyear; $prefix_vars{'t_day'} = sprintf("%02d", $prefix_vars{'t_day'}); $prefix_vars{'t_month'} = $month_abbr{$prefix_vars{'t_month'}}; # Take care of year overlapping if ("$prefix_vars{'t_year'}$prefix_vars{'t_month'}$prefix_vars{'t_day'}" > $current_date) { $prefix_vars{'t_year'} = substr($current_date, 0, 4) - 1; } } $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_time'}"; } } elsif ($fmt eq 'jsonlog') { %prefix_vars = parse_jsonlog_input($line); if (!exists $prefix_vars{'t_year'}) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = ($prefix_vars{'t_timestamp'} =~ $time_pattern); } $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; } elsif ($fmt =~ /pgbouncer/) { my @matches = ($line =~ $pgbouncer_log_parse1); if ($#matches >= 0) { for (my $i = 0 ; $i <= $#pgb_prefix_parse1 ; $i++) { $prefix_vars{$pgb_prefix_parse1[$i]} = $matches[$i]; } } } else { %prefix_vars = (); my @matches = ($line =~ $compiled_prefix); if ($#matches >= 0) { for (my $i = 0 ; $i <= $#prefix_params ; $i++) { $prefix_vars{$prefix_params[$i]} = $matches[$i]; } $prefix_vars{'t_client'} =~ s/\(.*// if ($fmt eq 'rds'); if ($prefix_vars{'t_mtimestamp'} && $prefix_vars{'t_mtimestamp'} =~ s/(\.\d+)$//) { $prefix_vars{'t_ms'} = $1; } if (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_mtimestamp'}) { $prefix_vars{'t_timestamp'} = $prefix_vars{'t_mtimestamp'}; } elsif (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_session_timestamp'}) { $prefix_vars{'t_timestamp'} = $prefix_vars{'t_session_timestamp'}; } elsif (!$prefix_vars{'t_timestamp'} && $prefix_vars{'t_epoch'}) { $prefix_vars{'t_timestamp'} = strftime("%Y-%m-%d %H:%M:%S", CORE::localtime($prefix_vars{'t_epoch'})); if ($prefix_vars{'t_epoch'} =~ /^\d{10}(\.\d{3})$/) { $prefix_vars{'t_timestamp'} .= $1; } } elsif ($prefix_vars{'t_timestamp'} =~ /^\d{10}(\.\d{3})$/) { my $ms = $1; $prefix_vars{'t_epoch'} = $prefix_vars{'t_timestamp'}; $prefix_vars{'t_timestamp'} = strftime("%Y-%m-%d %H:%M:%S", CORE::localtime($prefix_vars{'t_timestamp'})); $prefix_vars{'t_timestamp'} .= $ms; } } if ($prefix_vars{'t_timestamp'}) { ($prefix_vars{'t_year'}, $prefix_vars{'t_month'}, $prefix_vars{'t_day'}, $prefix_vars{'t_hour'}, $prefix_vars{'t_min'}, $prefix_vars{'t_sec'}) = ($prefix_vars{'t_timestamp'} =~ $time_pattern); } elsif ($prefix_vars{'t_year'}) { $prefix_vars{'t_timestamp'} = "$prefix_vars{'t_year'}-$prefix_vars{'t_month'}-$prefix_vars{'t_day'} $prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; } $prefix_vars{'t_time'} = "$prefix_vars{'t_hour'}:$prefix_vars{'t_min'}:$prefix_vars{'t_sec'}"; # Skip this line if there is no timestamp next if (!$prefix_vars{'t_timestamp'} || $prefix_vars{'t_timestamp'} eq '-- ::'); } # Unwanted line next if (!$prefix_vars{'t_timestamp'}); # This file has already been parsed if ($saved_date gt $prefix_vars{'t_timestamp'}) { close($lfile); return (0, "this file has already been parsed, timestamp $prefix_vars{'t_timestamp'} read at offset $startoffset is lower than saved timestamp: $saved_date"); } elsif ($saved_date eq $prefix_vars{'t_timestamp'}) { next; } else { $more_lines++; last; } } return ($more_lines, "more line after the saved position"); } # Method used to check if the file stores logs after the last incremental position or not # This position should have been saved in the incremental file and read in the $last_parsed at # start up. # # How does pgBadger detect that a log file need not to be parsed? # # In process_file(): # # File LAST_PARSED is not present in the incremental output directory # => the log is fully parsed. # # # File LAST_PARSED is present, read the size of the last parsed log and the timestamp from # this file then for each log file provided # # If the total size of the log (uncompressed) is smaller than the history size # or we can not determine the real file size of a compressed file (ex: bzip2) # => read the log from start and exclude all lines older than the history timestamp # # If logfile is input from stdin, in binary or csv format, it is download through an http request # => read the log from start and exclude all lines older than the history timestamp # # Call check_file_changed(), return code: # - -1 : the file must be parsed from the beginning # - 0 : the log has already be parsed # - 1 : the log must be parsed from the history offset # # - If this is a remote log downloaded using http[s] or [s]ftp, we can not move/seek to the historical position/offset # => return -1, read the log from start and exclude all lines older than the history timestamp # # - If this is a compressed remote log we can not move/seek to the historical position/offset # => return -1, read the log from start and exclude all lines older than the history timestamp # Use of zhead and ztail can cost too much in remote mode, just return -1 # # - If the log file is compressed we can not seek to history position, the entire file must be read # => return -1, this file must be parsed from start excluding all lines older than the history timestamp # # - If the history size is smaller than the total size fo the log file: # => move/seek to the history size/offset and look at the next line timestamp # though method detect_new_log_line(). # - If the history size is equal to the total size fo the log file: # => rewind/seek to history size/offset - 8192 # # - Then go through method detect_new_log_line(): # # => if the history timestamp is newer than the current line parsed # => return 0, this file has already been parsed # => if the current line parsed has the same timestamp than the history timestamp # => go to next line # => if the next line is newer than the history timestamp # => return 1, the log will be parsed starting from the history size/offset. # # - If more new line (1) has been returned by detect_new_log_line(), move/seek at the start of # of the log and check the timestamp of the first line. # => if the timestamp of the first line is newer than the history timestamp # => return -1, "the file is new, the entire file must be parsed" # => else # => return 1, "the file will parsed from history position and history date" # - else # => return 0, "no new line found in this log" # sub check_file_changed { my ($file, $totalsize, $fmt, $saved_date, $saved_pos, $look_at_beginning) = @_; # With http[s] or [s]ftp download we can not seek to the historical position if ($file =~ /^(http[s]*:|[s]*ftp:)/) { return (-1, "no incremental mode for http[s] or [s]ftp download"); } my $ssh_download = ($file =~ /^ssh:/i) ? 1 : 0; my $http_download = ($file =~ /^(http[s]*:|[s]*ftp:)/i) ? 1 : 0; my $iscompressed = ($file =~ $compress_extensions) ? 1 : 0; my $lfile = &get_log_file($file, $totalsize, $remote_host || $ssh_download); return (-1, "cannot open file $file") if (!defined $lfile); my ($gsec, $gmin, $ghour, $gmday, $gmon, $gyear, $gwday, $gyday, $gisdst) = localtime(time); $gyear += 1900; my $current_date = $gyear . sprintf("%02d", $gmon + 1) . sprintf("%02d", $gmday); my $startoffset = 0; # Compressed files do not allow seeking so we will return 0 or 2 if ($iscompressed) { close($lfile); return (-1, "seek into compressed file is not supported, the entire file must be parsed"); } %prefix_vars = (); # If seeking is not explicitely disabled if (!$look_at_beginning) { # do not seek if filesize is smaller than the seek position if ($saved_pos < $totalsize) { $lfile->seek($saved_pos || 0, 0); $startoffset = $saved_pos || 0; } # Case of file with same size elsif ($saved_pos == $totalsize) { # A log line can not be greater than 8192 so rewind to # previous line, we don't care if we rewind more line # before as if this is the same log they are in the past if ($saved_pos > 8192) { $lfile->seek($saved_pos - 8192, 0); $startoffset = $saved_pos - 8192; } else { $lfile->seek(0, 0); $startoffset = 0; } } } my ($more_lines, $msg) = detect_new_log_line($lfile, $fmt, $current_date, $gyear, $saved_date, $startoffset); if ($more_lines) { $lfile->seek(0, 0); $startoffset = 0; ($more_lines, $msg) = detect_new_log_line($lfile, $fmt, $current_date, $gyear, $saved_date, $startoffset); close($lfile); if ($more_lines) { return (-1, "the file is new, the entire file must be parsed"); } } else { close($lfile); return (0, "no new line found in this log"); } return (1, "the file will parsed from history position $startoffset and history date: $saved_date") ; } # Method used to check if we have already reached the last parsing position in incremental mode # This position should have been saved in the incremental file and read in the $last_parsed at # start up. sub check_incremental_position { my ($fmt, $cur_date, $line) = @_; if ($last_parsed && ($fmt !~ /pgbouncer/)) { if ($saved_last_line{datetime}) { if ($cur_date lt $saved_last_line{datetime}) { return 0; } elsif (!$last_line{datetime} && ($cur_date eq $saved_last_line{datetime})) { return 0; } } $last_line{datetime} = $cur_date; $last_line{orig} = $line; } elsif ($last_parsed) { if ($pgb_saved_last_line{datetime}) { if ($cur_date lt $pgb_saved_last_line{datetime}) { return 0; } elsif (!$pgb_last_line{datetime} && ($cur_date eq $pgb_saved_last_line{datetime})) { return 0 if ($line ne $pgb_saved_last_line{orig}); } } $pgb_last_line{datetime} = $cur_date; $pgb_last_line{orig} = $line; } # In incremental mode data are saved to disk per day if ($incremental) { $cur_date =~ s/\s.*$//; # Check if the current day has changed, if so save data $incr_date = $cur_date if (!$incr_date); if ($cur_date gt $incr_date) { # Get stats from all pending temporary storage foreach my $pid (sort {$cur_info{$a}{date} <=> $cur_info{$b}{date}} keys %cur_info) { # Stores last queries information &store_queries($pid, 1); } # Stores last temporary files and lock information foreach my $pid (keys %cur_temp_info) { &store_temporary_and_lock_infos($pid); } # Stores last cancelled queries information foreach my $pid (keys %cur_cancel_info) { &store_temporary_and_lock_infos($pid); } # Stores last temporary files and lock information foreach my $pid (keys %cur_lock_info) { &store_temporary_and_lock_infos($pid); } # set path and create subdirectories if ($incr_date =~ /^(\d+)-(\d+)-(\d+)/) { mkdir("$outdir/$1") if (!-d "$outdir/$1"); mkdir("$outdir/$1/$2") if (!-d "$outdir/$1/$2"); mkdir("$outdir/$1/$2/$3") if (!-d "$outdir/$1/$2/$3"); } else { &logmsg('ERROR', "invalid incremental date: $incr_date, can not create subdirectories."); } my $bpath = $incr_date; $bpath =~ s/\-/\//g; # Mark this directory as needing a reindex if (open(my $out, '>>' , "$last_parsed.tmp")) { flock($out, 2) || return 1; print $out "$incr_date\n"; close($out); } else { &logmsg('ERROR', "can't save last parsed line into $last_parsed.tmp, $!"); } # Save binary data my $filenum = $$; $filenum++ while (-e "$outdir/$bpath/$incr_date-$filenum.bin"); my $fhb = new IO::File ">$outdir/$bpath/$incr_date-$filenum.bin"; if (not defined $fhb) { localdie("FATAL: can't write to $outdir/$bpath/$incr_date-$filenum.bin, $!\n"); } &dump_as_binary($fhb); $fhb->close; $incr_date = $cur_date; &init_stats_vars(); } } return 1; } # Display message following the log level sub logmsg { my ($level, $str) = @_; return if ($quiet && !$debug && ($level ne 'FATAL')); return if (!$debug && ($level eq 'DEBUG')); if ($level =~ /(\d+)/) { print STDERR "\t" x $1; } print STDERR "$level: $str\n"; } # Remove quote from alias for normalisation sub remove_alias { my $str = shift(); $str =~ s/'//gs; return $str; } # Normalize SQL queries by removing parameters sub normalize_query { my $orig_query = shift; return if (!$orig_query); # replace \' and \ in string $orig_query =~ s/[\\]'/ESCPQUOTE/gs; $orig_query =~ s/[\\]/ESCAPEME/gs; # Remove comments /* ... */ if (!$keep_comments) { $orig_query =~ s/\/\*(.*?)\*\///gs; } # Keep case on object name between doublequote my %objnames = (); my $i = 0; while ($orig_query =~ s/("[^"]+")/%%OBJNAME$i%%/) { $objnames{$i} = $1; $i++; } # Set the entire query lowercase $orig_query = lc($orig_query); # Restore object name while ($orig_query =~ s/\%\%objname(\d+)\%\%/$objnames{$1}/gs) {}; %objnames = (); # Remove string content $orig_query =~ s/\\'//gs; $orig_query =~ s/'[^']*'/\?/gs; $orig_query =~ s/\?(\?)+/\?/gs; # Remove comments starting with -- if (!$keep_comments) { $orig_query =~ s/\s*--[^\n]+[\n]/\n/gs; } # Remove extra space, new line and tab characters by a single space $orig_query =~ s/\s+/ /gs; # Removed start of transaction if ($orig_query !~ /^\s*begin\s*;\s*$/) { $orig_query =~ s/^\s*begin\s*;\s*//gs } # Normalise alias with quote $orig_query =~ s/AS\s+"([^"]+)"/'AS "' . remove_alias($1) . '"'/eigs; # Remove NULL parameters $orig_query =~ s/=\s*null/= \?/gs; # remove temporary identifier between double quote my %identifiers = (); $i = 0; while ($orig_query =~ s/"([^"]+)"/\%sqlident$i\%/) { $identifiers{$i} = $1; $i++; } # Remove numbers $orig_query =~ s/([^a-z0-9_\$\-])-?\d+/$1\?/gs; # Remove hexadecimal numbers $orig_query =~ s/([^a-z_\$-])0x[0-9a-f]{1,10}/$1\?/gs; # Remove bind parameters $orig_query =~ s/\$\d+/\?/gs; # restore identifiers $orig_query =~ s/\%sqlident(\d+)\%/"$identifiers{$1}"/gs; # Remove IN values $orig_query =~ s/\bin\s*\([\'0x,\s\?]*\)/in (...)/gs; # Remove curor names in CURSOR and IN clauses $orig_query =~ s/\b(declare|in|deallocate|close)\s+"[^"]+"/$1 "..."/gs; # Normalise cursor name $orig_query =~ s/\bdeclare\s+[^"\s]+\s+cursor/declare "..." cursor/gs; $orig_query =~ s/\b(fetch\s+next\s+from)\s+[^\s]+/$1 "..."/gs; $orig_query =~ s/\b(deallocate|close)\s+[^"\s]+/$1 "..."/gs; # Remove any leading whitespace $orig_query =~ s/^\s+//; # Remove any trailing whitespace $orig_query =~ s/\s+$//; # Remove any whitespace before a semicolon $orig_query =~ s/\s+;/;/; # restore \' in string $orig_query =~ s/ESCPQUOTE/\\'/gis; $orig_query =~ s/ESCAPEME/\\/gis; return $orig_query; } sub anonymized_string { my ( $before, $original, $after, $cache ) = @_; # Prevent dates from being anonymized return $original if $original =~ m{\A\d\d\d\d[/:-]\d\d[/:-]\d\d\z}; return $original if $original =~ m{\A\d\d[/:-]\d\d[/:-]\d\d\d\d\z}; # Prevent dates format like DD/MM/YYYY HH24:MI:SS from being anonymized return $original if $original =~ m{ \A (?:FM|FX|TM)? (?: HH | HH12 | HH24 | MI | SS | MS | US | SSSS | AM | A\.M\. | am | a\.m\. | PM | P\.M\. | pm | p\.m\. | Y,YYY | YYYY | YYY | YY | Y | IYYY | IYY | IY | I | BC | B\.C\. | bc | b\.c\. | AD | A\.D\. | ad | a\.d\. | MONTH | Month | month | MON | Mon | mon | MM | DAY | Day | day | DY | Dy | dy | DDD | DD | D | W | WW | IW | CC | J | Q | RM | rm | TZ | tz | [\s/:-] )+ (?:TH|th|SP)? \z }; # Prevent interval from being anonymized return $original if ($before && ($before =~ /interval/i)); return $original if ($after && ($after =~ /^\)*::interval/i)); # Range of characters to use in anonymized strings my @chars = ( 'A' .. 'Z', 0 .. 9, 'a' .. 'z', '-', '_', '.' ); unless ( $cache->{ $original } ) { # Actual anonymized version generation $cache->{ $original } = join( '', map { $chars[ rand @chars ] } 1 .. 10 ); } return $cache->{ $original }; } sub anonymized_number { my ( $original, $cache ) = @_; # Range of number to use in anonymized strings my @numbers = ( 0 .. 9 ); unless ( $cache->{ $original } ) { # Actual anonymized version generation $cache->{ $original } = join( '', map { $numbers[ rand @numbers ] } 1 .. 4 ); } return $cache->{ $original }; } # Anonymize litteral in SQL queries by replacing parameters with fake values sub anonymize_query { my $orig_query = shift; return $orig_query if (!$orig_query || !$anonymize); # Variable to hold anonymized versions, so we can provide the same value # for the same input, within single query. my $anonymization_cache = {}; # Remove comments if (!$keep_comments) { $orig_query =~ s/\/\*(.*?)\*\///gs; } # Clean query $orig_query =~ s/\\'//g; $orig_query =~ s/('')+/\$EMPTYSTRING\$/g; # Anonymize each values $orig_query =~ s{ ([^\s\']+[\s\(]*) # before '([^']*)' # original ([\)]*::\w+)? # after }{$1 . "'" . anonymized_string($1, $2, $3, $anonymization_cache) . "'" . ($3||'')}xeg; $orig_query =~ s/\$EMPTYSTRING\$/''/gs; # obfuscate numbers too if this is not parameter indices ($1 ...) $anonymization_cache = {}; $orig_query =~ s{([^\$])\b(\d+)\b}{ $1 . anonymized_number($1, $anonymization_cache) }xeg; return $orig_query; } # Format numbers with comma for betterparam_cache reading sub comma_numbers { return 0 if ($#_ < 0); return 0 if (!$_[0]); my $text = reverse $_[0]; $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1$num_sep/g; return scalar reverse $text; } # Format numbers with comma for better reading sub pretty_print_size { my $val = shift; return 0 if (!$val); if ($val >= 1125899906842624) { $val = ($val / 1125899906842624); $val = sprintf("%0.2f", $val) . " PiB"; } elsif ($val >= 1099511627776) { $val = ($val / 1099511627776); $val = sprintf("%0.2f", $val) . " TiB"; } elsif ($val >= 1073741824) { $val = ($val / 1073741824); $val = sprintf("%0.2f", $val) . " GiB"; } elsif ($val >= 1048576) { $val = ($val / 1048576); $val = sprintf("%0.2f", $val) . " MiB"; } elsif ($val >= 1024) { $val = ($val / 1024); $val = sprintf("%0.2f", $val) . " KiB"; } else { $val = $val . " B"; } return $val; } # Format duration sub convert_time { my $time = shift; return '0ms' if (!$time); my $days = int($time / 86400000); $time -= ($days * 86400000); my $hours = int($time / 3600000); $time -= ($hours * 3600000); my $minutes = int($time / 60000); $time -= ($minutes * 60000); my $seconds = int($time / 1000); $time -= ($seconds * 1000); my $milliseconds = sprintf("%.3d", $time); $days = $days < 1 ? '' : $days . 'd'; $hours = $hours < 1 ? '' : $hours . 'h'; $minutes = $minutes < 1 ? '' : $minutes . 'm'; $seconds = $seconds < 1 ? '' : $seconds . 's'; $milliseconds = $milliseconds < 1 ? '' : $milliseconds . 'ms'; if ($days || $hours || $minutes) { $milliseconds = ''; } elsif ($seconds) { $milliseconds =~ s/\.\d+//; } $milliseconds =~ s/^[0]+// if ($milliseconds !~ /\./); $time = $days . $hours . $minutes . $seconds . $milliseconds; $time = '0ms' if ($time eq ''); return $time; } # Stores the top N queries generating the biggest temporary file sub set_top_tempfile_info { my ($curdb, $q, $sz, $date, $db, $user, $remote, $app, $info, $queryid) = @_; push(@{$top_tempfile_info{$curdb}}, [($sz, $date, $q, $db, $user, $remote, $app, $info, $queryid)]); my @tmp_top_tempfile_info = sort {$b->[0] <=> $a->[0]} @{$top_tempfile_info{$curdb}}; @{$top_tempfile_info{$curdb}} = (); for (my $i = 0; $i <= $#tmp_top_tempfile_info; $i++) { push(@{$top_tempfile_info{$curdb}}, $tmp_top_tempfile_info[$i]); last if ($i == $end_top); } } # Stores top N slowest sample queries sub set_top_prepare_bind_sample { my ($type, $q, $dt, $t, $param, $db, $user, $remote, $app, $queryid) = @_; return if ($sample <= 0); if ($type eq 'prepare') { $prepare_info{$db}{$q}{samples}{$dt}{query} = $q; $prepare_info{$db}{$q}{samples}{$dt}{date} = $t; $prepare_info{$db}{$q}{samples}{$dt}{db} = $db; $prepare_info{$db}{$q}{samples}{$dt}{user} = $user; $prepare_info{$db}{$q}{samples}{$dt}{remote} = $remote; $prepare_info{$db}{$q}{samples}{$dt}{app} = $app; $prepare_info{$db}{$q}{samples}{$dt}{queryid} = $queryid; $prepare_info{$db}{$q}{samples}{$dt}{params} = $param; my $i = 1; foreach my $k (sort {$b <=> $a} keys %{$prepare_info{$db}{$q}{samples}}) { if ($i > $sample) { delete $prepare_info{$db}{$q}{samples}{$k}; } $i++; } } if ($type eq 'bind') { $bind_info{$db}{$q}{samples}{$dt}{query} = $q; $bind_info{$db}{$q}{samples}{$dt}{date} = $t; $bind_info{$db}{$q}{samples}{$dt}{db} = $db; $bind_info{$db}{$q}{samples}{$dt}{user} = $user; $bind_info{$db}{$q}{samples}{$dt}{remote} = $remote; $bind_info{$db}{$q}{samples}{$dt}{app} = $app; $bind_info{$db}{$q}{samples}{$dt}{queryid} = $queryid; $bind_info{$db}{$q}{samples}{$dt}{params} = $param; my $i = 1; foreach my $k (sort {$b <=> $a} keys %{$bind_info{$db}{$q}{samples}}) { if ($i > $sample) { delete $bind_info{$db}{$q}{samples}{$k}; } $i++; } } } # Stores the top N queries cancelled sub set_top_cancelled_info { my ($curdb, $q, $sz, $date, $db, $user, $remote, $app, $queryid) = @_; push(@{$top_cancelled_info{$curdb}}, [($sz, $date, $q, $db, $user, $remote, $app, $queryid)]); my @tmp_top_cancelled_info = sort {$b->[0] <=> $a->[0]} @{$top_cancelled_info{$curdb}}; @{$top_cancelled_info{$curdb}} = (); for (my $i = 0; $i <= $#tmp_top_cancelled_info; $i++) { push(@{$top_cancelled_info{$curdb}}, $tmp_top_cancelled_info[$i]); last if ($i == $end_top); } } # Stores the top N queries waiting the most sub set_top_locked_info { my ($curdb, $q, $dt, $date, $db, $user, $remote, $app, $queryid) = @_; push(@{$top_locked_info{$curdb}}, [($dt, $date, $q, $db, $user, $remote, $app, $queryid)]); my @tmp_top_locked_info = sort {$b->[0] <=> $a->[0]} @{$top_locked_info{$curdb}}; @{$top_locked_info{$curdb}} = (); for (my $i = 0; $i <= $#tmp_top_locked_info; $i++) { push(@{$top_locked_info{$curdb}}, $tmp_top_locked_info[$i]); last if ($i == $end_top); } } # Stores the top N slowest queries sub set_top_slowest { my ($curdb, $q, $dt, $date, $db, $user, $remote, $app, $bind, $plan, $queryid) = @_; push(@{$top_slowest{$curdb}}, [($dt, $date, $q, $db, $user, $remote, $app, $bind, $plan, $queryid)]); my @tmp_top_slowest = sort {$b->[0] <=> $a->[0]} @{$top_slowest{$curdb}}; @{$top_slowest{$curdb}} = (); for (my $i = 0; $i <= $#tmp_top_slowest; $i++) { push(@{$top_slowest{$curdb}}, $tmp_top_slowest[$i]); last if ($i == $end_top); } } # Stores top N slowest sample queries sub set_top_sample { my ($curdb, $norm, $q, $dt, $date, $db, $user, $remote, $app, $bind, $plan, $queryid, $lfile) = @_; return if (!$norm || !$q || $sample <= 0); $normalyzed_info{$curdb}{$norm}{samples}{$dt}{query} = $q; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{date} = $date; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{db} = $db; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{user} = $user; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{remote} = $remote; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{app} = $app; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{bind} = $bind; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{plan} = $plan; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{queryid} = $queryid; $normalyzed_info{$curdb}{$norm}{samples}{$dt}{logfile} = $lfile; my $i = 1; foreach my $k (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$norm}{samples}}) { if ($i > $sample) { delete $normalyzed_info{$curdb}{$norm}{samples}{$k}; } $i++; } } # Stores top N error sample queries sub set_top_error_sample { my ($curdb, $q, $date, $real_error, $detail, $context, $statement, $hint, $db, $user, $app, $remote, $sqlstate, $queryid, $lfile) = @_; $errors_code{$curdb}{$sqlstate}++ if ($sqlstate); # Stop when we have our number of samples if (!exists $error_info{$curdb}{$q}{date} || ($#{$error_info{$curdb}{$q}{date}}+1 < $sample)) { # if ( $q =~ /deadlock detected/ || (!$statement && $detail && !grep(/^\Q$detail\E$/, @{$error_info{$curdb}{$q}{detail}})) || ($statement && !grep(/^\Q$statement\E$/, @{$error_info{$curdb}{$q}{statement}})) ) { push(@{$error_info{$curdb}{$q}{date}}, $date); push(@{$error_info{$curdb}{$q}{detail}}, $detail); push(@{$error_info{$curdb}{$q}{context}}, $context); push(@{$error_info{$curdb}{$q}{statement}}, $statement); push(@{$error_info{$curdb}{$q}{hint}}, $hint); push(@{$error_info{$curdb}{$q}{error}}, $real_error); push(@{$error_info{$curdb}{$q}{db}}, $db); push(@{$error_info{$curdb}{$q}{user}}, $user); push(@{$error_info{$curdb}{$q}{app}}, $app); push(@{$error_info{$curdb}{$q}{remote}}, $remote); push(@{$error_info{$curdb}{$q}{sqlstate}}, $sqlstate); push(@{$error_info{$curdb}{$q}{queryid}}, $queryid); push(@{$error_info{$curdb}{$q}{logfile}}, $lfile); } } } # Stores top N error sample from pgbouncer log sub pgb_set_top_error_sample { my ($q, $date, $real_error, $db, $user, $remote) = @_; # Stop when we have our number of samples if (!exists $pgb_error_info{$q}{date} || ($#{$pgb_error_info{$q}{date}} < $sample)) { push(@{$pgb_error_info{$q}{date}}, $date); push(@{$pgb_error_info{$q}{error}}, $real_error); push(@{$pgb_error_info{$q}{db}}, $db); push(@{$pgb_error_info{$q}{user}}, $user); push(@{$pgb_error_info{$q}{remote}}, $remote); } } sub get_log_limit { my $curdb = shift(); $overall_stat{$curdb}{'first_log_ts'} =~ /^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/; my ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s); if (!$log_timezone) { ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s) = ($1, $2, $3, $4, $5, $6); } else { ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s) = change_timezone($1, $2, $3, $4, $5, $6); } my $t_log_min = "$t_y-$t_mo-$t_d $t_h:$t_mi:$t_s"; $overall_stat{$curdb}{'last_log_ts'} =~ /^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/; if (!$log_timezone) { ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s) = ($1, $2, $3, $4, $5, $6); } else { ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s) = change_timezone($1, $2, $3, $4, $5, $6); } my $t_log_max = "$t_y-$t_mo-$t_d $t_h:$t_mi:$t_s"; return ($t_log_min, $t_log_max); } sub dump_as_text { my $curdb = shift; # Global information my $curdate = localtime(time); my $fmt_nlines = &comma_numbers($overall_stat{nlines}{$curdb}); my $total_time = timestr($td); $total_time =~ s/^([\.0-9]+) wallclock.*/$1/; $total_time = &convert_time($total_time * 1000); my $logfile_str = $log_files[0]; if ($#log_files > 0) { $logfile_str .= ', ..., ' . $log_files[-1]; } # Set logs limits my ($t_log_min, $t_log_max) = get_log_limit($curdb); print $fh qq{ pgBadger :: $report_title - Global information --------------------------------------------------- Generated on $curdate Log file: $logfile_str Parsed $fmt_nlines log entries in $total_time Log start from $t_log_min to $t_log_max }; # Dump normalized queries only if requested if ($dump_normalized_only) { if (!$query_numbering) { print $fh "Count\t\tQuery\n"; print $fh '-'x70,"\n"; } else { print $fh "#Num\tCount\t\tQuery\n"; print $fh '-'x80,"\n"; } foreach my $q (sort { $normalyzed_info{$curdb}{$b}{count} <=> $normalyzed_info{$curdb}{$a}{count} } keys %{$normalyzed_info{$curdb}}) { print $fh $query_numbering++, "\t" if ($query_numbering); print $fh "$normalyzed_info{$curdb}{$q}{count}\t$q\n"; } print $fh "\n\n"; print $fh "Report generated by pgBadger $VERSION ($project_url).\n"; return; } # Overall statistics my $fmt_unique = &comma_numbers(scalar keys %{$normalyzed_info{$curdb}}); my $fmt_queries = &comma_numbers($overall_stat{$curdb}{'queries_number'}); my $fmt_duration = &convert_time($overall_stat{$curdb}{'queries_duration'}{'execute'}+($overall_stat{$curdb}{'queries_duration'}{'prepare'}||0)+($overall_stat{$curdb}{'queries_duration'}{'bind'}||0)); $overall_stat{$curdb}{'first_query_ts'} ||= '-'; $overall_stat{$curdb}{'last_query_ts'} ||= '-'; print $fh qq{ - Overall statistics --------------------------------------------------- Number of unique normalized queries: $fmt_unique Number of queries: $fmt_queries Total query duration: $fmt_duration First query: $overall_stat{$curdb}{'first_query_ts'} Last query: $overall_stat{$curdb}{'last_query_ts'} }; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{query} <=> $overall_stat{$curdb}{'peak'}{$a}{query}} keys %{$overall_stat{$curdb}{'peak'}}) { print $fh "Query peak: ", &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{query}), " queries/s at $_"; last; } if (!$disable_error) { my $fmt_errors = &comma_numbers($overall_stat{$curdb}{'errors_number'}); my $fmt_unique_error = &comma_numbers(scalar keys %{$error_info{$curdb}}); print $fh qq{ Number of events: $fmt_errors Number of unique normalized events: $fmt_unique_error }; } if ($tempfile_info{$curdb}{count}) { my $fmt_temp_maxsise = &pretty_print_size($tempfile_info{$curdb}{maxsize}); my $fmt_temp_avsize = &pretty_print_size(sprintf("%.2f", ($tempfile_info{$curdb}{size} / $tempfile_info{$curdb}{count}))); print $fh qq{Number temporary files: $tempfile_info{$curdb}{count} Max size of temporary files: $fmt_temp_maxsise Average size of temporary files: $fmt_temp_avsize }; } if ($cancelled_info{$curdb}{count}) { print $fh qq{Number cancelled queries: $cancelled_info{$curdb}{count} }; } if (!$disable_session && $session_info{$curdb}{count}) { my $avg_session_duration = &convert_time($session_info{$curdb}{duration} / $session_info{$curdb}{count}); my $tot_session_duration = &convert_time($session_info{$curdb}{duration}); my $avg_queries = &comma_numbers(int($overall_stat{$curdb}{'queries_number'}/$session_info{$curdb}{count})); my $q_duration = $overall_stat{$curdb}{'queries_duration'}{'execute'}+($overall_stat{$curdb}{'queries_duration'}{'prepare'}||0)+($overall_stat{$curdb}{'queries_duration'}{'bind'}||0); my $avg_duration = &convert_time(int($q_duration/$session_info{$curdb}{count})); my $avg_idle_time = &convert_time( ($session_info{$curdb}{duration} - $q_duration) / ($session_info{$curdb}{count} || 1) ); $avg_idle_time = 'n/a' if (!$session_info{$curdb}{count}); print $fh qq{Total number of sessions: $session_info{$curdb}{count} Total duration of sessions: $tot_session_duration Average duration of sessions: $avg_session_duration Average queries per sessions: $avg_queries Average queries duration per sessions: $avg_duration Average idle time per session: $avg_idle_time }; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{session} <=> $overall_stat{$curdb}{'peak'}{$a}{session}} keys %{$overall_stat{$curdb}{'peak'}}) { next if (!$session_info{$curdb}{count}); print $fh "Session peak: ", &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{session}), " sessions at $_"; last; } } if (!$disable_connection && $connection_info{$curdb}{count}) { print $fh "Total number of connections: $connection_info{$curdb}{count}\n"; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{connection} <=> $overall_stat{$curdb}{'peak'}{$a}{connection}} keys %{$overall_stat{$curdb}{'peak'}}) { if ($overall_stat{$curdb}{'peak'}{$_}{connection} > 0) { print $fh "Connection peak: ", &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{connection}), " conn/s at $_"; } last; } } if (scalar keys %{$database_info{$curdb}} > 1) { print $fh "Total number of databases: ", scalar keys %{$database_info{$curdb}}, "\n"; } if (!$disable_hourly && $overall_stat{$curdb}{'queries_number'}) { print $fh qq{ - Hourly statistics ---------------------------------------------------- Report not supported by text format }; } # INSERT/DELETE/UPDATE/SELECT repartition my $totala = 0; foreach my $a (@SQL_ACTION) { $totala += $overall_stat{$curdb}{lc($a)}; } if (!$disable_type && $totala) { my $total = $overall_stat{$curdb}{'queries_number'} || 1; print $fh "\n- Queries by type ------------------------------------------------------\n\n"; print $fh "Type Count Percentage\n"; foreach my $a (@SQL_ACTION) { print $fh "$a: ", &comma_numbers($overall_stat{$curdb}{lc($a)}), " ", sprintf("%0.2f", ($overall_stat{$curdb}{lc($a)} * 100) / $total), "%\n"; } print $fh "OTHERS: ", &comma_numbers($total - $totala), " ", sprintf("%0.2f", (($total - $totala) * 100) / $total), "%\n" if (($total - $totala) > 0); print $fh "\n"; # Show request per database statistics if (scalar keys %{$database_info{$curdb}} > 1) { print $fh "\n- Request per database ------------------------------------------------------\n\n"; print $fh "Database Request type Count Duration\n"; foreach my $d (sort keys %{$database_info{$curdb}}) { print $fh "$d - ", &comma_numbers($database_info{$curdb}{$d}{count}), " ", &convert_time($database_info{$curdb}{$d}{duration}), "\n"; foreach my $r (sort keys %{$database_info{$curdb}{$d}}) { next if (($r eq 'count') || ($r =~ /duration/)); print $fh "\t$r ", &comma_numbers($database_info{$curdb}{$d}{$r}), " ", &convert_time($database_info{$curdb}{$d}{"$r|duration"}), "\n"; } } } # Show request per application statistics if (scalar keys %application_info > 1) { print $fh "\n- Request per application ------------------------------------------------------\n\n"; print $fh "Application Request type Count Duration\n"; foreach my $d (sort keys %{$application_info{$curdb}}) { print $fh "$d - ", &comma_numbers($application_info{$curdb}{$d}{count}), " ", &convert_time($application_info{$curdb}{$d}{duration}), "\n"; foreach my $r (sort keys %{$application_info{$curdb}{$d}}) { next if (($r eq 'count') || ($r =~ /duration/)); print $fh "\t$r ", &comma_numbers($application_info{$curdb}{$d}{$r}), " ", &convert_time($application_info{$curdb}{$d}{"$r|duration"}), "\n"; } } } # Show request per user statistics if (scalar keys %{$user_info{$curdb}} > 1) { print $fh "\n- Request per user ------------------------------------------------------\n\n"; print $fh "User Request type Count duration\n"; foreach my $d (sort keys %{$user_info{$curdb}}) { print $fh "$d - ", &comma_numbers($user_info{$curdb}{$d}{count}), " ", &convert_time($user_info{$curdb}{$d}{duration}), "\n"; foreach my $r (sort keys %{$user_info{$curdb}{$d}}) { next if (($r eq 'count') || ($r =~ /duration/)); print $fh "\t$r ", &comma_numbers($user_info{$curdb}{$d}{$r}), " ", &convert_time($user_info{$curdb}{$d}{"$r|duration"}), "\n"; } } } # Show request per host statistics if (scalar keys %{$host_info{$curdb}} > 1) { print $fh "\n- Request per host ------------------------------------------------------\n\n"; print $fh "Host Request type Count Duration\n"; foreach my $d (sort keys %{$host_info{$curdb}}) { print $fh "$d - ", &comma_numbers($host_info{$curdb}{$d}{count}), " ", &convert_time($host_info{$curdb}{$d}{duration}), "\n"; foreach my $r (sort keys %{$host_info{$curdb}{$d}}) { next if (($r eq 'count') || ($r =~ /duration/)); print $fh "\t$r ", &comma_numbers($host_info{$curdb}{$d}{$r}), " ", &convert_time($host_info{$curdb}{$d}{"$r|duration"}), "\n"; } } } } if (!$disable_lock && scalar keys %{$lock_info{$curdb}} > 0) { print $fh "\n- Locks by type ------------------------------------------------------\n\n"; print $fh "Type Object Count Total Duration Avg duration (s)\n"; my $total_count = 0; my $total_duration = 0; foreach my $t (sort keys %{$lock_info{$curdb}}) { print $fh "$t\t\t", &comma_numbers($lock_info{$curdb}{$t}{count}), " ", &convert_time($lock_info{$curdb}{$t}{duration}), " ", &convert_time($lock_info{$curdb}{$t}{duration} / $lock_info{$curdb}{$t}{count}), "\n"; foreach my $o (sort keys %{$lock_info{$curdb}{$t}}) { next if (($o eq 'count') || ($o eq 'duration') || ($o eq 'chronos')); print $fh "\t$o\t", &comma_numbers($lock_info{$curdb}{$t}{$o}{count}), " ", &convert_time($lock_info{$curdb}{$t}{$o}{duration}), " ", &convert_time($lock_info{$curdb}{$t}{$o}{duration} / $lock_info{$curdb}{$t}{$o}{count}), "\n"; } $total_count += $lock_info{$curdb}{$t}{count}; $total_duration += $lock_info{$curdb}{$t}{duration}; } print $fh "Total:\t\t\t", &comma_numbers($total_count), " ", &convert_time($total_duration), " ", &convert_time($total_duration / ($total_count || 1)), "\n"; } # Show session per database statistics if (!$disable_session && exists $session_info{$curdb}{database}) { print $fh "\n- Sessions per database ------------------------------------------------------\n\n"; print $fh "Database Count Total Duration Avg duration (s)\n"; foreach my $d (sort keys %{$session_info{$curdb}{database}}) { print $fh "$d - ", &comma_numbers($session_info{$curdb}{database}{$d}{count}), " ", &convert_time($session_info{$curdb}{database}{$d}{duration}), " ", &convert_time($session_info{$curdb}{database}{$d}{duration} / $session_info{$curdb}{database}{$d}{count}), "\n"; } } # Show session per user statistics if (!$disable_session && exists $session_info{$curdb}{user}) { print $fh "\n- Sessions per user ------------------------------------------------------\n\n"; print $fh "User Count Total Duration Avg duration (s)\n"; foreach my $d (sort keys %{$session_info{$curdb}{user}}) { print $fh "$d - ", &comma_numbers($session_info{$curdb}{user}{$d}{count}), " ", &convert_time($session_info{$curdb}{user}{$d}{duration}), " ", &convert_time($session_info{$curdb}{user}{$d}{duration} / $session_info{$curdb}{user}{$d}{count}), "\n"; } } # Show session per host statistics if (!$disable_session && exists $session_info{$curdb}{host}) { print $fh "\n- Sessions per host ------------------------------------------------------\n\n"; print $fh "User Count Total Duration Avg duration (s)\n"; foreach my $d (sort keys %{$session_info{$curdb}{host}}) { print $fh "$d - ", &comma_numbers($session_info{$curdb}{host}{$d}{count}), " ", &convert_time($session_info{$curdb}{host}{$d}{duration}), " ", &convert_time($session_info{$curdb}{host}{$d}{duration} / $session_info{$curdb}{host}{$d}{count}), "\n"; } } # Show session per application statistics if (!$disable_session && exists $session_info{$curdb}{app}) { print $fh "\n- Sessions per application ------------------------------------------------------\n\n"; print $fh "Application Count Total Duration Avg duration (s)\n"; foreach my $d (sort keys %{$session_info{$curdb}{app}}) { print $fh "$d - ", &comma_numbers($session_info{$curdb}{app}{$d}{count}), " ", &convert_time($session_info{$curdb}{app}{$d}{duration}), " ", &convert_time($session_info{$curdb}{app}{$d}{duration} / $session_info{$curdb}{app}{$d}{count}), "\n"; } } # Show connection per database statistics if (!$disable_connection && exists $connection_info{$curdb}{database}) { print $fh "\n- Connections per database ------------------------------------------------------\n\n"; print $fh "Database User Count\n"; foreach my $d (sort keys %{$connection_info{$curdb}{database}}) { print $fh "$d - ", &comma_numbers($connection_info{$curdb}{database}{$d}), "\n"; foreach my $u (sort keys %{$connection_info{$curdb}{user}}) { next if (!exists $connection_info{$curdb}{database_user}{$d}{$u}); print $fh "\t$u ", &comma_numbers($connection_info{$curdb}{database_user}{$d}{$u}), "\n"; } } print $fh "\nDatabase Host Count\n"; foreach my $d (sort keys %{$connection_info{$curdb}{database}}) { print $fh "$d - ", &comma_numbers($connection_info{$curdb}{database}{$d}), "\n"; foreach my $u (sort keys %{$connection_info{$curdb}{host}}) { next if (!exists $connection_info{$curdb}{database_host}{$d}{$u}); print $fh "\t$u ", &comma_numbers($connection_info{$curdb}{database_host}{$d}{$u}), "\n"; } } } # Show connection per user statistics if (!$disable_connection && exists $connection_info{$curdb}{user}) { print $fh "\n- Connections per user ------------------------------------------------------\n\n"; print $fh "User Count\n"; foreach my $d (sort keys %{$connection_info{$curdb}{user}}) { print $fh "$d - ", &comma_numbers($connection_info{$curdb}{user}{$d}), "\n"; } } # Show connection per host statistics if (!$disable_connection && exists $connection_info{$curdb}{host}) { print $fh "\n- Connections per host ------------------------------------------------------\n\n"; print $fh "Host Count\n"; foreach my $d (sort keys %{$connection_info{$curdb}{host}}) { print $fh "$d - ", &comma_numbers($connection_info{$curdb}{host}{$d}), "\n"; } } # Show lock wait detailed information if (!$disable_lock && scalar keys %{$lock_info{$curdb}} > 0) { my @top_locked_queries = (); foreach my $h (keys %{$normalyzed_info{$curdb}}) { if (exists($normalyzed_info{$curdb}{$h}{locks})) { push (@top_locked_queries, [$h, $normalyzed_info{$curdb}{$h}{locks}{count}, $normalyzed_info{$curdb}{$h}{locks}{wait}, $normalyzed_info{$curdb}{$h}{locks}{minwait}, $normalyzed_info{$curdb}{$h}{locks}{maxwait}]); } } # Most frequent waiting queries (N) @top_locked_queries = sort {$b->[2] <=> $a->[2]} @top_locked_queries; print $fh "\n- Most frequent waiting queries (N) -----------------------------------------\n\n"; print $fh "Rank Count Total wait time (s) Min/Max/Avg duration (s) Query\n"; for (my $i = 0 ; $i <= $#top_locked_queries; $i++) { last if ($i > $end_top); print $fh ($i + 1), ") ", $top_locked_queries[$i]->[1], " - ", &convert_time($top_locked_queries[$i]->[2]), " - ", &convert_time($top_locked_queries[$i]->[3]), "/", &convert_time($top_locked_queries[$i]->[4]), "/", &convert_time(($top_locked_queries[$i]->[2] / $top_locked_queries[$i]->[1])), " - ", $top_locked_queries[$i]->[0], "\n"; print $fh "--\n"; my $k = $top_locked_queries[$i]->[0]; my $j = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($j > $sample); my $ttl = $top_locked_queries[$i]->[1] || ''; my $db = ''; $db .= " - $normalyzed_info{$curdb}{$k}{samples}{$d}{date} - database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $db .= ", user: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $db .= ", remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $db .= ", app: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $db .= ", queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $db .= ", bind query: yes" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $db .= ", log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); $db =~ s/^, / - /; print $fh "\t- Example $j: ", &convert_time($d), "$db - ", &anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query}), "\n"; $j++; } } print $fh "\n"; @top_locked_queries = (); # Queries that waited the most @{$top_locked_info{$curdb}} = sort {$b->[1] <=> $a->[1]} @{$top_locked_info{$curdb}}; print $fh "\n- Queries that waited the mosts ---------------------------------------------\n\n"; print $fh "Rank Wait time (s) Query\n"; for (my $i = 0 ; $i <= $#{$top_locked_info{$curdb}} ; $i++) { my $ttl = $top_locked_info{$curdb}[$i]->[1] || ''; my $db = ''; $db .= " - database: $top_locked_info{$curdb}[$i]->[3]" if ($top_locked_info{$curdb}[$i]->[3]); $db .= ", user: $top_locked_info{$curdb}[$i]->[4]" if ($top_locked_info{$curdb}[$i]->[4]); $db .= ", remote: $top_locked_info{$curdb}[$i]->[5]" if ($top_locked_info{$curdb}[$i]->[5]); $db .= ", app: $top_locked_info{$curdb}[$i]->[6]" if ($top_locked_info{$curdb}[$i]->[6]); $db .= ", queryid: $top_locked_info{$curdb}[$i]->[7]" if ($top_locked_info{$curdb}[$i]->[7]); $db =~ s/^, / - /; print $fh ($i + 1), ") ", &convert_time($top_locked_info{$curdb}[$i]->[0]), " $ttl$db - ", &anonymize_query($top_locked_info{$curdb}[$i]->[2]), "\n"; print $fh "--\n"; } print $fh "\n"; } # Show temporary files detailed information if (!$disable_temporary && scalar keys %{$tempfile_info{$curdb}} > 0) { my @top_temporary = (); foreach my $h (keys %{$normalyzed_info{$curdb}}) { if (exists($normalyzed_info{$curdb}{$h}{tempfiles})) { push (@top_temporary, [$h, $normalyzed_info{$curdb}{$h}{tempfiles}{count}, $normalyzed_info{$curdb}{$h}{tempfiles}{size}, $normalyzed_info{$curdb}{$h}{tempfiles}{minsize}, $normalyzed_info{$curdb}{$h}{tempfiles}{maxsize}]); } } # Queries generating the most temporary files (N) @top_temporary = sort {$b->[1] <=> $a->[1]} @top_temporary; print $fh "\n- Queries generating the most temporary files (N) ---------------------------\n\n"; print $fh "Rank Count Total size Min/Max/Avg size Query\n"; my $idx = 1; for (my $i = 0 ; $i <= $#top_temporary ; $i++) { last if ($i > $end_top); print $fh $idx, ") ", $top_temporary[$i]->[1], " - ", &comma_numbers($top_temporary[$i]->[2]), " - ", &comma_numbers($top_temporary[$i]->[3]), "/", &comma_numbers($top_temporary[$i]->[4]), "/", &comma_numbers(sprintf("%.2f", $top_temporary[$i]->[2] / $top_temporary[$i]->[1])), " - ", &anonymize_query($top_temporary[$i]->[0]), "\n"; print $fh "--\n"; my $k = $top_temporary[$i]->[0]; if (scalar keys %{$normalyzed_info{$curdb}{$k}{samples}}) { my $j = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($j > $sample); my $db = ''; $db .= "$normalyzed_info{$curdb}{$k}{samples}{$d}{date} - database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $db .= ", user: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $db .= ", remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $db .= ", app: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $db .= ", queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $db .= ", bind query: yes" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $db .= ", log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); $db =~ s/^, / - /; print $fh "\t- Example $j: ", &convert_time($d), " - $db - ", &anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query}), "\n"; $j++; } } $idx++; } @top_temporary = (); # Top queries generating the largest temporary files @{$top_tempfile_info{$curdb}} = sort {$b->[1] <=> $a->[1]} @{$top_tempfile_info{$curdb}}; print $fh "\n- Queries generating the largest temporary files ----------------------------\n\n"; print $fh "Rank Size Query\n"; for (my $i = 0 ; $i <= $#{$top_tempfile_info{$curdb}} ; $i++) { my $ttl = $top_tempfile_info{$curdb}[$i]->[1] || ''; my $db = ''; $db .= " - database: $top_tempfile_info{$curdb}[$i]->[3]" if ($top_tempfile_info{$curdb}[$i]->[3]); $db .= ", user: $top_tempfile_info{$curdb}[$i]->[4]" if ($top_tempfile_info{$curdb}[$i]->[4]); $db .= ", remote: $top_tempfile_info{$curdb}[$i]->[5]" if ($top_tempfile_info{$curdb}[$i]->[5]); $db .= ", app: $top_tempfile_info{$curdb}[$i]->[6]" if ($top_tempfile_info{$curdb}[$i]->[6]); $db .= ", info: $top_tempfile_info{$curdb}[$i]->[7]" if ($top_tempfile_info{$curdb}[$i]->[7]); $db .= ", queryid: $top_tempfile_info{$curdb}[$i]->[8]" if ($top_tempfile_info{$curdb}[$i]->[8]); $db =~ s/^, / - /; print $fh ($i + 1), ") ", &comma_numbers($top_tempfile_info{$curdb}[$i]->[0]), " - $ttl$db - ", &anonymize_query($top_tempfile_info{$curdb}[$i]->[2]), "\n"; } print $fh "\n"; } # Show cancelled queries detailed information if (!$disable_query && scalar keys %{$cancelled_info{$curdb}} > 0) { my @top_cancelled = (); foreach my $h (keys %{$normalyzed_info{$curdb}}) { if (exists($normalyzed_info{$curdb}{$h}{cancelled})) { push (@top_cancelled, [$h, $normalyzed_info{$curdb}{$h}{cancelled}{count}]); } } # Queries generating the most cancelled files (N) @top_cancelled = sort {$b->[1] <=> $a->[1]} @top_cancelled; print $fh "\n- Queries most cancelled (N) ---------------------------\n\n"; print $fh "Rank Count Query\n"; my $idx = 1; for (my $i = 0 ; $i <= $#top_cancelled ; $i++) { last if ($i > $end_top); print $fh $idx, ") ", $top_cancelled[$i]->[1], " - ", $top_cancelled[$i]->[0], "\n"; print $fh "--\n"; my $k = $top_cancelled[$i]->[0]; if (scalar keys %{$normalyzed_info{$curdb}{$k}{samples}}) { my $j = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($j > $sample); my $db = ''; $db .= "$normalyzed_info{$curdb}{$k}{samples}{$d}{date} - database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}" if ($normalyzed_info{$k}{samples}{$d}{db}); $db .= ", user: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $db .= ", remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $db .= ", app: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $db .= ", queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $db .= ", bind query: yes" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $db .= ", log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); $db =~ s/^, / - /; print $fh "\t- Example $j: ", &convert_time($d), " - $db - ", &anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query}), "\n"; $j++; } } $idx++; } @top_cancelled = (); # Top queries generating the largest cancelled files @{$top_cancelled_info{$curdb}} = sort {$b->[1] <=> $a->[1]} @{$top_cancelled_info{$curdb}}; print $fh "\n- Queries generating the most cancellation ----------------------------\n\n"; print $fh "Rank Times cancelled Query\n"; for (my $i = 0 ; $i <= $#{$top_cancelled_info{$curdb}} ; $i++) { my $ttl = $top_cancelled_info{$curdb}[$i]->[1] || ''; my $db = ''; $db .= " - database: $top_cancelled_info{$curdb}[$i]->[3]" if ($top_cancelled_info{$curdb}[$i]->[3]); $db .= ", user: $top_cancelled_info{$curdb}[$i]->[4]" if ($top_cancelled_info{$curdb}[$i]->[4]); $db .= ", remote: $top_cancelled_info{$curdb}[$i]->[5]" if ($top_cancelled_info{$curdb}[$i]->[5]); $db .= ", app: $top_cancelled_info{$curdb}[$i]->[6]" if ($top_cancelled_info{$curdb}[$i]->[6]); $db .= ", queryid: $top_cancelled_info{$curdb}[$i]->[7]" if ($top_cancelled_info{$curdb}[$i]->[7]); $db =~ s/^, / - /; print $fh ($i + 1), ") ", &comma_numbers($top_cancelled_info{$curdb}[$i]->[0]), " - $ttl$db - ", &anonymize_query($top_cancelled_info{$curdb}[$i]->[2]), "\n"; } print $fh "\n"; } # Show top information if (!$disable_query && ($#{$top_slowest{$curdb}} >= 0)) { print $fh "\n- Slowest queries ------------------------------------------------------\n\n"; print $fh "Rank Duration (s) Query\n"; for (my $i = 0 ; $i <= $#{$top_slowest{$curdb}} ; $i++) { my $db = ''; $db .= " database: $top_slowest{$curdb}[$i]->[3]" if ($top_slowest{$curdb}[$i]->[3]); $db .= ", user: $top_slowest{$curdb}[$i]->[4]" if ($top_slowest{$curdb}[$i]->[4]); $db .= ", remote: $top_slowest{$curdb}[$i]->[5]" if ($top_slowest{$curdb}[$i]->[5]); $db .= ", app: $top_slowest{$curdb}[$i]->[6]" if ($top_slowest{$curdb}[$i]->[6]); $db .= ", queryid: $top_slowest{$curdb}[$i]->[9]" if ($top_slowest{$curdb}[$i]->[9]); $db .= ", bind query: yes" if ($top_slowest{$curdb}[$i]->[7]); $db =~ s/^, //; print $fh $i + 1, ") " . &convert_time($top_slowest{$curdb}[$i]->[0]) . "$db - " . &anonymize_query($top_slowest{$curdb}[$i]->[2]) . "\n"; print $fh "--\n"; } print $fh "\n- Queries that took up the most time (N) -------------------------------\n\n"; print $fh "Rank Total duration Times executed Min/Max/Avg duration (s) Query\n"; my $idx = 1; foreach my $k (sort {$normalyzed_info{$curdb}{$b}{duration} <=> $normalyzed_info{$curdb}{$a}{duration}} keys %{$normalyzed_info{$curdb}}) { next if (!$normalyzed_info{$curdb}{$k}{count}); last if ($idx > $top); my $q = $k; if ($normalyzed_info{$curdb}{$k}{count} == 1) { foreach (keys %{$normalyzed_info{$curdb}{$k}{samples}}) { $q = $normalyzed_info{$curdb}{$k}{samples}{$_}{query}; last; } } $q = &anonymize_query($q); $normalyzed_info{$curdb}{$k}{average} = $normalyzed_info{$curdb}{$k}{duration} / $normalyzed_info{$curdb}{$k}{count}; print $fh "$idx) " . &convert_time($normalyzed_info{$curdb}{$k}{duration}) . " - " . &comma_numbers($normalyzed_info{$curdb}{$k}{count}) . " - " . &convert_time($normalyzed_info{$curdb}{$k}{min}) . "/" . &convert_time($normalyzed_info{$curdb}{$k}{max}) . "/" . &convert_time($normalyzed_info{$curdb}{$k}{average}) . " - $q\n"; print $fh "--\n"; my $j = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($j > $sample); my $db = ''; $db .= " - database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $db .= ", user: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $db .= ", remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $db .= ", app: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $db .= ", queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $db .= ", bind query: yes" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $db .= ", log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); $db =~ s/^, / - /; print $fh "\t- Example $j: ", &convert_time($d), "$db - ", &anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query}), "\n"; $j++; } $idx++; } } if (!$disable_query && (scalar keys %{$normalyzed_info{$curdb}} > 0)) { print $fh "\n- Most frequent queries (N) --------------------------------------------\n\n"; print $fh "Rank Times executed Total duration Min/Max/Avg duration (s) Query\n"; my $idx = 1; foreach my $k (sort {$normalyzed_info{$curdb}{$b}{count} <=> $normalyzed_info{$curdb}{$a}{count}} keys %{$normalyzed_info{$curdb}}) { next if (!$normalyzed_info{$curdb}{$k}{count}); last if ($idx > $top); my $q = $k; if ($normalyzed_info{$curdb}{$k}{count} == 1) { foreach (keys %{$normalyzed_info{$curdb}{$k}{samples}}) { $q = $normalyzed_info{$curdb}{$k}{samples}{$_}{query}; last; } } $q = &anonymize_query($q); print $fh "$idx) " . &comma_numbers($normalyzed_info{$curdb}{$k}{count}) . " - " . &convert_time($normalyzed_info{$curdb}{$k}{duration}) . " - " . &convert_time($normalyzed_info{$curdb}{$k}{min}) . "/" . &convert_time($normalyzed_info{$curdb}{$k}{max}) . "/" . &convert_time($normalyzed_info{$curdb}{$k}{duration} / $normalyzed_info{$curdb}{$k}{count}) . " - $q\n"; print $fh "--\n"; my $i = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($i > $sample); my $db = ''; $db .= " - database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $db .= ", user: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $db .= ", remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $db .= ", app: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $db .= ", queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $db .= ", bind query: yes" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $db .= ", log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); $db =~ s/^, / - /; print $fh "\tExample $i: ", &convert_time($d), "$db - ", &anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query}), "\n"; $i++; } $idx++; } } if (!$disable_query && ($#{$top_slowest{$curdb}} >= 0)) { print $fh "\n- Slowest queries (N) --------------------------------------------------\n\n"; print $fh "Rank Min/Max/Avg duration (s) Times executed Total duration Query\n"; my $idx = 1; foreach my $k (sort {$normalyzed_info{$curdb}{$b}{average} <=> $normalyzed_info{$curdb}{$a}{average}} keys %{$normalyzed_info{$curdb}}) { next if (!$normalyzed_info{$curdb}{$k}{count}); last if ($idx > $top); my $q = $k; if ($normalyzed_info{$curdb}{$k}{count} == 1) { foreach (keys %{$normalyzed_info{$curdb}{$k}{samples}}) { $q = $normalyzed_info{$curdb}{$k}{samples}{$_}{query}; last; } } $q = &anonymize_query($q); print $fh "$idx) " . &convert_time($normalyzed_info{$curdb}{$k}{min}) . "/" . &convert_time($normalyzed_info{$curdb}{$k}{max}) . "/" . &convert_time($normalyzed_info{$curdb}{$k}{average}) . " - " . &comma_numbers($normalyzed_info{$curdb}{$k}{count}) . " - " . &convert_time($normalyzed_info{$curdb}{$k}{duration}) . " - $q\n"; print $fh "--\n"; my $i = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($i > $sample); my $db = ''; $db .= " - database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $db .= ", user: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $db .= ", remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $db .= ", app: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $db .= ", queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $db .= ", bind query: yes" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{yes}); $db .= ", log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); $db =~ s/^, / - /; print $fh "\tExample $i: ", &convert_time($d), "$db - ", &anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query}), "\n"; $i++; } $idx++; } } @{$top_slowest{$curdb}} = (); if (!$disable_error) { &show_error_as_text($curdb); } # Show pgbouncer session per database statistics if (exists $pgb_session_info{database}) { print $fh "\n- pgBouncer sessions per database --------------------------------------------\n\n"; print $fh "Database Count Total Duration Avg duration (s)\n"; foreach my $d (sort keys %{$pgb_session_info{database}}) { print $fh "$d - ", &comma_numbers($pgb_session_info{database}{$d}{count}), " ", &convert_time($pgb_session_info{database}{$d}{duration}), " ", &convert_time($pgb_session_info{database}{$d}{duration} / $pgb_session_info{database}{$d}{count}), "\n"; } } # Show pgbouncer session per user statistics if (exists $pgb_session_info{user}) { print $fh "\n- pgBouncer sessions per user ------------------------------------------------\n\n"; print $fh "User Count Total Duration Avg duration (s)\n"; foreach my $d (sort keys %{$pgb_session_info{user}}) { print $fh "$d - ", &comma_numbers($pgb_session_info{user}{$d}{count}), " ", &convert_time($pgb_session_info{user}{$d}{duration}), " ", &convert_time($pgb_session_info{user}{$d}{duration} / $pgb_session_info{user}{$d}{count}), "\n"; } } # Show pgbouncer session per host statistics if (exists $pgb_session_info{host}) { print $fh "\n- pgBouncer sessions per host ------------------------------------------------\n\n"; print $fh "User Count Total Duration Avg duration (s)\n"; foreach my $d (sort keys %{$pgb_session_info{host}}) { print $fh "$d - ", &comma_numbers($pgb_session_info{host}{$d}{count}), " ", &convert_time($pgb_session_info{host}{$d}{duration}), " ", &convert_time($pgb_session_info{host}{$d}{duration} / $pgb_session_info{host}{$d}{count}), "\n"; } } # Show pgbouncer session per application statistics if (exists $pgb_session_info{app}) { print $fh "\n- pgBouncer sessions per application -----------------------------------------\n\n"; print $fh "Application Count Total Duration Avg duration (s)\n"; foreach my $d (sort keys %{$pgb_session_info{app}}) { print $fh "$d - ", &comma_numbers($pgb_session_info{app}{$d}{count}), " ", &convert_time($pgb_session_info{app}{$d}{duration}), " ", &convert_time($pgb_session_info{app}{$d}{duration} / $pgb_session_info{app}{$d}{count}), "\n"; } } # Show pgbouncer connection per database statistics if (exists $pgb_connection_info{database}) { print $fh "\n- pgBouncer connections per database -----------------------------------------\n\n"; print $fh "Database User Count\n"; foreach my $d (sort keys %{$pgb_connection_info{database}}) { print $fh "$d - ", &comma_numbers($pgb_connection_info{database}{$d}), "\n"; foreach my $u (sort keys %{$pgb_connection_info{user}}) { next if (!exists $pgb_connection_info{database_user}{$d}{$u}); print $fh "\t$u ", &comma_numbers($pgb_connection_info{database_user}{$d}{$u}), "\n"; } } print $fh "\nDatabase Host Count\n"; foreach my $d (sort keys %{$pgb_connection_info{database}}) { print $fh "$d - ", &comma_numbers($pgb_connection_info{database}{$d}), "\n"; foreach my $u (sort keys %{$pgb_connection_info{host}}) { next if (!exists $pgb_connection_info{database_host}{$d}{$u}); print $fh "\t$u ", &comma_numbers($pgb_connection_info{database_host}{$d}{$u}), "\n"; } } } # Show pgbouncer connection per user statistics if (exists $pgb_connection_info{user}) { print $fh "\n- pgBouncer connections per user ---------------------------------------------\n\n"; print $fh "User Count\n"; foreach my $d (sort keys %{$pgb_connection_info{user}}) { print $fh "$d - ", &comma_numbers($pgb_connection_info{user}{$d}), "\n"; } } # Show pgbouncer connection per host statistics if (exists $pgb_connection_info{host}) { print $fh "\n- pgBouncer connections per host --------------------------------------------\n\n"; print $fh "Host Count\n"; foreach my $d (sort keys %{$pgb_connection_info{host}}) { print $fh "$d - ", &comma_numbers($pgb_connection_info{host}{$d}), "\n"; } } if (!$disable_error) { &show_pgb_error_as_text(); } print $fh "\n\n"; print $fh "Report generated by pgBadger $VERSION ($project_url).\n"; } sub dump_error_as_text { my $curdb = shift; # Global information my $curdate = localtime(time); my $fmt_nlines = &comma_numbers($overall_stat{nlines}{$curdb}); my $total_time = timestr($td); $total_time =~ s/^([\.0-9]+) wallclock.*/$1/; $total_time = &convert_time($total_time * 1000); my $logfile_str = $log_files[0]; if ($#log_files > 0) { $logfile_str .= ', ..., ' . $log_files[-1]; } $report_title ||= 'PostgreSQL Log Analyzer'; # Set logs limits my ($t_log_min, $t_log_max) = get_log_limit($curdb); print $fh qq{ pgBadger :: $report_title - Global information --------------------------------------------------- Generated on $curdate Log file: $logfile_str Parsed $fmt_nlines log entries in $total_time Log start from $t_log_min to $t_log_max }; &show_error_as_text($curdb); print $fh "\n\n"; &show_pgb_error_as_text(); print $fh "\n\n"; print $fh "Report generated by pgBadger $VERSION ($project_url).\n"; } # We change temporary log level from LOG to ERROR # to store these messages into the error report sub change_log_level { my $msg = shift; return 1 if ($msg =~ /parameter "[^"]+" changed to "[^"]+"/); return 1 if ($msg =~ /database system was/); return 1 if ($msg =~ /recovery has paused/); return 1 if ($msg =~ /ending cancel to blocking autovacuum/); return 1 if ($msg =~ /skipping analyze of/); return 1 if ($msg =~ /using stale statistics/); return 1 if ($msg =~ /replication command:/); return 1 if ($msg =~ /still waiting for/); return 1 if ($msg =~ /server process.*was terminated by signal/); return 1 if ($msg =~ /could not (receive|send) data (from|to) client/); return 1 if ($msg =~ /logical decoding found consistent point at/); return 1 if ($msg =~ /starting logical decoding for slot/); return 1 if ($msg =~ /unexpected EOF/); return 1 if ($msg =~ /incomplete startup packet/); return 1 if ($msg =~ /detected deadlock while waiting for/); return 0; } sub revert_log_level { my $msg = shift; return ($msg, 1) if ($msg =~ s/ERROR: (parameter "[^"]+" changed to)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (database system was)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (recovery has paused)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (sending cancel to blocking autovacuum)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (skipping analyze of)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (using stale statistics)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (received replication command:)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (.*still waiting for)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (server process.*was terminated by signal)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (could not (?:receive|send) data (?:from|to) client)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (logical decoding found consistent point at)/LOG: $1/); return ($msg, 1) if ($msg =~ s/ERROR: (starting logical decoding for slot)/LOG: $1/); return ($msg, 0); } sub revert_log_level_by_reference { my $str = shift; $$str =~ s/ERROR: (parameter "[^"]+" changed to)/LOG: $1/; $$str =~ s/ERROR: (database system was)/LOG: $1/; $$str =~ s/ERROR: (recovery has paused)/LOG: $1/; $$str =~ s/ERROR: (sending cancel to blocking autovacuum)/LOG: $1/; $$str =~ s/ERROR: (skipping analyze of)/LOG: $1/; $$str =~ s/ERROR: (using stale statistics)/LOG: $1/; $$str =~ s/ERROR: (received replication command:)/LOG: $1/; $$str =~ s/ERROR: (.*still waiting for)/LOG: $1/; $$str =~ s/ERROR: (server process.*was terminated by signal)/LOG: $1/; $$str =~ s/ERROR: (could not (?:receive|send) data (?:from|to) client)/LOG: $1/; $$str =~ s/ERROR: (logical decoding found consistent point at)/LOG: $1/; $$str =~ s/ERROR: (starting logical decoding for slot)/LOG: $1/; } sub show_error_as_text { my $curdb = shift; return if (scalar keys %error_info == 0); print $fh "\n- Most frequent events (N) ---------------------------------------------\n\n"; my $idx = 1; foreach my $k (sort {$error_info{$curdb}{$b}{count} <=> $error_info{$curdb}{$a}{count}} keys %{$error_info{$curdb}}) { next if (!$error_info{$curdb}{$k}{count}); last if ($idx > $top); last if (!$error_info{$curdb}{$k}{count}); my ($msg, $ret) = &revert_log_level($k); if ($error_info{$curdb}{$k}{count} > 1) { print $fh "$idx) " . &comma_numbers($error_info{$curdb}{$k}{count}) . " - $msg\n"; print $fh "--\n"; my $j = 1; for (my $i = 0 ; $i <= $#{$error_info{$curdb}{$k}{date}} ; $i++) { next if ($i == $sample); print $fh "\t- Example $j: $error_info{$curdb}{$k}{date}[$i] - $error_info{$curdb}{$k}{error}[$i]\n"; print $fh "\t\tDetail: $error_info{$curdb}{$k}{detail}[$i]\n" if ($error_info{$curdb}{$k}{detail}[$i]); print $fh "\t\tContext: $error_info{$curdb}{$k}{context}[$i]\n" if ($error_info{$curdb}{$k}{context}[$i]); print $fh "\t\tHint: $error_info{$curdb}{$k}{hint}[$i]\n" if ($error_info{$curdb}{$k}{hint}[$i]); print $fh "\t\tStatement: ", &anonymize_query($error_info{$curdb}{$k}{statement}[$i]), "\n" if ($error_info{$curdb}{$k}{statement}[$i]); print $fh "\t\tQueryid: $error_info{$curdb}{$k}{queryid}[$i]\n" if ($error_info{$curdb}{$k}{queryid}[$i]); print $fh "\t\tDatabase: $error_info{$curdb}{$k}{db}[$i]\n" if ($error_info{$curdb}{$k}{db}[$i]); print $fh "\t\tLog file: $error_info{$curdb}{$k}{logfile}[$i]\n" if ($error_info{$curdb}{$k}{logfile}[$i]); $j++; } } elsif ($error_info{$curdb}{$k}{error}[0]) { ($msg, $ret) = &revert_log_level($error_info{$curdb}{$k}{error}[0]); if ($sample) { print $fh "$idx) " . &comma_numbers($error_info{$curdb}{$k}{count}) . " - $error_info{$curdb}{$k}{error}[0]\n"; print $fh "--\n"; print $fh "\t- Date: $error_info{$curdb}{$k}{date}[0]\n"; print $fh "\t\tDetail: $error_info{$curdb}{$k}{detail}[0]\n" if ($error_info{$curdb}{$k}{detail}[0]); print $fh "\t\tContext: $error_info{$curdb}{$k}{context}[0]\n" if ($error_info{$curdb}{$k}{context}[0]); print $fh "\t\tHint: $error_info{$curdb}{$k}{hint}[0]\n" if ($error_info{$curdb}{$k}{hint}[0]); print $fh "\t\tStatement: ", &anonymize_query($error_info{$curdb}{$k}{statement}[0]), "\n" if ($error_info{$curdb}{$k}{statement}[0]); print $fh "\t\tQueryid: $error_info{$curdb}{$k}{queryid}[0]\n" if ($error_info{$curdb}{$k}{queryid}[0]); print $fh "\t\tDatabase: $error_info{$curdb}{$k}{db}[0]\n" if ($error_info{$curdb}{$k}{db}[0]); print $fh "\t\tLog file: $error_info{$curdb}{$k}{logfile}[0]\n" if ($error_info{$curdb}{$k}{logfile}[0]); } else { print $fh "$idx) " . &comma_numbers($error_info{$curdb}{$k}{count}) . " - $msg\n"; print $fh "--\n"; } } $idx++; } if (scalar keys %{$logs_type{$curdb}} > 0) { print $fh "\n- Logs per type ---------------------------------------------\n\n"; my $total_logs = 0; foreach my $d (keys %{$logs_type{$curdb}}) { $total_logs += $logs_type{$curdb}{$d}; } print $fh "Logs type Count Percentage\n"; foreach my $d (sort keys %{$logs_type{$curdb}}) { next if (!$logs_type{$curdb}{$d}); print $fh "$d\t\t", &comma_numbers($logs_type{$curdb}{$d}), "\t", sprintf("%0.2f", ($logs_type{$curdb}{$d} * 100) / $total_logs), "%\n"; } } if (scalar keys %{$errors_code{$curdb}} > 0) { print $fh "\n- Logs per type ---------------------------------------------\n\n"; my $total_logs = 0; foreach my $d (keys %{$errors_code{$curdb}}) { $total_logs += $errors_code{$curdb}{$d}; } print $fh "Errors class code Count Percentage\n"; foreach my $d (sort keys %{$errors_code{$curdb}}) { next if (!$errors_code{$curdb}{$d}); print $fh "$CLASS_ERROR_CODE{$d}\t$d\t\t", &comma_numbers($errors_code{$curdb}{$d}), "\t", sprintf("%0.2f", ($errors_code{$curdb}{$d} * 100) / $total_logs), "%\n"; } } } sub show_pgb_error_as_text { return if (scalar keys %pgb_error_info == 0); print $fh "\n- Most frequent events (N) ---------------------------------------------\n\n"; my $idx = 1; foreach my $k (sort {$pgb_error_info{$b}{count} <=> $pgb_error_info{$a}{count}} keys %pgb_error_info) { next if (!$pgb_error_info{$k}{count}); last if ($idx > $top); my $msg = $k; if ($pgb_error_info{$k}{count} > 1) { print $fh "$idx) " . &comma_numbers($pgb_error_info{$k}{count}) . " - $msg\n"; print $fh "--\n"; my $j = 1; for (my $i = 0 ; $i <= $#{$pgb_error_info{$k}{date}} ; $i++) { last if ($i == $sample); print $fh "\t- Example $j: $pgb_error_info{$k}{date}[$i] - $pgb_error_info{$k}{error}[$i]\n"; print $fh "\t\tDatabase: $pgb_error_info{$k}{db}[$i]\n" if ($pgb_error_info{$k}{db}[$i]); print $fh "\t\tUser: $pgb_error_info{$k}{user}[$i]\n" if ($pgb_error_info{$k}{user}[$i]); print $fh "\t\tClient: $pgb_error_info{$k}{remote}[$i]\n" if ($pgb_error_info{$k}{remote}[$i]); $j++; } } else { if ($sample) { print $fh "$idx) " . &comma_numbers($pgb_error_info{$k}{count}) . " - $pgb_error_info{$k}{error}[0]\n"; print $fh "--\n"; print $fh "\t- Date: $pgb_error_info{$k}{date}[0]\n"; print $fh "\t\tDatabase: $pgb_error_info{$k}{db}[0]\n" if ($pgb_error_info{$k}{db}[0]); print $fh "\t\tUser: $pgb_error_info{$k}{user}[0]\n" if ($pgb_error_info{$k}{user}[0]); print $fh "\t\tClient: $pgb_error_info{$k}{remote}[0]\n" if ($pgb_error_info{$k}{remote}[0]); } else { print $fh "$idx) " . &comma_numbers($pgb_error_info{$k}{count}) . " - $msg\n"; print $fh "--\n"; } } $idx++; } } sub html_header { my $uri = shift; my $curdb = shift; my $date = localtime(time); my $global_info = &print_global_information($curdb); my @tmpjscode = @jscode; my $path_prefix = ''; $path_prefix = '../' if ($report_per_database); for (my $i = 0; $i <= $#tmpjscode; $i++) { $tmpjscode[$i] =~ s/EDIT_URI/$path_prefix$uri/; } my $local_title = 'PostgreSQL Log Analyzer'; if ($report_title) { $local_title = $report_title; } $report_title ||= 'pgBadger'; print $fh qq{ pgBadger :: $local_title @tmpjscode $EXPLAIN_POST
}; } # Create global information section sub print_global_information { my $curdb = shift(); my $curdate = localtime(time); my $fmt_nlines = &comma_numbers($overall_stat{nlines}{$curdb}); my $t3 = Benchmark->new; my $td = timediff($t3, $t0); my $total_time = timestr($td); $total_time =~ s/^([\.0-9]+) wallclock.*/$1/; $total_time = &convert_time($total_time * 1000); my $logfile_str = $log_files[0]; if ($#log_files > 0) { $logfile_str .= ', ..., ' . $log_files[-1]; } # Set logs limits my ($t_log_min, $t_log_max) = get_log_limit($curdb); return qq{ }; } sub print_overall_statistics { my $curdb = shift(); my $fmt_unique = &comma_numbers(scalar keys %{$normalyzed_info{$curdb}}); my $fmt_queries = &comma_numbers($overall_stat{$curdb}{'queries_number'}); my $avg_queries = &comma_numbers(int($overall_stat{$curdb}{'queries_number'}/($session_info{$curdb}{count} || 1))); my $q_duration = ($overall_stat{$curdb}{'queries_duration'}{'execute'}||0)+($overall_stat{$curdb}{'queries_duration'}{'prepare'}||0)+($overall_stat{$curdb}{'queries_duration'}{'bind'}||0); my $fmt_duration_prepare = &convert_time($overall_stat{$curdb}{'queries_duration'}{'prepare'}||0); my $fmt_duration_bind = &convert_time($overall_stat{$curdb}{'queries_duration'}{'bind'}||0); my $fmt_duration_execute = &convert_time($overall_stat{$curdb}{'queries_duration'}{'execute'}||0); my $fmt_duration = &convert_time($q_duration); $overall_stat{$curdb}{'first_query_ts'} ||= '-'; $overall_stat{$curdb}{'last_query_ts'} ||= '-'; my $query_peak = 0; my $query_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{query} <=> $overall_stat{$curdb}{'peak'}{$a}{query}} keys %{$overall_stat{$curdb}{'peak'}}) { $query_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{query}); $query_peak_date = $_ if ($query_peak); last; } my $avg_duration = &convert_time(int($q_duration/($session_info{$curdb}{count} || 1))); my $fmt_errors = &comma_numbers($overall_stat{$curdb}{'errors_number'}); my $fmt_unique_error = &comma_numbers(scalar keys %{$error_info{$curdb}}); my $autovacuum_count = &comma_numbers($autovacuum_info{$curdb}{count}); my $autoanalyze_count = &comma_numbers($autoanalyze_info{$curdb}{count}); my $tempfile_count = &comma_numbers($tempfile_info{$curdb}{count}); my $cancelled_count = &comma_numbers($cancelled_info{$curdb}{count}); my $fmt_temp_maxsise = &pretty_print_size($tempfile_info{$curdb}{maxsize}); my $fmt_temp_avsize = &pretty_print_size(sprintf("%.2f", $tempfile_info{$curdb}{size} / ($tempfile_info{$curdb}{count} || 1))); my $session_count = &comma_numbers($session_info{$curdb}{count}); my $avg_session_duration = &convert_time($session_info{$curdb}{duration} / ($session_info{$curdb}{count} || 1)); my $tot_session_duration = &convert_time($session_info{$curdb}{duration}); my $connection_count = &comma_numbers($connection_info{$curdb}{count}); my $avg_idle_time = &convert_time( ($session_info{$curdb}{duration} - $q_duration) / ($session_info{$curdb}{count} || 1)); $avg_idle_time = 'n/a' if (!$session_info{$curdb}{count}); my $connection_peak = 0; my $connection_peak_date = ''; my $session_peak = 0; my $session_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{connection} <=> $overall_stat{$curdb}{'peak'}{$a}{connection}} keys %{$overall_stat{$curdb}{'peak'}}) { $connection_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{connection}); $connection_peak_date = $_ if ($connection_peak); last; } foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{session} <=> $overall_stat{$curdb}{'peak'}{$a}{session}} keys %{$overall_stat{$curdb}{'peak'}}) { next if (!$session_count); $session_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{session}); $session_peak_date = $_ if ($session_peak); last; } my $main_error = 0; my $total = 0; foreach my $k (sort {$error_info{$curdb}{$b}{count} <=> $error_info{$curdb}{$a}{count}} keys %{$error_info{$curdb}}) { next if (!$error_info{$curdb}{$k}{count}); $main_error = &comma_numbers($error_info{$curdb}{$k}{count}) if (!$main_error); $total += $error_info{$curdb}{$k}{count}; } $total = &comma_numbers($total); my $db_count = scalar keys %{$database_info{$curdb}}; print $fh qq{

Overview

Global Stats

  • $fmt_unique Number of unique normalized queries
  • $fmt_queries Number of queries
  • $fmt_duration Total query duration
  • $overall_stat{$curdb}{'first_query_ts'} First query
  • $overall_stat{$curdb}{'last_query_ts'} Last query
  • $query_peak queries/s at $query_peak_date Query peak
  • $fmt_duration Total query duration
  • $fmt_duration_prepare Prepare/parse total duration
  • $fmt_duration_bind Bind total duration
  • $fmt_duration_execute Execute total duration
  • $fmt_errors Number of events
  • $fmt_unique_error Number of unique normalized events
  • $main_error Max number of times the same event was reported
  • $cancelled_count Number of cancellation
  • $autovacuum_count Total number of automatic vacuums
  • $autoanalyze_count Total number of automatic analyzes
  • $tempfile_count Number temporary file
  • $fmt_temp_maxsise Max size of temporary file
  • $fmt_temp_avsize Average size of temporary file
  • $session_count Total number of sessions
  • $session_peak sessions at $session_peak_date Session peak
  • $tot_session_duration Total duration of sessions
  • $avg_session_duration Average duration of sessions
  • $avg_queries Average queries per session
  • $avg_duration Average queries duration per session
  • $avg_idle_time Average idle time per session
  • $connection_count Total number of connections
  • }; if ($connection_count) { print $fh qq{
  • $connection_peak connections/s at $connection_peak_date Connection peak
  • }; } print $fh qq{
  • $db_count Total number of databases
}; } sub print_general_activity { my $curdb = shift; my $queries = ''; my $select_queries = ''; my $write_queries = ''; my $prepared_queries = ''; my $connections = ''; my $sessions = ''; foreach my $d (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { my $c = 1; $d =~ /^\d{4}(\d{2})(\d{2})$/; my $zday = "$abbr_month{$1} $2"; foreach my $h (sort {$a <=> $b} keys %{$per_minute_info{$curdb}{$d}}) { my %cur_period_info = (); my $read_average_duration = 0; my $read_average_count = 0; my $write_average_duration = 0; my $write_average_count = 0; my %all_query_duration=(); foreach my $m (keys %{$per_minute_info{$curdb}{$d}{$h}}) { $cur_period_info{$curdb}{count} += ($per_minute_info{$curdb}{$d}{$h}{$m}{query}{count} || 0); $cur_period_info{$curdb}{duration} += ($per_minute_info{$curdb}{$d}{$h}{$m}{query}{duration} || 0); $cur_period_info{$curdb}{min} = $per_minute_info{$curdb}{$d}{$h}{$m}{query}{min} if (!exists $cur_period_info{$curdb}{min} || ($per_minute_info{$curdb}{$d}{$h}{$m}{query}{min} < $cur_period_info{$curdb}{min})); $cur_period_info{$curdb}{max} = $per_minute_info{$curdb}{$d}{$h}{$m}{query}{max} if (!exists $cur_period_info{$curdb}{max} || ($per_minute_info{$curdb}{$d}{$h}{$m}{query}{max} > $cur_period_info{$curdb}{max})); push(@{$all_query_duration{'query'}}, $per_minute_info{$curdb}{$d}{$h}{$m}{query}{duration}||0); foreach my $a (@SQL_ACTION) { $cur_period_info{$curdb}{$a}{count} += ($per_minute_info{$curdb}{$d}{$h}{$m}{lc($a)}{count} || 0); $cur_period_info{$curdb}{$a}{duration} += ($per_minute_info{$curdb}{$d}{$h}{$m}{lc($a)}{duration} || 0); push(@{$all_query_duration{$a}}, $per_minute_info{$curdb}{$d}{$h}{$m}{lc($a)}{duration}||0); $cur_period_info{$curdb}{usual} += ($per_minute_info{$curdb}{$d}{$h}{$m}{lc($a)}{count} || 0); } $cur_period_info{$curdb}{prepare} += ($per_minute_info{$curdb}{$d}{$h}{$m}{prepare} || 0); $cur_period_info{$curdb}{execute} += ($per_minute_info{$curdb}{$d}{$h}{$m}{execute} || 0); } $cur_period_info{$curdb}{average} = $cur_period_info{$curdb}{duration} / ($cur_period_info{$curdb}{count} || 1); $read_average_duration = ($cur_period_info{$curdb}{'SELECT'}{duration} + $cur_period_info{$curdb}{'COPY TO'}{duration}); $read_average_count = ($cur_period_info{$curdb}{'SELECT'}{count} + $cur_period_info{$curdb}{'COPY TO'}{count}); $cur_period_info{$curdb}{'SELECT'}{average} = $cur_period_info{$curdb}{'SELECT'}{duration} / ($cur_period_info{$curdb}{'SELECT'}{count} || 1); $write_average_duration = ($cur_period_info{$curdb}{'INSERT'}{duration} + $cur_period_info{$curdb}{'UPDATE'}{duration} + $cur_period_info{$curdb}{'DELETE'}{duration} + $cur_period_info{$curdb}{'COPY FROM'}{duration}); $write_average_count = ($cur_period_info{$curdb}{'INSERT'}{count} + $cur_period_info{$curdb}{'UPDATE'}{count} + $cur_period_info{$curdb}{'DELETE'}{count} + $cur_period_info{$curdb}{'COPY FROM'}{count}); $zday = " " if ($c > 1); $c++; my $count = &comma_numbers($cur_period_info{$curdb}{count}); my $min = &convert_time($cur_period_info{$curdb}{min}); my $max = &convert_time($cur_period_info{$curdb}{max}); my $average = &convert_time($cur_period_info{$curdb}{average}); my %percentile = (); foreach my $lp (@LATENCY_PERCENTILE) { $cur_period_info{$curdb}{$lp}{percentileindex} = int(@{$all_query_duration{'query'}} * $lp / 100) ; @{$all_query_duration{'query'}}= sort{ $a <=> $b } @{$all_query_duration{'query'}}; $cur_period_info{$curdb}{$lp}{percentile} = $all_query_duration{'query'}[$cur_period_info{$curdb}{$lp}{percentileindex}]; $percentile{$lp} = &convert_time($cur_period_info{$curdb}{$lp}{percentile}); @{$all_query_duration{'READ'}}= sort{ $a <=> $b } (@{$all_query_duration{'SELECT'}}, @{$all_query_duration{'COPY TO'}}); $cur_period_info{$curdb}{'READ'}{$lp}{percentileindex} = int(@{$all_query_duration{'READ'}} * $lp / 100) ; $cur_period_info{$curdb}{'READ'}{$lp}{percentile} = $all_query_duration{'READ'}[$cur_period_info{$curdb}{'READ'}{$lp}{percentileindex}]; $percentile{'READ'}{$lp} = &convert_time($cur_period_info{$curdb}{'READ'}{$lp}{percentile}); @{$all_query_duration{'WRITE'}}= sort{ $a <=> $b } (@{$all_query_duration{'INSERT'}},@{$all_query_duration{'UPDATE'}},@{$all_query_duration{'DELETE'}},@{$all_query_duration{'COPY FROM'}}); $cur_period_info{$curdb}{'WRITE'}{$lp}{percentileindex} = int(@{$all_query_duration{'WRITE'}} * $lp / 100) ; $cur_period_info{$curdb}{'WRITE'}{$lp}{percentile} = $all_query_duration{'WRITE'}[$cur_period_info{$curdb}{'WRITE'}{$lp}{percentileindex}]; $percentile{'WRITE'}{$lp} = &convert_time($cur_period_info{$curdb}{'WRITE'}{$lp}{percentile}); } $queries .= qq{ $zday $h $count $min $max $average }; foreach my $lp (@LATENCY_PERCENTILE) { $queries .= "$percentile{$lp}\n"; } $queries .= qq{ }; $count = &comma_numbers($cur_period_info{$curdb}{'SELECT'}{count}); my $copyto_count = &comma_numbers($cur_period_info{$curdb}{'COPY TO'}{count}); $average = &convert_time($read_average_duration / ($read_average_count || 1)); $select_queries .= qq{ $zday $h $count $copyto_count $average }; foreach my $lp (@LATENCY_PERCENTILE) { $select_queries .= "$percentile{'READ'}{$lp}\n"; } $select_queries .= qq{ }; my $insert_count = &comma_numbers($cur_period_info{$curdb}{'INSERT'}{count}); my $update_count = &comma_numbers($cur_period_info{$curdb}{'UPDATE'}{count}); my $delete_count = &comma_numbers($cur_period_info{$curdb}{'DELETE'}{count}); my $copyfrom_count = &comma_numbers($cur_period_info{$curdb}{'COPY FROM'}{count}); my $write_average = &convert_time($write_average_duration / ($write_average_count || 1)); $write_queries .= qq{ $zday $h $insert_count $update_count $delete_count $copyfrom_count $write_average} ; foreach my $lp (@LATENCY_PERCENTILE) { $write_queries .= "$percentile{'WRITE'}{$lp}\n"; } $write_queries .= qq{ }; my $prepare_count = &comma_numbers($cur_period_info{$curdb}{prepare}); my $execute_count = &comma_numbers($cur_period_info{$curdb}{execute}); my $bind_prepare = &comma_numbers(sprintf("%.2f", $cur_period_info{$curdb}{execute}/($cur_period_info{$curdb}{prepare}||1))); my $prepare_usual = &comma_numbers(sprintf("%.2f", ($cur_period_info{$curdb}{prepare}/($cur_period_info{$curdb}{usual}||1)) * 100)) . "%"; $prepared_queries .= qq{ $zday $h $prepare_count $execute_count $bind_prepare $prepare_usual }; $count = &comma_numbers($connection_info{$curdb}{chronos}{"$d"}{"$h"}{count}); $average = &comma_numbers(sprintf("%0.2f", $connection_info{$curdb}{chronos}{"$d"}{"$h"}{count} / 3600)); $connections .= qq{ $zday $h $count $average/s }; $count = &comma_numbers($session_info{$curdb}{chronos}{"$d"}{"$h"}{count}); $cur_period_info{$curdb}{'session'}{average} = $session_info{$curdb}{chronos}{"$d"}{"$h"}{duration} / ($session_info{$curdb}{chronos}{"$d"}{"$h"}{count} || 1); $average = &convert_time($cur_period_info{$curdb}{'session'}{average}); my $avg_idle = &convert_time(($session_info{$curdb}{chronos}{"$d"}{"$h"}{duration} - $cur_period_info{$curdb}{duration}) / ($session_info{$curdb}{chronos}{"$d"}{"$h"}{count} || 1)); $sessions .= qq{ $zday $h $count $average $avg_idle }; } } # Set default values $queries = qq{$NODATA} if (!$queries); $select_queries = qq{$NODATA} if (!$select_queries); $write_queries = qq{$NODATA} if (!$write_queries); $prepared_queries = qq{$NODATA} if (!$prepared_queries); $connections = qq{$NODATA} if (!$connections); $sessions = qq{$NODATA} if (!$sessions); print $fh qq{

General Activity

$queries
Day Hour Count Min duration Max duration Avg duration Latency Percentile(90) Latency Percentile(95) Latency Percentile(99)
$select_queries
Day Hour SELECT COPY TO Average Duration Latency Percentile(90) Latency Percentile(95) Latency Percentile(99)
$write_queries
Day Hour INSERT UPDATE DELETE COPY FROM Average Duration Latency Percentile(90) Latency Percentile(95) Latency Percentile(99)
$prepared_queries
Day Hour Prepare Bind Bind/Prepare Percentage of prepare
$connections
Day Hour Count Average / Second
$sessions
Day Hour Count Average Duration Average idle time
Back to the top of the General Activity table
}; } sub print_sql_traffic { my $curdb = shift; my $bind_vs_prepared = sprintf("%.2f", $overall_stat{$curdb}{'execute'} / ($overall_stat{$curdb}{'prepare'} || 1)); my $total_usual_queries = 0; map { $total_usual_queries += $overall_stat{$curdb}{lc($_)}; } @SQL_ACTION; my $prepared_vs_normal = sprintf("%.2f", ($overall_stat{$curdb}{'execute'} / ($total_usual_queries || 1))*100); my $query_peak = 0; my $query_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{query} <=> $overall_stat{$curdb}{'peak'}{$a}{query}} keys %{$overall_stat{$curdb}{'peak'}}) { $query_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{query}); $query_peak_date = $_ if ($query_peak); last; } my $select_peak = 0; my $select_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{select} <=> $overall_stat{$curdb}{'peak'}{$a}{select}} keys %{$overall_stat{$curdb}{'peak'}}) { $select_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{select}); $select_peak_date = $_ if ($select_peak); last; } my $write_peak = 0; my $write_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{write} <=> $overall_stat{$curdb}{'peak'}{$a}{write}} keys %{$overall_stat{$curdb}{'peak'}}) { $write_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{write}); $write_peak_date = $_ if ($write_peak); last; } my $fmt_duration = &convert_time($overall_stat{$curdb}{'queries_duration'}{'execute'}+($overall_stat{$curdb}{'queries_duration'}{'prepare'}||0)+($overall_stat{$curdb}{'queries_duration'}{'bind'}||0)); print $fh qq{

SQL Traffic

Key values

  • $query_peak queries/s Query Peak
  • $query_peak_date Date
$drawn_graphs{queriespersecond_graph}
}; delete $drawn_graphs{queriespersecond_graph}; print $fh qq{

SELECT Traffic

Key values

  • $select_peak queries/s Query Peak
  • $select_peak_date Date
$drawn_graphs{selectqueries_graph}
}; delete $drawn_graphs{selectqueries_graph}; print $fh qq{

INSERT/UPDATE/DELETE Traffic

Key values

  • $write_peak queries/s Query Peak
  • $write_peak_date Date
$drawn_graphs{writequeries_graph}
}; delete $drawn_graphs{writequeries_graph}; print $fh qq{

Queries duration

Key values

  • $fmt_duration Total query duration
$drawn_graphs{durationqueries_graph}
}; delete $drawn_graphs{durationqueries_graph}; print $fh qq{

Prepared queries ratio

Key values

  • $bind_vs_prepared Ratio of bind vs prepare
  • $prepared_vs_normal % Ratio between prepared and "usual" statements
$drawn_graphs{bindpreparequeries_graph}
}; delete $drawn_graphs{bindpreparequeries_graph}; } sub print_pgbouncer_stats { my $curdb = shift; my $request_peak = 0; my $request_peak_date = ''; foreach (sort {$pgb_overall_stat{'peak'}{$b}{t_req} <=> $pgb_overall_stat{'peak'}{$a}{t_req}} keys %{$pgb_overall_stat{'peak'}}) { $request_peak = &comma_numbers($pgb_overall_stat{'peak'}{$_}{t_req}); $request_peak_date = $_; last; } my $inbytes_peak = 0; my $inbytes_peak_date = ''; foreach (sort {$pgb_overall_stat{'peak'}{$b}{t_inbytes} <=> $pgb_overall_stat{'peak'}{$a}{t_inbytes}} keys %{$pgb_overall_stat{'peak'}}) { $inbytes_peak = &comma_numbers($pgb_overall_stat{'peak'}{$_}{t_inbytes}); $inbytes_peak_date = $_; last; } my $outbytes_peak = 0; my $outbytes_peak_date = ''; foreach (sort {$pgb_overall_stat{'peak'}{$b}{t_outbytes} <=> $pgb_overall_stat{'peak'}{$a}{t_outbytes}} keys %{$pgb_overall_stat{'peak'}}) { $outbytes_peak = &comma_numbers($pgb_overall_stat{'peak'}{$_}{t_outbytes}); $outbytes_peak_date = $_; last; } my $avgduration_peak = 0; my $avgduration_peak_date = ''; foreach (sort {$pgb_overall_stat{'peak'}{$b}{t_avgduration} <=> $pgb_overall_stat{'peak'}{$a}{t_avgduration}} keys %{$pgb_overall_stat{'peak'}}) { $avgduration_peak = &convert_time($pgb_overall_stat{'peak'}{$_}{t_avgduration}); $avgduration_peak_date = $_; last; } my $avgwaiting_peak = 0; my $avgwaiting_peak_date = ''; foreach (sort {$pgb_overall_stat{'peak'}{$b}{t_avgwaiting} <=> $pgb_overall_stat{'peak'}{$a}{t_avgwaiting}} keys %{$pgb_overall_stat{'peak'}}) { $avgwaiting_peak = &convert_time($pgb_overall_stat{'peak'}{$_}{t_avgwaiting}); $avgwaiting_peak_date = $_; last; } print $fh qq{

Request Throughput

Key values

  • $request_peak queries/s Request Peak
  • $request_peak_date Date
$drawn_graphs{pgb_requestpersecond_graph}
}; delete $drawn_graphs{pgb_requestpersecond_graph}; print $fh qq{

Bytes I/O Throughput

Key values

  • $inbytes_peak Bytes/s In Bytes Peak
  • $inbytes_peak_date Date
  • $outbytes_peak Bytes/s Out Bytes Peak
  • $outbytes_peak_date Date
$drawn_graphs{pgb_bytepersecond_graph}
}; delete $drawn_graphs{pgb_bytepersecond_graph}; print $fh qq{

Average Query Duration

Key values

  • $avgduration_peak Average Duration Peak
  • $avgduration_peak_date Date
$drawn_graphs{pgb_avgduration_graph}
}; delete $drawn_graphs{pgb_avgduration_graph}; print $fh qq{

Average Waiting Time

Key values

  • $avgwaiting_peak Average Waiting Peak
  • $avgwaiting_peak_date Date
$drawn_graphs{pgb_avgwaiting_graph}
}; delete $drawn_graphs{pgb_avgwaiting_graph}; } sub compute_query_graphs { my $curdb = shift; my %graph_data = (); if ($graph) { foreach my $tm (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { $tm =~ /(\d{4})(\d{2})(\d{2})/; my $y = $1 - 1900; my $mo = $2 - 1; my $d = $3; foreach my $h ("00" .. "23") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}); my %q_dataavg = (); my %a_dataavg = (); my %c_dataavg = (); my %s_dataavg = (); my %p_dataavg = (); foreach my $m ("00" .. "59") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}{$m}); my $rd = &average_per_minutes($m, $avg_minutes); if (!exists $p_dataavg{prepare}{"$rd"}) { $p_dataavg{prepare}{"$rd"} = 0; $p_dataavg{execute}{"$rd"} = 0; $q_dataavg{count}{"$rd"} = 0; $q_dataavg{duration}{"$rd"} = 0; $q_dataavg{max}{"$rd"} = 0; $q_dataavg{min}{"$rd"} = 0; if (!$disable_query) { foreach my $action (@SQL_ACTION) { $a_dataavg{$action}{count}{"$rd"} = 0; $a_dataavg{$action}{duration}{"$rd"} = 0; $a_dataavg{$action}{max}{"$rd"} = 0; $a_dataavg{$action}{min}{"$rd"} = 0; } $a_dataavg{write}{count}{"$rd"} = 0; $a_dataavg{write}{duration}{"$rd"} = 0; } $c_dataavg{average}{"$rd"} = 0; $c_dataavg{max}{"$rd"} = 0; $c_dataavg{min}{"$rd"} = 0; $s_dataavg{average}{"$rd"} = 0; $s_dataavg{max}{"$rd"} = 0; $s_dataavg{min}{"$rd"} = 0; } if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{prepare}) { $p_dataavg{prepare}{"$rd"} += $per_minute_info{$curdb}{$tm}{$h}{$m}{prepare}; } elsif (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{parse}) { $p_dataavg{prepare}{"$rd"} += $per_minute_info{$curdb}{$tm}{$h}{$m}{parse}; } $p_dataavg{execute}{"$rd"} += $per_minute_info{$curdb}{$tm}{$h}{$m}{execute} if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{execute}); if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{query}) { # Average per minute $q_dataavg{count}{"$rd"} += $per_minute_info{$curdb}{$tm}{$h}{$m}{query}{count}; if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{query}{duration}) { $q_dataavg{duration}{"$rd"} += $per_minute_info{$curdb}{$tm}{$h}{$m}{query}{duration}; } # Search minimum and maximum during this minute foreach my $s (keys %{$per_minute_info{$curdb}{$tm}{$h}{$m}{query}{second}}) { $q_dataavg{max}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{query}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{query}{second}{$s} > $q_dataavg{max}{"$rd"}); $q_dataavg{min}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{query}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{query}{second}{$s} < $q_dataavg{min}{"$rd"}); } if (!$disable_query) { foreach my $action (@SQL_ACTION) { $a_dataavg{$action}{count}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{count} || 0); $a_dataavg{$action}{duration}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{duration} || 0); if ( ($action ne 'SELECT') && exists $per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{count}) { $a_dataavg{write}{count}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{count} || 0); $a_dataavg{write}{duration}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{duration} || 0); } # Search minimum and maximum during this minute foreach my $s (keys %{$per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{second}}) { $a_dataavg{$action}{max}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{second}{$s} > $a_dataavg{$action}{max}{"$rd"}); $a_dataavg{$action}{min}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{lc($action)}{second}{$s} < $a_dataavg{$action}{min}{"$rd"}); } } } } if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{connection}) { # Average per minute $c_dataavg{average}{"$rd"} += $per_minute_info{$curdb}{$tm}{$h}{$m}{connection}{count}; # Search minimum and maximum during this minute foreach my $s (keys %{$per_minute_info{$curdb}{$tm}{$h}{$m}{connection}{second}}) { $c_dataavg{max}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{connection}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{connection}{second}{$s} > $c_dataavg{max}{"$rd"}); $c_dataavg{min}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{connection}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{connection}{second}{$s} < $c_dataavg{min}{"$rd"}); } delete $per_minute_info{$curdb}{$tm}{$h}{$m}{connection}; } if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{session}) { # Average per minute $s_dataavg{average}{"$rd"} += $per_minute_info{$curdb}{$tm}{$h}{$m}{session}{count}; # Search minimum and maximum during this minute foreach my $s (keys %{$per_minute_info{$curdb}{$tm}{$h}{$m}{session}{second}}) { $s_dataavg{max}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{session}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{session}{second}{$s} > $s_dataavg{max}{"$rd"}); $s_dataavg{min}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{session}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{session}{second}{$s} < $s_dataavg{min}{"$rd"}); } delete $per_minute_info{$curdb}{$tm}{$h}{$m}{session}; } } foreach my $rd (@avgs) { my $t = timegm_nocheck(0, $rd, $h, $d, $mo, $y) * 1000; $t += ($timezone*1000); next if ($t < $t_min); last if ($t > $t_max); if (exists $q_dataavg{count}) { # Average queries per minute $graph_data{query} .= "[$t, " . int(($q_dataavg{count}{"$rd"} || 0) / (60 * $avg_minutes)) . "],"; # Max queries per minute $graph_data{'query-max'} .= "[$t, " . ($q_dataavg{max}{"$rd"} || 0) . "],"; # Min queries per minute $graph_data{'query-min'} .= "[$t, " . ($q_dataavg{min}{"$rd"} || 0) . "],"; # Average duration per minute $graph_data{query4} .= "[$t, " . sprintf("%.3f", ($q_dataavg{duration}{"$rd"} || 0) / ($q_dataavg{count}{"$rd"} || 1)) . "],"; } if (scalar keys %c_dataavg) { # Average connections per minute $graph_data{conn_avg} .= "[$t, " . int(($c_dataavg{average}{"$rd"} || 0) / (60 * $avg_minutes)) . "],"; # Max connections per minute $graph_data{conn_max} .= "[$t, " . ($c_dataavg{max}{"$rd"} || 0) . "],"; # Min connections per minute $graph_data{conn_min} .= "[$t, " . ($c_dataavg{min}{"$rd"} || 0) . "],"; } if (scalar keys %s_dataavg) { # Average connections per minute $graph_data{sess_avg} .= "[$t, " . int(($s_dataavg{average}{"$rd"} || 0) / (60 * $avg_minutes)) . "],"; # Max connections per minute $graph_data{sess_max} .= "[$t, " . ($s_dataavg{max}{"$rd"} || 0) . "],"; # Min connections per minute $graph_data{sess_min} .= "[$t, " . ($s_dataavg{min}{"$rd"} || 0) . "],"; } if (!$disable_query && (scalar keys %a_dataavg > 0)) { foreach my $action (@SQL_ACTION) { next if ($select_only && ($action ne 'SELECT')); # Average queries per minute $graph_data{"$action"} .= "[$t, " . int(($a_dataavg{$action}{count}{"$rd"} || 0) / (60 * $avg_minutes)) . "],"; if ($action eq 'SELECT') { # Max queries per minute $graph_data{"$action-max"} .= "[$t, " . ($a_dataavg{$action}{max}{"$rd"} || 0) . "],"; # Min queries per minute $graph_data{"$action-min"} .= "[$t, " . ($a_dataavg{$action}{min}{"$rd"} || 0) . "],"; # Average query duration $graph_data{"$action-2"} .= "[$t, " . sprintf("%.3f", ($a_dataavg{$action}{duration}{"$rd"} || 0) / ($a_dataavg{$action}{count}{"$rd"} || 1)) . "],"; } elsif ($action eq 'DDL') { # Average query duration $graph_data{"write"} .= "[$t, " . sprintf("%.3f", ($a_dataavg{write}{duration}{"$rd"} || 0) / ($a_dataavg{write}{count}{"$rd"} || 1)) . "],"; } } } if (!$disable_query && (scalar keys %p_dataavg> 0)) { $graph_data{prepare} .= "[$t, " . ($p_dataavg{prepare}{"$rd"} || 0) . "],"; $graph_data{execute} .= "[$t, " . ($p_dataavg{execute}{"$rd"} || 0) . "],"; $graph_data{ratio_bind_prepare} .= "[$t, " . sprintf("%.2f", ($p_dataavg{execute}{"$rd"} || 0) / ($p_dataavg{prepare}{"$rd"} || 1)) . "],"; } } } } foreach (keys %graph_data) { $graph_data{$_} =~ s/,$//; } } $drawn_graphs{'queriespersecond_graph'} = &jqplot_linegraph( $graphid++, 'queriespersecond_graph', $graph_data{'query-max'}, $graph_data{query}, $graph_data{'query-min'}, 'Queries per second (' . $avg_minutes . ' minutes average)', 'Queries per second', 'Maximum', 'Average', 'Minimum' ); $drawn_graphs{'connectionspersecond_graph'} = &jqplot_linegraph( $graphid++, 'connectionspersecond_graph', $graph_data{conn_max}, $graph_data{conn_avg}, $graph_data{conn_min}, 'Connections per second (' . $avg_minutes . ' minutes average)', 'Connections per second', 'Maximum', 'Average', 'Minimum' ); $drawn_graphs{'sessionspersecond_graph'} = &jqplot_linegraph( $graphid++, 'sessionspersecond_graph', $graph_data{sess_max}, $graph_data{sess_avg}, $graph_data{sess_min}, 'Number of sessions/second (' . $avg_minutes . ' minutes average)', 'Sessions', 'Maximum', 'Average', 'Minimum' ); $drawn_graphs{'selectqueries_graph'} = &jqplot_linegraph( $graphid++, 'selectqueries_graph', $graph_data{"SELECT-max"}, $graph_data{"SELECT"}, $graph_data{"SELECT-min"}, 'SELECT queries (' . $avg_minutes . ' minutes period)', 'Queries per second', 'Maximum', 'Average', 'Minimum' ); $drawn_graphs{'writequeries_graph'} = &jqplot_linegraph( $graphid++, 'writequeries_graph', $graph_data{"DELETE"}, $graph_data{"INSERT"}, $graph_data{"UPDATE"}, 'Write queries (' . $avg_minutes . ' minutes period)', 'Queries', 'DELETE queries', 'INSERT queries', 'UPDATE queries' ); if (!$select_only) { $drawn_graphs{'durationqueries_graph'} = &jqplot_linegraph( $graphid++, 'durationqueries_graph', $graph_data{query4}, $graph_data{"SELECT-2"}, $graph_data{write}, 'Average queries duration (' . $avg_minutes . ' minutes average)', 'Duration', 'All queries', 'Select queries', 'Write queries' ); } else { $drawn_graphs{'durationqueries_graph'} = &jqplot_linegraph( $graphid++, 'durationqueries_graph', $graph_data{query4}, '', '', 'Average queries duration (' . $avg_minutes . ' minutes average)', 'Duration', 'Select queries' ); } $drawn_graphs{'bindpreparequeries_graph'} = &jqplot_linegraph( $graphid++, 'bindpreparequeries_graph', $graph_data{prepare}, $graph_data{"execute"}, $graph_data{ratio_bind_prepare}, 'Bind versus prepare statements (' . $avg_minutes . ' minutes average)', 'Number of statements', 'Prepare/Parse', 'Execute/Bind', 'Bind vs prepare' ); } sub compute_pgbouncer_graphs { my %graph_data = (); if ($graph) { foreach my $tm (sort {$a <=> $b} keys %pgb_per_minute_info) { $tm =~ /(\d{4})(\d{2})(\d{2})/; my $y = $1 - 1900; my $mo = $2 - 1; my $d = $3; foreach my $h ("00" .. "23") { next if (!exists $pgb_per_minute_info{$tm}{$h}); my %c_dataavg = (); my %s_dataavg = (); foreach my $m ("00" .. "59") { my $t = timegm_nocheck(0, $m, $h, $d, $mo, $y) * 1000; $t += ($timezone*1000); next if ($t < $t_min); last if ($t > $t_max); # pgBouncer stats are generate each minutes, always keep this interval $graph_data{'request'} .= "[$t, " . ($pgb_per_minute_info{$tm}{$h}{$m}{t_req} || 0) . "],"; $graph_data{'inbytes'} .= "[$t, " . ($pgb_per_minute_info{$tm}{$h}{$m}{t_inbytes} || 0) . "],"; $graph_data{'outbytes'} .= "[$t, " . ($pgb_per_minute_info{$tm}{$h}{$m}{t_outbytes} || 0) . "],"; $graph_data{'avgduration'} .= "[$t, " . ($pgb_per_minute_info{$tm}{$h}{$m}{t_avgduration} || 0) . "],"; $graph_data{'avgwaiting'} .= "[$t, " . ($pgb_per_minute_info{$tm}{$h}{$m}{t_avgwaiting} || 0) . "],"; next if (!exists $pgb_per_minute_info{$tm}{$h}{$m}); my $rd = &average_per_minutes($m, $avg_minutes); if (exists $pgb_per_minute_info{$tm}{$h}{$m}{connection}) { # Average per minute $c_dataavg{average}{"$rd"} += $pgb_per_minute_info{$tm}{$h}{$m}{connection}{count}; # Search minimum and maximum during this minute foreach my $s (keys %{$pgb_per_minute_info{$tm}{$h}{$m}{connection}{second}}) { $c_dataavg{max}{"$rd"} = $pgb_per_minute_info{$tm}{$h}{$m}{connection}{second}{$s} if ($pgb_per_minute_info{$tm}{$h}{$m}{connection}{second}{$s} > $c_dataavg{max}{"$rd"}); $c_dataavg{min}{"$rd"} = $pgb_per_minute_info{$tm}{$h}{$m}{connection}{second}{$s} if ($pgb_per_minute_info{$tm}{$h}{$m}{connection}{second}{$s} < $c_dataavg{min}{"$rd"}); } delete $pgb_per_minute_info{$tm}{$h}{$m}{connection}; } if (exists $pgb_per_minute_info{$tm}{$h}{$m}{session}) { # Average per minute $s_dataavg{average}{"$rd"} += $pgb_per_minute_info{$tm}{$h}{$m}{session}{count}; # Search minimum and maximum during this minute foreach my $s (keys %{$pgb_per_minute_info{$tm}{$h}{$m}{session}{second}}) { $s_dataavg{max}{"$rd"} = $pgb_per_minute_info{$tm}{$h}{$m}{session}{second}{$s} if ($pgb_per_minute_info{$tm}{$h}{$m}{session}{second}{$s} > $s_dataavg{max}{"$rd"}); $s_dataavg{min}{"$rd"} = $pgb_per_minute_info{$tm}{$h}{$m}{session}{second}{$s} if ($pgb_per_minute_info{$tm}{$h}{$m}{session}{second}{$s} < $s_dataavg{min}{"$rd"}); } delete $pgb_per_minute_info{$tm}{$h}{$m}{session}; } } foreach my $rd (@avgs) { my $t = timegm_nocheck(0, $rd, $h, $d, $mo, $y) * 1000; $t += ($timezone*1000); next if ($t < $t_min); last if ($t > $t_max); if (scalar keys %c_dataavg) { # Average connections per minute $graph_data{conn_avg} .= "[$t, " . int(($c_dataavg{average}{"$rd"} || 0) / (60 * $avg_minutes)) . "],"; # Max connections per minute $graph_data{conn_max} .= "[$t, " . ($c_dataavg{max}{"$rd"} || 0) . "],"; # Min connections per minute $graph_data{conn_min} .= "[$t, " . ($c_dataavg{min}{"$rd"} || 0) . "],"; } if (scalar keys %s_dataavg) { # Average connections per minute $graph_data{sess_avg} .= "[$t, " . int(($s_dataavg{average}{"$rd"} || 0) / (60 * $avg_minutes)) . "],"; # Max connections per minute $graph_data{sess_max} .= "[$t, " . ($s_dataavg{max}{"$rd"} || 0) . "],"; # Min connections per minute $graph_data{sess_min} .= "[$t, " . ($s_dataavg{min}{"$rd"} || 0) . "],"; } } } } foreach (keys %graph_data) { $graph_data{$_} =~ s/,$//; } } $drawn_graphs{'pgb_requestpersecond_graph'} = &jqplot_linegraph( $graphid++, 'pgb_requestpersecond_graph', $graph_data{request},'',,'','Request per seconds (1 minute average)', '', 'Request per second'); $drawn_graphs{'pgb_bytepersecond_graph'} = &jqplot_linegraph( $graphid++, 'pgb_bytepersecond_graph', $graph_data{inbytes},$graph_data{'outbytes'},'','Bytes I/O per seconds (1 minute average)', 'size', 'In b/s', 'Out b/s'); $drawn_graphs{'pgb_avgduration_graph'} = &jqplot_linegraph( $graphid++, 'pgb_avgduration_graph', $graph_data{avgduration},'','', 'Average query duration (1 minute average)', 'duration', 'Duration'); $drawn_graphs{'pgb_avgwaiting_graph'} = &jqplot_linegraph( $graphid++, 'pgb_avgwaiting_graph', $graph_data{avgwaiting},'','', 'Average waiting time (1 minute average)', 'waiting', 'Waiting'); $drawn_graphs{'pgb_connectionspersecond_graph'} = &jqplot_linegraph( $graphid++, 'pgb_connectionspersecond_graph', $graph_data{conn_max}, $graph_data{conn_avg}, $graph_data{conn_min}, 'Connections per second (' . $avg_minutes . ' minutes average)', 'Connections per second', 'Maximum', 'Average', 'Minimum' ); $drawn_graphs{'pgb_sessionspersecond_graph'} = &jqplot_linegraph( $graphid++, 'pgb_sessionspersecond_graph', $graph_data{sess_max}, $graph_data{sess_avg}, $graph_data{sess_min}, 'Number of sessions/second (' . $avg_minutes . ' minutes average)', 'Sessions', 'Maximum', 'Average', 'Minimum' ); } sub print_established_connection { my $curdb = shift; my $connection_peak = 0; my $connection_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{connection} <=> $overall_stat{$curdb}{'peak'}{$a}{connection}} keys %{$overall_stat{$curdb}{'peak'}}) { $connection_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{connection}); $connection_peak_date = $_ if ($connection_peak); last; } print $fh qq{

Established Connections

Key values

  • $connection_peak connections Connection Peak
  • $connection_peak_date Date
$drawn_graphs{connectionspersecond_graph}
}; delete $drawn_graphs{connectionspersecond_graph}; } sub print_established_pgb_connection { my $connection_peak = 0; my $connection_peak_date = ''; foreach (sort {$pgb_overall_stat{'peak'}{$b}{connection} <=> $pgb_overall_stat{'peak'}{$a}{connection}} keys %{$pgb_overall_stat{'peak'}}) { $connection_peak = &comma_numbers($pgb_overall_stat{'peak'}{$_}{connection}); $connection_peak_date = $_; last; } print $fh qq{

Established Connections

Key values

  • $connection_peak connections Connection Peak
  • $connection_peak_date Date
$drawn_graphs{pgb_connectionspersecond_graph}
}; delete $drawn_graphs{pgb_connectionspersecond_graph}; } sub print_user_connection { my $curdb = shift; my %infos = (); my $total_count = 0; my $c = 0; my $conn_user_info = ''; my @main_user = ('unknown',0); foreach my $u (sort keys %{$connection_info{$curdb}{user}}) { $conn_user_info .= "$u" . &comma_numbers($connection_info{$curdb}{user}{$u}) . ""; $total_count += $connection_info{$curdb}{user}{$u}; if ($main_user[1] < $connection_info{$curdb}{user}{$u}) { $main_user[0] = $u; $main_user[1] = $connection_info{$curdb}{user}{$u}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$connection_info{$curdb}{user}}) { if ((($connection_info{$curdb}{user}{$d} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $connection_info{$curdb}{user}{$d} || 0; } else { $infos{"Sum connections < $pie_percentage_limit%"} += $connection_info{$curdb}{user}{$d} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum connections < $pie_percentage_limit%"}; delete $infos{"Sum connections < $pie_percentage_limit%"}; } } $drawn_graphs{userconnections_graph} = &jqplot_piegraph($graphid++, 'graph_userconnections', 'Connections per user', %infos); $total_count = &comma_numbers($total_count); print $fh qq{

Connections per user

Key values

  • $main_user[0] Main User
  • $total_count connections Total
$drawn_graphs{userconnections_graph}
$conn_user_info
User Count
}; delete $drawn_graphs{userconnections_graph}; } sub print_user_pgb_connection { my %infos = (); my $total_count = 0; my $c = 0; my $conn_user_info = ''; my @main_user = ('unknown',0); foreach my $u (sort keys %{$pgb_connection_info{user}}) { $conn_user_info .= "$u" . &comma_numbers($pgb_connection_info{user}{$u}) . ""; $total_count += $pgb_connection_info{user}{$u}; if ($main_user[1] < $pgb_connection_info{user}{$u}) { $main_user[0] = $u; $main_user[1] = $pgb_connection_info{user}{$u}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$pgb_connection_info{user}}) { if ((($pgb_connection_info{user}{$d} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $pgb_connection_info{user}{$d} || 0; } else { $infos{"Sum connections < $pie_percentage_limit%"} += $pgb_connection_info{user}{$d} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum connections < $pie_percentage_limit%"}; delete $infos{"Sum connections < $pie_percentage_limit%"}; } } $drawn_graphs{pgb_userconnections_graph} = &jqplot_piegraph($graphid++, 'graph_pgb_userconnections', 'Connections per user', %infos); $total_count = &comma_numbers($total_count); print $fh qq{

Connections per user

Key values

  • $main_user[0] Main User
  • $total_count connections Total
$drawn_graphs{pgb_userconnections_graph}
$conn_user_info
User Count
}; delete $drawn_graphs{pgb_userconnections_graph}; } sub print_host_connection { my $curdb = shift(); my %infos = (); my $total_count = 0; my $c = 0; my $conn_host_info = ''; my @main_host = ('unknown',0); foreach my $h (sort keys %{$connection_info{$curdb}{host}}) { $conn_host_info .= "$h" . &comma_numbers($connection_info{$curdb}{host}{$h}) . ""; $total_count += $connection_info{$curdb}{host}{$h}; if ($main_host[1] < $connection_info{$curdb}{host}{$h}) { $main_host[0] = $h; $main_host[1] = $connection_info{$curdb}{host}{$h}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$connection_info{$curdb}{host}}) { if ((($connection_info{$curdb}{host}{$d} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $connection_info{$curdb}{host}{$d} || 0; } else { $infos{"Sum connections < $pie_percentage_limit%"} += $connection_info{$curdb}{host}{$d} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum connections < $pie_percentage_limit%"}; delete $infos{"Sum connections < $pie_percentage_limit%"}; } } $drawn_graphs{hostconnections_graph} = &jqplot_piegraph($graphid++, 'graph_hostconnections', 'Connections per host', %infos); $total_count = &comma_numbers($total_count); print $fh qq{

Connections per host

Key values

  • $main_host[0] Main host with $main_host[1] connections
  • $total_count Total connections
$drawn_graphs{hostconnections_graph}
$conn_host_info
Host Count
}; delete $drawn_graphs{hostconnections_graph}; } sub print_host_pgb_connection { my %infos = (); my $total_count = 0; my $c = 0; my $conn_host_info = ''; my @main_host = ('unknown',0); foreach my $h (sort keys %{$pgb_connection_info{host}}) { $conn_host_info .= "$h" . &comma_numbers($pgb_connection_info{host}{$h}) . ""; $total_count += $pgb_connection_info{host}{$h}; if ($main_host[1] < $pgb_connection_info{host}{$h}) { $main_host[0] = $h; $main_host[1] = $pgb_connection_info{host}{$h}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$pgb_connection_info{host}}) { if ((($pgb_connection_info{host}{$d} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $pgb_connection_info{host}{$d} || 0; } else { $infos{"Sum connections < $pie_percentage_limit%"} += $pgb_connection_info{host}{$d} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum connections < $pie_percentage_limit%"}; delete $infos{"Sum connections < $pie_percentage_limit%"}; } } $drawn_graphs{pgb_hostconnections_graph} = &jqplot_piegraph($graphid++, 'graph_pgb_hostconnections', 'Connections per host', %infos); $total_count = &comma_numbers($total_count); print $fh qq{

Connections per host

Key values

  • $main_host[0] Main host with $main_host[1] connections
  • $total_count Total connections
$drawn_graphs{pgb_hostconnections_graph}
$conn_host_info
Host Count
}; delete $drawn_graphs{pgb_hostconnections_graph}; } sub print_database_connection { my $curdb = shift; my %infos = (); my $total_count = 0; my $conn_database_info = ''; my @main_database = ('unknown',0); foreach my $d (sort keys %{$connection_info{$curdb}{database}}) { $conn_database_info .= "$d " . &comma_numbers($connection_info{$curdb}{database}{$d}) . ""; $total_count += $connection_info{$curdb}{database}{$d}; if ($main_database[1] < $connection_info{$curdb}{database}{$d}) { $main_database[0] = $d; $main_database[1] = $connection_info{$curdb}{database}{$d}; } foreach my $u (sort keys %{$connection_info{$curdb}{user}}) { next if (!exists $connection_info{$curdb}{database_user}{$d}{$u}); $conn_database_info .= " $u" . &comma_numbers($connection_info{$curdb}{database_user}{$d}{$u}) . ""; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$connection_info{$curdb}{database}}) { if ((($connection_info{$curdb}{database}{$d} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $connection_info{$curdb}{database}{$d} || 0; } else { $infos{"Sum connections < $pie_percentage_limit%"} += $connection_info{$curdb}{database}{$d} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum connections < $pie_percentage_limit%"}; delete $infos{"Sum connections < $pie_percentage_limit%"}; } } $drawn_graphs{databaseconnections_graph} = &jqplot_piegraph($graphid++, 'graph_databaseconnections', 'Connections per database', %infos); $total_count = &comma_numbers($total_count); print $fh qq{

Connections per database

Key values

  • $main_database[0] Main Database
  • $total_count connections Total
$drawn_graphs{databaseconnections_graph}
$conn_database_info
Database User Count
}; delete $drawn_graphs{databaseconnections_graph}; } sub print_database_pgb_connection { my %infos = (); my $total_count = 0; my $conn_database_info = ''; my @main_database = ('unknown',0); foreach my $d (sort keys %{$pgb_connection_info{database}}) { $conn_database_info .= "$d " . &comma_numbers($pgb_connection_info{database}{$d}) . ""; $total_count += $pgb_connection_info{database}{$d}; if ($main_database[1] < $pgb_connection_info{database}{$d}) { $main_database[0] = $d; $main_database[1] = $pgb_connection_info{database}{$d}; } foreach my $u (sort keys %{$pgb_connection_info{user}}) { next if (!exists $pgb_connection_info{database_user}{$d}{$u}); $conn_database_info .= " $u" . &comma_numbers($pgb_connection_info{database_user}{$d}{$u}) . ""; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$pgb_connection_info{database}}) { if ((($pgb_connection_info{database}{$d} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $pgb_connection_info{database}{$d} || 0; } else { $infos{"Sum connections < $pie_percentage_limit%"} += $pgb_connection_info{database}{$d} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum connections < $pie_percentage_limit%"}; delete $infos{"Sum connections < $pie_percentage_limit%"}; } } $drawn_graphs{pgb_databaseconnections_graph} = &jqplot_piegraph($graphid++, 'graph_pgb_databaseconnections', 'Connections per database', %infos); $total_count = &comma_numbers($total_count); print $fh qq{

Connections per database

Key values

  • $main_database[0] Main Database
  • $total_count connections Total
$drawn_graphs{pgb_databaseconnections_graph}
$conn_database_info
Database User Count
}; delete $drawn_graphs{pgb_databaseconnections_graph}; } sub print_simultaneous_session { my $curdb = shift; my $session_peak = 0; my $session_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{session} <=> $overall_stat{$curdb}{'peak'}{$a}{session}} keys %{$overall_stat{$curdb}{'peak'}}) { $session_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{session}); $session_peak_date = $_ if ($session_peak); last; } print $fh qq{

Simultaneous sessions

Key values

  • $session_peak sessions Session Peak
  • $session_peak_date Date
$drawn_graphs{sessionspersecond_graph}
}; delete $drawn_graphs{sessionspersecond_graph}; } sub print_simultaneous_pgb_session { my $session_peak = 0; my $session_peak_date = ''; foreach (sort {$pgb_overall_stat{'peak'}{$b}{session} <=> $pgb_overall_stat{'peak'}{$a}{session}} keys %{$pgb_overall_stat{'peak'}}) { $session_peak = &comma_numbers($pgb_overall_stat{'peak'}{$_}{session}); $session_peak_date = $_; last; } print $fh qq{

Simultaneous sessions

Key values

  • $session_peak sessions Session Peak
  • $session_peak_date Date
$drawn_graphs{pgb_sessionspersecond_graph}
}; delete $drawn_graphs{pgb_sessionspersecond_graph}; } sub print_histogram_session_times { my $curdb = shift; my %data = (); my $histogram_info = ''; my $most_range = ''; my $most_range_value = ''; for (my $i = 1; $i <= $#histogram_session_time; $i++) { $histogram_info .= "" . &convert_time($histogram_session_time[$i-1]) . '-' . &convert_time($histogram_session_time[$i]) . "" . &comma_numbers($overall_stat{$curdb}{histogram}{session_time}{$histogram_session_time[$i-1]}) . "" . sprintf("%0.2f", ($overall_stat{$curdb}{histogram}{session_time}{$histogram_session_time[$i-1]} * 100) / ($overall_stat{$curdb}{histogram}{session_total}||1)) . "%"; $data{"$histogram_session_time[$i-1]-$histogram_session_time[$i]ms"} = ($overall_stat{$curdb}{histogram}{session_time}{$histogram_session_time[$i-1]} || 0); if ($overall_stat{$curdb}{histogram}{session_time}{$histogram_session_time[$i-1]} > $most_range_value) { $most_range = "$histogram_session_time[$i-1]-$histogram_session_time[$i]ms"; $most_range_value = $overall_stat{$curdb}{histogram}{session_time}{$histogram_session_time[$i-1]}; } } if ($graph) { if ($overall_stat{$curdb}{histogram}{session_total} > 0) { $histogram_info .= " > " . &convert_time($histogram_session_time[-1]) . "" . &comma_numbers($overall_stat{$curdb}{histogram}{session_time}{'-1'}) . "" . sprintf("%0.2f", ($overall_stat{$curdb}{histogram}{session_time}{'-1'} * 100) / ($overall_stat{$curdb}{histogram}{session_total}||1)) . "%"; $data{"> $histogram_session_time[-1]ms"} = ($overall_stat{$curdb}{histogram}{session_time}{"-1"} || 0); if ($overall_stat{$curdb}{histogram}{session_time}{"-1"} > $most_range_value) { $most_range = "> $histogram_session_time[-1]ms"; $most_range_value = $overall_stat{$curdb}{histogram}{session_time}{"-1"}; } $drawn_graphs{histogram_session_times_graph} = &jqplot_duration_histograph($graphid++, 'graph_histogram_session_times', 'Sessions', \@histogram_session_time, %data); } else { $histogram_info = qq{$NODATA}; $drawn_graphs{histogram_session_times_graph} = qq{$NODATA}; } } else { $histogram_info = qq{$NODATA}; $drawn_graphs{histogram_session_times_graph} = qq{$NODATA}; } $most_range_value = &comma_numbers($most_range_value) if ($most_range_value); print $fh qq{

Histogram of session times

Key values

  • $most_range_value $most_range duration
$drawn_graphs{histogram_session_times_graph}
$histogram_info
Range Count Percentage
}; delete $drawn_graphs{histogram_session_times_graph}; } sub print_histogram_pgb_session_times { my %data = (); my $histogram_info = ''; my $most_range = ''; my $most_range_value = ''; for (my $i = 1; $i <= $#histogram_session_time; $i++) { $histogram_info .= "" . &convert_time($histogram_session_time[$i-1]) . '-' . &convert_time($histogram_session_time[$i]) . "" . &comma_numbers($pgb_overall_stat{histogram}{session_time}{$histogram_session_time[$i-1]}) . "" . sprintf("%0.2f", ($pgb_overall_stat{histogram}{session_time}{$histogram_session_time[$i-1]} * 100) / ($pgb_overall_stat{histogram}{session_total}||1)) . "%"; $data{"$histogram_session_time[$i-1]-$histogram_session_time[$i]ms"} = ($pgb_overall_stat{histogram}{session_time}{$histogram_session_time[$i-1]} || 0); if ($pgb_overall_stat{histogram}{session_time}{$histogram_session_time[$i-1]} > $most_range_value) { $most_range = "$histogram_session_time[$i-1]-$histogram_session_time[$i]ms"; $most_range_value = $pgb_overall_stat{histogram}{session_time}{$histogram_session_time[$i-1]}; } } if ($pgb_overall_stat{histogram}{session_total} > 0) { $histogram_info .= " > " . &convert_time($histogram_session_time[-1]) . "" . &comma_numbers($pgb_overall_stat{histogram}{session_time}{'-1'}) . "" . sprintf("%0.2f", ($pgb_overall_stat{histogram}{session_time}{'-1'} * 100) / ($pgb_overall_stat{histogram}{session_total}||1)) . "%"; $data{"> $histogram_session_time[-1]ms"} = ($pgb_overall_stat{histogram}{session_time}{"-1"} || 0); if ($pgb_overall_stat{histogram}{session_time}{"-1"} > $most_range_value) { $most_range = "> $histogram_session_time[-1]ms"; $most_range_value = $pgb_overall_stat{histogram}{session_time}{"-1"}; } $drawn_graphs{pgb_histogram_session_times_graph} = &jqplot_duration_histograph($graphid++, 'graph_pgb_histogram_session_times', 'Sessions', \@histogram_session_time, %data); } else { $histogram_info = qq{$NODATA}; $drawn_graphs{pgb_histogram_session_times_graph} = qq{$NODATA}; } $most_range_value = &comma_numbers($most_range_value) if ($most_range_value); print $fh qq{

Histogram of session times

Key values

  • $most_range_value $most_range duration
$drawn_graphs{pgb_histogram_session_times_graph}
$histogram_info
Range Count Percentage
}; delete $drawn_graphs{pgb_histogram_session_times_graph}; } sub print_user_session { my $curdb = shift; my %infos = (); my $total_count = 0; my $c = 0; my $sess_user_info = ''; my @main_user = ('unknown',0); foreach my $u (sort keys %{$session_info{$curdb}{user}}) { $session_info{$curdb}{user}{$u}{count} ||= 1; $sess_user_info .= "$u" . &comma_numbers($session_info{$curdb}{user}{$u}{count}) . "" . &convert_time($session_info{$curdb}{user}{$u}{duration}) . "" . &convert_time($session_info{$curdb}{user}{$u}{duration} / $session_info{$curdb}{user}{$u}{count}) . ""; $total_count += $session_info{$curdb}{user}{$u}{count}; if ($main_user[1] < $session_info{$curdb}{user}{$u}{count}) { $main_user[0] = $u; $main_user[1] = $session_info{$curdb}{user}{$u}{count}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$session_info{$curdb}{user}}) { if ((($session_info{$curdb}{user}{$d}{count} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $session_info{$curdb}{user}{$d}{count} || 0; } else { $infos{"Sum sessions < $pie_percentage_limit%"} += $session_info{$curdb}{user}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum sessions < $pie_percentage_limit%"}; delete $infos{"Sum sessions < $pie_percentage_limit%"}; } } $drawn_graphs{usersessions_graph} = &jqplot_piegraph($graphid++, 'graph_usersessions', 'Sessions per user', %infos); $sess_user_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); print $fh qq{

Sessions per user

Key values

  • $main_user[0] Main User
  • $total_count sessions Total
$drawn_graphs{usersessions_graph}
$sess_user_info
User Count Total Duration Average Duration
}; delete $drawn_graphs{usersessions_graph}; } sub print_user_pgb_session { my %infos = (); my $total_count = 0; my $c = 0; my $sess_user_info = ''; my @main_user = ('unknown',0); foreach my $u (sort keys %{$pgb_session_info{user}}) { $sess_user_info .= "$u" . &comma_numbers($pgb_session_info{user}{$u}{count}) . "" . &convert_time($pgb_session_info{user}{$u}{duration}) . "" . &convert_time($pgb_session_info{user}{$u}{duration} / $pgb_session_info{user}{$u}{count}) . ""; $total_count += $pgb_session_info{user}{$u}{count}; if ($main_user[1] < $pgb_session_info{user}{$u}{count}) { $main_user[0] = $u; $main_user[1] = $pgb_session_info{user}{$u}{count}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$pgb_session_info{user}}) { if ((($pgb_session_info{user}{$d}{count} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $pgb_session_info{user}{$d}{count} || 0; } else { $infos{"Sum sessions < $pie_percentage_limit%"} += $pgb_session_info{user}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum sessions < $pie_percentage_limit%"}; delete $infos{"Sum sessions < $pie_percentage_limit%"}; } } $drawn_graphs{pgb_usersessions_graph} = &jqplot_piegraph($graphid++, 'graph_pgb_usersessions', 'Sessions per user', %infos); $sess_user_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); print $fh qq{

Sessions per user

Key values

  • $main_user[0] Main User
  • $total_count sessions Total
$drawn_graphs{pgb_usersessions_graph}
$sess_user_info
User Count Total Duration Average Duration
}; delete $drawn_graphs{pgb_usersessions_graph}; } sub print_host_session { my $curdb = shift; my %infos = (); my $total_count = 0; my $c = 0; my $sess_host_info = ''; my @main_host = ('unknown',0); foreach my $h (sort keys %{$session_info{$curdb}{host}}) { $sess_host_info .= "$h" . &comma_numbers($session_info{$curdb}{host}{$h}{count}) . "" . &convert_time($session_info{$curdb}{host}{$h}{duration}) . "" . &convert_time($session_info{$curdb}{host}{$h}{duration} / $session_info{$curdb}{host}{$h}{count}) . ""; $total_count += $session_info{$curdb}{host}{$h}{count}; if ($main_host[1] < $session_info{$curdb}{host}{$h}{count}) { $main_host[0] = $h; $main_host[1] = $session_info{$curdb}{host}{$h}{count}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$session_info{$curdb}{host}}) { if ((($session_info{$curdb}{host}{$d}{count} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $session_info{$curdb}{host}{$d}{count} || 0; } else { $infos{"Sum sessions < $pie_percentage_limit%"} += $session_info{$curdb}{host}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum sessions < $pie_percentage_limit%"}; delete $infos{"Sum sessions < $pie_percentage_limit%"}; } } $drawn_graphs{hostsessions_graph} = &jqplot_piegraph($graphid++, 'graph_hostsessions', 'Sessions per host', %infos); $sess_host_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); print $fh qq{

Sessions per host

Key values

  • $main_host[0] Main Host
  • $total_count sessions Total
$drawn_graphs{hostsessions_graph}
$sess_host_info
Host Count Total Duration Average Duration
}; delete $drawn_graphs{hostsessions_graph}; } sub print_host_pgb_session { my %infos = (); my $total_count = 0; my $c = 0; my $sess_host_info = ''; my @main_host = ('unknown',0); foreach my $h (sort keys %{$pgb_session_info{host}}) { $sess_host_info .= "$h" . &comma_numbers($pgb_session_info{host}{$h}{count}) . "" . &convert_time($pgb_session_info{host}{$h}{duration}) . "" . &convert_time($pgb_session_info{host}{$h}{duration} / $pgb_session_info{host}{$h}{count}) . ""; $total_count += $pgb_session_info{host}{$h}{count}; if ($main_host[1] < $pgb_session_info{host}{$h}{count}) { $main_host[0] = $h; $main_host[1] = $pgb_session_info{host}{$h}{count}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$pgb_session_info{host}}) { if ((($pgb_session_info{host}{$d}{count} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $pgb_session_info{host}{$d}{count} || 0; } else { $infos{"Sum sessions < $pie_percentage_limit%"} += $pgb_session_info{host}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum sessions < $pie_percentage_limit%"}; delete $infos{"Sum sessions < $pie_percentage_limit%"}; } } $drawn_graphs{pgb_hostsessions_graph} = &jqplot_piegraph($graphid++, 'graph_pgb_hostsessions', 'Sessions per host', %infos); $sess_host_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); print $fh qq{

Sessions per host

Key values

  • $main_host[0] Main Host
  • $total_count sessions Total
$drawn_graphs{pgb_hostsessions_graph}
$sess_host_info
Host Count Total Duration Average Duration
}; delete $drawn_graphs{pgb_hostsessions_graph}; } sub print_app_session { my $curdb = shift; my %infos = (); my $total_count = 0; my $c = 0; my $sess_app_info = ''; my @main_app = ('unknown',0); foreach my $h (sort keys %{$session_info{$curdb}{app}}) { $sess_app_info .= "$h" . &comma_numbers($session_info{$curdb}{app}{$h}{count}) . "" . &convert_time($session_info{$curdb}{app}{$h}{duration}) . "" . &convert_time($session_info{$curdb}{app}{$h}{duration} / $session_info{$curdb}{app}{$h}{count}) . ""; $total_count += $session_info{$curdb}{app}{$h}{count}; if ($main_app[1] < $session_info{$curdb}{app}{$h}{count}) { $main_app[0] = $h; $main_app[1] = $session_info{$curdb}{app}{$h}{count}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$session_info{$curdb}{app}}) { if ((($session_info{$curdb}{app}{$d}{count} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $session_info{$curdb}{app}{$d}{count} || 0; } else { $infos{"Sum sessions < $pie_percentage_limit%"} += $session_info{$curdb}{app}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum sessions < $pie_percentage_limit%"}; delete $infos{"Sum sessions < $pie_percentage_limit%"}; } } $drawn_graphs{appsessions_graph} = &jqplot_piegraph($graphid++, 'graph_appsessions', 'Sessions per application', %infos); $sess_app_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); print $fh qq{

Sessions per application

Key values

  • $main_app[0] Main Application
  • $total_count sessions Total
$drawn_graphs{appsessions_graph}
$sess_app_info
Application Count Total Duration Average Duration
}; delete $drawn_graphs{appsessions_graph}; } sub print_database_session { my $curdb = shift; my %infos = (); my $total_count = 0; my $sess_database_info = ''; my @main_database = ('unknown',0); foreach my $d (sort keys %{$session_info{$curdb}{database}}) { $sess_database_info .= "$d" . &comma_numbers($session_info{$curdb}{database}{$d}{count}) . "" . &convert_time($session_info{$curdb}{database}{$d}{duration}) . "" . &convert_time($session_info{$curdb}{database}{$d}{duration} / $session_info{$curdb}{database}{$d}{count}) . ""; $total_count += $session_info{$curdb}{database}{$d}{count}; if ($main_database[1] < $session_info{$curdb}{database}{$d}{count}) { $main_database[0] = $d; $main_database[1] = $session_info{$curdb}{database}{$d}{count}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$session_info{$curdb}{database}}) { if ((($session_info{$curdb}{database}{$d}{count} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $session_info{$curdb}{database}{$d}{count} || 0; } else { $infos{"Sum sessions < $pie_percentage_limit%"} += $session_info{$curdb}{database}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum sessions < $pie_percentage_limit%"}; delete $infos{"Sum sessions < $pie_percentage_limit%"}; } } $drawn_graphs{databasesessions_graph} = &jqplot_piegraph($graphid++, 'graph_databasesessions', 'Sessions per database', %infos); $sess_database_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); print $fh qq{

Sessions per database

Key values

  • $main_database[0] Main Database
  • $total_count sessions Total
$drawn_graphs{databasesessions_graph}
$sess_database_info
Database Count Total Duration Average Duration
}; delete $drawn_graphs{databasesessions_graph}; } sub print_database_pgb_session { my %infos = (); my $total_count = 0; my $sess_database_info = ''; my @main_database = ('unknown',0); foreach my $d (sort keys %{$pgb_session_info{database}}) { $sess_database_info .= "$d" . &comma_numbers($pgb_session_info{database}{$d}{count}) . "" . &convert_time($pgb_session_info{database}{$d}{duration}) . "" . &convert_time($pgb_session_info{database}{$d}{duration} / $pgb_session_info{database}{$d}{count}) . ""; $total_count += $pgb_session_info{database}{$d}{count}; if ($main_database[1] < $pgb_session_info{database}{$d}{count}) { $main_database[0] = $d; $main_database[1] = $pgb_session_info{database}{$d}{count}; } } if ($graph) { my @small = (); foreach my $d (sort keys %{$pgb_session_info{database}}) { if ((($pgb_session_info{database}{$d}{count} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $pgb_session_info{database}{$d}{count} || 0; } else { $infos{"Sum sessions < $pie_percentage_limit%"} += $pgb_session_info{database}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum sessions < $pie_percentage_limit%"}; delete $infos{"Sum sessions < $pie_percentage_limit%"}; } } $drawn_graphs{pgb_databasesessions_graph} = &jqplot_piegraph($graphid++, 'graph_pgb_databasesessions', 'Sessions per database', %infos); $sess_database_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); print $fh qq{

Sessions per database

Key values

  • $main_database[0] Main Database
  • $total_count sessions Total
$drawn_graphs{pgb_databasesessions_graph}
$sess_database_info
Database User Count Total Duration Average Duration
}; delete $drawn_graphs{pgb_databasesessions_graph}; } sub print_checkpoint { my $curdb = shift; # checkpoint my %graph_data = (); if ($graph) { foreach my $tm (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { $tm =~ /(\d{4})(\d{2})(\d{2})/; my $y = $1 - 1900; my $mo = $2 - 1; my $d = $3; foreach my $h ("00" .. "23") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}); my %chk_dataavg = (); my %t_dataavg = (); my %v_dataavg = (); foreach my $m ("00" .. "59") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}{$m}); my $rd = &average_per_minutes($m, $avg_minutes); if ($checkpoint_info{wbuffer}) { $chk_dataavg{wbuffer}{"$rd"} = 0 if (!exists $chk_dataavg{wbuffer}{"$rd"}); $chk_dataavg{file_added}{"$rd"} = 0 if (!exists $chk_dataavg{file_added}{"$rd"}); $chk_dataavg{file_removed}{"$rd"} = 0 if (!exists $chk_dataavg{file_removed}{"$rd"}); $chk_dataavg{file_recycled}{"$rd"} = 0 if (!exists $chk_dataavg{file_recycled}{"$rd"}); if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}) { $chk_dataavg{wbuffer}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}{wbuffer} || 0); $chk_dataavg{file_added}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}{file_added} || 0); $chk_dataavg{file_removed}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}{file_removed} || 0); $chk_dataavg{file_recycled}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}{file_recycled} || 0); } } if (exists $checkpoint_info{distance} || exists $checkpoint_info{estimate}) { $chk_dataavg{distance}{"$rd"} = 0 if (!exists $chk_dataavg{distance}{"$rd"}); $chk_dataavg{estimate}{"$rd"} = 0 if (!exists $chk_dataavg{estimate}{"$rd"}); if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}) { $chk_dataavg{distance}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}{distance} || 0) * 1000; $chk_dataavg{distance_count}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}{distance_count} || 1); $chk_dataavg{estimate}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{checkpoint}{estimate} || 0) * 1000; } } } foreach my $rd (@avgs) { my $t = timegm_nocheck(0, $rd, $h, $d, $mo, $y) * 1000; $t += ($timezone*1000); next if ($t < $t_min); last if ($t > $t_max); # Average of written checkpoint buffers and wal files if (exists $chk_dataavg{wbuffer}) { $graph_data{wbuffer} .= "[$t, " . ($chk_dataavg{wbuffer}{"$rd"} || 0) . "],"; $graph_data{file_added} .= "[$t, " . ($chk_dataavg{file_added}{"$rd"} || 0) . "],"; $graph_data{file_removed} .= "[$t, " . ($chk_dataavg{file_removed}{"$rd"} || 0) . "],"; $graph_data{file_recycled} .= "[$t, " . ($chk_dataavg{file_recycled}{"$rd"} || 0) . "],"; } if (exists $chk_dataavg{distance} || $chk_dataavg{estimate}) { $graph_data{distance} .= "[$t, " . int(($chk_dataavg{distance}{"$rd"}/($chk_dataavg{distance_count}{"$rd"} || 1)) || 0) . "],"; $graph_data{estimate} .= "[$t, " . int(($chk_dataavg{estimate}{"$rd"}/($chk_dataavg{distance_count}{"$rd"} || 1)) || 0) . "],"; } } } } foreach (keys %graph_data) { $graph_data{$_} =~ s/,$//; } } # Checkpoint buffers and files $drawn_graphs{checkpointwritebuffers_graph} = &jqplot_linegraph($graphid++, 'checkpointwritebuffers_graph', $graph_data{wbuffer}, '', '', 'Checkpoint write buffers (' . $avg_minutes . ' minutes period)', 'Buffers', 'Write buffers', '', '' ); $drawn_graphs{checkpointfiles_graph} = &jqplot_linegraph($graphid++, 'checkpointfiles_graph', $graph_data{file_added}, $graph_data{file_removed}, $graph_data{file_recycled}, 'Checkpoint Wal files usage (' . $avg_minutes . ' minutes period)', 'Number of files', 'Added', 'Removed', 'Recycled' ); $drawn_graphs{checkpointdistance_graph} = &jqplot_linegraph($graphid++, 'checkpointdistance_graph', $graph_data{distance}, $graph_data{estimate}, '', 'Checkpoint mean distance and estimate (' . $avg_minutes . ' minutes period)', 'Number of bytes', 'distance', 'estimate' ); my $checkpoint_wbuffer_peak = 0; my $checkpoint_wbuffer_peak_date = ''; foreach (sort { $overall_checkpoint{'peak'}{$b}{checkpoint_wbuffer} <=> $overall_checkpoint{'peak'}{$a}{checkpoint_wbuffer} } keys %{$overall_checkpoint{'peak'}}) { $checkpoint_wbuffer_peak = &comma_numbers($overall_checkpoint{'peak'}{$_}{checkpoint_wbuffer}); $checkpoint_wbuffer_peak_date = $_; last; } my $walfile_usage_peak = 0; my $walfile_usage_peak_date = ''; foreach (sort { $overall_checkpoint{'peak'}{$b}{walfile_usage} <=> $overall_checkpoint{'peak'}{$a}{walfile_usage} } keys %{$overall_checkpoint{'peak'}}) { $walfile_usage_peak = &comma_numbers($overall_checkpoint{'peak'}{$_}{walfile_usage}); $walfile_usage_peak_date = $_; last; } my $checkpoint_distance_peak = 0; my $checkpoint_distance_peak_date = ''; foreach (sort { $overall_checkpoint{'peak'}{$b}{distance} <=> $overall_checkpoint{'peak'}{$a}{distance} } keys %{$overall_checkpoint{'peak'}}) { $checkpoint_distance_peak = &comma_numbers(sprintf("%.2f", $overall_checkpoint{'peak'}{$_}{distance}/1024)); $checkpoint_distance_peak_date = $_; last; } print $fh qq{

Checkpoints / Restartpoints

Checkpoints Buffers

Key values

  • $checkpoint_wbuffer_peak buffers Checkpoint Peak
  • $checkpoint_wbuffer_peak_date Date
  • $overall_checkpoint{checkpoint_write} seconds Highest write time
  • $overall_checkpoint{checkpoint_sync} seconds Sync time
$drawn_graphs{checkpointwritebuffers_graph}
}; delete $drawn_graphs{checkpointwritebuffers_graph}; print $fh qq{

Checkpoints Wal files

Key values

  • $walfile_usage_peak files Wal files usage Peak
  • $walfile_usage_peak_date Date
$drawn_graphs{checkpointfiles_graph}
}; delete $drawn_graphs{checkpointfiles_graph}; print $fh qq{

Checkpoints distance

Key values

  • $checkpoint_distance_peak Mo Distance Peak
  • $checkpoint_distance_peak_date Date
$drawn_graphs{checkpointdistance_graph}
}; delete $drawn_graphs{checkpointdistance_graph}; my $buffers = ''; my $files = ''; my $warnings = ''; my $distance = ''; foreach my $d (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { $d =~ /^\d{4}(\d{2})(\d{2})$/; my $zday = "$abbr_month{$1} $2"; foreach my $h (sort {$a <=> $b} keys %{$per_minute_info{$curdb}{$d}}) { $buffers .= "$zday$h"; $files .= "$zday$h"; $warnings .= "$zday$h"; $distance .= "$zday$h"; $zday = ''; my %cinf = (); my %rinf = (); my %cainf = (); my %rainf = (); my %dinf = (); foreach my $m (keys %{$per_minute_info{$curdb}{$d}{$h}}) { if (exists $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}) { $cinf{wbuffer} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{wbuffer}; $cinf{file_added} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{file_added}; $cinf{file_removed} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{file_removed}; $cinf{file_recycled} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{file_recycled}; $cinf{write} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{write}; $cinf{sync} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{sync}; $cinf{total} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{total}; $cainf{sync_files} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{sync_files}; $cainf{sync_avg} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{sync_avg}; $cainf{sync_longest} = $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{sync_longest} if ($per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{sync_longest} > $cainf{sync_longest}); } if (exists $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{warning}) { $cinf{warning} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{warning}; $cinf{warning_seconds} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{warning_seconds}; } if (exists $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{distance} || $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{estimate}) { $dinf{distance}{sum} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{distance}; $dinf{estimate}{sum} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{estimate}; $dinf{distance}{count} += $per_minute_info{$curdb}{$d}{$h}{$m}{checkpoint}{distance_count}; } } if (scalar keys %cinf) { $buffers .= "" . &comma_numbers($cinf{wbuffer}) . "" . &comma_numbers($cinf{write}) . 's' . "" . &comma_numbers($cinf{sync}) . 's' . "" . &comma_numbers($cinf{total}) . 's' . ""; $files .= "" . &comma_numbers($cinf{file_added}) . "" . &comma_numbers($cinf{file_removed}) . "" . &comma_numbers($cinf{file_recycled}) . "" . &comma_numbers($cainf{sync_files}) . "" . &comma_numbers($cainf{sync_longest}) . 's' . "" . &comma_numbers($cainf{sync_avg}) . 's' . ""; } else { $buffers .= "00s0s0s"; $files .= "00000s0s"; } if (exists $cinf{warning}) { $warnings .= "" . &comma_numbers($cinf{warning}) . "" . &comma_numbers(sprintf( "%.2f", ($cinf{warning_seconds} || 0) / ($cinf{warning} || 1))) . "s"; } else { $warnings .= "00s"; } if (exists $dinf{distance} || $dinf{estimate}) { $distance .= "" . &comma_numbers(sprintf( "%.2f", $dinf{distance}{sum}/($dinf{distance}{count}||1))) . " kB" . &comma_numbers(sprintf( "%.2f", $dinf{estimate}{sum}/($dinf{distance}{count}||1))) . " kB"; } else { $distance .= "00"; } } } $buffers = qq{$NODATA} if (!$buffers); $files = qq{$NODATA} if (!$files); $warnings = qq{$NODATA} if (!$warnings); $distance = qq{$NODATA} if (!$distance); print $fh qq{

Checkpoints Activity

$buffers
Day Hour Written buffers Write time Sync time Total time
$files
Day Hour Added Removed Recycled Synced files Longest sync Average sync
$warnings
Day Hour Count Avg time (sec)
$distance
Day Hour Mean distance Mean estimate
Back to the top of the Checkpoint Activity table
}; } sub print_checkpoint_cause { my $curdb = shift; my %infos = (); my $total_count = 0; my $chkp_info = ''; my @main_checkpoint = ('unknown',0); foreach my $c (sort { $checkpoint_info{starting}{$b} <=> $checkpoint_info{starting}{$a} } keys %{$checkpoint_info{starting}}) { $chkp_info .= "$c" . &comma_numbers($checkpoint_info{starting}{$c}) . ""; $total_count += $checkpoint_info{starting}{$c}; if ($main_checkpoint[1] < $checkpoint_info{starting}{$c}) { $main_checkpoint[0] = $c; $main_checkpoint[1] = $checkpoint_info{starting}{$c}; } } $chkp_info .= "Total" . &comma_numbers($total_count) . ""; if ($graph) { my @small = (); foreach my $c (sort keys %{$checkpoint_info{starting}}) { if ((($checkpoint_info{starting}{$c} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$c} = $checkpoint_info{starting}{$c} || 0; } else { $infos{"Sum checkpoints cause < $pie_percentage_limit%"} += $checkpoint_info{starting}{$c} || 0; push(@small, $c); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum checkpoints cause < $pie_percentage_limit%"}; delete $infos{"Sum checkpoints cause < $pie_percentage_limit%"}; } } $drawn_graphs{checkpointcause_graph} = &jqplot_piegraph($graphid++, 'graph_checkpointcause', 'Checkpoint causes', %infos); $total_count = &comma_numbers($total_count); my $database = ''; if ($main_checkpoint[0] =~ s/^([^\.]+)\.//) { $database = $1; } $chkp_info = qq{$NODATA} if (!$total_count); print $fh qq{

Checkpoint causes

Key values

  • $main_checkpoint[0] ($main_checkpoint[1]) Main checkpoint cause
  • $total_count checkpoints Total
$drawn_graphs{checkpointcause_graph}
$chkp_info
Cause Number of checkpoints
}; delete $drawn_graphs{checkpointcause_graph}; } sub print_temporary_file { my $curdb = shift; # checkpoint my %graph_data = (); if ($graph) { foreach my $tm (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { $tm =~ /(\d{4})(\d{2})(\d{2})/; my $y = $1 - 1900; my $mo = $2 - 1; my $d = $3; foreach my $h ("00" .. "23") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}); my %chk_dataavg = (); my %t_dataavg = (); my %v_dataavg = (); foreach my $m ("00" .. "59") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}{$m}); my $rd = &average_per_minutes($m, $avg_minutes); if ($tempfile_info{$curdb}{count}) { $t_dataavg{size}{"$rd"} = 0 if (!exists $t_dataavg{size}{"$rd"}); $t_dataavg{count}{"$rd"} = 0 if (!exists $t_dataavg{count}{"$rd"}); if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{'tempfile'}) { $t_dataavg{size}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{'tempfile'}{size} || 0); $t_dataavg{count}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{'tempfile'}{count} || 0); } } } foreach my $rd (@avgs) { my $t = timegm_nocheck(0, $rd, $h, $d, $mo, $y) * 1000; $t += ($timezone*1000); next if ($t < $t_min); last if ($t > $t_max); if (exists $t_dataavg{size}) { $graph_data{size} .= "[$t, " . ($t_dataavg{size}{"$rd"} || 0) . "],"; $graph_data{count} .= "[$t, " . ($t_dataavg{count}{"$rd"} || 0) . "],"; } } } } foreach (keys %graph_data) { $graph_data{$_} =~ s/,$//; } } # Temporary file size $drawn_graphs{temporarydata_graph} = &jqplot_linegraph($graphid++, 'temporarydata_graph', $graph_data{size}, '', '', 'Size of temporary files (' . $avg_minutes . ' minutes period)', 'Size of files', 'Size of files' ); # Temporary file number $drawn_graphs{temporaryfile_graph} = &jqplot_linegraph($graphid++, 'temporaryfile_graph', $graph_data{count}, '', '', 'Number of temporary files (' . $avg_minutes . ' minutes period)', 'Number of files', 'Number of files' ); my $tempfile_size_peak = 0; my $tempfile_size_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{tempfile_size} <=> $overall_stat{$curdb}{'peak'}{$a}{tempfile_size}} keys %{$overall_stat{$curdb}{'peak'}}) { $tempfile_size_peak = &pretty_print_size($overall_stat{$curdb}{'peak'}{$_}{tempfile_size}); $tempfile_size_peak_date = $_ if ($tempfile_size_peak); last; } print $fh qq{

Temporary Files

Size of temporary files

Key values

  • $tempfile_size_peak Temp Files size Peak
  • $tempfile_size_peak_date Date
$drawn_graphs{temporarydata_graph}
}; delete $drawn_graphs{temporarydata_graph}; my $tempfile_count_peak = 0; my $tempfile_count_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{tempfile_count} <=> $overall_stat{$curdb}{'peak'}{$a}{tempfile_count}} keys %{$overall_stat{$curdb}{'peak'}}) { $tempfile_count_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{tempfile_count}); $tempfile_count_peak_date = $_ if ($tempfile_count_peak); last; } print $fh qq{

Number of temporary files

Key values

  • $tempfile_count_peak per second Temp Files Peak
  • $tempfile_count_peak_date Date
$drawn_graphs{temporaryfile_graph}
}; delete $drawn_graphs{temporaryfile_graph}; my $tempfiles_activity = ''; foreach my $d (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { $d =~ /^\d{4}(\d{2})(\d{2})$/; my $zday = "$abbr_month{$1} $2"; foreach my $h (sort {$a <=> $b} keys %{$per_minute_info{$curdb}{$d}}) { $tempfiles_activity .= "$zday$h"; $zday = ""; my %tinf = (); foreach my $m (keys %{$per_minute_info{$curdb}{$d}{$h}}) { if (exists $per_minute_info{$curdb}{$d}{$h}{$m}{'tempfile'}) { $tinf{size} += $per_minute_info{$curdb}{$d}{$h}{$m}{'tempfile'}{size}; $tinf{count} += $per_minute_info{$curdb}{$d}{$h}{$m}{'tempfile'}{count}; } } if (scalar keys %tinf) { my $temp_average = &pretty_print_size(sprintf("%.2f", $tinf{size} / $tinf{count})); $tempfiles_activity .= "" . &comma_numbers($tinf{count}) . "" . &pretty_print_size($tinf{size}) . "" . "$temp_average"; } else { $tempfiles_activity .= "000"; } } } $tempfiles_activity = qq{$NODATA} if (!$tempfiles_activity); print $fh qq{

Temporary Files Activity

$tempfiles_activity
Day Hour Count Total size Average size
Back to the top of the Temporary Files Activity table
}; } sub print_cancelled_queries { my $curdb = shift; my %graph_data = (); if ($graph) { foreach my $tm (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { $tm =~ /(\d{4})(\d{2})(\d{2})/; my $y = $1 - 1900; my $mo = $2 - 1; my $d = $3; foreach my $h ("00" .. "23") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}); my %chk_dataavg = (); my %t_dataavg = (); my %v_dataavg = (); foreach my $m ("00" .. "59") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}{$m}); my $rd = &average_per_minutes($m, $avg_minutes); if ($cancelled_info{count}) { $t_dataavg{count}{"$rd"} = 0 if (!exists $t_dataavg{count}{"$rd"}); if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{cancelled}) { $t_dataavg{count}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{cancelled}{count} || 0); } } } foreach my $rd (@avgs) { my $t = timegm_nocheck(0, $rd, $h, $d, $mo, $y) * 1000; $t += ($timezone*1000); next if ($t < $t_min); last if ($t > $t_max); if (exists $t_dataavg{count}) { $graph_data{count} .= "[$t, " . ($t_dataavg{count}{"$rd"} || 0) . "],"; } } } } foreach (keys %graph_data) { $graph_data{$_} =~ s/,$//; } } # Number of cancelled queries graph $drawn_graphs{cancelledqueries_graph} = &jqplot_linegraph($graphid++, 'cancelledqueries_graph', $graph_data{count}, '', '', 'Number of cancelled queries (' . $avg_minutes . ' minutes period)', 'Number of cancellation', 'Number of cancellation' ); my $cancelled_count_peak = 0; my $cancelled_count_peak_date = ''; foreach (sort {$overall_stat{$curdb}{'peak'}{$b}{cancelled_count} <=> $overall_stat{$curdb}{'peak'}{$a}{cancelled_count}} keys %{$overall_stat{$curdb}{'peak'}}) { $cancelled_count_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{cancelled_count}); $cancelled_count_peak_date = $_; last; } print $fh qq{

Number of cancelled queries

Key values

  • $cancelled_count_peak per second Cancelled query Peak
  • $cancelled_count_peak_date Date
$drawn_graphs{cancelledqueries_graph}
}; delete $drawn_graphs{cancelledqueries_graph}; } sub print_analyze_per_table { my $curdb = shift; # ANALYZE stats per table my %infos = (); my $total_count = 0; my $analyze_info = ''; my @main_analyze = ('unknown',0); foreach my $t (sort { $autoanalyze_info{$curdb}{tables}{$b}{analyzes} <=> $autoanalyze_info{$curdb}{tables}{$a}{analyzes} } keys %{$autoanalyze_info{$curdb}{tables}}) { $analyze_info .= "$t" . $autoanalyze_info{$curdb}{tables}{$t}{analyzes} . ""; $total_count += $autoanalyze_info{$curdb}{tables}{$t}{analyzes}; if ($main_analyze[1] < $autoanalyze_info{$curdb}{tables}{$t}{analyzes}) { $main_analyze[0] = $t; $main_analyze[1] = $autoanalyze_info{$curdb}{tables}{$t}{analyzes}; } } $analyze_info .= "Total" . &comma_numbers($total_count) . ""; if ($graph) { my @small = (); foreach my $d (sort keys %{$autoanalyze_info{$curdb}{tables}}) { if ((($autoanalyze_info{$curdb}{tables}{$d}{analyzes} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $autoanalyze_info{$curdb}{tables}{$d}{analyzes} || 0; } else { $infos{"Sum analyzes < $pie_percentage_limit%"} += $autoanalyze_info{$curdb}{tables}{$d}{analyzes} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum analyzes < $pie_percentage_limit%"}; delete $infos{"Sum analyzes < $pie_percentage_limit%"}; } } $drawn_graphs{tableanalyzes_graph} = &jqplot_piegraph($graphid++, 'graph_tableanalyzes', 'Analyzes per tables', %infos); $total_count = &comma_numbers($total_count); my $database = ''; if ($main_analyze[0] =~ s/^([^\.]+)\.//) { $database = $1; } $analyze_info = qq{$NODATA} if (!$total_count); print $fh qq{

Analyzes per table

Key values

  • $main_analyze[0] ($main_analyze[1]) Main table analyzed (database $database)
  • $total_count analyzes Total
$drawn_graphs{tableanalyzes_graph}
$analyze_info
Table Number of analyzes
}; delete $drawn_graphs{tableanalyzes_graph}; } sub print_vacuum { my $curdb = shift; # checkpoint my %graph_data = (); foreach my $tm (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { $tm =~ /(\d{4})(\d{2})(\d{2})/; my $y = $1 - 1900; my $mo = $2 - 1; my $d = $3; my $has_data = 0; foreach my $h ("00" .. "23") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}); my %chk_dataavg = (); my %t_dataavg = (); my %v_dataavg = (); foreach my $m ("00" .. "59") { next if (!exists $per_minute_info{$curdb}{$tm}{$h}{$m}); my $rd = &average_per_minutes($m, $avg_minutes); $v_dataavg{acount}{"$rd"} = 0 if (!exists $v_dataavg{acount}{"$rd"}); $v_dataavg{vcount}{"$rd"} = 0 if (!exists $v_dataavg{vcount}{"$rd"}); $v_dataavg{vmax}{"$rd"} = 0 if (!exists $v_dataavg{vmax}{"$rd"}); $v_dataavg{vmin}{"$rd"} = 0 if (!exists $v_dataavg{vmin}{"$rd"}); if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{autovacuum}) { $v_dataavg{vcount}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{autovacuum}{count} || 0); # Search minimum and maximum during this minute foreach my $s (keys %{$per_minute_info{$curdb}{$tm}{$h}{$m}{autovacuum}{second}}) { $v_dataavg{vmax}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{autovacuum}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{autovacuum}{second}{$s} > $v_dataavg{vmax}{"$rd"}); $v_dataavg{vmin}{"$rd"} = $per_minute_info{$curdb}{$tm}{$h}{$m}{autovacuum}{second}{$s} if ($per_minute_info{$curdb}{$tm}{$h}{$m}{autovacuum}{second}{$d} < $v_dataavg{vmin}{"$rd"}); } # delete $per_minute_info{$curdb}{$tm}{$h}{$m}{autovacuum}; # Used in report printing the #vacuums-activity } if (exists $per_minute_info{$curdb}{$tm}{$h}{$m}{autoanalyze}) { $v_dataavg{acount}{"$rd"} += ($per_minute_info{$curdb}{$tm}{$h}{$m}{autoanalyze}{count} || 0); } if ($v_dataavg{acount}{"$rd"} || $v_dataavg{vcount}{"$rd"}) { $has_data = 1; } } if ($has_data) { foreach my $rd (@avgs) { my $t = timegm_nocheck(0, $rd, $h, $d, $mo, $y) * 1000; $t += ($timezone*1000); next if ($t < $t_min); last if ($t > $t_max); if (scalar keys %v_dataavg) { # Average autovacuums per minute $graph_data{autovacuum_avg} .= "[$t, " . int(($v_dataavg{vcount}{"$rd"} || 0) / (60 * $avg_minutes)) . "],"; # Max autovacuums per minute $graph_data{autovacuum_max} .= "[$t, " . ($v_dataavg{vmax}{"$rd"} || 0) . "],"; # Min autovacuums per minute $graph_data{autovacuum_min} .= "[$t, " . ($v_dataavg{vmin}{"$rd"} || 0) . "],"; } if (exists $v_dataavg{vcount}{"$rd"}) { $graph_data{vcount} .= "[$t, " . ($v_dataavg{vcount}{"$rd"} || 0) . "],"; } if (exists $v_dataavg{acount}{"$rd"}) { $graph_data{acount} .= "[$t, " . ($v_dataavg{acount}{"$rd"} || 0) . "],"; } } } } } foreach (keys %graph_data) { $graph_data{$_} =~ s/,$//; } # VACUUMs vs ANALYZEs chart $drawn_graphs{autovacuum_graph} = $NODATA; $drawn_graphs{autovacuumpersecond_graph} = $NODATA; if ($graph) { $drawn_graphs{autovacuum_graph} = &jqplot_linegraph($graphid++, 'autovacuum_graph', $graph_data{vcount}, $graph_data{acount}, '', 'Autovacuum actions (' . $avg_minutes . ' minutes period)', '', 'VACUUMs', 'ANALYZEs' ); $drawn_graphs{autovacuumspersecond_graph} = &jqplot_linegraph($graphid++, 'autovacuumspersecond_graph', $graph_data{autovacuum_max}, $graph_data{autovacuum_avg}, $graph_data{autovacuum_min}, 'Average Autovacuum Duration (' . $avg_minutes . ' minutes average)', 'Average Autovacuum Duration', 'Maximum', 'Average', 'Mininum' ); } my $vacuum_size_peak = 0; my $vacuum_size_peak_date = ''; foreach (sort { $overall_stat{$curdb}{'peak'}{$b}{vacuum_size} <=> $overall_stat{$curdb}{'peak'}{$a}{vacuum_size} } keys %{$overall_stat{$curdb}{'peak'}}) { $vacuum_size_peak = &comma_numbers($overall_stat{$curdb}{'peak'}{$_}{vacuum_size}); $vacuum_size_peak_date = $_; last; } my $autovacuum_peak_system_usage_db = ''; if ($autovacuum_info{$curdb}{peak}{system_usage}{table} =~ s/^([^\.]+)\.//) { $autovacuum_peak_system_usage_db = $1; } my $autoanalyze_peak_system_usage_db = ''; if ($autoanalyze_info{$curdb}{peak}{system_usage}{table} =~ s/^([^\.]+)\.//) { $autoanalyze_peak_system_usage_db = $1; } $autovacuum_info{$curdb}{peak}{system_usage}{elapsed} ||= 0; $autoanalyze_info{$curdb}{peak}{system_usage}{elapsed} ||= 0; print $fh qq{

Vacuums

Vacuums / Analyzes Distribution

Key values

  • $autovacuum_info{$curdb}{peak}{system_usage}{elapsed} sec Highest CPU-cost vacuum
    Table $autovacuum_info{$curdb}{peak}{system_usage}{table}
    Database $autovacuum_peak_system_usage_db
  • $autovacuum_info{$curdb}{peak}{system_usage}{date} Date
  • $autoanalyze_info{$curdb}{peak}{system_usage}{elapsed} sec Highest CPU-cost analyze
    Table $autoanalyze_info{$curdb}{peak}{system_usage}{table}
    Database $autovacuum_peak_system_usage_db
  • $autoanalyze_info{$curdb}{peak}{system_usage}{date} Date
$drawn_graphs{autovacuum_graph}
}; delete $drawn_graphs{autovacuum_graph}; print $fh qq{

Average Autovacuum Duration

Key values

  • $autovacuum_info{$curdb}{peak}{system_usage}{elapsed} sec Highest CPU-cost vacuum
    Table $autovacuum_info{$curdb}{peak}{system_usage}{table}
    Database $autovacuum_peak_system_usage_db
  • $autovacuum_info{$curdb}{peak}{system_usage}{date} Date
$drawn_graphs{autovacuumspersecond_graph}
}; delete $drawn_graphs{autovacuum_graph}; # ANALYZE stats per table &print_analyze_per_table($curdb); # VACUUM stats per table &print_vacuum_per_table($curdb); &print_vacuum_throughput($curdb); # Show tuples and pages removed per table &print_vacuum_tuple_removed($curdb); &print_vacuum_page_removed($curdb); my $vacuum_activity = ''; foreach my $d (sort {$a <=> $b} keys %{$per_minute_info{$curdb}}) { my $c = 1; $d =~ /^\d{4}(\d{2})(\d{2})$/; my $zday = "$abbr_month{$1} $2"; foreach my $h (sort {$a <=> $b} keys %{$per_minute_info{$curdb}{$d}}) { $vacuum_activity .= "$zday$h"; $zday = ""; my %ainf = (); foreach my $m (keys %{$per_minute_info{$curdb}{$d}{$h}}) { if (exists $per_minute_info{$curdb}{$d}{$h}{$m}{autovacuum}{count}) { $ainf{vcount} += $per_minute_info{$curdb}{$d}{$h}{$m}{autovacuum}{count}; } if (exists $per_minute_info{$curdb}{$d}{$h}{$m}{autoanalyze}{count}) { $ainf{acount} += $per_minute_info{$curdb}{$d}{$h}{$m}{autoanalyze}{count}; } } if (scalar keys %ainf) { $vacuum_activity .= "" . &comma_numbers($ainf{vcount}) . ""; } else { $vacuum_activity .= "0"; } if (scalar keys %ainf) { $vacuum_activity .= "" . &comma_numbers($ainf{acount}) . ""; } else { $vacuum_activity .= "0"; } } } $vacuum_activity = qq{$NODATA} if (!$vacuum_activity); print $fh qq{

Autovacuum Activity

$vacuum_activity
Day Hour VACUUMs ANALYZEs
Back to the top of the Autovacuum Activity table
}; } sub print_vacuum_per_table { my $curdb = shift; # VACUUM stats per table my $total_count = 0; my $total_idxscan = 0; my $total_hits = 0; my $total_misses = 0; my $total_dirtied = 0; my $total_skippins = 0; my $total_skipfrozen = 0; my $total_records = 0; my $total_full_page = 0; my $total_bytes = 0; my $total_pages_frozen = 0; my $total_tuples_frozen = 0; my $vacuum_info = ''; my @main_vacuum = ('unknown',0); foreach my $t (sort { $autovacuum_info{$curdb}{tables}{$b}{vacuums} <=> $autovacuum_info{$curdb}{tables}{$a}{vacuums} } keys %{$autovacuum_info{$curdb}{tables}}) { $vacuum_info .= "$t" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{vacuums}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{idxscans}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{hits}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{missed}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{dirtied}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{skip_pins}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{skip_frozen}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{wal_record}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{wal_full_page}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{wal_bytes}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{pages}{frozen}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{tuples}{frozen}) . ""; $total_count += $autovacuum_info{$curdb}{tables}{$t}{vacuums}; $total_idxscan += $autovacuum_info{$curdb}{tables}{$t}{idxscans}; $total_hits += $autovacuum_info{$curdb}{tables}{$t}{hits}; $total_misses += $autovacuum_info{$curdb}{tables}{$t}{misses}; $total_dirtied += $autovacuum_info{$curdb}{tables}{$t}{dirtied}; $total_skippins += $autovacuum_info{$curdb}{tables}{$t}{skip_pins}; $total_skipfrozen += $autovacuum_info{$curdb}{tables}{$t}{skip_frozen}; $total_records += $autovacuum_info{$curdb}{tables}{$t}{wal_record}; $total_full_page += $autovacuum_info{$curdb}{tables}{$t}{wal_full_page}; $total_bytes += $autovacuum_info{$curdb}{tables}{$t}{wal_bytes}; $total_pages_frozen += $autovacuum_info{$curdb}{tables}{$t}{pages}{frozen}; $total_tuples_frozen += $autovacuum_info{$curdb}{tables}{$t}{tuples}{frozen}; if ($main_vacuum[1] < $autovacuum_info{$curdb}{tables}{$t}{vacuums}) { $main_vacuum[0] = $t; $main_vacuum[1] = $autovacuum_info{$curdb}{tables}{$t}{vacuums}; } } $vacuum_info .= "Total" . &comma_numbers($total_count); $vacuum_info .= "" . &comma_numbers($total_idxscan); $vacuum_info .= "" . &comma_numbers($total_hits); $vacuum_info .= "" . &comma_numbers($total_misses); $vacuum_info .= "" . &comma_numbers($total_dirtied); $vacuum_info .= "" . &comma_numbers($total_skippins); $vacuum_info .= "" . &comma_numbers($total_skipfrozen); $vacuum_info .= "" . &comma_numbers($total_records); $vacuum_info .= "" . &comma_numbers($total_full_page); $vacuum_info .= "" . &comma_numbers($total_bytes); $vacuum_info .= "" . &comma_numbers($total_pages_frozen); $vacuum_info .= "" . &comma_numbers($total_tuples_frozen); $vacuum_info .= ""; my %infos = (); my @small = (); foreach my $d (sort keys %{$autovacuum_info{$curdb}{tables}}) { if ((($autovacuum_info{$curdb}{tables}{$d}{vacuums} * 100) / ($total_count||1)) > $pie_percentage_limit) { $infos{$d} = $autovacuum_info{$curdb}{tables}{$d}{vacuums} || 0; } else { $infos{"Sum vacuums < $pie_percentage_limit%"} += $autovacuum_info{$curdb}{tables}{$d}{vacuums} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum vacuums < $pie_percentage_limit%"}; delete $infos{"Sum vacuums < $pie_percentage_limit%"}; } $drawn_graphs{tablevacuums_graph} = $NODATA; if ($graph) { $drawn_graphs{tablevacuums_graph} = &jqplot_piegraph($graphid++, 'graph_tablevacuums', 'Vacuums per tables', %infos); } $vacuum_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); my $database = ''; if ($main_vacuum[0] =~ s/^([^\.]+)\.//) { $database = $1; } print $fh qq{

Vacuums per table

Key values

  • $main_vacuum[0] ($main_vacuum[1]) Main table vacuumed on database $database
  • $total_count vacuums Total
$drawn_graphs{tablevacuums_graph}
$vacuum_info
 IndexBuffer usageSkippedWAL usageFrozen
Table Vacuums scans hits misses dirtied pins frozen records full page bytes pages tuples
}; delete $drawn_graphs{tablevacuums_graph}; } sub print_vacuum_throughput { my $curdb = shift; # VACUUM stats per table my $total_read = 0; my $total_write = 0; my $total_elapsed = 0; my $vacuum_info = ''; my @main_elapsed = ('unknown',0); my @main_write = ('unknown',0); my @main_read = ('unknown',0); foreach my $t (sort { $autovacuum_info{$curdb}{tables}{$b}{vacuums} <=> $autovacuum_info{$curdb}{tables}{$a}{vacuums} } keys %{$autovacuum_info{$curdb}{tables}}) { $vacuum_info .= "$t" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{read}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{write}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{elapsed}) . ""; $total_read += $autovacuum_info{$curdb}{tables}{$t}{read}; $total_write += $autovacuum_info{$curdb}{tables}{$t}{write}; $total_elapsed += $autovacuum_info{$curdb}{tables}{$t}{elapsed}; if ($main_elapsed[1] < $autovacuum_info{$curdb}{tables}{$t}{elapsed}) { $main_elapsed[0] = $t; $main_elapsed[1] = $autovacuum_info{$curdb}{tables}{$t}{elapsed}; } if ($main_write[1] < $autovacuum_info{$curdb}{tables}{$t}{write}) { $main_write[0] = $t; $main_write[1] = $autovacuum_info{$curdb}{tables}{$t}{write}; } if ($main_read[1] < $autovacuum_info{$curdb}{tables}{$t}{read}) { $main_read[0] = $t; $main_read[1] = $autovacuum_info{$curdb}{tables}{$t}{read}; } } $vacuum_info .= "Total" . &comma_numbers($total_read); $vacuum_info .= "" . &comma_numbers($total_write); $vacuum_info .= "" . &comma_numbers($total_elapsed); $vacuum_info .= ""; my %infos = (); my @small = (); foreach my $d (sort keys %{$autovacuum_info{$curdb}{tables}}) { if ((($autovacuum_info{$curdb}{tables}{$d}{elapsed} * 100) / ($total_elapsed||1)) > $pie_percentage_limit) { $infos{$d} = $autovacuum_info{$curdb}{tables}{$d}{elapsed} || 0; } else { $infos{"Sum CPU elapsed < $pie_percentage_limit%"} += $autovacuum_info{$curdb}{tables}{$d}{elapsed} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum CPU elapsed < $pie_percentage_limit%"}; delete $infos{"Sum CPU elapsed < $pie_percentage_limit%"}; } $drawn_graphs{tablevacuumselapsed_graph} = $NODATA; if ($graph) { $drawn_graphs{tablevacuumselapsed_graph} = &jqplot_piegraph($graphid++, 'graph_tablevacuumselapsed', 'CPU elapsed per tables', %infos); } $vacuum_info = qq{$NODATA} if (!$total_elapsed); $total_elapsed = &comma_numbers($total_elapsed); my $database = ''; if ($main_elapsed[0] =~ s/^([^\.]+)\.//) { $database = $1; } $main_read[0] =~ s/^([^\.]+)\.//; $main_write[0] =~ s/^([^\.]+)\.//; print $fh qq{

Vacuum throughput per table

Key values

  • $main_elapsed[0] ($main_elapsed[1]) Max CPU elapsed for vacuum on database $database
  • $main_read[0] ($main_read[1] ms) Max I/O read time for vacuum on database $database
  • $main_write[0] ($main_write[1] ms) Max I/O write time for vacuum on database $database
$drawn_graphs{tablevacuumselapsed_graph}
$vacuum_info
 I/O timing (ms)CPU (s)
Table read write elapsed
}; delete $drawn_graphs{tablevacuumselapsed_graph}; } sub print_vacuum_tuple_removed { my $curdb = shift; # VACUUM stats per table my $total_count = 0; my $total_idxscan = 0; my $total_tuple_remove = 0; my $total_tuple_remain = 0; my $total_tuple_notremovable = 0; my $total_page_remove = 0; my $total_page_remain = 0; my $vacuum_info = ''; my @main_tuple = ('unknown',0); foreach my $t (sort { $autovacuum_info{$curdb}{tables}{$b}{tuples}{removed} <=> $autovacuum_info{$curdb}{tables}{$a}{tuples}{removed} } keys %{$autovacuum_info{$curdb}{tables}}) { $vacuum_info .= "$t" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{vacuums}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{idxscans}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{tuples}{removed}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{tuples}{remain}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{tuples}{notremovable}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{pages}{removed}) . "" . &comma_numbers($autovacuum_info{$curdb}{tables}{$t}{pages}{remain}) . ""; $total_count += $autovacuum_info{$curdb}{tables}{$t}{vacuums}; $total_idxscan += $autovacuum_info{$curdb}{tables}{$t}{idxscans}; $total_tuple_remove += $autovacuum_info{$curdb}{tables}{$t}{tuples}{removed}; $total_tuple_remain += $autovacuum_info{$curdb}{tables}{$t}{tuples}{remain}; $total_tuple_notremovable += $autovacuum_info{$curdb}{tables}{$t}{tuples}{notremovable}; $total_page_remove += $autovacuum_info{$curdb}{tables}{$t}{pages}{removed}; $total_page_remain += $autovacuum_info{$curdb}{tables}{$t}{pages}{remain}; if ($main_tuple[1] < $autovacuum_info{$curdb}{tables}{$t}{tuples}{removed}) { $main_tuple[0] = $t; $main_tuple[1] = $autovacuum_info{$curdb}{tables}{$t}{tuples}{removed}; } } $vacuum_info .= "Total" . &comma_numbers($total_count); $vacuum_info .= "" . &comma_numbers($total_idxscan); $vacuum_info .= "" . &comma_numbers($total_tuple_remove); $vacuum_info .= "" . &comma_numbers($total_tuple_remain); $vacuum_info .= "" . &comma_numbers($total_tuple_notremovable); $vacuum_info .= "" . &comma_numbers($total_page_remove); $vacuum_info .= "" . &comma_numbers($total_page_remain); $vacuum_info .= ""; my %infos_tuple = (); my @small = (); foreach my $d (sort keys %{$autovacuum_info{$curdb}{tables}}) { if ((($autovacuum_info{$curdb}{tables}{$d}{tuples}{removed} * 100) / ($total_tuple_remove||1)) > $pie_percentage_limit) { $infos_tuple{$d} = $autovacuum_info{$curdb}{tables}{$d}{tuples}{removed} || 0; } else { $infos_tuple{"Sum tuples removed < $pie_percentage_limit%"} += $autovacuum_info{$curdb}{tables}{$d}{tuples}{removed} || 0; push(@small, $d); } } if ($#small == 0) { $infos_tuple{$small[0]} = $infos_tuple{"Sum tuples removed < $pie_percentage_limit%"}; delete $infos_tuple{"Sum tuples removed < $pie_percentage_limit%"}; } $drawn_graphs{tuplevacuums_graph} = $NODATA; if ($graph) { $drawn_graphs{tuplevacuums_graph} = &jqplot_piegraph($graphid++, 'graph_tuplevacuums', 'Tuples removed per tables', %infos_tuple); } $vacuum_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); my $database = ''; if ($main_tuple[0] =~ s/^([^\.]+)\.//) { $database = $1; } print $fh qq{

Tuples removed per table

Key values

  • $main_tuple[0] ($main_tuple[1]) Main table with removed tuples on database $database
  • $total_tuple_remove tuples Total removed
$drawn_graphs{tuplevacuums_graph}
$vacuum_info
 IndexTuplesPages
Table Vacuums scans removed remain not yet removable removed remain
}; delete $drawn_graphs{tuplevacuums_graph}; } sub print_vacuum_page_removed { my $curdb = shift; # VACUUM stats per table my $total_count = 0; my $total_idxscan = 0; my $total_tuple = 0; my $total_page = 0; my $vacuum_info = ''; my @main_tuple = ('unknown',0); my @main_page = ('unknown',0); my %infos_page = (); my @small = (); foreach my $t (sort { $autovacuum_info{$curdb}{tables}{$b}{pages}{removed} <=> $autovacuum_info{$curdb}{tables}{$a}{pages}{removed} } keys %{$autovacuum_info{$curdb}{tables}}) { $vacuum_info .= "$t" . $autovacuum_info{$curdb}{tables}{$t}{vacuums} . "" . $autovacuum_info{$curdb}{tables}{$t}{idxscans} . "" . $autovacuum_info{$curdb}{tables}{$t}{tuples}{removed} . "" . $autovacuum_info{$curdb}{tables}{$t}{pages}{removed} . ""; $total_count += $autovacuum_info{$curdb}{tables}{$t}{vacuums}; $total_idxscan += $autovacuum_info{$curdb}{tables}{$t}{idxscans}; $total_tuple += $autovacuum_info{$curdb}{tables}{$t}{tuples}{removed}; $total_page += $autovacuum_info{$curdb}{tables}{$t}{pages}{removed}; if ($main_page[1] < $autovacuum_info{$curdb}{tables}{$t}{pages}{removed}) { $main_page[0] = $t; $main_page[1] = $autovacuum_info{$curdb}{tables}{$t}{pages}{removed}; } if ($autovacuum_info{$curdb}{tables}{$t}{pages}{removed} > 0) { if ((($autovacuum_info{$curdb}{tables}{$t}{pages}{removed} * 100) / ($total_page || 1)) > $pie_percentage_limit) { $infos_page{$t} = $autovacuum_info{$curdb}{tables}{$t}{pages}{removed} || 0; } else { $infos_page{"Sum pages removed < $pie_percentage_limit%"} += $autovacuum_info{$curdb}{tables}{$t}{pages}{removed} || 0; push(@small, $t); } } } $vacuum_info .= "Total" . &comma_numbers($total_count) . "" . &comma_numbers($total_idxscan) . "" . &comma_numbers($total_tuple) . "" . &comma_numbers($total_page) . ""; if ($#small == 0) { $infos_page{$small[0]} = $infos_page{"Sum pages removed < $pie_percentage_limit%"}; delete $infos_page{"Sum pages removed < $pie_percentage_limit%"}; } $drawn_graphs{pagevacuums_graph} = $NODATA; if ($graph) { $drawn_graphs{pagevacuums_graph} = &jqplot_piegraph($graphid++, 'graph_pagevacuums', 'Pages removed per tables', %infos_page); } $vacuum_info = qq{$NODATA} if (!$total_count); $total_count = &comma_numbers($total_count); my $database = 'unknown'; if ($main_page[0] =~ s/^([^\.]+)\.//) { $database = $1; } print $fh qq{

Pages removed per table

Key values

  • $main_page[0] ($main_page[1]) Main table with removed pages on database $database
  • $total_page pages Total removed
$drawn_graphs{pagevacuums_graph}
$vacuum_info
Table Number of vacuums Index scans Tuples removed Pages removed
}; delete $drawn_graphs{pagevacuums_graph}; } sub print_lock_type { my $curdb = shift; my %locktype = (); my $total_count = 0; my $total_duration = 0; my $locktype_info = ''; my @main_locktype = ('unknown',0); foreach my $t (sort keys %{$lock_info{$curdb}}) { $locktype_info .= "$t" . &comma_numbers($lock_info{$curdb}{$t}{count}) . "" . &convert_time($lock_info{$curdb}{$t}{duration}) . "" . &convert_time($lock_info{$curdb}{$t}{duration} / ($lock_info{$curdb}{$t}{count} || 1)) . ""; $total_count += $lock_info{$curdb}{$t}{count}; $total_duration += $lock_info{$curdb}{$t}{duration}; if ($main_locktype[1] < $lock_info{$curdb}{$t}{count}) { $main_locktype[0] = $t; $main_locktype[1] = $lock_info{$curdb}{$t}{count}; } foreach my $o (sort keys %{$lock_info{$curdb}{$t}}) { next if (($o eq 'count') || ($o eq 'duration') || ($o eq 'chronos')); $locktype_info .= "$o" . &comma_numbers($lock_info{$curdb}{$t}{$o}{count}) . "" . &convert_time($lock_info{$curdb}{$t}{$o}{duration}) . "" . &convert_time($lock_info{$curdb}{$t}{$o}{duration} / $lock_info{$curdb}{$t}{$o}{count}) . "\n"; } } if ($total_count > 0) { $locktype_info .= "Total" . &comma_numbers($total_count) . "" . &convert_time($total_duration) . "" . &convert_time($total_duration / ($total_count || 1)) . ""; } else { $locktype_info = qq{$NODATA}; } if ($graph) { my @small = (); foreach my $d (sort keys %{$lock_info{$curdb}}) { if ((($lock_info{$curdb}{$d}{count} * 100) / ($total_count||1)) > $pie_percentage_limit) { $locktype{$d} = $lock_info{$curdb}{$d}{count} || 0; } else { $locktype{"Sum lock types < $pie_percentage_limit%"} += $lock_info{$curdb}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $locktype{$small[0]} = $locktype{"Sum types < $pie_percentage_limit%"}; delete $locktype{"Sum lock types < $pie_percentage_limit%"}; } } $drawn_graphs{lockbytype_graph} = &jqplot_piegraph($graphid++, 'graph_lockbytype', 'Type of locks', %locktype); $total_count = &comma_numbers($total_count); print $fh qq{

Locks

Locks by types

Key values

  • $main_locktype[0] Main Lock Type
  • $total_count locks Total
$drawn_graphs{lockbytype_graph}
$locktype_info
Type Object Count Total Duration Average Duration (s)
}; delete $drawn_graphs{lockbytype_graph}; } sub print_query_type { my $curdb = shift; my %data = (); my $total_queries = 0; my $total_select = 0; my $total_write = 0; foreach my $a (@SQL_ACTION) { $total_queries += $overall_stat{$curdb}{lc($a)}; if ($a eq 'SELECT') { $total_select += $overall_stat{$curdb}{lc($a)}; } elsif ($a ne 'OTHERS') { $total_write += $overall_stat{$curdb}{lc($a)}; } } my $total = $overall_stat{$curdb}{'queries_number'}; my $querytype_info = ''; foreach my $a (@SQL_ACTION) { $querytype_info .= "$a" . &comma_numbers($overall_stat{$curdb}{lc($a)}) . "" . sprintf("%0.2f", ($overall_stat{$curdb}{lc($a)} * 100) / ($total||1)) . "%"; } if (($total - $total_queries) > 0) { $querytype_info .= "OTHERS" . &comma_numbers($total - $total_queries) . "" . sprintf("%0.2f", (($total - $total_queries) * 100) / ($total||1)) . "%"; } $querytype_info = qq{$NODATA} if (!$total); if ($graph && $total) { foreach my $t (@SQL_ACTION) { if ((($overall_stat{$curdb}{lc($t)} * 100) / ($total||1)) > $pie_percentage_limit) { $data{$t} = $overall_stat{$curdb}{lc($t)} || 0; } else { $data{"Sum query types < $pie_percentage_limit%"} += $overall_stat{$curdb}{lc($t)} || 0; } } if (((($total - $total_queries) * 100) / ($total||1)) > $pie_percentage_limit) { $data{'Others'} = $total - $total_queries; } else { $data{"Sum query types < $pie_percentage_limit%"} += $total - $total_queries; } } $drawn_graphs{queriesbytype_graph} = &jqplot_piegraph($graphid++, 'graph_queriesbytype', 'Type of queries', %data); $total_select = &comma_numbers($total_select); $total_write = &comma_numbers($total_write); print $fh qq{

Queries

Queries by type

Key values

  • $total_select Total read queries
  • $total_write Total write queries
$drawn_graphs{queriesbytype_graph}
$querytype_info
Type Count Percentage
}; delete $drawn_graphs{queriesbytype_graph}; } sub print_query_per_database { my $curdb = shift; my %infos = (); my $total_count = 0; my $query_database_info = ''; my @main_database = ('unknown', 0); my @main_database_duration = ('unknown', 0); foreach my $d (sort keys %{$database_info{$curdb}}) { $query_database_info .= "$dTotal" . &comma_numbers($database_info{$curdb}{$d}{count}) . "" . &convert_time($database_info{$curdb}{$d}{duration}) . ""; $total_count += $database_info{$curdb}{$d}{count}; if ($main_database[1] < $database_info{$curdb}{$d}{count}) { $main_database[0] = $d; $main_database[1] = $database_info{$curdb}{$d}{count}; } if ($main_database_duration[1] < $database_info{$curdb}{$d}{duration}) { $main_database_duration[0] = $d; $main_database_duration[1] = $database_info{$curdb}{$d}{duration}; } foreach my $r (sort keys %{$database_info{$curdb}{$d}}) { next if (($r eq 'count') || ($r =~ /duration/)); $query_database_info .= "$r" . &comma_numbers($database_info{$curdb}{$d}{$r}) . "" . &convert_time($database_info{$curdb}{$d}{"$r|duration"}) . ""; } } $query_database_info = qq{$NODATA} if (!$total_count); if ($graph) { my @small = (); foreach my $d (sort keys %{$database_info{$curdb}}) { if ((($database_info{$curdb}{$d}{count} * 100) / ($total_count || 1)) > $pie_percentage_limit) { $infos{$d} = $database_info{$curdb}{$d}{count} || 0; } else { $infos{"Sum queries per databases < $pie_percentage_limit%"} += $database_info{$curdb}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum queries per databases < $pie_percentage_limit%"}; delete $infos{"Sum queries per databases < $pie_percentage_limit%"}; } } $drawn_graphs{queriesbydatabase_graph} = &jqplot_piegraph($graphid++, 'graph_queriesbydatabase', 'Queries per database', %infos); $main_database[1] = &comma_numbers($main_database[1]); $main_database_duration[1] = &convert_time($main_database_duration[1]); print $fh qq{

Queries by database

Key values

  • $main_database[0] Main database
  • $main_database[1] Requests
  • $main_database_duration[1] ($main_database_duration[0])
  • Main time consuming database
$drawn_graphs{queriesbydatabase_graph}
$query_database_info
Database Request type Count Duration
}; delete $drawn_graphs{queriesbydatabase_graph}; } sub print_query_per_application { my $curdb = shift; my %infos = (); my $total_count = 0; my $query_application_info = ''; my @main_application = ('unknown', 0); my @main_application_duration = ('unknown', 0); foreach my $d (sort keys %{$application_info{$curdb}}) { $query_application_info .= "$dTotal" . &comma_numbers($application_info{$curdb}{$d}{count}) . "" . &convert_time($application_info{$curdb}{$d}{duration}) . ""; $total_count += $application_info{$curdb}{$d}{count}; if ($main_application[1] < $application_info{$curdb}{$d}{count}) { $main_application[0] = $d; $main_application[1] = $application_info{$curdb}{$d}{count}; } if ($main_application_duration[1] < $application_info{$curdb}{$d}{duration}) { $main_application_duration[0] = $d; $main_application_duration[1] = $application_info{$curdb}{$d}{duration}; } foreach my $r (sort keys %{$application_info{$curdb}{$d}}) { next if (($r eq 'count') || ($r =~ /duration/)); $query_application_info .= "$r" . &comma_numbers($application_info{$curdb}{$d}{$r}) . "" . &convert_time($application_info{$curdb}{$d}{"$r|duration"}) . ""; } } $query_application_info = qq{$NODATA} if (!$total_count); if ($graph) { my @small = (); foreach my $d (sort keys %{$application_info{$curdb}}) { if ((($application_info{$curdb}{$d}{count} * 100) / ($total_count || 1)) > $pie_percentage_limit) { $infos{$d} = $application_info{$curdb}{$d}{count} || 0; } else { $infos{"Sum queries per applications < $pie_percentage_limit%"} += $application_info{$curdb}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum queries per applications < $pie_percentage_limit%"}; delete $infos{"Sum queries per applications < $pie_percentage_limit%"}; } } $drawn_graphs{queriesbyapplication_graph} = &jqplot_piegraph($graphid++, 'graph_queriesbyapplication', 'Queries per application', %infos); $main_application[1] = &comma_numbers($main_application[1]); $main_application_duration[1] = &convert_time($main_application_duration[1]); print $fh qq{

Queries by application

Key values

  • $main_application[0] Main application
  • $main_application[1] Requests
  • $main_application_duration[1] ($main_application_duration[0])
  • Main time consuming application
$drawn_graphs{queriesbyapplication_graph}
$query_application_info
Application Request type Count Duration
}; delete $drawn_graphs{queriesbyapplication_graph}; } sub print_query_per_user { my $curdb = shift; my %infos = (); my $total_count = 0; my $total_duration = 0; my $query_user_info = ''; my @main_user = ('unknown', 0); my @main_user_duration = ('unknown', 0); foreach my $d (sort keys %{$user_info{$curdb}}) { $query_user_info .= "$dTotal" . &comma_numbers($user_info{$curdb}{$d}{count}) . "" . &convert_time($user_info{$curdb}{$d}{duration}) . ""; $total_count += $user_info{$curdb}{$d}{count}; $total_duration += $user_info{$curdb}{$d}{duration}; if ($main_user[1] < $user_info{$curdb}{$d}{count}) { $main_user[0] = $d; $main_user[1] = $user_info{$curdb}{$d}{count}; } if ($main_user_duration[1] < $user_info{$curdb}{$d}{duration}) { $main_user_duration[0] = $d; $main_user_duration[1] = $user_info{$curdb}{$d}{duration}; } foreach my $r (sort keys %{$user_info{$curdb}{$d}}) { next if (($r eq 'count') || ($r =~ /duration/)); $query_user_info .= "$r" . &comma_numbers($user_info{$curdb}{$d}{$r}) . "" . &convert_time($user_info{$curdb}{$d}{"$r|duration"}) . ""; } } $query_user_info = qq{$NODATA} if (!$total_count); if ($graph) { my %small = (); foreach my $d (sort keys %{$user_info{$curdb}}) { if ((($user_info{$curdb}{$d}{count} * 100) / ($total_count || 1)) > $pie_percentage_limit) { $infos{queries}{$d} = $user_info{$curdb}{$d}{count} || 0; } else { $infos{queries}{"Sum queries per users < $pie_percentage_limit%"} += $user_info{$curdb}{$d}{count} || 0; push(@{$small{queries}}, $d); } if ((($user_info{$curdb}{$d}{duration} * 100) / ($total_duration || 1)) > $pie_percentage_limit) { $infos{duration}{$d} = $user_info{$curdb}{$d}{duration} || 0; } else { $infos{duration}{"Sum duration per users < $pie_percentage_limit%"} += $user_info{$curdb}{$d}{duration} || 0; push(@{$small{duration}}, $d); } } if ($#{$small{queries}} == 0) { $infos{queries}{$small{queries}[0]} = $infos{queries}{"Sum queries per users < $pie_percentage_limit%"}; delete $infos{queries}{"Sum queries per users < $pie_percentage_limit%"}; } if ($#{$small{duration}} == 0){ $infos{duration}{$small{duration}[0]} = $infos{duration}{"Sum duration per users < $pie_percentage_limit%"}; delete $infos{duration}{"Sum duration per users < $pie_percentage_limit%"}; } } $drawn_graphs{queriesbyuser_graph} = &jqplot_piegraph($graphid++, 'graph_queriesbyuser', 'Queries per user', %{$infos{queries}}); $drawn_graphs{durationbyuser_graph} = &jqplot_piegraph($graphid++, 'graph_durationbyuser', 'Duration per user', %{$infos{duration}}); $main_user[1] = &comma_numbers($main_user[1]); $main_user_duration[1] = &convert_time($main_user_duration[1]); print $fh qq{

Queries by user

Key values

  • $main_user[0] Main user
  • $main_user[1] Requests
$drawn_graphs{queriesbyuser_graph}
$query_user_info
User Request type Count Duration
}; delete $drawn_graphs{queriesbyuser_graph}; print $fh qq{

Duration by user

Key values

  • $main_user_duration[1] ($main_user_duration[0]) Main time consuming user
$drawn_graphs{durationbyuser_graph}
$query_user_info
User Request type Count Duration
}; delete $drawn_graphs{durationbyuser_graph}; } sub print_query_per_host { my $curdb = shift; my %infos = (); my $total_count = 0; my $query_host_info = ''; my @main_host = ('unknown', 0); my @main_host_duration = ('unknown', 0); foreach my $d (sort keys %{$host_info{$curdb}}) { $query_host_info .= "$dTotal" . &comma_numbers($host_info{$curdb}{$d}{count}) . "" . &convert_time($host_info{$curdb}{$d}{duration}) . ""; $total_count += $host_info{$curdb}{$d}{count}; if ($main_host[1] < $host_info{$curdb}{$d}{count}) { $main_host[0] = $d; $main_host[1] = $host_info{$curdb}{$d}{count}; } if ($main_host_duration[1] < $host_info{$curdb}{$d}{duration}) { $main_host_duration[0] = $d; $main_host_duration[1] = $host_info{$curdb}{$d}{duration}; } foreach my $r (sort keys %{$host_info{$curdb}{$d}}) { next if (($r eq 'count') || ($r =~ /duration/)); $query_host_info .= "$r" . &comma_numbers($host_info{$curdb}{$d}{$r}) . "" . &convert_time($host_info{$curdb}{$d}{"$r|duration"}) . ""; } } $query_host_info = qq{$NODATA} if (!$total_count); if ($graph) { my @small = (); foreach my $d (sort keys %{$host_info{$curdb}}) { if ((($host_info{$curdb}{$d}{count} * 100) / ($total_count || 1)) > $pie_percentage_limit) { $infos{$d} = $host_info{$curdb}{$d}{count} || 0; } else { $infos{"Sum queries per hosts < $pie_percentage_limit%"} += $host_info{$curdb}{$d}{count} || 0; push(@small, $d); } } if ($#small == 0) { $infos{$small[0]} = $infos{"Sum queries per hosts < $pie_percentage_limit%"}; delete $infos{"Sum queries per hosts < $pie_percentage_limit%"}; } } $drawn_graphs{queriesbyhost_graph} = &jqplot_piegraph($graphid++, 'graph_queriesbyhost', 'Queries per host', %infos); $main_host[1] = &comma_numbers($main_host[1]); $main_host_duration[1] = &convert_time($main_host_duration[1]); print $fh qq{

Queries by host

Key values

  • $main_host[0] Main host
  • $main_host[1] Requests
  • $main_host_duration[1] ($main_host_duration[0])
  • Main time consuming host
$drawn_graphs{queriesbyhost_graph}
$query_host_info
Host Request type Count Duration
}; delete $drawn_graphs{queriesbyhost_graph}; } sub display_plan { my ($id, $plan) = @_; # Only TEXT format plan can be sent to Depesz site. if ($plan !~ /Node Type:|"Node Type":|Node-Type/s) { return "
Explain plan
\n
\n
" . $plan . "
\n
\n"; } else { return "
Explain plan
\n
\n
" . $plan . "
\n
\n"; } } sub print_lock_queries_report { my $curdb = shift; my @top_locked_queries = (); foreach my $h (keys %{$normalyzed_info{$curdb}}) { if (exists($normalyzed_info{$curdb}{$h}{locks})) { push (@top_locked_queries, [$h, $normalyzed_info{$curdb}{$h}{locks}{count}, $normalyzed_info{$curdb}{$h}{locks}{wait}, $normalyzed_info{$curdb}{$h}{locks}{minwait}, $normalyzed_info{$curdb}{$h}{locks}{maxwait}]); } } # Most frequent waiting queries (N) @top_locked_queries = sort {$b->[2] <=> $a->[2]} @top_locked_queries; print $fh qq{

Most frequent waiting queries (N)

}; my $rank = 1; for (my $i = 0 ; $i <= $#top_locked_queries ; $i++) { my $count = &comma_numbers($top_locked_queries[$i]->[1]); my $total_time = &convert_time($top_locked_queries[$i]->[2]); my $min_time = &convert_time($top_locked_queries[$i]->[3]); my $max_time = &convert_time($top_locked_queries[$i]->[4]); my $avg_time = &convert_time($top_locked_queries[$i]->[2] / ($top_locked_queries[$i]->[1] || 1)); my $query = &highlight_code(&anonymize_query($top_locked_queries[$i]->[0])); my $md5 = ''; $md5 = 'md5: ' . md5_hex($top_locked_queries[$i]->[0]) if ($enable_checksum); my $example = qq{

}; my $k = $top_locked_queries[$i]->[0]; $example = '' if (scalar keys %{$normalyzed_info{$curdb}{$k}{samples}} == 0); print $fh qq{ }; } $rank++; } if ($#top_locked_queries == -1) { print $fh qq{}; } print $fh qq{
Rank Count Total time Min time Max time Avg duration Query
$rank $count $total_time $min_time $max_time $avg_time
$query
$md5 $example
}; if (scalar keys %{$normalyzed_info{$curdb}{$k}{samples}}) { my $idx = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($idx > $sample); $query = &highlight_code(&anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query})); $md5 = ''; $md5 = 'md5: ' . md5_hex($normalyzed_info{$curdb}{$k}{samples}{$d}{query}) if ($enable_checksum); my $details = "Date: $normalyzed_info{$curdb}{$k}{samples}{$d}{date}\n"; $details .= "Duration: " . &convert_time($d) . "\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{duration}); $details .= "Database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $details .= "User: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $details .= "Remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $details .= "Application: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $details .= "Queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $details .= "Bind query: yes\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $details .= "Log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); my $explain = ''; if ($normalyzed_info{$curdb}{$k}{samples}{$d}{plan}) { $explain = &display_plan("query-a-explain-$rank", $normalyzed_info{$curdb}{$k}{samples}{$d}{plan}); } print $fh qq{
$query
$md5
$details
$explain
}; $idx++; } print $fh qq{

$NODATA
}; @top_locked_queries = (); # Queries that waited the most @{$top_locked_info{$curdb}} = sort {$b->[1] <=> $a->[1]} @{$top_locked_info{$curdb}}; print $fh qq{

Queries that waited the most

}; $rank = 1; for (my $i = 0 ; $i <= $#{$top_locked_info{$curdb}} ; $i++) { my $query = &highlight_code(&anonymize_query($top_locked_info{$curdb}[$i]->[2])); my $md5 = ''; $md5 = 'md5: ' . md5_hex($top_locked_info{$curdb}[$i]->[2]) if ($enable_checksum); my $details = "[ Date: " . ($top_locked_info{$curdb}[$i]->[1] || ''); $details .= " - Database: $top_locked_info{$curdb}[$i]->[3]" if ($top_locked_info{$curdb}[$i]->[3]); $details .= " - User: $top_locked_info{$curdb}[$i]->[4]" if ($top_locked_info{$curdb}[$i]->[4]); $details .= " - Remote: $top_locked_info{$curdb}[$i]->[5]" if ($top_locked_info{$curdb}[$i]->[5]); $details .= " - Application: $top_locked_info{$curdb}[$i]->[6]" if ($top_locked_info{$curdb}[$i]->[6]); $details .= " - Queryid: $top_locked_info{$curdb}[$i]->[7]" if ($top_locked_info{$curdb}[$i]->[7]); $details .= " - Bind query: yes" if ($top_locked_info{$curdb}[$i]->[8]); $details .= " ]"; my $time = &convert_time($top_locked_info{$curdb}[$i]->[0]); print $fh qq{ }; $rank++; } if ($#{$top_locked_info{$curdb}} == -1) { print $fh qq{}; } print $fh qq{
Rank Wait time Query
$rank $time
$query
$md5
$details
$NODATA
}; } sub print_tempfile_report { my $curdb = shift; my @top_temporary = (); foreach my $h (keys %{$normalyzed_info{$curdb}}) { if (exists($normalyzed_info{$curdb}{$h}{tempfiles})) { push (@top_temporary, [$h, $normalyzed_info{$curdb}{$h}{tempfiles}{count}, $normalyzed_info{$curdb}{$h}{tempfiles}{size}, $normalyzed_info{$curdb}{$h}{tempfiles}{minsize}, $normalyzed_info{$curdb}{$h}{tempfiles}{maxsize}]); } } # Queries generating the most temporary files (N) if ($#top_temporary >= 0) { @top_temporary = sort { $b->[1] <=> $a->[1] } @top_temporary; print $fh qq{

Queries generating the most temporary files (N)

}; my $rank = 1; for (my $i = 0 ; $i <= $#top_temporary ; $i++) { my $count = &comma_numbers($top_temporary[$i]->[1]); my $total_size = &pretty_print_size($top_temporary[$i]->[2]); my $min_size = &pretty_print_size($top_temporary[$i]->[3]); my $max_size = &pretty_print_size($top_temporary[$i]->[4]); my $avg_size = &pretty_print_size($top_temporary[$i]->[2] / ($top_temporary[$i]->[1] || 1)); my $query = &highlight_code(&anonymize_query($top_temporary[$i]->[0])); my $md5 = ''; $md5 = 'md5: ' . md5_hex($top_temporary[$i]->[0]) if ($enable_checksum); my $example = qq{

}; my $k = $top_temporary[$i]->[0]; $example = '' if (scalar keys %{$normalyzed_info{$curdb}{$k}{samples}} == 0); print $fh qq{ }; } $rank++; } print $fh qq{
Rank Count Total size Min size Max size Avg size Query
$rank $count $total_size $min_size $max_size $avg_size
$query
$md5 $example
}; if (scalar keys %{$normalyzed_info{$curdb}{$k}{samples}}) { my $idx = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($idx > $sample); $query = &highlight_code(&anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query})); my $md5 = ''; $md5 = 'md5: ' . md5_hex($normalyzed_info{$curdb}{$k}{samples}{$d}{query}) if ($enable_checksum); my $details = "Date: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{date} . "\n"; $details .= "Duration: " . &convert_time($d) . "\n"; $details .= "Database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $details .= "User: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $details .= "Remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $details .= "Application: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $details .= "Queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $details .= "Log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); if (exists $top_tempfile_info{$curdb} && $#{$top_tempfile_info{$curdb}} >= $i) { $details .= "Info: $top_tempfile_info{$curdb}[$i]->[7]" if ($top_tempfile_info{$curdb}[$i]->[7]); } print $fh qq{
$query
$md5
$details
}; $idx++ } print $fh qq{

}; @top_temporary = (); } # Top queries generating the largest temporary files if ($#{$top_tempfile_info{$curdb}} >= 0) { @{$top_tempfile_info{$curdb}} = sort {$b->[0] <=> $a->[0]} @{$top_tempfile_info{$curdb}}; my $largest = &comma_numbers($top_temporary[0]->[0]); print $fh qq{

Queries generating the largest temporary files

}; my $rank = 1; for (my $i = 0 ; $i <= $#{$top_tempfile_info{$curdb}} ; $i++) { my $size = &pretty_print_size($top_tempfile_info{$curdb}[$i]->[0]); my $details = "[ Date: $top_tempfile_info{$curdb}[$i]->[1]"; $details .= " - Database: $top_tempfile_info{$curdb}[$i]->[3]" if ($top_tempfile_info{$curdb}[$i]->[3]); $details .= " - User: $top_tempfile_info{$curdb}[$i]->[4]" if ($top_tempfile_info{$curdb}[$i]->[4]); $details .= " - Remote: $top_tempfile_info{$curdb}[$i]->[5]" if ($top_tempfile_info{$curdb}[$i]->[5]); $details .= " - Application: $top_tempfile_info{$curdb}[$i]->[6]" if ($top_tempfile_info{$curdb}[$i]->[6]); $details .= " - Queryid: $top_tempfile_info{$curdb}[$i]->[8]" if ($top_tempfile_info{$curdb}[$i]->[8]); $details .= " ]"; $details .= "\nInfo: $top_tempfile_info{$curdb}[$i]->[7]" if ($top_tempfile_info{$curdb}[$i]->[7]); my $query = &highlight_code(&anonymize_query($top_tempfile_info{$curdb}[$i]->[2])); my $md5 = ''; $md5 = 'md5: ' . md5_hex($top_tempfile_info{$curdb}[$i]->[2]) if ($enable_checksum); print $fh qq{ }; $rank++; } print $fh qq{
Rank Size Query
$rank $size
$query
$md5
$details
}; @{$top_tempfile_info{$curdb}} = (); } } sub print_cancelled_report { my $curdb = shift(); my @top_cancelled = (); foreach my $h (keys %{$normalyzed_info{$curdb}}) { if (exists($normalyzed_info{$curdb}{$h}{cancelled})) { push (@top_cancelled, [$h, $normalyzed_info{$curdb}{$h}{cancelled}{count}]); } } # Queries generating the most cancellation (N) if ($#top_cancelled >= 0) { @top_cancelled = sort {$b->[1] <=> $a->[1]} @top_cancelled; print $fh qq{

Queries generating the most cancellation (N)

}; my $rank = 1; for (my $i = 0 ; $i <= $#top_cancelled ; $i++) { my $count = &comma_numbers($top_cancelled[$i]->[1]); my $query = &highlight_code($top_cancelled[$i]->[0]); my $md5 = ''; $md5 = 'md5: ' . md5_hex($top_cancelled[$i]->[0]) if ($enable_checksum); my $example = qq{

}; my $k = $top_cancelled[$i]->[0]; $example = '' if (scalar keys %{$normalyzed_info{$curdb}{$k}{samples}} == 0); print $fh qq{ }; } $rank++; } print $fh qq{
Rank Count Query
$rank $count
$query
$md5 $example
}; if (scalar keys %{$normalyzed_info{$curdb}{$k}{samples}}) { my $idx = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($idx > $sample); $query = &highlight_code($normalyzed_info{$curdb}{$k}{samples}{$d}{query}); my $md5 = ''; $md5 = 'md5: ' . md5_hex($normalyzed_info{$curdb}{$k}{samples}{$d}{query}) if ($enable_checksum); my $details = "Duration: " . &convert_time($d) . "
"; $details .= "Database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}
" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $details .= "User: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}
" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $details .= "Remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}
" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $details .= "Application: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}
" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $details .= "Queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}
" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $details .= "Bind query: yes
" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $details .= "log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); print $fh qq{
$query
$md5
$details
}; $idx++ } print $fh qq{

}; @top_cancelled = (); } # Top queries generating the most cancellation if ($#{$top_cancelled_info{$curdb}} >= 0) { @{$top_cancelled_info{$curdb}} = sort {$b->[0] <=> $a->[0]} @{$top_cancelled_info{$curdb}}; my $largest = &comma_numbers($top_cancelled_info{$curdb}[0]->[0]); print $fh qq{

Queries most cancelled

}; my $rank = 1; for (my $i = 0 ; $i <= $#{$top_cancelled_info{$curdb}} ; $i++) { my $count = &comma_numbers($top_cancelled_info{$curdb}[$i]->[0]); my $details = "[ Date: $top_cancelled_info{$curdb}[$i]->[1]"; $details .= " - Database: $top_cancelled_info{$curdb}[$i]->[3]" if ($top_cancelled_info{$curdb}[$i]->[3]); $details .= " - User: $top_cancelled_info{$curdb}[$i]->[4]" if ($top_cancelled_info{$curdb}[$i]->[4]); $details .= " - Remote: $top_cancelled_info{$curdb}[$i]->[5]" if ($top_cancelled_info{$curdb}[$i]->[5]); $details .= " - Application: $top_cancelled_info{$curdb}[$i]->[6]" if ($top_cancelled_info{$curdb}[$i]->[6]); $details .= " - Queryid: $top_cancelled_info{$curdb}[$i]->[7]" if ($top_cancelled_info{$curdb}[$i]->[7]); $details .= " - Bind yes: yes" if ($top_cancelled_info{$curdb}[$i]->[8]); $details .= " ]"; my $query = &highlight_code(&anonymize_query($top_cancelled_info{$curdb}[$i]->[2])); my $md5 = ''; $md5 = 'md5: ' . md5_hex($top_cancelled_info{$curdb}[$i]->[2]) if ($enable_checksum); print $fh qq{ }; $rank++; } print $fh qq{
Rank Number Query
$rank $count
$query
$md5
$details
}; @{$top_cancelled_info{$curdb}} = (); } } sub print_histogram_query_times { my $curdb = shift; my %data = (); my $histogram_info = ''; my $most_range = ''; my $most_range_value = ''; for (my $i = 1; $i <= $#histogram_query_time; $i++) { $histogram_info .= "$histogram_query_time[$i-1]-$histogram_query_time[$i]ms" . &comma_numbers($overall_stat{$curdb}{histogram}{query_time}{$histogram_query_time[$i-1]}) . "" . sprintf("%0.2f", ($overall_stat{$curdb}{histogram}{query_time}{$histogram_query_time[$i-1]} * 100) / ($overall_stat{$curdb}{histogram}{query_total}||1)) . "%"; $data{"$histogram_query_time[$i-1]-$histogram_query_time[$i]ms"} = ($overall_stat{$curdb}{histogram}{query_time}{$histogram_query_time[$i-1]} || 0); if ($overall_stat{$curdb}{histogram}{query_time}{$histogram_query_time[$i-1]} > $most_range_value) { $most_range = "$histogram_query_time[$i-1]-$histogram_query_time[$i]ms"; $most_range_value = $overall_stat{$curdb}{histogram}{query_time}{$histogram_query_time[$i-1]}; } } if ($overall_stat{$curdb}{histogram}{query_total} > 0) { $data{"> $histogram_query_time[-1]ms"} = ($overall_stat{$curdb}{histogram}{query_time}{"-1"} || 0); $histogram_info .= " > $histogram_query_time[-1]ms" . &comma_numbers($overall_stat{$curdb}{histogram}{query_time}{'-1'}) . "" . sprintf("%0.2f", ($overall_stat{$curdb}{histogram}{query_time}{'-1'} * 100) / ($overall_stat{$curdb}{histogram}{query_total}||1)) . "%"; $data{"> $histogram_query_time[-1]ms"} = $overall_stat{$curdb}{histogram}{query_time}{"-1"} if ($overall_stat{$curdb}{histogram}{query_time}{"-1"} > 0); if ($overall_stat{$curdb}{histogram}{query_time}{"-1"} > $most_range_value) { $most_range = "> $histogram_query_time[-1]ms"; $most_range_value = $overall_stat{$curdb}{histogram}{query_time}{"-1"}; } } else { $histogram_info = qq{$NODATA}; } $drawn_graphs{histogram_query_times_graph} = $NODATA; if ($graph) { $drawn_graphs{histogram_query_times_graph} = &jqplot_duration_histograph($graphid++, 'graph_histogram_query_times', 'Queries', \@histogram_query_time, %data); } $most_range_value = &comma_numbers($most_range_value) if ($most_range_value); print $fh qq{

Top Queries

Histogram of query times

Key values

  • $most_range_value $most_range duration
$drawn_graphs{histogram_query_times_graph}
$histogram_info
Range Count Percentage
}; delete $drawn_graphs{histogram_query_times_graph}; } sub print_slowest_individual_queries { my $curdb = shift; print $fh qq{

Slowest individual queries

}; my $idx = 1; for (my $i = 0 ; $i <= $#{$top_slowest{$curdb}} ; $i++) { my $rank = $i + 1; my $duration = &convert_time($top_slowest{$curdb}[$i]->[0]); my $date = $top_slowest{$curdb}[$i]->[1] || ''; my $details = "[ Date: " . ($top_slowest{$curdb}[$i]->[1] || ''); $details .= " - Database: $top_slowest{$curdb}[$i]->[3]" if ($top_slowest{$curdb}[$i]->[3]); $details .= " - User: $top_slowest{$curdb}[$i]->[4]" if ($top_slowest{$curdb}[$i]->[4]); $details .= " - Remote: $top_slowest{$curdb}[$i]->[5]" if ($top_slowest{$curdb}[$i]->[5]); $details .= " - Application: $top_slowest{$curdb}[$i]->[6]" if ($top_slowest{$curdb}[$i]->[6]); $details .= " - Queryid: $top_slowest{$curdb}[$i]->[9]" if ($top_slowest{$curdb}[$i]->[9]); $details .= " - Bind query: yes" if ($top_slowest{$curdb}[$i]->[7]); $details .= " ]"; my $explain = ''; if ($top_slowest{$curdb}[$i]->[8]) { $explain = &display_plan("query-d-explain-$rank-$idx", $top_slowest{$curdb}[$i]->[8]); } my $query = &highlight_code(&anonymize_query($top_slowest{$curdb}[$i]->[2])); my $md5 = ''; $md5 = 'md5: ' . md5_hex($top_slowest{$curdb}[$i]->[2]) if ($enable_checksum); print $fh qq{ }; $idx++; } if ($#{$top_slowest{$curdb}} == -1) { print $fh qq{}; } print $fh qq{
Rank Duration Query
$rank $duration
$query
$md5
$details
$explain
$NODATA
}; } sub print_time_consuming { my $curdb = shift; print $fh qq{

Time consuming queries (N)

}; my $rank = 1; my $found = 0; foreach my $k (sort {$normalyzed_info{$curdb}{$b}{duration} <=> $normalyzed_info{$curdb}{$a}{duration}} keys %{$normalyzed_info{$curdb}}) { next if (!$normalyzed_info{$curdb}{$k}{count} || !exists $normalyzed_info{$curdb}{$k}{duration}); last if ($rank > $top); $found++; $normalyzed_info{$curdb}{$k}{average} = $normalyzed_info{$curdb}{$k}{duration} / $normalyzed_info{$curdb}{$k}{count}; my $duration = &convert_time($normalyzed_info{$curdb}{$k}{duration}); my $count = &comma_numbers($normalyzed_info{$curdb}{$k}{count}); my $min = &convert_time($normalyzed_info{$curdb}{$k}{min}); my $max = &convert_time($normalyzed_info{$curdb}{$k}{max}); my $avg = &convert_time($normalyzed_info{$curdb}{$k}{average}); my $query = &highlight_code($k); my $md5 = ''; $md5 = 'md5: ' . md5_hex($k) if ($enable_checksum); my $details = ''; my %hourly_count = (); my %hourly_duration = (); my $days = 0; foreach my $d (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}}) { $d =~ /^(\d{4})(\d{2})(\d{2})$/; $days++; my $zday = "$abbr_month{$2} $3"; my $dd = $3; my $mo = $2 -1 ; my $y = $1 - 1900; foreach my $h (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}{$d}}) { my $t = timegm_nocheck(0, 0, $h, $dd, $mo, $y); $t += $timezone; my $ht = sprintf("%02d", (localtime($t))[2]); $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{average} = $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{duration} / ($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{count} || 1); $details .= ""; $zday = ""; foreach my $m (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min}}) { my $rd = &average_per_minutes($m, $histo_avg_minutes); $hourly_count{"$ht:$rd"} += $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min}{$m}; $hourly_duration{"$ht:$rd"} += ($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min_duration}{$m} || 0); } if ($#histo_avgs > 0) { foreach my $rd (@histo_avgs) { next if (!exists $hourly_count{"$ht:$rd"}); $details .= ""; } } } } # Set graph dataset my %graph_data = (); foreach my $h ("00" .. "23") { foreach my $rd (@histo_avgs) { $graph_data{count} .= "['$h:$rd'," . ($hourly_count{"$h:$rd"} || 0) . "],"; $graph_data{duration} .= "['$h:$rd'," . (int($hourly_duration{"$h:$rd"} / ($hourly_count{"$h:$rd"} || 1)) || 0) . "],"; } } $graph_data{count} =~ s/,$//; $graph_data{duration} =~ s/,$//; %hourly_count = (); %hourly_duration = (); my $users_involved = ''; if (scalar keys %{$normalyzed_info{$curdb}{$k}{users}} > 0) { $users_involved = qq{}; } my $apps_involved = ''; if (scalar keys %{$normalyzed_info{$curdb}{$k}{apps}} > 0) { $apps_involved = qq{}; } my $query_histo = ''; if ($graph) { $query_histo = &jqplot_histograph($graphid++, 'time_consuming_queries_details_'.$rank, $graph_data{count}, $graph_data{duration}, 'Queries', 'Avg. duration'); } print $fh qq{ }; $rank++; } if (!$found) { print $fh qq{}; } print $fh qq{
Rank Total duration Times executed Min duration Max duration Avg duration Query
$zday$ht" . &comma_numbers($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{count}) . "" . &convert_time($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{duration}) . "" . &convert_time($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{average}) . "
$zday$ht:$rd" . &comma_numbers($hourly_count{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}/($hourly_count{"$ht:$rd"}||1)) . "
$rank $duration $count

Details

$min $max $avg
$query
$md5

Times Reported Time consuming queries #$rank

$query_histo $details
Day Hour Count Duration Avg duration

$users_involved $apps_involved

}; if (scalar keys %{$normalyzed_info{$curdb}{$k}{users}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$normalyzed_info{$curdb}{$k}{users}{$b}{duration} <=> $normalyzed_info{$curdb}{$k}{users}{$a}{duration}} keys %{$normalyzed_info{$curdb}{$k}{users}}) { if ($normalyzed_info{$curdb}{$k}{users}{$u}{duration} > 0) { my $details = "[ User: $u"; $details .= " - Total duration: ".&convert_time($normalyzed_info{$curdb}{$k}{users}{$u}{duration}); $details .= " - Times executed: $normalyzed_info{$curdb}{$k}{users}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } if (scalar keys %{$normalyzed_info{$curdb}{$k}{apps}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$normalyzed_info{$curdb}{$k}{apps}{$b}{duration} <=> $normalyzed_info{$curdb}{$k}{apps}{$a}{duration}} keys %{$normalyzed_info{$curdb}{$k}{apps}}) { if ($normalyzed_info{$curdb}{$k}{apps}{$u}{duration} > 0) { my $details = "[ Application: $u"; $details .= " - Total duration: ".&convert_time($normalyzed_info{$curdb}{$k}{apps}{$u}{duration}); $details .= " - Times executed: $normalyzed_info{$curdb}{$k}{apps}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } print $fh qq{
}; my $idx = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($idx > $sample); my $details = "Date: $normalyzed_info{$curdb}{$k}{samples}{$d}{date}\n"; $details .= "Duration: " . &convert_time($d) . "\n"; $details .= "Database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $details .= "User: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $details .= "Remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $details .= "Application: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $details .= "Queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $details .= "Bind query: yes\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $details .= "Log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); my $explain = ''; if ($normalyzed_info{$curdb}{$k}{samples}{$d}{plan}) { $explain = &display_plan("query-e-explain-$rank-$idx", $normalyzed_info{$curdb}{$k}{samples}{$d}{plan}); } $query = &highlight_code(&anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query})); my $md5 = ''; $md5 = 'md5: ' . md5_hex($normalyzed_info{$curdb}{$k}{samples}{$d}{query}) if ($enable_checksum); print $fh qq{
$query
$md5
$details
$explain
}; $idx++; } print $fh qq{

$NODATA
}; } sub print_most_frequent { my $curdb = shift; print $fh qq{

Most frequent queries (N)

}; my $rank = 1; foreach my $k (sort { $normalyzed_info{$curdb}{$b}{count} <=> $normalyzed_info{$curdb}{$a}{count} or $normalyzed_info{$curdb}{$b}{duration} <=> $normalyzed_info{$curdb}{$a}{duration} } keys %{$normalyzed_info{$curdb}}) { next if (!$normalyzed_info{$curdb}{$k}{count}); last if ($rank > $top); $normalyzed_info{$curdb}{$k}{average} = $normalyzed_info{$curdb}{$k}{duration} / $normalyzed_info{$curdb}{$k}{count}; my $duration = &convert_time($normalyzed_info{$curdb}{$k}{duration}); my $count = &comma_numbers($normalyzed_info{$curdb}{$k}{count}); my $min = &convert_time($normalyzed_info{$curdb}{$k}{min}); my $max = &convert_time($normalyzed_info{$curdb}{$k}{max}); my $avg = &convert_time($normalyzed_info{$curdb}{$k}{average}); my $query = &highlight_code($k); my $md5 = ''; $md5 = 'md5: ' . md5_hex($k) if ($enable_checksum); my %hourly_count = (); my %hourly_duration = (); my $days = 0; my $details = ''; foreach my $d (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}}) { $d =~ /^(\d{4})(\d{2})(\d{2})$/; $days++; my $zday = "$abbr_month{$2} $3"; my $dd = $3; my $mo = $2 - 1; my $y = $1 - 1900; foreach my $h (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}{$d}}) { my $t = timegm_nocheck(0, 0, $h, $dd, $mo, $y); $t += $timezone; my $ht = sprintf("%02d", (localtime($t))[2]); $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{average} = $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{duration} / $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{count}; $details .= ""; $zday = ""; foreach my $m (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min}}) { my $rd = &average_per_minutes($m, $histo_avg_minutes); $hourly_count{"$ht:$rd"} += $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min}{$m}; $hourly_duration{"$ht:$rd"} += ($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min_duration}{$m} || 0); } if ($#histo_avgs > 0) { foreach my $rd (@histo_avgs) { next if (!exists $hourly_count{"$ht:$rd"}); $details .= ""; } } } } # Set graph dataset my %graph_data = (); foreach my $h ("00" .. "23") { foreach my $rd (@histo_avgs) { $graph_data{count} .= "['$h:$rd'," . ($hourly_count{"$h:$rd"} || 0) . "],"; $graph_data{duration} .= "['$h:$rd'," . (int($hourly_duration{"$h:$rd"} / ($hourly_count{"$h:$rd"} || 1)) || 0) . "],"; } } $graph_data{count} =~ s/,$//; $graph_data{duration} =~ s/,$//; %hourly_count = (); %hourly_duration = (); my $query_histo = ''; if ($graph) { $query_histo = &jqplot_histograph($graphid++, 'most_frequent_queries_details_'.$rank, $graph_data{count}, $graph_data{duration}, 'Queries', 'Avg. duration'); } my $users_involved = ''; if (scalar keys %{$normalyzed_info{$curdb}{$k}{users}} > 0) { $users_involved = qq{}; } my $apps_involved = ''; if (scalar keys %{$normalyzed_info{$curdb}{$k}{apps}} > 0) { $apps_involved = qq{}; } print $fh qq{ }; $rank++; } if (scalar keys %{$normalyzed_info{$curdb}} == 0) { print $fh qq{}; } print $fh qq{
Rank Times executed Total duration Min duration Max duration Avg duration Query
$zday$ht" . &comma_numbers($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{count}) . "" . &convert_time($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{duration}) . "" . &convert_time($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{average}) . "
$zday$ht:$rd" . &comma_numbers($hourly_count{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}/($hourly_count{"$ht:$rd"}||1)) . "
$rank $count

Details

$duration $min $max $avg
$query
$md5

Times Reported Time consuming queries #$rank

$query_histo $details
Day Hour Count Duration Avg duration

$users_involved $apps_involved

}; if (scalar keys %{$normalyzed_info{$curdb}{$k}{users}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$normalyzed_info{$curdb}{$k}{users}{$b}{duration} <=> $normalyzed_info{$curdb}{$k}{users}{$a}{duration}} keys %{$normalyzed_info{$curdb}{$k}{users}}) { if ($normalyzed_info{$curdb}{$k}{users}{$u}{duration} > 0) { my $details = "[ User: $u"; $details .= " - Total duration: ".&convert_time($normalyzed_info{$curdb}{$k}{users}{$u}{duration}); $details .= " - Times executed: $normalyzed_info{$curdb}{$k}{users}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } if (scalar keys %{$normalyzed_info{$curdb}{$k}{apps}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$normalyzed_info{$curdb}{$k}{apps}{$b}{duration} <=> $normalyzed_info{$curdb}{$k}{apps}{$a}{duration}} keys %{$normalyzed_info{$curdb}{$k}{apps}}) { if ($normalyzed_info{$curdb}{$k}{apps}{$u}{duration} > 0) { my $details = "[ Application: $u"; $details .= " - Total duration: ".&convert_time($normalyzed_info{$curdb}{$k}{apps}{$u}{duration}); $details .= " - Times executed: $normalyzed_info{$curdb}{$k}{apps}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } print $fh qq{
}; my $idx = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($idx > $sample); my $details = "Date: $normalyzed_info{$curdb}{$k}{samples}{$d}{date}\n"; $details .= "Duration: " . &convert_time($d) . "\n"; $details .= "Database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $details .= "User: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $details .= "Remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $details .= "Application: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $details .= "Queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $details .= "Bind query: yes\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $details .= "Log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); my $explain = ''; if ($normalyzed_info{$curdb}{$k}{samples}{$d}{plan}) { $explain = &display_plan("query-f-explain-$rank-$idx", $normalyzed_info{$curdb}{$k}{samples}{$d}{plan}); } $query = &highlight_code(&anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query})); my $md5 = ''; $md5 = 'md5: ' . md5_hex($normalyzed_info{$curdb}{$k}{samples}{$d}{query}) if ($enable_checksum); print $fh qq{
$query
$md5
$details
$explain
}; $idx++; } print $fh qq{

$NODATA
}; } sub print_slowest_queries { my $curdb = shift; print $fh qq{

Normalized slowest queries (N)

}; my $rank = 1; my $found = 0; foreach my $k (sort {$normalyzed_info{$curdb}{$b}{average} <=> $normalyzed_info{$curdb}{$a}{average}} keys %{$normalyzed_info{$curdb}}) { next if (!$k || !$normalyzed_info{$curdb}{$k}{count} || !exists $normalyzed_info{$curdb}{$k}{duration}); last if ($rank > $top); $found++; $normalyzed_info{$curdb}{$k}{average} = $normalyzed_info{$curdb}{$k}{duration} / $normalyzed_info{$curdb}{$k}{count}; my $duration = &convert_time($normalyzed_info{$curdb}{$k}{duration}); my $count = &comma_numbers($normalyzed_info{$curdb}{$k}{count}); my $min = &convert_time($normalyzed_info{$curdb}{$k}{min}); my $max = &convert_time($normalyzed_info{$curdb}{$k}{max}); my $avg = &convert_time($normalyzed_info{$curdb}{$k}{average}); my $query = &highlight_code($k); my $md5 = ''; $md5 = 'md5: ' . md5_hex($k) if ($enable_checksum); my $details = ''; my %hourly_count = (); my %hourly_duration = (); my $days = 0; foreach my $d (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}}) { my $c = 1; $d =~ /^(\d{4})(\d{2})(\d{2})$/; $days++; my $zday = "$abbr_month{$2} $3"; my $dd = $3; my $mo = $2 - 1; my $y = $1 - 1900; foreach my $h (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}{$d}}) { my $t = timegm_nocheck(0, 0, $h, $dd, $mo, $y); $t += $timezone; my $ht = sprintf("%02d", (localtime($t))[2]); $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{average} = $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{duration} / $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{count}; $details .= ""; $zday = ""; foreach my $m (sort keys %{$normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min}}) { my $rd = &average_per_minutes($m, $histo_avg_minutes); $hourly_count{"$ht:$rd"} += $normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min}{$m}; $hourly_duration{"$ht:$rd"} += ($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{min_duration}{$m} || 0); } if ($#histo_avgs > 0) { foreach my $rd (@histo_avgs) { next if (!exists $hourly_count{"$ht:$rd"}); $details .= ""; } } } } # Set graph dataset my %graph_data = (); foreach my $h ("00" .. "23") { foreach my $rd (@histo_avgs) { $graph_data{count} .= "['$h:$rd'," . ($hourly_count{"$h:$rd"} || 0) . "],"; $graph_data{duration} .= "['$h:$rd'," . (int($hourly_duration{"$h:$rd"} / ($hourly_count{"$h:$rd"} || 1)) || 0) . "],"; } } $graph_data{count} =~ s/,$//; $graph_data{duration} =~ s/,$//; %hourly_count = (); %hourly_duration = (); my $query_histo = ''; if ($graph) { $query_histo = &jqplot_histograph($graphid++, 'normalized_slowest_queries_details_'.$rank, $graph_data{count}, $graph_data{duration}, 'Queries', 'Avg. duration'); } my $users_involved = ''; if (scalar keys %{$normalyzed_info{$curdb}{$k}{users}} > 0) { $users_involved = qq{}; } my $apps_involved = ''; if (scalar keys %{$normalyzed_info{$curdb}{$k}{apps}} > 0) { $apps_involved = qq{}; } print $fh qq{ }; $rank++; } if (!$found) { print $fh qq{}; } print $fh qq{
Rank Min duration Max duration Avg duration Times executed Total duration Query
$zday$ht" . &comma_numbers($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{count}) . "" . &convert_time($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{duration}) . "" . &convert_time($normalyzed_info{$curdb}{$k}{chronos}{$d}{$h}{average}) . "
$zday$ht:$rd" . &comma_numbers($hourly_count{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}/($hourly_count{"$ht:$rd"}||1)) . "
$rank $min $max $avg $count

Details

$duration
$query
$md5

Times Reported Time consuming queries #$rank

$query_histo $details
Day Hour Count Duration Avg duration

$users_involved

}; if (scalar keys %{$normalyzed_info{$curdb}{$k}{users}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$normalyzed_info{$curdb}{$k}{users}{$b}{duration} <=> $normalyzed_info{$curdb}{$k}{users}{$a}{duration}} keys %{$normalyzed_info{$curdb}{$k}{users}}) { if ($normalyzed_info{$curdb}{$k}{users}{$u}{duration} > 0) { my $details = "[ User: $u"; $details .= " - Total duration: ".&convert_time($normalyzed_info{$curdb}{$k}{users}{$u}{duration}); $details .= " - Times executed: $normalyzed_info{$curdb}{$k}{users}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } if (scalar keys %{$normalyzed_info{$curdb}{$k}{apps}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$normalyzed_info{$curdb}{$k}{apps}{$b}{duration} <=> $normalyzed_info{$curdb}{$k}{apps}{$a}{duration}} keys %{$normalyzed_info{$curdb}{$k}{apps}}) { if ($normalyzed_info{$curdb}{$k}{apps}{$u}{duration} > 0) { my $details = "[ Application: $u"; $details .= " - Total duration: ".&convert_time($normalyzed_info{$curdb}{$k}{apps}{$u}{duration}); $details .= " - Times executed: $normalyzed_info{$curdb}{$k}{apps}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } print $fh qq{
}; my $idx = 1; foreach my $d (sort {$b <=> $a} keys %{$normalyzed_info{$curdb}{$k}{samples}}) { last if ($idx > $sample); my $details = "Date: $normalyzed_info{$curdb}{$k}{samples}{$d}{date}\n"; $details .= "Duration: " . &convert_time($d) . "\n"; $details .= "Database: $normalyzed_info{$curdb}{$k}{samples}{$d}{db}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{db}); $details .= "User: $normalyzed_info{$curdb}{$k}{samples}{$d}{user}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{user}); $details .= "Remote: $normalyzed_info{$curdb}{$k}{samples}{$d}{remote}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{remote}); $details .= "Application: $normalyzed_info{$curdb}{$k}{samples}{$d}{app}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{app}); $details .= "Queryid: $normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{queryid}); $details .= "Bind query: yes\n" if ($normalyzed_info{$curdb}{$k}{samples}{$d}{bind}); $details .= "Log file: " . $normalyzed_info{$curdb}{$k}{samples}{$d}{logfile} if ($normalyzed_info{$curdb}{$k}{samples}{$d}{logfile}); my $explain = ''; if ($normalyzed_info{$curdb}{$k}{samples}{$d}{plan}) { $explain = &display_plan("query-g-explain-$rank-$idx", $normalyzed_info{$curdb}{$k}{samples}{$d}{plan}); } $query = &highlight_code(&anonymize_query($normalyzed_info{$curdb}{$k}{samples}{$d}{query})); my $md5 = ''; $md5 = 'md5: ' . md5_hex($normalyzed_info{$curdb}{$k}{samples}{$d}{query}) if ($enable_checksum); print $fh qq{
$query
$md5
$details
$explain
}; $idx++; } print $fh qq{

$NODATA
}; } sub print_prepare_consuming { my $curdb = shift; print $fh qq{

Time consuming prepare

}; my $rank = 1; my $found = 0; foreach my $k (sort {$prepare_info{$curdb}{$b}{duration} <=> $prepare_info{$curdb}{$a}{duration}} keys %{$prepare_info{$curdb}}) { next if (!$prepare_info{$curdb}{$k}{count} || !exists $prepare_info{$curdb}{$k}{duration}); last if ($rank > $top); $found++; $prepare_info{$curdb}{$k}{average} = $prepare_info{$curdb}{$k}{duration} / $prepare_info{$curdb}{$k}{count}; my $duration = &convert_time($prepare_info{$curdb}{$k}{duration}); my $count = &comma_numbers($prepare_info{$curdb}{$k}{count}); my $min = &convert_time($prepare_info{$curdb}{$k}{min}); my $max = &convert_time($prepare_info{$curdb}{$k}{max}); my $avg = &convert_time($prepare_info{$curdb}{$k}{average}); my $query = &highlight_code(&anonymize_query($k)); my $md5 = ''; $md5 = 'md5: ' . md5_hex($k) if ($enable_checksum); my $details = ''; my %hourly_count = (); my %hourly_duration = (); my $days = 0; foreach my $d (sort keys %{$prepare_info{$curdb}{$k}{chronos}}) { $d =~ /^(\d{4})(\d{2})(\d{2})$/; $days++; my $zday = "$abbr_month{$2} $3"; my $dd = $3; my $mo = $2 -1 ; my $y = $1 - 1900; foreach my $h (sort keys %{$prepare_info{$curdb}{$k}{chronos}{$d}}) { my $t = timegm_nocheck(0, 0, $h, $dd, $mo, $y); $t += $timezone; my $ht = sprintf("%02d", (localtime($t))[2]); $prepare_info{$curdb}{$k}{chronos}{$d}{$h}{average} = $prepare_info{$curdb}{$k}{chronos}{$d}{$h}{duration} / ($prepare_info{$curdb}{$k}{chronos}{$d}{$h}{count} || 1); $details .= ""; $zday = ""; foreach my $m (sort keys %{$prepare_info{$curdb}{$k}{chronos}{$d}{$h}{min}}) { my $rd = &average_per_minutes($m, $histo_avg_minutes); $hourly_count{"$ht:$rd"} += $prepare_info{$curdb}{$k}{chronos}{$d}{$h}{min}{$m}; $hourly_duration{"$ht:$rd"} += ($prepare_info{$curdb}{$k}{chronos}{$d}{$h}{min_duration}{$m} || 0); } if ($#histo_avgs > 0) { foreach my $rd (@histo_avgs) { next if (!exists $hourly_count{"$ht:$rd"}); $details .= ""; } } } } # Set graph dataset my %graph_data = (); foreach my $h ("00" .. "23") { foreach my $rd (@histo_avgs) { $graph_data{count} .= "['$h:$rd'," . ($hourly_count{"$h:$rd"} || 0) . "],"; $graph_data{duration} .= "['$h:$rd'," . (int($hourly_duration{"$h:$rd"} / ($hourly_count{"$h:$rd"} || 1)) || 0) . "],"; } } $graph_data{count} =~ s/,$//; $graph_data{duration} =~ s/,$//; %hourly_count = (); %hourly_duration = (); my $users_involved = ''; if (scalar keys %{$prepare_info{$curdb}{$k}{users}} > 0) { $users_involved = qq{}; } my $apps_involved = ''; if (scalar keys %{$prepare_info{$curdb}{$k}{apps}} > 0) { $apps_involved = qq{}; } my $query_histo = ''; if ($graph) { $query_histo = &jqplot_histograph($graphid++, 'time_consuming_prepare_details_'.$rank, $graph_data{count}, $graph_data{duration}, 'Queries', 'Avg. duration'); } print $fh qq{ }; $rank++; } if (!$found) { print $fh qq{}; } print $fh qq{
Rank Total duration Times executed Min duration Max duration Avg duration Query
$zday$ht" . &comma_numbers($prepare_info{$curdb}{$k}{chronos}{$d}{$h}{count}) . "" . &convert_time($prepare_info{$curdb}{$k}{chronos}{$d}{$h}{duration}) . "" . &convert_time($prepare_info{$curdb}{$k}{chronos}{$d}{$h}{average}) . "
$zday$ht:$rd" . &comma_numbers($hourly_count{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}/($hourly_count{"$ht:$rd"}||1)) . "
$rank $duration $count

Details

$min $max $avg
$query
$md5

Times Reported Time consuming prepare #$rank

$query_histo $details
Day Hour Count Duration Avg duration

$users_involved $apps_involved

}; if (scalar keys %{$prepare_info{$curdb}{$k}{users}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$prepare_info{$curdb}{$k}{users}{$b}{duration} <=> $prepare_info{$curdb}{$k}{users}{$a}{duration}} keys %{$prepare_info{$curdb}{$k}{users}}) { if ($prepare_info{$curdb}{$k}{users}{$u}{duration} > 0) { my $details = "[ User: $u"; $details .= " - Total duration: ".&convert_time($prepare_info{$curdb}{$k}{users}{$u}{duration}); $details .= " - Times executed: $prepare_info{$curdb}{$k}{users}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } if (scalar keys %{$prepare_info{$curdb}{$k}{apps}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$prepare_info{$curdb}{$k}{apps}{$b}{duration} <=> $prepare_info{$curdb}{$k}{apps}{$a}{duration}} keys %{$prepare_info{$curdb}{$k}{apps}}) { if ($prepare_info{$curdb}{$k}{apps}{$u}{duration} > 0) { my $details = "[ Application: $u"; $details .= " - Total duration: ".&convert_time($prepare_info{$curdb}{$k}{apps}{$u}{duration}); $details .= " - Times executed: $prepare_info{$curdb}{$k}{apps}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } print $fh qq{
}; my $idx = 1; foreach my $d (sort {$b <=> $a} keys %{$prepare_info{$curdb}{$k}{samples}}) { last if ($idx > $sample); my $details = "Date: $prepare_info{$curdb}{$k}{samples}{$d}{date}\n"; $details .= "Duration: " . &convert_time($d) . "\n"; $details .= "Database: $prepare_info{$curdb}{$k}{samples}{$d}{db}\n" if ($prepare_info{$curdb}{$k}{samples}{$d}{db}); $details .= "User: $prepare_info{$curdb}{$k}{samples}{$d}{user}\n" if ($prepare_info{$curdb}{$k}{samples}{$d}{user}); $details .= "Remote: $prepare_info{$curdb}{$k}{samples}{$d}{remote}\n" if ($prepare_info{$curdb}{$k}{samples}{$d}{remote}); $details .= "Application: $prepare_info{$curdb}{$k}{samples}{$d}{app}\n" if ($prepare_info{$curdb}{$k}{samples}{$d}{app}); $details .= "Queryid: $prepare_info{$curdb}{$k}{samples}{$d}{queryid}\n" if ($prepare_info{$curdb}{$k}{samples}{$d}{queryid}); $details .= "parameters: ". &anonymize_query($prepare_info{$curdb}{$k}{samples}{$d}{params}) . "\n" if ($prepare_info{$curdb}{$k}{samples}{$d}{params}); $query = &highlight_code(&anonymize_query($prepare_info{$curdb}{$k}{samples}{$d}{query})); my $md5 = ''; $md5 = 'md5: ' . md5_hex($prepare_info{$curdb}{$k}{samples}{$d}{query}) if ($enable_checksum); print $fh qq{
$query
$md5
$details
}; $idx++; } print $fh qq{

$NODATA
}; } sub print_bind_consuming { my $curdb = shift; print $fh qq{

Time consuming bind

}; my $rank = 1; my $found = 0; foreach my $k (sort {$bind_info{$curdb}{$b}{duration} <=> $bind_info{$curdb}{$a}{duration}} keys %{$bind_info{$curdb}}) { next if (!$bind_info{$curdb}{$k}{count} || !exists $bind_info{$curdb}{$k}{duration}); last if ($rank > $top); $found++; $bind_info{$curdb}{$k}{average} = $bind_info{$curdb}{$k}{duration} / $bind_info{$curdb}{$k}{count}; my $duration = &convert_time($bind_info{$curdb}{$k}{duration}); my $count = &comma_numbers($bind_info{$curdb}{$k}{count}); my $min = &convert_time($bind_info{$curdb}{$k}{min}); my $max = &convert_time($bind_info{$curdb}{$k}{max}); my $avg = &convert_time($bind_info{$curdb}{$k}{average}); my $query = &highlight_code(&anonymize_query($k)); my $md5 = ''; $md5 = 'md5: ' . md5_hex($k) if ($enable_checksum); my $details = ''; my %hourly_count = (); my %hourly_duration = (); my $days = 0; foreach my $d (sort keys %{$bind_info{$curdb}{$k}{chronos}}) { $d =~ /^(\d{4})(\d{2})(\d{2})$/; $days++; my $zday = "$abbr_month{$2} $3"; my $dd = $3; my $mo = $2 -1 ; my $y = $1 - 1900; foreach my $h (sort keys %{$bind_info{$curdb}{$k}{chronos}{$d}}) { my $t = timegm_nocheck(0, 0, $h, $dd, $mo, $y); $t += $timezone; my $ht = sprintf("%02d", (localtime($t))[2]); $bind_info{$curdb}{$k}{chronos}{$d}{$h}{average} = $bind_info{$curdb}{$k}{chronos}{$d}{$h}{duration} / ($bind_info{$curdb}{$k}{chronos}{$d}{$h}{count} || 1); $details .= ""; $zday = ""; foreach my $m (sort keys %{$bind_info{$curdb}{$k}{chronos}{$d}{$h}{min}}) { my $rd = &average_per_minutes($m, $histo_avg_minutes); $hourly_count{"$ht:$rd"} += $bind_info{$curdb}{$k}{chronos}{$d}{$h}{min}{$m}; $hourly_duration{"$ht:$rd"} += ($bind_info{$curdb}{$k}{chronos}{$d}{$h}{min_duration}{$m} || 0); } if ($#histo_avgs > 0) { foreach my $rd (@histo_avgs) { next if (!exists $hourly_count{"$ht:$rd"}); $details .= ""; } } } } # Set graph dataset my %graph_data = (); foreach my $h ("00" .. "23") { foreach my $rd (@histo_avgs) { $graph_data{count} .= "['$h:$rd'," . ($hourly_count{"$h:$rd"} || 0) . "],"; $graph_data{duration} .= "['$h:$rd'," . (int($hourly_duration{"$h:$rd"} / ($hourly_count{"$h:$rd"} || 1)) || 0) . "],"; } } $graph_data{count} =~ s/,$//; $graph_data{duration} =~ s/,$//; %hourly_count = (); %hourly_duration = (); my $users_involved = ''; if (scalar keys %{$bind_info{$curdb}{$k}{users}} > 0) { $users_involved = qq{}; } my $apps_involved = ''; if (scalar keys %{$bind_info{$curdb}{$k}{apps}} > 0) { $apps_involved = qq{}; } my $query_histo = ''; if ($graph) { $query_histo = &jqplot_histograph($graphid++, 'time_consuming_bind_details_'.$rank, $graph_data{count}, $graph_data{duration}, 'Queries', 'Avg. duration'); } print $fh qq{ }; $rank++; } if (!$found) { print $fh qq{}; } print $fh qq{
Rank Total duration Times executed Min duration Max duration Avg duration Query
$zday$ht" . &comma_numbers($bind_info{$curdb}{$k}{chronos}{$d}{$h}{count}) . "" . &convert_time($bind_info{$curdb}{$k}{chronos}{$d}{$h}{duration}) . "" . &convert_time($bind_info{$curdb}{$k}{chronos}{$d}{$h}{average}) . "
$zday$ht:$rd" . &comma_numbers($hourly_count{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}) . "" . &convert_time($hourly_duration{"$ht:$rd"}/($hourly_count{"$ht:$rd"}||1)) . "
$rank $duration $count

Details

$min $max $avg
$query
$md5

Times Reported Time consuming bind #$rank

$query_histo $details
Day Hour Count Duration Avg duration

$users_involved $apps_involved

}; if (scalar keys %{$bind_info{$curdb}{$k}{users}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$bind_info{$curdb}{$k}{users}{$b}{duration} <=> $bind_info{$curdb}{$k}{users}{$a}{duration}} keys %{$bind_info{$curdb}{$k}{users}}) { if ($bind_info{$curdb}{$k}{users}{$u}{duration} > 0) { my $details = "[ User: $u"; $details .= " - Total duration: ".&convert_time($bind_info{$curdb}{$k}{users}{$u}{duration}); $details .= " - Times executed: $bind_info{$curdb}{$k}{users}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } if (scalar keys %{$bind_info{$curdb}{$k}{apps}} > 0) { print $fh qq{
}; my $idx = 1; foreach my $u (sort {$bind_info{$curdb}{$k}{apps}{$b}{duration} <=> $bind_info{$curdb}{$k}{apps}{$a}{duration}} keys %{$bind_info{$curdb}{$k}{apps}}) { if ($bind_info{$curdb}{$k}{apps}{$u}{duration} > 0) { my $details = "[ Application: $u"; $details .= " - Total duration: ".&convert_time($bind_info{$curdb}{$k}{apps}{$u}{duration}); $details .= " - Times executed: $bind_info{$curdb}{$k}{apps}{$u}{count}"; $details .= " ]\n"; print $fh qq{
$details
}; $idx++; } } print $fh qq{

}; } print $fh qq{
}; my $idx = 1; foreach my $d (sort {$b <=> $a} keys %{$bind_info{$curdb}{$k}{samples}}) { last if ($idx > $sample); my $details = "Date: $bind_info{$curdb}{$k}{samples}{$d}{date}\n"; $details .= "Duration: " . &convert_time($d) . "\n"; $details .= "Database: $bind_info{$curdb}{$k}{samples}{$d}{db}\n" if ($bind_info{$curdb}{$k}{samples}{$d}{db}); $details .= "User: $bind_info{$curdb}{$k}{samples}{$d}{user}\n" if ($bind_info{$curdb}{$k}{samples}{$d}{user}); $details .= "Remote: $bind_info{$curdb}{$k}{samples}{$d}{remote}\n" if ($bind_info{$curdb}{$k}{samples}{$d}{remote}); $details .= "Application: $bind_info{$curdb}{$k}{samples}{$d}{app}\n" if ($bind_info{$curdb}{$k}{samples}{$d}{app}); $details .= "Queryid: $bind_info{$curdb}{$k}{samples}{$d}{queryid}\n" if ($bind_info{$curdb}{$k}{samples}{$d}{queryid}); $details .= "parameters: " . &anonymize_query($bind_info{$curdb}{$k}{samples}{$d}{params}) . "\n" if ($bind_info{$curdb}{$k}{samples}{$d}{params}); $query = &highlight_code(&anonymize_query($bind_info{$curdb}{$k}{samples}{$d}{query})); my $md5 = ''; $md5 = 'md5: ' . md5_hex($bind_info{$curdb}{$k}{samples}{$d}{query}) if ($enable_checksum); print $fh qq{
$query
$md5
$details
}; $idx++; } print $fh qq{

$NODATA
}; } sub dump_as_html { my $uri = shift; my $curdb = shift; # Dump the html header &html_header($uri, $curdb); # Set graphs limits if ($overall_stat{$curdb}{'first_log_ts'} =~ /^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/) { my ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s); if (!$log_timezone) { ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s) = ($1, $2, $3, $4, $5, $6); } else { ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s) = change_timezone($1, $2, $3, $4, $5, $6); } $t_min = timegm_nocheck(0, $t_mi, $t_h, $t_d, $t_mo - 1, $t_y) * 1000; $t_min += ($timezone*1000); $t_min -= ($avg_minutes * 60000); } if ($overall_stat{$curdb}{'last_log_ts'} =~ /^(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)/) { my ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s); if (!$log_timezone) { ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s) = ($1, $2, $3, $4, $5, $6); } else { ($t_y, $t_mo, $t_d, $t_h, $t_mi, $t_s) = change_timezone($1, $2, $3, $4, $5, $6); } $t_max = timegm_nocheck(59, $t_mi, $t_h, $t_d, $t_mo - 1, $t_y) * 1000; $t_max += ($timezone*1000); $t_max += ($avg_minutes * 60000); } if (!$error_only) { if (!$pgbouncer_only) { # Overall statistics print $fh qq{
  • }; &print_overall_statistics($curdb); } if (!$disable_hourly && !$pgbouncer_only) { # Build graphs based on hourly stat &compute_query_graphs($curdb); # Show global SQL traffic &print_sql_traffic($curdb); # Show hourly statistics &print_general_activity($curdb); } if (!$disable_connection && !$pgbouncer_only) { print $fh qq{