Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
executable file 1420 lines (1265 sloc) 35.7 KB
#!/usr/bin/env ruby
require 'English'
require 'optparse'
require 'cgi'
require 'thread'
require 'shellwords'
require 'time'
require 'erb'
# For ruby 1.8.5 on CentOS 5.6.
class String
unless method_defined?(:start_with?)
def start_with?(string)
/\A#{Regexp.escape(string)}/ =~ self
end
end
end
class Array
unless method_defined?(:each_slice)
def each_slice(n)
sub_elements = []
each do |element|
sub_elements << element
if sub_elements.size == n
yield(sub_elements)
sub_elements = []
end
end
yield(sub_elements) unless sub_elements.empty?
end
end
end
class GroongaQueryLogAnalyzer
class Error < StandardError
end
class UnsupportedReporter < Error
end
def initialize
setup_options
end
def run(argv=nil)
log_paths = @option_parser.parse!(argv || ARGV)
stream = @options[:stream]
dynamic_sort = @options[:dynamic_sort]
statistics = SizedStatistics.new
statistics.apply_options(@options)
parser = QueryLogParser.new
if stream
streamer = Streamer.new(create_reporter(statistics))
streamer.start
process_statistic = lambda do |statistic|
streamer << statistic
end
elsif dynamic_sort
process_statistic = lambda do |statistic|
statistics << statistic
end
else
full_statistics = []
process_statistic = lambda do |statistic|
full_statistics << statistic
end
end
if log_paths.empty?
parser.parse(ARGF, &process_statistic)
else
log_paths.each do |log_path|
File.open(log_path) do |log|
parser.parse(log, &process_statistic)
end
end
end
if stream
streamer.finish
return
end
statistics.replace(full_statistics) unless dynamic_sort
reporter = create_reporter(statistics)
reporter.apply_options(@options)
reporter.report
end
private
def setup_options
@options = {}
@options[:n_entries] = 10
@options[:order] = "-elapsed"
@options[:color] = :auto
@options[:output] = "-"
@options[:slow_operation_threshold] = 0.1
@options[:slow_response_threshold] = 0.2
@options[:reporter] = "console"
@options[:dynamic_sort] = true
@options[:stream] = false
@options[:report_summary] = true
@option_parser = OptionParser.new do |parser|
parser.banner += " LOG1 ..."
parser.on("-n", "--n-entries=N",
Integer,
"Show top N entries",
"(#{@options[:n_entries]})") do |n|
@options[:n_entries] = n
end
available_orders = ["elapsed", "-elapsed", "start-time", "-start-time"]
parser.on("--order=ORDER",
available_orders,
"Sort by ORDER",
"available values: [#{available_orders.join(', ')}]",
"(#{@options[:order]})") do |order|
@options[:order] = order
end
color_options = [
[:auto, :auto],
["-", false],
["no", false],
["false", false],
["+", true],
["yes", true],
["true", true],
]
parser.on("--[no-]color=[auto]",
color_options,
"Enable color output",
"(#{@options[:color]})") do |color|
if color.nil?
@options[:color] = true
else
@options[:color] = color
end
end
parser.on("--output=PATH",
"Output to PATH.",
"'-' PATH means standard output.",
"(#{@options[:output]})") do |output|
@options[:output] = output
end
parser.on("--slow-operation-threshold=THRESHOLD",
Float,
"Use THRESHOLD seconds to detect slow operations.",
"(#{@options[:slow_operation_threshold]})") do |threshold|
@options[:slow_operation_threshold] = threshold
end
parser.on("--slow-response-threshold=THRESHOLD",
Float,
"Use THRESHOLD seconds to detect slow operations.",
"(#{@options[:sloq_response_threshold]})") do |threshold|
@options[:sloq_response_threshold] = threshold
end
available_reporters = ["console", "json", "html"]
parser.on("--reporter=REPORTER",
available_reporters,
"Reports statistics by REPORTER.",
"available values: [#{available_reporters.join(', ')}]",
"(#{@options[:reporter]})") do |reporter|
@options[:reporter] = reporter
end
parser.on("--[no-]dynamic-sort",
"Sorts dynamically.",
"Memory and CPU usage reduced for large query log.",
"(#{@options[:dynamic_sort]})") do |sort|
@options[:dynamic_sort] = sort
end
parser.on("--[no-]stream",
"Outputs analyzed query on the fly.",
"NOTE: --n-entries and --order are ignored.",
"(#{@options[:stream]})") do |stream|
@options[:stream] = stream
end
parser.on("--[no-]report-summary",
"Reports summary at the end.",
"(#{@options[:report_summary]})") do |report_summary|
@options[:report_summary] = report_summary
end
end
def create_reporter(statistics)
case @options[:reporter]
when "json"
require 'json'
JSONQueryLogReporter.new(statistics)
when "html"
HTMLQueryLogReporter.new(statistics)
else
ConsoleQueryLogReporter.new(statistics)
end
end
def create_stream_reporter
case @options[:reporter]
when "json"
require 'json'
StreamJSONQueryLogReporter.new
when "html"
raise UnsupportedReporter, "HTML reporter doesn't support --stream."
else
StreamConsoleQueryLogReporter.new
end
end
end
class Command
class << self
@@registered_commands = {}
def register(name, klass)
@@registered_commands[name] = klass
end
def parse(input)
if input.start_with?("/d/")
parse_uri_path(input)
else
parse_command_line(input)
end
end
private
def parse_uri_path(path)
name, parameters_string = path.split(/\?/, 2)
parameters = {}
parameters_string.split(/&/).each do |parameter_string|
key, value = parameter_string.split(/\=/, 2)
parameters[key] = CGI.unescape(value)
end
name = name.gsub(/\A\/d\//, '')
name, output_type = name.split(/\./, 2)
parameters["output_type"] = output_type if output_type
command_class = @@registered_commands[name] || self
command_class.new(name, parameters)
end
def parse_command_line(command_line)
name, *options = Shellwords.shellwords(command_line)
parameters = {}
options.each_slice(2) do |key, value|
parameters[key.gsub(/\A--/, '')] = value
end
command_class = @@registered_commands[name] || self
command_class.new(name, parameters)
end
end
attr_reader :name, :parameters
def initialize(name, parameters)
@name = name
@parameters = parameters
end
def ==(other)
other.is_a?(self.class) and
@name == other.name and
@parameters == other.parameters
end
end
class SelectCommand < Command
register("select", self)
def sortby
@parameters["sortby"]
end
def scorer
@parameters["scorer"]
end
def query
@parameters["query"]
end
def filter
@parameters["filter"]
end
def conditions
@conditions ||= filter.split(/(?:&&|&!|\|\|)/).collect do |condition|
condition = condition.strip
condition = condition.gsub(/\A[\s\(]*/, '')
condition = condition.gsub(/[\s\)]*\z/, '') unless /\(/ =~ condition
condition
end
end
def drilldowns
@drilldowns ||= (@parameters["drilldown"] || "").split(/\s*,\s*/)
end
def output_columns
@parameters["output_columns"]
end
end
class Statistic
attr_reader :context_id, :start_time, :raw_command
attr_reader :elapsed, :return_code
attr_accessor :slow_operation_threshold, :slow_response_threshold
def initialize(context_id)
@context_id = context_id
@start_time = nil
@command = nil
@raw_command = nil
@operations = []
@elapsed = nil
@return_code = 0
@slow_operation_threshold = 0.1
@slow_response_threshold = 0.2
end
def start(start_time, command)
@start_time = start_time
@raw_command = command
end
def finish(elapsed, return_code)
@elapsed = elapsed
@return_code = return_code
end
def command
@command ||= Command.parse(@raw_command)
end
def elapsed_in_seconds
nano_seconds_to_seconds(@elapsed)
end
def last_time
@start_time + elapsed_in_seconds
end
def slow?
elapsed_in_seconds >= @slow_response_threshold
end
def each_operation
previous_elapsed = 0
ensure_parse_command
operation_context_context = {
:filter_index => 0,
:drilldown_index => 0,
}
@operations.each_with_index do |operation, i|
relative_elapsed = operation[:elapsed] - previous_elapsed
relative_elapsed_in_seconds = nano_seconds_to_seconds(relative_elapsed)
previous_elapsed = operation[:elapsed]
parsed_operation = {
:i => i,
:elapsed => operation[:elapsed],
:elapsed_in_seconds => nano_seconds_to_seconds(operation[:elapsed]),
:relative_elapsed => relative_elapsed,
:relative_elapsed_in_seconds => relative_elapsed_in_seconds,
:name => operation[:name],
:context => operation_context(operation[:name],
operation_context_context),
:n_records => operation[:n_records],
:slow? => slow_operation?(relative_elapsed_in_seconds),
}
yield parsed_operation
end
end
def add_operation(operation)
@operations << operation
end
def operations
_operations = []
each_operation do |operation|
_operations << operation
end
_operations
end
def select_command?
command.name == "select"
end
private
def nano_seconds_to_seconds(nano_seconds)
nano_seconds / 1000.0 / 1000.0 / 1000.0
end
def operation_context(label, context)
case label
when "filter"
if @select_command.query and context[:query_used].nil?
context[:query_used] = true
"query: #{@select_command.query}"
else
index = context[:filter_index]
context[:filter_index] += 1
@select_command.conditions[index]
end
when "sort"
@select_command.sortby
when "score"
@select_command.scorer
when "output"
@select_command.output_columns
when "drilldown"
index = context[:drilldown_index]
context[:drilldown_index] += 1
@select_command.drilldowns[index]
else
nil
end
end
def ensure_parse_command
return unless select_command?
@select_command = SelectCommand.parse(@raw_command)
end
def slow_operation?(elapsed)
elapsed >= @slow_operation_threshold
end
end
class SizedGroupedOperations < Array
def initialize
@max_size = 10
@sorter = create_sorter
end
def apply_options(options)
@max_size = options[:n_entries]
end
def each
i = 0
super do |grouped_operation|
break if i >= @max_size
i += 1
yield(grouped_operation)
end
end
def <<(operation)
each do |grouped_operation|
if grouped_operation[:name] == operation[:name] and
grouped_operation[:context] == operation[:context]
elapsed = operation[:relative_elapsed_in_seconds]
grouped_operation[:total_elapsed] += elapsed
grouped_operation[:n_operations] += 1
replace(sort_by(&@sorter))
return self
end
end
grouped_operation = {
:name => operation[:name],
:context => operation[:context],
:n_operations => 1,
:total_elapsed => operation[:relative_elapsed_in_seconds],
}
buffer_size = @max_size * 100
if size < buffer_size
super(grouped_operation)
replace(sort_by(&@sorter))
else
if @sorter.call(grouped_operation) < @sorter.call(last)
super(grouped_operation)
sorted_operations = sort_by(&@sorter)
sorted_operations.pop
replace(sorted_operations)
end
end
self
end
private
def create_sorter
lambda do |grouped_operation|
-grouped_operation[:total_elapsed]
end
end
end
class SizedStatistics < Array
attr_reader :n_responses, :n_slow_responses, :n_slow_operations
attr_reader :slow_operations, :total_elapsed
attr_reader :start_time, :last_time
attr_accessor :slow_operation_threshold, :slow_response_threshold
def initialize
@max_size = 10
self.order = "-elapsed"
@slow_operation_threshold = 0.1
@slow_response_threshold = 0.2
@start_time = nil
@last_time = nil
@n_responses = 0
@n_slow_responses = 0
@n_slow_operations = 0
@slow_operations = SizedGroupedOperations.new
@total_elapsed = 0
@collect_slow_statistics = true
end
def order=(new_order)
@order = new_order
@sorter = create_sorter
end
def apply_options(options)
@max_size = options[:n_entries] || @max_size
self.order = options[:order] || @order
@slow_operation_threshold =
options[:slow_operation_threshold] || @slow_operation_threshold
@slow_response_threshold =
options[:slow_response_threshold] || @slow_response_threshold
unless options[:report_summary].nil?
@collect_slow_statistics = options[:report_summary]
end
@slow_operations.apply_options(options)
end
def <<(statistic)
update_statistic(statistic)
if size < @max_size
super(statistic)
replace(self)
else
if @sorter.call(statistic) < @sorter.call(last)
super(statistic)
replace(self)
end
end
self
end
def replace(other)
sorted_other = other.sort_by(&@sorter)
if sorted_other.size > @max_size
super(sorted_other[0, @max_size])
else
super(sorted_other)
end
end
def responses_per_second
_period = period
if _period.zero?
0
else
@n_responses.to_f / _period
end
end
def slow_response_ratio
if @n_responses.zero?
0
else
(@n_slow_responses.to_f / @n_responses) * 100
end
end
def period
if @start_time and @last_time
@last_time - @start_time
else
0
end
end
def each_slow_operation
@slow_operations.each do |grouped_operation|
total_elapsed = grouped_operation[:total_elapsed]
n_operations = grouped_operation[:n_operations]
ratios = {
:total_elapsed_ratio => total_elapsed / @total_elapsed * 100,
:n_operations_ratio => n_operations / @n_slow_operations.to_f * 100,
}
yield(grouped_operation.merge(ratios))
end
end
private
def create_sorter
case @order
when "-elapsed"
lambda do |statistic|
-statistic.elapsed
end
when "elapsed"
lambda do |statistic|
statistic.elapsed
end
when "-start-time"
lambda do |statistic|
-statistic.start_time
end
else
lambda do |statistic|
statistic.start_time
end
end
end
def update_statistic(statistic)
statistic.slow_response_threshold = @slow_response_threshold
statistic.slow_operation_threshold = @slow_operation_threshold
@start_time ||= statistic.start_time
@start_time = [@start_time, statistic.start_time].min
@last_time ||= statistic.last_time
@last_time = [@last_time, statistic.last_time].max
@n_responses += 1
@total_elapsed += statistic.elapsed_in_seconds
return unless @collect_slow_statistics
if statistic.slow?
@n_slow_responses += 1
if statistic.select_command?
statistic.each_operation do |operation|
next unless operation[:slow?]
@n_slow_operations += 1
@slow_operations << operation
end
end
end
end
end
class QueryLogParser
def initialize
@mutex = Mutex.new
end
def parse(input, &block)
current_statistics = {}
input.each_line do |line|
case line
when /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\.(\d+)\|(.+?)\|([>:<])/
year, month, day, hour, minutes, seconds, micro_seconds =
$1, $2, $3, $4, $5, $6, $7
context_id = $8
type = $9
rest = $POSTMATCH.strip
time_stamp = Time.local(year, month, day, hour, minutes, seconds,
micro_seconds)
parse_line(current_statistics,
time_stamp, context_id, type, rest, &block)
end
end
end
private
def parse_line(current_statistics,
time_stamp, context_id, type, rest, &block)
case type
when ">"
statistic = Statistic.new(context_id)
statistic.start(time_stamp, rest)
current_statistics[context_id] = statistic
when ":"
return unless /\A(\d+) (.+)\((\d+)\)/ =~ rest
elapsed = $1
name = $2
n_records = $3.to_i
statistic = current_statistics[context_id]
return if statistic.nil?
statistic.add_operation(:name => name,
:elapsed => elapsed.to_i,
:n_records => n_records)
when "<"
return unless /\A(\d+) rc=(\d+)/ =~ rest
elapsed = $1
return_code = $2
statistic = current_statistics.delete(context_id)
return if statistic.nil?
statistic.finish(elapsed.to_i, return_code.to_i)
block.call(statistic)
end
end
end
class Streamer
def initialize(reporter)
@reporter = reporter
end
def start
@reporter.start
end
def <<(statistic)
@reporter.report_statistic(statistic)
end
def finish
@reporter.finish
end
end
class QueryLogReporter
include Enumerable
attr_reader :output
def initialize(statistics)
@statistics = statistics
@report_summary = true
@output = $stdout
end
def apply_options(options)
self.output = options[:output] || @output
unless options[:report_summary].nil?
@report_summary = options[:report_summary]
end
end
def output=(output)
@output = output
@output = $stdout if @output == "-"
end
def each
@statistics.each do |statistic|
yield statistic
end
end
def report
setup do
report_summary if @report_summary
report_statistics
end
end
def report_statistics
each do |statistic|
report_statistic(statistic)
end
end
private
def setup
setup_output do
start
yield
finish
end
end
def setup_output
original_output = @output
if @output.is_a?(String)
File.open(@output, "w") do |output|
@output = output
yield(@output)
end
else
yield(@output)
end
ensure
@output = original_output
end
def write(*args)
@output.write(*args)
end
def format_time(time)
if time.nil?
"NaN"
else
time.strftime("%Y-%m-%d %H:%M:%S.%u")
end
end
end
class ConsoleQueryLogReporter < QueryLogReporter
class Color
NAMES = ["black", "red", "green", "yellow",
"blue", "magenta", "cyan", "white"]
attr_reader :name
def initialize(name, options={})
@name = name
@foreground = options[:foreground]
@foreground = true if @foreground.nil?
@intensity = options[:intensity]
@bold = options[:bold]
@italic = options[:italic]
@underline = options[:underline]
end
def foreground?
@foreground
end
def intensity?
@intensity
end
def bold?
@bold
end
def italic?
@italic
end
def underline?
@underline
end
def ==(other)
self.class === other and
[name, foreground?, intensity?,
bold?, italic?, underline?] ==
[other.name, other.foreground?, other.intensity?,
other.bold?, other.italic?, other.underline?]
end
def sequence
sequence = []
if @name == "none"
elsif @name == "reset"
sequence << "0"
else
foreground_parameter = foreground? ? 3 : 4
foreground_parameter += 6 if intensity?
sequence << "#{foreground_parameter}#{NAMES.index(@name)}"
end
sequence << "1" if bold?
sequence << "3" if italic?
sequence << "4" if underline?
sequence
end
def escape_sequence
"\e[#{sequence.join(';')}m"
end
def +(other)
MixColor.new([self, other])
end
end
class MixColor
attr_reader :colors
def initialize(colors)
@colors = colors
end
def sequence
@colors.inject([]) do |result, color|
result + color.sequence
end
end
def escape_sequence
"\e[#{sequence.join(';')}m"
end
def +(other)
self.class.new([self, other])
end
def ==(other)
self.class === other and colors == other.colors
end
end
def initialize(statistics)
super
@color = :auto
@reset_color = Color.new("reset")
@color_schema = {
:elapsed => {:foreground => :white, :background => :green},
:time => {:foreground => :white, :background => :cyan},
:slow => {:foreground => :white, :background => :red},
}
end
def apply_options(options)
super
@color = options[:color] || @color
end
def report_statistics
write("\n")
write("Slow Queries:\n")
super
end
def report_statistic(statistic)
@index += 1
write("%*d) %s" % [@digit, @index, format_heading(statistic)])
report_parameters(statistic)
report_operations(statistic)
end
def start
@index = 0
if @statistics.size.zero?
@digit = 1
else
@digit = Math.log10(@statistics.size).truncate + 1
end
end
def finish
end
private
def setup
super do
setup_color do
yield
end
end
end
def report_summary
write("Summary:\n")
write(" Threshold:\n")
write(" slow response : #{@statistics.slow_response_threshold}\n")
write(" slow operation : #{@statistics.slow_operation_threshold}\n")
write(" # of responses : #{@statistics.n_responses}\n")
write(" # of slow responses : #{@statistics.n_slow_responses}\n")
write(" responses/sec : #{@statistics.responses_per_second}\n")
write(" start time : #{format_time(@statistics.start_time)}\n")
write(" last time : #{format_time(@statistics.last_time)}\n")
write(" period(sec) : #{@statistics.period}\n")
slow_response_ratio = @statistics.slow_response_ratio
write(" slow response ratio : %5.3f%%\n" % slow_response_ratio)
write(" total response time : #{@statistics.total_elapsed}\n")
report_slow_operations
end
def report_slow_operations
write(" Slow Operations:\n")
total_elapsed_digit = nil
total_elapsed_decimal_digit = 6
n_operations_digit = nil
@statistics.each_slow_operation do |grouped_operation|
total_elapsed = grouped_operation[:total_elapsed]
total_elapsed_digit ||= Math.log10(total_elapsed).truncate + 1
n_operations = grouped_operation[:n_operations]
n_operations_digit ||= Math.log10(n_operations).truncate + 1
parameters = [total_elapsed_digit + 1 + total_elapsed_decimal_digit,
total_elapsed_decimal_digit,
total_elapsed,
grouped_operation[:total_elapsed_ratio],
n_operations_digit,
n_operations,
grouped_operation[:n_operations_ratio],
grouped_operation[:name],
grouped_operation[:context]]
write(" [%*.*f](%5.2f%%) [%*d](%5.2f%%) %9s: %s\n" % parameters)
end
end
def report_parameters(statistic)
command = statistic.command
write(" name: <#{command.name}>\n")
write(" parameters:\n")
command.parameters.each do |key, value|
write(" <#{key}>: <#{value}>\n")
end
end
def report_operations(statistic)
statistic.each_operation do |operation|
relative_elapsed_in_seconds = operation[:relative_elapsed_in_seconds]
formatted_elapsed = "%8.8f" % relative_elapsed_in_seconds
if operation[:slow?]
formatted_elapsed = colorize(formatted_elapsed, :slow)
end
operation_report = " %2d) %s: %10s" % [operation[:i] + 1,
formatted_elapsed,
operation[:name]]
if operation[:n_records]
operation_report << "(%6d)" % operation[:n_records]
else
operation_report << "(%6s)" % ""
end
context = operation[:context]
if context
context = colorize(context, :slow) if operation[:slow?]
operation_report << " " << context
end
write("#{operation_report}\n")
end
write("\n")
end
def guess_color_availability(output)
return false unless output.tty?
case ENV["TERM"]
when /term(?:-color)?\z/, "screen"
true
else
return true if ENV["EMACS"] == "t"
false
end
end
def setup_color
color = @color
@color = guess_color_availability(@output) if @color == :auto
yield
ensure
@color = color
end
def format_heading(statistic)
formatted_elapsed = colorize("%8.8f" % statistic.elapsed_in_seconds,
:elapsed)
"[%s-%s (%s)](%d): %s" % [format_time(statistic.start_time),
format_time(statistic.last_time),
formatted_elapsed,
statistic.return_code,
statistic.raw_command]
end
def format_time(time)
colorize(super, :time)
end
def colorize(text, schema_name)
return text unless @color
options = @color_schema[schema_name]
color = Color.new("none")
if options[:foreground]
color += Color.new(options[:foreground].to_s, :bold => true)
end
if options[:background]
color += Color.new(options[:background].to_s, :foreground => false)
end
"%s%s%s" % [color.escape_sequence, text, @reset_color.escape_sequence]
end
end
class JSONQueryLogReporter < QueryLogReporter
def report_statistic(statistic)
write(",") if @index > 0
write("\n")
write(format_statistic(statistic))
@index += 1
end
def start
@index = 0
write("[")
end
def finish
write("\n")
write("]\n")
end
def report_summary
# TODO
end
private
def format_statistic(statistic)
data = {
"start_time" => statistic.start_time.to_i,
"last_time" => statistic.last_time.to_i,
"elapsed" => statistic.elapsed_in_seconds,
"return_code" => statistic.return_code,
}
command = statistic.command
parameters = command.parameters.collect do |key, value|
{"key" => key, "value" => value}
end
data["command"] = {
"raw" => statistic.raw_command,
"name" => command.name,
"parameters" => parameters,
}
operations = []
statistic.each_operation do |operation|
operation_data = {}
operation_data["name"] = operation[:name]
operation_data["relative_elapsed"] = operation[:relative_elapsed_in_seconds]
operation_data["context"] = operation[:context]
operations << operation_data
end
data["operations"] = operations
JSON.generate(data)
end
end
class HTMLQueryLogReporter < QueryLogReporter
include ERB::Util
def report_statistic(statistic)
write(",") if @index > 0
write("\n")
write(format_statistic(statistic))
@index += 1
end
def start
write(header)
end
def finish
write(footer)
end
def report_summary
summary_html = erb(<<-EOH, __LINE__ + 1, binding)
<h2>Summary</h2>
<div class="summary">
<%= analyze_parameters %>
<%= metrics %>
<%= slow_operations %>
</div>
EOH
write(summary_html)
end
def report_statistics
write(statistics_header)
super
write(statistics_footer)
end
def report_statistic(statistic)
command = statistic.command
statistic_html = erb(<<-EOH, __LINE__ + 1, binding)
<div class="statistic-heading">
<h3>Command</h3>
<div class="metrics">
[<%= format_time(statistic.start_time) %>
-
<%= format_time(statistic.last_time) %>
(<%= format_elapsed(statistic.elapsed_in_seconds,
:slow? => statistic.slow?) %>)]
(<%= span({:class => "return-code"}, h(statistic.return_code)) %>)
</div>
<%= div({:class => "raw-command"}, h(statistic.raw_command)) %>
</div>
<div class="statistic-parameters">
<h3>Parameters</h3>
<dl>
<dt>name</dt>
<dd><%= h(command.name) %></dd>
<% command.parameters.each do |key, value| %>
<dt><%= h(key) %></dt>
<dd><%= h(value) %></dd>
<% end %>
</dl>
</div>
<div class="statistic-operations">
<h3>Operations</h3>
<ol>
<% statistic.each_operation do |operation| %>
<li>
<%= format_elapsed(operation[:relative_elapsed_in_seconds],
:slow? => operation[:slow?]) %>:
<%= span({:class => "name"}, h(operation[:name])) %>:
<%= span({:class => "context"}, h(operation[:context])) %>
</li>
<% end %>
</ol>
</div>
EOH
write(statistic_html)
end
private
def erb(content, line, _binding=nil)
_erb = ERB.new(content, nil, "<>")
eval(_erb.src, _binding || binding, __FILE__, line)
end
def header
erb(<<-EOH, __LINE__ + 1)
<html>
<head>
<title>groonga query analyzer</title>
<style>
table,
table tr,
table tr th,
table tr td
{
border: 1px solid black;
}
span.slow
{
color: red;
}
div.parameters
{
float: left;
padding: 2em;
}
div.parameters h3
{
text-align: center;
}
div.parameters table
{
margin-right: auto;
margin-left: auto;
}
div.statistics
{
clear: both;
}
td.elapsed,
td.ratio,
td.n
{
text-align: right;
}
td.name
{
text-align: center;
}
</style>
</head>
<body>
<h1>groonga query analyzer</h1>
EOH
end
def footer
erb(<<-EOH, __LINE__ + 1)
</body>
</html>
EOH
end
def statistics_header
erb(<<-EOH, __LINE__ + 1)
<h2>Slow Queries</h2>
<div>
EOH
end
def statistics_footer
erb(<<-EOH, __LINE__ + 1)
</div>
EOH
end
def analyze_parameters
erb(<<-EOH, __LINE__ + 1)
<div class="parameters">
<h3>Analyze Parameters</h3>
<table>
<tr><th>Name</th><th>Value</th></tr>
<tr>
<th>Slow response threshold</th>
<td><%= h(@statistics.slow_response_threshold) %>sec</td>
</tr>
<tr>
<th>Slow operation threshold</th>
<td><%= h(@statistics.slow_operation_threshold) %>sec</td>
</tr>
</table>
</div>
EOH
end
def metrics
erb(<<-EOH, __LINE__ + 1)
<div class="parameters">
<h3>Metrics</h3>
<table>
<tr><th>Name</th><th>Value</th></tr>
<tr>
<th># of responses</th>
<td><%= h(@statistics.n_responses) %></td>
</tr>
<tr>
<th># of slow responses</th>
<td><%= h(@statistics.n_slow_responses) %></td>
</tr>
<tr>
<th>responses/sec</th>
<td><%= h(@statistics.responses_per_second) %></td>
</tr>
<tr>
<th>start time</th>
<td><%= format_time(@statistics.start_time) %></td>
</tr>
<tr>
<th>last time</th>
<td><%= format_time(@statistics.last_time) %></td>
</tr>
<tr>
<th>period</th>
<td><%= h(@statistics.period) %>sec</td>
</tr>
<tr>
<th>slow response ratio</th>
<td><%= h(@statistics.slow_response_ratio) %>%</td>
</tr>
<tr>
<th>total response time</th>
<td><%= h(@statistics.total_elapsed) %>sec</td>
</tr>
</table>
</div>
EOH
end
def slow_operations
erb(<<-EOH, __LINE__ + 1)
<div class="statistics">
<h3>Slow Operations</h3>
<table class="slow-operations">
<tr>
<th>total elapsed(sec)</th>
<th>total elapsed(%)</th>
<th># of operations</th>
<th># of operations(%)</th>
<th>operation name</th>
<th>context</th>
</tr>
<% @statistics.each_slow_operation do |grouped_operation| %>
<tr>
<td class="elapsed">
<%= format_elapsed(grouped_operation[:total_elapsed]) %>
</td>
<td class="ratio">
<%= format_ratio(grouped_operation[:total_elapsed_ratio]) %>
</td>
<td class="n">
<%= h(grouped_operation[:n_operations]) %>
</td>
<td class="ratio">
<%= format_ratio(grouped_operation[:n_operations_ratio]) %>
</td>
<td class="name"><%= h(grouped_operation[:name]) %></td>
<td class="context">
<%= format_context(grouped_operation[:context]) %>
</td>
</tr>
<% end %>
</table>
</div>
EOH
end
def format_time(time)
span({:class => "time"}, h(super))
end
def format_elapsed(elapsed, options={})
formatted_elapsed = span({:class => "elapsed"}, h("%8.8f" % elapsed))
if options[:slow?]
formatted_elapsed = span({:class => "slow"}, formatted_elapsed)
end
formatted_elapsed
end
def format_ratio(ratio)
h("%5.2f%%" % ratio)
end
def format_context(context)
h(context).gsub(/,/, ",<wbr />")
end
def tag(name, attributes, content)
html = "<#{name}"
html_attributes = attributes.collect do |key, value|
"#{h(key)}=\"#{h(value)}\""
end
html << " #{html_attributes.join(' ')}" unless attributes.empty?
html << ">"
html << content
html << "</#{name}>"
html
end
def span(attributes, content)
tag("span", attributes, content)
end
def div(attributes, content)
tag("div", attributes, content)
end
end
end
if __FILE__ == $0
analyzer = GroongaQueryLogAnalyzer.new
begin
analyzer.run
rescue GroongaQueryLogAnalyzer::Error
$stderr.puts($!.message)
exit(false)
end
end
Jump to Line
Something went wrong with that request. Please try again.