# -*- ruby -*- # vim: set noet nosta sw=4 ts=4 : # # PostgreSQL statistic gatherer. # Mahlon E. Smith # # Based on queries by Kenny Gorman. # http://www.kennygorman.com/wordpress/?page_id=491 # # An example gnuplot input script is included in the __END__ block # of this script. Using it, you can feed the output this script # generates to gnuplot (after removing header lines) to generate # some nice performance charts. # require 'ostruct' require 'optparse' require 'etc' require 'ysql' ### PostgreSQL Stats. Fetch information from pg_stat_* tables. ### Optionally run in a continuous loop, displaying deltas. ### class Stats VERSION = %q$Id$ def initialize( opts ) @opts = opts @db = YSQL.connect( :dbname => opts.database, :host => opts.host, :port => opts.port, :user => opts.user, :password => opts.pass, :sslmode => 'prefer' ) @last = nil end ###### public ###### ### Primary loop. Gather statistics and generate deltas. ### def run run_count = 0 loop do current_stat = self.get_stats # First run, store and continue # if @last.nil? @last = current_stat sleep @opts.interval next end # headers # if run_count == 0 || run_count % 50 == 0 puts "%-20s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s" % %w[ time commits rollbks blksrd blkshit bkends seqscan seqtprd idxscn idxtrd ins upd del locks activeq ] end # calculate deltas # delta = current_stat.inject({}) do |h, pair| stat, val = *pair if %w[ activeq locks bkends ].include?( stat ) h[stat] = current_stat[stat].to_i else h[stat] = current_stat[stat].to_i - @last[stat].to_i end h end delta[ 'time' ] = Time.now.strftime('%F %T') # new values # puts "%-20s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s%12s" % [ delta['time'], delta['commits'], delta['rollbks'], delta['blksrd'], delta['blkshit'], delta['bkends'], delta['seqscan'], delta['seqtprd'], delta['idxscn'], delta['idxtrd'], delta['ins'], delta['upd'], delta['del'], delta['locks'], delta['activeq'] ] @last = current_stat run_count += 1 sleep @opts.interval end end ### Query the database for performance measurements. Returns a hash. ### def get_stats res = @db.exec %Q{ SELECT MAX(stat_db.xact_commit) AS commits, MAX(stat_db.xact_rollback) AS rollbks, MAX(stat_db.blks_read) AS blksrd, MAX(stat_db.blks_hit) AS blkshit, MAX(stat_db.numbackends) AS bkends, SUM(stat_tables.seq_scan) AS seqscan, SUM(stat_tables.seq_tup_read) AS seqtprd, SUM(stat_tables.idx_scan) AS idxscn, SUM(stat_tables.idx_tup_fetch) AS idxtrd, SUM(stat_tables.n_tup_ins) AS ins, SUM(stat_tables.n_tup_upd) AS upd, SUM(stat_tables.n_tup_del) AS del, MAX(stat_locks.locks) AS locks, MAX(activity.sess) AS activeq FROM pg_stat_database AS stat_db, pg_stat_user_tables AS stat_tables, (SELECT COUNT(*) AS locks FROM pg_locks ) AS stat_locks, (SELECT COUNT(*) AS sess FROM pg_stat_activity WHERE current_query <> '') AS activity WHERE stat_db.datname = '%s'; } % [ @opts.database ] return res[0] end end ### Parse command line arguments. Return a struct of global options. ### def parse_args( args ) options = OpenStruct.new options.database = Etc.getpwuid( Process.uid ).name options.host = '127.0.0.1' options.port = 5432 options.user = Etc.getpwuid( Process.uid ).name options.sslmode = 'disable' options.interval = 5 opts = OptionParser.new do |opts| opts.banner = "Usage: #{$0} [options]" opts.separator '' opts.separator 'Connection options:' opts.on( '-d', '--database DBNAME', "specify the database to connect to (default: \"#{options.database}\")" ) do |db| options.database = db end opts.on( '-h', '--host HOSTNAME', 'database server host' ) do |host| options.host = host end opts.on( '-p', '--port PORT', Integer, "database server port (default: \"#{options.port}\")" ) do |port| options.port = port end opts.on( '-U', '--user NAME', "database user name (default: \"#{options.user}\")" ) do |user| options.user = user end opts.on( '-W', 'force password prompt' ) do |pw| print 'Password: ' begin system 'stty -echo' options.pass = gets.chomp ensure system 'stty echo' puts end end opts.separator '' opts.separator 'Other options:' opts.on( '-i', '--interval SECONDS', Integer, "refresh interval in seconds (default: \"#{options.interval}\")") do |seconds| options.interval = seconds end opts.on_tail( '--help', 'show this help, then exit' ) do $stderr.puts opts exit end opts.on_tail( '--version', 'output version information, then exit' ) do puts Stats::VERSION exit end end opts.parse!( args ) return options end ### Go! ### if __FILE__ == $0 $stdout.sync = true Stats.new( parse_args( ARGV ) ).run end __END__ ###################################################################### ### T E R M I N A L O P T I O N S ###################################################################### #set terminal png nocrop enhanced font arial 8 size '800x600' x000000 xffffff x444444 #set output 'graph.png' set terminal pdf linewidth 4 size 11,8 set output 'graph.pdf' #set terminal aqua ###################################################################### ### O P T I O N S F O R A L L G R A P H S ###################################################################### set multiplot layout 2,1 title "PostgreSQL Statistics\n5 second sample rate (smoothed)" set grid x y set key right vertical outside set key nobox set xdata time set timefmt "%Y-%m-%d.%H:%M:%S" set format x "%l%p" set xtic rotate by -45 input_file = "database_stats.txt" # edit to taste! set xrange ["2012-04-16.00:00:00":"2012-04-17.00:00:00"] ###################################################################### ### G R A P H 1 ###################################################################### set title "Database Operations and Connection Totals" set yrange [0:200] plot \ input_file using 1:2 title "Commits" with lines smooth bezier, \ input_file using 1:3 title "Rollbacks" with lines smooth bezier, \ input_file using 1:11 title "Inserts" with lines smooth bezier, \ input_file using 1:12 title "Updates" with lines smooth bezier, \ input_file using 1:13 title "Deletes" with lines smooth bezier, \ input_file using 1:6 title "Backends (total)" with lines, \ input_file using 1:15 title "Active queries (total)" with lines smooth bezier ###################################################################### ### G R A P H 2 ###################################################################### set title "Backend Performance" set yrange [0:10000] plot \ input_file using 1:4 title "Block (cache) reads" with lines smooth bezier, \ input_file using 1:5 title "Block (cache) hits" with lines smooth bezier, \ input_file using 1:7 title "Sequence scans" with lines smooth bezier, \ input_file using 1:8 title "Sequence tuple reads" with lines smooth bezier, \ input_file using 1:9 title "Index scans" with lines smooth bezier, \ input_file using 1:10 title "Index tuple reads" with lines smooth bezier ###################################################################### ### C L E A N U P ###################################################################### unset multiplot reset