groonga-query-log 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.yardopts +5 -0
- data/Gemfile +21 -0
- data/README.md +49 -0
- data/Rakefile +51 -0
- data/bin/groonga-query-log-analyzer +28 -0
- data/doc/text/lgpl-2.1.txt +502 -0
- data/doc/text/news.md +5 -0
- data/groonga-query-log.gemspec +65 -0
- data/lib/groonga/query-log.rb +21 -0
- data/lib/groonga/query-log/analyzer.rb +212 -0
- data/lib/groonga/query-log/analyzer/reporter.rb +101 -0
- data/lib/groonga/query-log/analyzer/reporter/console.rb +291 -0
- data/lib/groonga/query-log/analyzer/reporter/html.rb +325 -0
- data/lib/groonga/query-log/analyzer/reporter/json.rb +78 -0
- data/lib/groonga/query-log/analyzer/sized-grouped-operations.rb +84 -0
- data/lib/groonga/query-log/analyzer/sized-statistics.rb +172 -0
- data/lib/groonga/query-log/analyzer/statistic.rb +160 -0
- data/lib/groonga/query-log/analyzer/streamer.rb +42 -0
- data/lib/groonga/query-log/parser.rb +77 -0
- data/lib/groonga/query-log/version.rb +23 -0
- data/test/command/test-select.rb +162 -0
- data/test/fixtures/n_entries.expected +19 -0
- data/test/fixtures/no-report-summary.expected +15 -0
- data/test/fixtures/order/-elapsed.expected +28 -0
- data/test/fixtures/order/-start-time.expected +28 -0
- data/test/fixtures/order/elapsed.expected +28 -0
- data/test/fixtures/order/start-time.expected +28 -0
- data/test/fixtures/query.log +7 -0
- data/test/fixtures/reporter/console.expected +28 -0
- data/test/fixtures/reporter/html.expected +196 -0
- data/test/fixtures/reporter/json.expected +4 -0
- data/test/groonga-query-log-test-utils.rb +79 -0
- data/test/run-test.rb +43 -0
- data/test/test-analyzer.rb +82 -0
- data/test/test-parser.rb +90 -0
- metadata +235 -0
@@ -0,0 +1,172 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2011-2012 Kouhei Sutou <kou@clear-code.com>
|
4
|
+
# Copyright (C) 2012 Haruka Yoshihara <yoshihara@clear-code.com>
|
5
|
+
#
|
6
|
+
# This library is free software; you can redistribute it and/or
|
7
|
+
# modify it under the terms of the GNU Lesser General Public
|
8
|
+
# License as published by the Free Software Foundation; either
|
9
|
+
# version 2.1 of the License, or (at your option) any later version.
|
10
|
+
#
|
11
|
+
# This library is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14
|
+
# Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public
|
17
|
+
# License along with this library; if not, write to the Free Software
|
18
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
19
|
+
|
20
|
+
require "groonga/query-log/analyzer/sized-grouped-operations"
|
21
|
+
require "groonga/query-log/analyzer/reporter/console"
|
22
|
+
require "groonga/query-log/analyzer/reporter/html"
|
23
|
+
require "groonga/query-log/analyzer/reporter/json"
|
24
|
+
|
25
|
+
module Groonga
|
26
|
+
module QueryLog
|
27
|
+
class Analyzer
|
28
|
+
class SizedStatistics < Array
|
29
|
+
attr_reader :n_responses, :n_slow_responses, :n_slow_operations
|
30
|
+
attr_reader :slow_operations, :total_elapsed
|
31
|
+
attr_reader :start_time, :last_time
|
32
|
+
attr_accessor :slow_operation_threshold, :slow_response_threshold
|
33
|
+
def initialize
|
34
|
+
@max_size = 10
|
35
|
+
self.order = "-elapsed"
|
36
|
+
@slow_operation_threshold = 0.1
|
37
|
+
@slow_response_threshold = 0.2
|
38
|
+
@start_time = nil
|
39
|
+
@last_time = nil
|
40
|
+
@n_responses = 0
|
41
|
+
@n_slow_responses = 0
|
42
|
+
@n_slow_operations = 0
|
43
|
+
@slow_operations = SizedGroupedOperations.new
|
44
|
+
@total_elapsed = 0
|
45
|
+
@collect_slow_statistics = true
|
46
|
+
end
|
47
|
+
|
48
|
+
def order=(new_order)
|
49
|
+
@order = new_order
|
50
|
+
@sorter = create_sorter
|
51
|
+
end
|
52
|
+
|
53
|
+
def apply_options(options)
|
54
|
+
@max_size = options[:n_entries] || @max_size
|
55
|
+
self.order = options[:order] || @order
|
56
|
+
@slow_operation_threshold =
|
57
|
+
options[:slow_operation_threshold] || @slow_operation_threshold
|
58
|
+
@slow_response_threshold =
|
59
|
+
options[:slow_response_threshold] || @slow_response_threshold
|
60
|
+
unless options[:report_summary].nil?
|
61
|
+
@collect_slow_statistics = options[:report_summary]
|
62
|
+
end
|
63
|
+
@slow_operations.apply_options(options)
|
64
|
+
end
|
65
|
+
|
66
|
+
def <<(statistic)
|
67
|
+
update_statistic(statistic)
|
68
|
+
if size < @max_size
|
69
|
+
super(statistic)
|
70
|
+
replace(self)
|
71
|
+
else
|
72
|
+
if @sorter.call(statistic) < @sorter.call(last)
|
73
|
+
super(statistic)
|
74
|
+
replace(self)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
self
|
78
|
+
end
|
79
|
+
|
80
|
+
def replace(other)
|
81
|
+
sorted_other = other.sort_by(&@sorter)
|
82
|
+
if sorted_other.size > @max_size
|
83
|
+
super(sorted_other[0, @max_size])
|
84
|
+
else
|
85
|
+
super(sorted_other)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def responses_per_second
|
90
|
+
_period = period
|
91
|
+
if _period.zero?
|
92
|
+
0
|
93
|
+
else
|
94
|
+
@n_responses.to_f / _period
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def slow_response_ratio
|
99
|
+
if @n_responses.zero?
|
100
|
+
0
|
101
|
+
else
|
102
|
+
(@n_slow_responses.to_f / @n_responses) * 100
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def period
|
107
|
+
if @start_time and @last_time
|
108
|
+
@last_time - @start_time
|
109
|
+
else
|
110
|
+
0
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def each_slow_operation
|
115
|
+
@slow_operations.each do |grouped_operation|
|
116
|
+
total_elapsed = grouped_operation[:total_elapsed]
|
117
|
+
n_operations = grouped_operation[:n_operations]
|
118
|
+
ratios = {
|
119
|
+
:total_elapsed_ratio => total_elapsed / @total_elapsed * 100,
|
120
|
+
:n_operations_ratio => n_operations / @n_slow_operations.to_f * 100,
|
121
|
+
}
|
122
|
+
yield(grouped_operation.merge(ratios))
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
def create_sorter
|
128
|
+
case @order
|
129
|
+
when "-elapsed"
|
130
|
+
lambda do |statistic|
|
131
|
+
-statistic.elapsed
|
132
|
+
end
|
133
|
+
when "elapsed"
|
134
|
+
lambda do |statistic|
|
135
|
+
statistic.elapsed
|
136
|
+
end
|
137
|
+
when "-start-time"
|
138
|
+
lambda do |statistic|
|
139
|
+
-statistic.start_time.to_f
|
140
|
+
end
|
141
|
+
else
|
142
|
+
lambda do |statistic|
|
143
|
+
statistic.start_time.to_f
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def update_statistic(statistic)
|
149
|
+
statistic.slow_response_threshold = @slow_response_threshold
|
150
|
+
statistic.slow_operation_threshold = @slow_operation_threshold
|
151
|
+
@start_time ||= statistic.start_time
|
152
|
+
@start_time = [@start_time, statistic.start_time].min
|
153
|
+
@last_time ||= statistic.last_time
|
154
|
+
@last_time = [@last_time, statistic.last_time].max
|
155
|
+
@n_responses += 1
|
156
|
+
@total_elapsed += statistic.elapsed_in_seconds
|
157
|
+
return unless @collect_slow_statistics
|
158
|
+
if statistic.slow?
|
159
|
+
@n_slow_responses += 1
|
160
|
+
if statistic.select_command?
|
161
|
+
statistic.each_operation do |operation|
|
162
|
+
next unless operation[:slow?]
|
163
|
+
@n_slow_operations += 1
|
164
|
+
@slow_operations << operation
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2011-2012 Kouhei Sutou <kou@clear-code.com>
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
|
19
|
+
require "groonga/command"
|
20
|
+
|
21
|
+
module Groonga
|
22
|
+
module QueryLog
|
23
|
+
class Analyzer
|
24
|
+
class Statistic
|
25
|
+
attr_reader :context_id, :start_time, :raw_command
|
26
|
+
attr_reader :elapsed, :return_code
|
27
|
+
attr_accessor :slow_operation_threshold, :slow_response_threshold
|
28
|
+
def initialize(context_id)
|
29
|
+
@context_id = context_id
|
30
|
+
@start_time = nil
|
31
|
+
@command = nil
|
32
|
+
@raw_command = nil
|
33
|
+
@operations = []
|
34
|
+
@elapsed = nil
|
35
|
+
@return_code = 0
|
36
|
+
@slow_operation_threshold = 0.1
|
37
|
+
@slow_response_threshold = 0.2
|
38
|
+
end
|
39
|
+
|
40
|
+
def start(start_time, command)
|
41
|
+
@start_time = start_time
|
42
|
+
@raw_command = command
|
43
|
+
end
|
44
|
+
|
45
|
+
def finish(elapsed, return_code)
|
46
|
+
@elapsed = elapsed
|
47
|
+
@return_code = return_code
|
48
|
+
end
|
49
|
+
|
50
|
+
def command
|
51
|
+
Groonga::Command::Parser.parse(@raw_command) do |status, command|
|
52
|
+
case status
|
53
|
+
when :on_load_start
|
54
|
+
@loading = false
|
55
|
+
@command ||= command
|
56
|
+
when :on_command
|
57
|
+
@command ||= command
|
58
|
+
end
|
59
|
+
end
|
60
|
+
@command
|
61
|
+
end
|
62
|
+
|
63
|
+
def elapsed_in_seconds
|
64
|
+
nano_seconds_to_seconds(@elapsed)
|
65
|
+
end
|
66
|
+
|
67
|
+
def last_time
|
68
|
+
@start_time + elapsed_in_seconds
|
69
|
+
end
|
70
|
+
|
71
|
+
def slow?
|
72
|
+
elapsed_in_seconds >= @slow_response_threshold
|
73
|
+
end
|
74
|
+
|
75
|
+
def each_operation
|
76
|
+
previous_elapsed = 0
|
77
|
+
ensure_parse_command
|
78
|
+
operation_context_context = {
|
79
|
+
:filter_index => 0,
|
80
|
+
:drilldown_index => 0,
|
81
|
+
}
|
82
|
+
@operations.each_with_index do |operation, i|
|
83
|
+
relative_elapsed = operation[:elapsed] - previous_elapsed
|
84
|
+
relative_elapsed_in_seconds = nano_seconds_to_seconds(relative_elapsed)
|
85
|
+
previous_elapsed = operation[:elapsed]
|
86
|
+
parsed_operation = {
|
87
|
+
:i => i,
|
88
|
+
:elapsed => operation[:elapsed],
|
89
|
+
:elapsed_in_seconds => nano_seconds_to_seconds(operation[:elapsed]),
|
90
|
+
:relative_elapsed => relative_elapsed,
|
91
|
+
:relative_elapsed_in_seconds => relative_elapsed_in_seconds,
|
92
|
+
:name => operation[:name],
|
93
|
+
:context => operation_context(operation[:name],
|
94
|
+
operation_context_context),
|
95
|
+
:n_records => operation[:n_records],
|
96
|
+
:slow? => slow_operation?(relative_elapsed_in_seconds),
|
97
|
+
}
|
98
|
+
yield parsed_operation
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_operation(operation)
|
103
|
+
@operations << operation
|
104
|
+
end
|
105
|
+
|
106
|
+
def operations
|
107
|
+
_operations = []
|
108
|
+
each_operation do |operation|
|
109
|
+
_operations << operation
|
110
|
+
end
|
111
|
+
_operations
|
112
|
+
end
|
113
|
+
|
114
|
+
def select_command?
|
115
|
+
command.name == "select"
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
def nano_seconds_to_seconds(nano_seconds)
|
120
|
+
nano_seconds / 1000.0 / 1000.0 / 1000.0
|
121
|
+
end
|
122
|
+
|
123
|
+
def operation_context(label, context)
|
124
|
+
case label
|
125
|
+
when "filter"
|
126
|
+
if @select_command.query and context[:query_used].nil?
|
127
|
+
context[:query_used] = true
|
128
|
+
"query: #{@select_command.query}"
|
129
|
+
else
|
130
|
+
index = context[:filter_index]
|
131
|
+
context[:filter_index] += 1
|
132
|
+
@select_command.conditions[index]
|
133
|
+
end
|
134
|
+
when "sort"
|
135
|
+
@select_command.sortby
|
136
|
+
when "score"
|
137
|
+
@select_command.scorer
|
138
|
+
when "output"
|
139
|
+
@select_command.output_columns
|
140
|
+
when "drilldown"
|
141
|
+
index = context[:drilldown_index]
|
142
|
+
context[:drilldown_index] += 1
|
143
|
+
@select_command.drilldowns[index]
|
144
|
+
else
|
145
|
+
nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def ensure_parse_command
|
150
|
+
return unless select_command?
|
151
|
+
@select_command = Groonga::Command::Parser.parse(@raw_command)
|
152
|
+
end
|
153
|
+
|
154
|
+
def slow_operation?(elapsed)
|
155
|
+
elapsed >= @slow_operation_threshold
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2011-2012 Kouhei Sutou <kou@clear-code.com>
|
4
|
+
# Copyright (C) 2012 Haruka Yoshihara <yoshihara@clear-code.com>
|
5
|
+
#
|
6
|
+
# This library is free software; you can redistribute it and/or
|
7
|
+
# modify it under the terms of the GNU Lesser General Public
|
8
|
+
# License as published by the Free Software Foundation; either
|
9
|
+
# version 2.1 of the License, or (at your option) any later version.
|
10
|
+
#
|
11
|
+
# This library is distributed in the hope that it will be useful,
|
12
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
13
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
14
|
+
# Lesser General Public License for more details.
|
15
|
+
#
|
16
|
+
# You should have received a copy of the GNU Lesser General Public
|
17
|
+
# License along with this library; if not, write to the Free Software
|
18
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
19
|
+
|
20
|
+
module Groonga
|
21
|
+
module QueryLog
|
22
|
+
class Analyzer
|
23
|
+
class Streamer
|
24
|
+
def initialize(reporter)
|
25
|
+
@reporter = reporter
|
26
|
+
end
|
27
|
+
|
28
|
+
def start
|
29
|
+
@reporter.start
|
30
|
+
end
|
31
|
+
|
32
|
+
def <<(statistic)
|
33
|
+
@reporter.report_statistic(statistic)
|
34
|
+
end
|
35
|
+
|
36
|
+
def finish
|
37
|
+
@reporter.finish
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2011-2012 Kouhei Sutou <kou@clear-code.com>
|
4
|
+
#
|
5
|
+
# This library is free software; you can redistribute it and/or
|
6
|
+
# modify it under the terms of the GNU Lesser General Public
|
7
|
+
# License as published by the Free Software Foundation; either
|
8
|
+
# version 2.1 of the License, or (at your option) any later version.
|
9
|
+
#
|
10
|
+
# This library is distributed in the hope that it will be useful,
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
13
|
+
# Lesser General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU Lesser General Public
|
16
|
+
# License along with this library; if not, write to the Free Software
|
17
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
18
|
+
|
19
|
+
require "English"
|
20
|
+
|
21
|
+
require "groonga/query-log/analyzer/statistic"
|
22
|
+
|
23
|
+
module Groonga
|
24
|
+
module QueryLog
|
25
|
+
class Parser
|
26
|
+
def initialize
|
27
|
+
end
|
28
|
+
|
29
|
+
def parse(input, &block)
|
30
|
+
current_statistics = {}
|
31
|
+
input.each_line do |line|
|
32
|
+
case line
|
33
|
+
when /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\.(\d+)\|(.+?)\|([>:<])/
|
34
|
+
year, month, day, hour, minutes, seconds, micro_seconds =
|
35
|
+
$1, $2, $3, $4, $5, $6, $7
|
36
|
+
context_id = $8
|
37
|
+
type = $9
|
38
|
+
rest = $POSTMATCH.strip
|
39
|
+
time_stamp = Time.local(year, month, day, hour, minutes, seconds,
|
40
|
+
micro_seconds)
|
41
|
+
parse_line(current_statistics,
|
42
|
+
time_stamp, context_id, type, rest, &block)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
def parse_line(current_statistics,
|
49
|
+
time_stamp, context_id, type, rest, &block)
|
50
|
+
case type
|
51
|
+
when ">"
|
52
|
+
statistic = Analyzer::Statistic.new(context_id)
|
53
|
+
statistic.start(time_stamp, rest)
|
54
|
+
current_statistics[context_id] = statistic
|
55
|
+
when ":"
|
56
|
+
return unless /\A(\d+) (.+)\((\d+)\)/ =~ rest
|
57
|
+
elapsed = $1
|
58
|
+
name = $2
|
59
|
+
n_records = $3.to_i
|
60
|
+
statistic = current_statistics[context_id]
|
61
|
+
return if statistic.nil?
|
62
|
+
statistic.add_operation(:name => name,
|
63
|
+
:elapsed => elapsed.to_i,
|
64
|
+
:n_records => n_records)
|
65
|
+
when "<"
|
66
|
+
return unless /\A(\d+) rc=(\d+)/ =~ rest
|
67
|
+
elapsed = $1
|
68
|
+
return_code = $2
|
69
|
+
statistic = current_statistics.delete(context_id)
|
70
|
+
return if statistic.nil?
|
71
|
+
statistic.finish(elapsed.to_i, return_code.to_i)
|
72
|
+
block.call(statistic)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|