groonga-query-log 1.0.1 → 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Gemfile +2 -2
- data/README.md +3 -1
- data/Rakefile +1 -1
- data/bin/groonga-query-log-detect-memory-leak +23 -0
- data/bin/groonga-query-log-replay +23 -0
- data/doc/text/news.md +15 -0
- data/groonga-query-log.gemspec +4 -2
- data/lib/groonga/query-log.rb +1 -0
- data/lib/groonga/query-log/analyzer.rb +3 -3
- data/lib/groonga/query-log/analyzer/reporter/html.rb +1 -0
- data/lib/groonga/query-log/analyzer/statistic.rb +1 -1
- data/lib/groonga/query-log/command/detect-memory-leak.rb +90 -0
- data/lib/groonga/query-log/command/replay.rb +117 -0
- data/lib/groonga/query-log/extractor.rb +4 -4
- data/lib/groonga/query-log/memory-leak-detector.rb +126 -0
- data/lib/groonga/query-log/parser.rb +3 -2
- data/lib/groonga/query-log/replayer.rb +196 -0
- data/lib/groonga/query-log/version.rb +2 -2
- data/test/run-test.rb +22 -7
- data/test/test-parser.rb +38 -12
- data/test/test-replayer.rb +119 -0
- metadata +87 -73
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2011-
|
3
|
+
# Copyright (C) 2011-2013 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -38,6 +38,7 @@ module Groonga
|
|
38
38
|
def parse(input, &block)
|
39
39
|
current_statistics = {}
|
40
40
|
input.each_line do |line|
|
41
|
+
next unless line.valid_encoding?
|
41
42
|
case line
|
42
43
|
when /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)\.(\d+)\|(.+?)\|([>:<])/
|
43
44
|
year, month, day, hour, minutes, seconds, micro_seconds =
|
@@ -72,7 +73,7 @@ module Groonga
|
|
72
73
|
:elapsed => elapsed.to_i,
|
73
74
|
:n_records => n_records)
|
74
75
|
when "<"
|
75
|
-
return unless /\A(\d+) rc=(
|
76
|
+
return unless /\A(\d+) rc=(-?\d+)/ =~ rest
|
76
77
|
elapsed = $1
|
77
78
|
return_code = $2
|
78
79
|
statistic = current_statistics.delete(context_id)
|
@@ -0,0 +1,196 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2013 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 "time"
|
20
|
+
require "thread"
|
21
|
+
|
22
|
+
require "groonga/client"
|
23
|
+
|
24
|
+
require "groonga/query-log/parser"
|
25
|
+
|
26
|
+
module Groonga
|
27
|
+
module QueryLog
|
28
|
+
class Replayer
|
29
|
+
def initialize(options)
|
30
|
+
@options = options
|
31
|
+
@queue = SizedQueue.new(@options.request_queue_size)
|
32
|
+
@responses = Queue.new
|
33
|
+
end
|
34
|
+
|
35
|
+
def replay(input)
|
36
|
+
producer = run_producer(input)
|
37
|
+
consumers = run_consumers
|
38
|
+
response_logger = run_response_logger
|
39
|
+
producer.join
|
40
|
+
consumers.each(&:join)
|
41
|
+
response_logger.join
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def run_producer(input)
|
46
|
+
Thread.new do
|
47
|
+
parser = Parser.new
|
48
|
+
id = 0
|
49
|
+
@options.create_request_output do |output|
|
50
|
+
parser.parse(input) do |statistic|
|
51
|
+
next unless target_command?(statistic.command)
|
52
|
+
# TODO: validate orignal_source is one line
|
53
|
+
output.puts(statistic.command.original_source)
|
54
|
+
@queue.push([id, statistic])
|
55
|
+
id += 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
@options.n_clients.times do
|
59
|
+
@queue.push(nil)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def run_consumers
|
65
|
+
@options.n_clients.times.collect do
|
66
|
+
Thread.new do
|
67
|
+
loop do
|
68
|
+
break if run_consumer
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def run_consumer
|
75
|
+
@options.create_client do |client|
|
76
|
+
loop do
|
77
|
+
id, statistic = @queue.pop
|
78
|
+
if id.nil?
|
79
|
+
@responses.push(nil)
|
80
|
+
return true
|
81
|
+
end
|
82
|
+
begin
|
83
|
+
replay_command(client, id, statistic.command)
|
84
|
+
rescue Groonga::Client::Connection::Error
|
85
|
+
# TODO: add error log mechanism
|
86
|
+
$stderr.puts(Time.now.iso8601)
|
87
|
+
$stderr.puts(statistic.command.original_source)
|
88
|
+
$stderr.puts($!.raw_error.message)
|
89
|
+
$stderr.puts($!.raw_error.backtrace)
|
90
|
+
return false
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def replay_command(client, id, command)
|
97
|
+
command["cache"] = "no" if @options.disable_cache?
|
98
|
+
response = client.execute(command)
|
99
|
+
@responses.push(response)
|
100
|
+
end
|
101
|
+
|
102
|
+
def run_response_logger
|
103
|
+
Thread.new do
|
104
|
+
@options.create_responses_output do |output|
|
105
|
+
loop do
|
106
|
+
response = @responses.pop
|
107
|
+
break if response.nil?
|
108
|
+
# TODO: ensure response is one line
|
109
|
+
# TODO: reorder by ID
|
110
|
+
output.puts(response.raw)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def target_command?(command)
|
117
|
+
@options.target_command_name?(command.name)
|
118
|
+
end
|
119
|
+
|
120
|
+
class NullOutput
|
121
|
+
class << self
|
122
|
+
def open
|
123
|
+
output = new
|
124
|
+
if block_given?
|
125
|
+
yield(output)
|
126
|
+
else
|
127
|
+
output
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def puts(string)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Options
|
137
|
+
attr_accessor :host
|
138
|
+
attr_accessor :port
|
139
|
+
attr_accessor :protocol
|
140
|
+
attr_accessor :n_clients
|
141
|
+
attr_writer :request_queue_size
|
142
|
+
attr_accessor :target_command_names
|
143
|
+
def initialize
|
144
|
+
@host = "127.0.0.1"
|
145
|
+
@port = 10041
|
146
|
+
@protocol = :gqtp
|
147
|
+
@n_clients = 8
|
148
|
+
@request_queue_size = nil
|
149
|
+
@disable_cache = false
|
150
|
+
@requests_path = nil
|
151
|
+
@responses_path = nil
|
152
|
+
@target_command_names = ["*"]
|
153
|
+
end
|
154
|
+
|
155
|
+
def create_client(&block)
|
156
|
+
Groonga::Client.open(:host => @host,
|
157
|
+
:port => @port,
|
158
|
+
:protocol => @protocol,
|
159
|
+
&block)
|
160
|
+
end
|
161
|
+
|
162
|
+
def create_request_output(&block)
|
163
|
+
if @requests_path
|
164
|
+
File.open(@requests_path, "w", &block)
|
165
|
+
else
|
166
|
+
NullOutput.open(&block)
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def create_responses_output(&block)
|
171
|
+
if @responses_path
|
172
|
+
File.open(@responses_path, "w", &block)
|
173
|
+
else
|
174
|
+
NullOutput.open(&block)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def request_queue_size
|
179
|
+
@request_queue_size || @n_clients * 3
|
180
|
+
end
|
181
|
+
|
182
|
+
def disable_cache?
|
183
|
+
@disable_cache
|
184
|
+
end
|
185
|
+
|
186
|
+
def target_command_name?(name)
|
187
|
+
@target_command_names.any? do |name_pattern|
|
188
|
+
flags = 0
|
189
|
+
flags |= File::FNM_EXTGLOB if File.const_defined?(:FNM_EXTGLOB)
|
190
|
+
File.fnmatch(name_pattern, name, flags)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
|
3
|
+
# Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -18,6 +18,6 @@
|
|
18
18
|
|
19
19
|
module Groonga
|
20
20
|
module QueryLog
|
21
|
-
VERSION = "1.0.
|
21
|
+
VERSION = "1.0.2"
|
22
22
|
end
|
23
23
|
end
|
data/test/run-test.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
#
|
3
|
-
# Copyright (C) 2012 Kouhei Sutou <kou@clear-code.com>
|
3
|
+
# Copyright (C) 2012-2013 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -18,20 +18,35 @@
|
|
18
18
|
|
19
19
|
$VERBOSE = true
|
20
20
|
|
21
|
-
|
21
|
+
require "pathname"
|
22
22
|
|
23
|
-
base_dir =
|
24
|
-
|
25
|
-
|
23
|
+
base_dir = Pathname.new(__FILE__).dirname.parent.expand_path
|
24
|
+
top_dir = base_dir.parent
|
25
|
+
|
26
|
+
gqtp_base_dir = top_dir + "gqtp"
|
27
|
+
gqtp_lib_dir = gqtp_base_dir + "lib"
|
28
|
+
$LOAD_PATH.unshift(gqtp_lib_dir.to_s)
|
29
|
+
|
30
|
+
groonga_client_base_dir = top_dir + "groonga-client"
|
31
|
+
groonga_client_lib_dir = groonga_client_base_dir + "lib"
|
32
|
+
$LOAD_PATH.unshift(groonga_client_lib_dir.to_s)
|
33
|
+
|
34
|
+
groonga_command_base_dir = top_dir + "groonga-command"
|
35
|
+
groonga_command_lib_dir = groonga_command_base_dir + "lib"
|
36
|
+
$LOAD_PATH.unshift(groonga_command_lib_dir.to_s)
|
37
|
+
|
38
|
+
lib_dir = base_dir + "lib"
|
39
|
+
test_dir = base_dir + "test"
|
26
40
|
|
27
41
|
require "test-unit"
|
28
42
|
require "test/unit/notify"
|
43
|
+
require "test/unit/rr"
|
29
44
|
|
30
45
|
Test::Unit::Priority.enable
|
31
46
|
|
32
|
-
$LOAD_PATH.unshift(lib_dir)
|
47
|
+
$LOAD_PATH.unshift(lib_dir.to_s)
|
33
48
|
|
34
|
-
$LOAD_PATH.unshift(test_dir)
|
49
|
+
$LOAD_PATH.unshift(test_dir.to_s)
|
35
50
|
require "groonga-query-log-test-utils"
|
36
51
|
|
37
52
|
Dir.glob("#{base_dir}/test/**/test{_,-}*.rb") do |file|
|
data/test/test-parser.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
#
|
3
|
-
# Copyright (C) 2011-
|
3
|
+
# Copyright (C) 2011-2013 Kouhei Sutou <kou@clear-code.com>
|
4
4
|
#
|
5
5
|
# This library is free software; you can redistribute it and/or
|
6
6
|
# modify it under the terms of the GNU Lesser General Public
|
@@ -18,16 +18,27 @@
|
|
18
18
|
|
19
19
|
class ParserTest < Test::Unit::TestCase
|
20
20
|
def test_load
|
21
|
-
|
21
|
+
statistics = parse(<<-LOG)
|
22
22
|
2012-12-13 11:15:21.628105|0x7fff148c8a50|>load --table Video
|
23
23
|
2012-12-13 11:15:21.645119|0x7fff148c8a50|<000000017041150 rc=0
|
24
|
-
|
24
|
+
LOG
|
25
|
+
parsed_command = statistics.first.command
|
26
|
+
assert_instance_of(Groonga::Command::Load, parsed_command)
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_ignore_invalid_line
|
30
|
+
garbage = "\x80"
|
31
|
+
statistics = parse(<<-LOG)
|
32
|
+
2012-12-13 11:15:20.628105|0x7fff148c8a50|>#{garbage}
|
33
|
+
2012-12-13 11:15:21.628105|0x7fff148c8a50|>load --table Video
|
34
|
+
2012-12-13 11:15:21.645119|0x7fff148c8a50|<000000017041150 rc=0
|
35
|
+
LOG
|
25
36
|
parsed_command = statistics.first.command
|
26
37
|
assert_instance_of(Groonga::Command::Load, parsed_command)
|
27
38
|
end
|
28
39
|
|
29
40
|
private
|
30
|
-
def
|
41
|
+
def parse(log)
|
31
42
|
statistics = []
|
32
43
|
parser = Groonga::QueryLog::Parser.new
|
33
44
|
parser.parse(StringIO.new(log)) do |statistic|
|
@@ -36,13 +47,9 @@ EOL
|
|
36
47
|
statistics
|
37
48
|
end
|
38
49
|
|
39
|
-
def log
|
40
|
-
@log
|
41
|
-
end
|
42
|
-
|
43
50
|
class StatisticOperationTest < self
|
44
51
|
def setup
|
45
|
-
@
|
52
|
+
@statistics = parse(<<-LOG)
|
46
53
|
2011-06-02 16:27:04.731685|5091e5c0|>/d/select.join?table=Entries&filter=local_name+%40+%22gsub%22+%26%26+description+%40+%22string%22&sortby=_score&output_columns=_key&drilldown=name,class
|
47
54
|
2011-06-02 16:27:04.733539|5091e5c0|:000000001849451 filter(15)
|
48
55
|
2011-06-02 16:27:04.734978|5091e5c0|:000000003293459 filter(13)
|
@@ -52,11 +59,12 @@ EOL
|
|
52
59
|
2011-06-02 16:27:04.735606|5091e5c0|:000000003921419 drilldown(3)
|
53
60
|
2011-06-02 16:27:04.735762|5091e5c0|:000000004077552 drilldown(2)
|
54
61
|
2011-06-02 16:27:04.735808|5091e5c0|<000000004123726 rc=0
|
55
|
-
|
62
|
+
LOG
|
63
|
+
@statistic = @statistics.first
|
56
64
|
end
|
57
65
|
|
58
66
|
def test_context
|
59
|
-
operations =
|
67
|
+
operations = @statistic.operations.collect do |operation|
|
60
68
|
[operation[:name], operation[:context]]
|
61
69
|
end
|
62
70
|
expected = [
|
@@ -72,7 +80,7 @@ EOL
|
|
72
80
|
end
|
73
81
|
|
74
82
|
def test_n_records
|
75
|
-
operations =
|
83
|
+
operations = @statistic.operations.collect do |operation|
|
76
84
|
[operation[:name], operation[:n_records]]
|
77
85
|
end
|
78
86
|
expected = [
|
@@ -87,4 +95,22 @@ EOL
|
|
87
95
|
assert_equal(expected, operations)
|
88
96
|
end
|
89
97
|
end
|
98
|
+
|
99
|
+
class TestRC < self
|
100
|
+
def test_success
|
101
|
+
statistics = parse(<<-LOG)
|
102
|
+
2012-12-13 11:15:21.628105|0x7fff148c8a50|>table_create --name Videos
|
103
|
+
2012-12-13 11:15:21.645119|0x7fff148c8a50|<000000017041150 rc=0
|
104
|
+
LOG
|
105
|
+
assert_equal([0], statistics.collect(&:return_code))
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_fialure
|
109
|
+
statistics = parse(<<-LOG)
|
110
|
+
2012-12-13 11:15:21.628105|0x7fff148c8a50|>table_create --name Videos
|
111
|
+
2012-12-13 11:15:21.645119|0x7fff148c8a50|<000000017041150 rc=-22
|
112
|
+
LOG
|
113
|
+
assert_equal([-22], statistics.collect(&:return_code))
|
114
|
+
end
|
115
|
+
end
|
90
116
|
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# -*- coding: utf-8 -*-
|
2
|
+
#
|
3
|
+
# Copyright (C) 2013 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
|
+
class ReplayerTest < Test::Unit::TestCase
|
20
|
+
class OptionTest < self
|
21
|
+
class ClientTest < self
|
22
|
+
def setup
|
23
|
+
@options = Groonga::QueryLog::Replayer::Options.new
|
24
|
+
@options.n_clients = 1
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_host
|
28
|
+
host = "example.com"
|
29
|
+
@options.host = host
|
30
|
+
mock_client_open(:host => host)
|
31
|
+
replay
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_port
|
35
|
+
port = 2929
|
36
|
+
@options.port = 2929
|
37
|
+
mock_client_open(:port => 2929)
|
38
|
+
replay
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def replay
|
43
|
+
replayer = Groonga::QueryLog::Replayer.new(@options)
|
44
|
+
replayer.replay(StringIO.new(""))
|
45
|
+
end
|
46
|
+
|
47
|
+
def mock_client_open(expected_options)
|
48
|
+
client = Object.new
|
49
|
+
default_options = {
|
50
|
+
:host => "127.0.0.1",
|
51
|
+
:port => 10041,
|
52
|
+
:protocol => :gqtp,
|
53
|
+
}
|
54
|
+
expected_open_options = default_options.merge(expected_options)
|
55
|
+
mock(Groonga::Client).open(expected_open_options).yields(client) do
|
56
|
+
client
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class TargetCommandNameTest < self
|
62
|
+
def setup
|
63
|
+
@options = Groonga::QueryLog::Replayer::Options.new
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_default
|
67
|
+
assert_true(@options.target_command_name?("shutdown"))
|
68
|
+
end
|
69
|
+
|
70
|
+
class GlobTest < self
|
71
|
+
def setup
|
72
|
+
super
|
73
|
+
@options.target_command_names = ["se*"]
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_match
|
77
|
+
assert_true(@options.target_command_name?("select"))
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_not_match
|
81
|
+
assert_false(@options.target_command_name?("status"))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class ExtGlobTest < self
|
86
|
+
def setup
|
87
|
+
super
|
88
|
+
@options.target_command_names = ["s{elect,tatus}"]
|
89
|
+
unless File.const_defined?(:FNM_EXTGLOB)
|
90
|
+
omit("File:::FNM_EXTGLOB (Ruby 2.0.0 or later) is required.")
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_match
|
95
|
+
assert_true(@options.target_command_name?("select"))
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_not_match
|
99
|
+
assert_false(@options.target_command_name?("selectX"))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class ExactMatchTest < self
|
104
|
+
def setup
|
105
|
+
super
|
106
|
+
@options.target_command_names = ["select"]
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_match
|
110
|
+
assert_true(@options.target_command_name?("select"))
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_not_match
|
114
|
+
assert_false(@options.target_command_name?("selectX"))
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|