groonga-query-log 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|