fluent-plugin-groonga 1.0.0

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.
@@ -0,0 +1,5 @@
1
+ # News
2
+
3
+ ## 1.0.0: 2012-11-29
4
+
5
+ The first release!!!
@@ -0,0 +1,42 @@
1
+ # -*- mode: ruby; coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 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 version 2.1 as published by the Free Software Foundation.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+
18
+ Gem::Specification.new do |spec|
19
+ spec.name = "fluent-plugin-groonga"
20
+ spec.version = "1.0.0"
21
+ spec.authors = ["Kouhei Sutou"]
22
+ spec.email = ["kou@clear-code.com"]
23
+ spec.summary = "Fluentd plugin collection for groonga users"
24
+ spec.description =
25
+ "Groonga users can replicate their data by fluent-plugin-groonga"
26
+ spec.homepage = "https://github.com/groonga/fluent-plugin-groonga"
27
+
28
+ spec.files = ["README.md", "Gemfile", "#{spec.name}.gemspec"]
29
+ spec.files += Dir.glob("lib/**/*.rb")
30
+ spec.files += Dir.glob("sample/**/*")
31
+ spec.files += Dir.glob("doc/text/**/*")
32
+ spec.require_paths = ["lib"]
33
+
34
+ spec.add_runtime_dependency("fluentd")
35
+ spec.add_runtime_dependency("gqtp")
36
+ spec.add_runtime_dependency("groonga-command")
37
+
38
+ spec.add_development_dependency("rake")
39
+ spec.add_development_dependency("bundler")
40
+ spec.add_development_dependency("test-unit")
41
+ spec.add_development_dependency("test-unit-notify")
42
+ end
@@ -0,0 +1,264 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 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 version 2.1 as published by the Free Software Foundation.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+
18
+ require "English"
19
+ require "webrick/httputils"
20
+
21
+ require "http_parser"
22
+
23
+ require "gqtp"
24
+ require "groonga/command"
25
+
26
+ module Fluent
27
+ class GroongaInput < Input
28
+ Plugin.register_input("groonga", self)
29
+
30
+ def initialize
31
+ super
32
+ end
33
+
34
+ config_param :protocol, :string, :default => "http"
35
+
36
+ def configure(conf)
37
+ super
38
+ case @protocol
39
+ when "http"
40
+ @input = HTTPInput.new
41
+ when "gqtp"
42
+ @input = GQTPInput.new
43
+ else
44
+ message = "unknown protocol: <#{@protocol.inspect}>"
45
+ $log.error message
46
+ raise ConfigError, message
47
+ end
48
+ @input.configure(conf)
49
+ end
50
+
51
+ def start
52
+ @input.start
53
+ end
54
+
55
+ def shutdown
56
+ @input.shutdown
57
+ end
58
+
59
+ class Repeater < Coolio::TCPSocket
60
+ def initialize(socket, handler)
61
+ super(socket)
62
+ @handler = handler
63
+ end
64
+
65
+ def on_read(data)
66
+ @handler.write(data)
67
+ end
68
+
69
+ def on_close
70
+ @handler.close
71
+ end
72
+ end
73
+
74
+ class BaseInput
75
+ include Configurable
76
+ include DetachMultiProcessMixin
77
+
78
+ config_param :bind, :string, :default => "0.0.0.0"
79
+ config_param :port, :integer, :default => 10041
80
+ config_param :real_host, :string
81
+ config_param :real_port, :integer, :default => 10041
82
+ DEFAULT_EMIT_COMMANDS = [
83
+ /\Atable_/,
84
+ /\Acolumn_/,
85
+ "load",
86
+ ]
87
+ config_param :emit_commands, :default => DEFAULT_EMIT_COMMANDS do |value|
88
+ commands = value.split(/\s*,\s*/)
89
+ commands.collect do |command|
90
+ if /\A\/(.*)\/(i)?\z/ =~ command
91
+ pattern = $1
92
+ flag_mark = $2
93
+ flag = 0
94
+ flag |= Regexp::IGNORECASE if flag_mark == "i"
95
+ Regexp.new(pattern, flag)
96
+ else
97
+ command
98
+ end
99
+ end
100
+ end
101
+
102
+ def start
103
+ listen_socket = TCPServer.new(@bind, @port)
104
+ detach_multi_process do
105
+ @loop = Coolio::Loop.new
106
+
107
+ @socket = Coolio::TCPServer.new(listen_socket, nil,
108
+ handler_class, self)
109
+ @loop.attach(@socket)
110
+
111
+ @shutdown_notifier = Coolio::AsyncWatcher.new
112
+ @loop.attach(@shutdown_notifier)
113
+
114
+ @thread = Thread.new do
115
+ run
116
+ end
117
+ end
118
+ end
119
+
120
+ def shutdown
121
+ @loop.stop
122
+ @socket.close
123
+ @shutdown_notifier.signal
124
+ @thread.join
125
+ end
126
+
127
+ def create_repeater(client)
128
+ repeater = Repeater.connect(@real_host, @real_port, client)
129
+ repeater.attach(@loop)
130
+ repeater
131
+ end
132
+
133
+ def emit(command, params)
134
+ return unless emit_command?(command)
135
+ Engine.emit("groonga.command.#{command}", Engine.now, params)
136
+ end
137
+
138
+ private
139
+ def run
140
+ @loop.run
141
+ rescue
142
+ $log.error "unexpected error", :error => $!.to_s
143
+ $log.error_backtrace
144
+ end
145
+
146
+ def emit_command?(command)
147
+ return true if @emit_commands.empty?
148
+ @emit_commands.any? do |pattern|
149
+ pattern === command
150
+ end
151
+ end
152
+ end
153
+
154
+ class HTTPInput < BaseInput
155
+ private
156
+ def handler_class
157
+ Handler
158
+ end
159
+
160
+ class Handler < Coolio::Socket
161
+ def initialize(socket, input)
162
+ super(socket)
163
+ @input = input
164
+ end
165
+
166
+ def on_connect
167
+ @parser = HTTP::Parser.new(self)
168
+ @repeater = @input.create_repeater(self)
169
+ end
170
+
171
+ def on_read(data)
172
+ @parser << data
173
+ @repeater.write(data)
174
+ end
175
+
176
+ def on_message_begin
177
+ @body = ""
178
+ end
179
+
180
+ def on_headers_complete(headers)
181
+ end
182
+
183
+ def on_body(chunk)
184
+ @body << chunk
185
+ end
186
+
187
+ def on_message_complete
188
+ params = WEBrick::HTTPUtils.parse_query(@parser.query_string)
189
+ path_info = @parser.request_path
190
+ case path_info
191
+ when /\A\/d\//
192
+ command = $POSTMATCH
193
+ if command == "load"
194
+ params["values"] = @body unless @body.empty?
195
+ end
196
+ @input.emit(command, params)
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ class GQTPInput < BaseInput
203
+ private
204
+ def handler_class
205
+ Handler
206
+ end
207
+
208
+ class Handler < Coolio::Socket
209
+ def initialize(socket, input)
210
+ super(socket)
211
+ @input = input
212
+ end
213
+
214
+ def on_connect
215
+ @parser = Parser.new(@input)
216
+ @repeater = @input.create_repeater(self)
217
+ end
218
+
219
+ def on_read(data)
220
+ @parser << data
221
+ @repeater.write(data)
222
+ end
223
+
224
+ def on_close
225
+ @parser.close
226
+ end
227
+ end
228
+
229
+ class Parser < GQTP::Parser
230
+ def initialize(input)
231
+ super()
232
+ @input = input
233
+ initialize_command_parser
234
+ end
235
+
236
+ def on_body(chunk)
237
+ @command_parser << chunk
238
+ end
239
+
240
+ def on_complete
241
+ @command_parser << "\n"
242
+ end
243
+
244
+ def close
245
+ @command_parser.finish
246
+ end
247
+
248
+ private
249
+ def initialize_command_parser
250
+ @command_parser = Groonga::Command::Parser.new
251
+ @command_parser.on_command do |command|
252
+ @input.emit(command.name, command.arguments)
253
+ end
254
+ @command_parser.on_load_value do |command, value|
255
+ arguments = command.arguments.dup
256
+ arguments[:columns] = command.columns.join(", ")
257
+ arguments[:values] = Yajl::Encoder.encode([value])
258
+ @input.emit(command.name, arguments)
259
+ end
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,239 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # Copyright (C) 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 version 2.1 as published by the Free Software Foundation.
8
+ #
9
+ # This library is distributed in the hope that it will be useful,
10
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
11
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12
+ # Lesser General Public License for more details.
13
+ #
14
+ # You should have received a copy of the GNU Lesser General Public
15
+ # License along with this library; if not, write to the Free Software
16
+ # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
+
18
+ require "fileutils"
19
+ require "cgi/util"
20
+
21
+ module Fluent
22
+ class GroongaOutput < BufferedOutput
23
+ Plugin.register_output("groonga", self)
24
+
25
+ def initialize
26
+ super
27
+ end
28
+
29
+ config_param :protocol, :string, :default => "http"
30
+ config_param :table, :string, :default => nil
31
+
32
+ def configure(conf)
33
+ super
34
+ case @protocol
35
+ when "http"
36
+ @client = HTTPClient.new
37
+ when "gqtp"
38
+ @client = GQTPClient.new
39
+ when "command"
40
+ @client = CommandClient.new
41
+ end
42
+ @client.configure(conf)
43
+ end
44
+
45
+ def start
46
+ super
47
+ @client.start
48
+ end
49
+
50
+ def shutdown
51
+ super
52
+ @client.shutdown
53
+ end
54
+
55
+ def format(tag, time, record)
56
+ [tag, time, record].to_msgpack
57
+ end
58
+
59
+ def write(chunk)
60
+ chunk.msgpack_each do |tag, time, arguments|
61
+ if /\Agroonga\.command\./ =~ tag
62
+ name = $POSTMATCH
63
+ send_command(name, arguments)
64
+ else
65
+ store_chunk(chunk)
66
+ end
67
+ end
68
+ end
69
+
70
+ private
71
+ def send_command(name, arguments)
72
+ command_class = Groonga::Command.find(name)
73
+ command = command_class.new(name, arguments)
74
+ @client.send(command)
75
+ end
76
+
77
+ def store_chunk(chunk)
78
+ return if @table.nil?
79
+
80
+ values = []
81
+ chunk.each do |time, value|
82
+ values << value
83
+ end
84
+ arguments = {
85
+ "table" => @table,
86
+ "values" => Yajl::Enocder.encode(values),
87
+ }
88
+ send_command("load", arguments)
89
+ end
90
+
91
+ class HTTPClient
92
+ include Configurable
93
+
94
+ config_param :host, :string, :default => "localhost"
95
+ config_param :port, :integer, :default => 10041
96
+
97
+ def start
98
+ @loop = Coolio::Loop.new
99
+ end
100
+
101
+ def shutdown
102
+ end
103
+
104
+ def send(command)
105
+ client = GroongaHTTPClient.connect(@host, @port)
106
+ client.request("GET", command.to_uri_format)
107
+ @loop.attach(client)
108
+ @loop.run
109
+ end
110
+
111
+ class GroongaHTTPClient < Coolio::HttpClient
112
+ def on_body_data(data)
113
+ end
114
+ end
115
+ end
116
+
117
+ class GQTPClient
118
+ include Configurable
119
+
120
+ config_param :host, :string, :default => "localhost"
121
+ config_param :port, :integer, :default => 10041
122
+
123
+ def start
124
+ @loop = Coolio::Loop.new
125
+ @client = nil
126
+ end
127
+
128
+ def shutdown
129
+ return if @client.nil?
130
+ @client.send("shutdown") do
131
+ @loop.stop
132
+ end
133
+ @loop.run
134
+ end
135
+
136
+ def send(command)
137
+ @client ||= GQTP::Client.new(:host => @host,
138
+ :port => @port,
139
+ :connection => :coolio,
140
+ :loop => @loop)
141
+ @client.send(command.to_command_format) do |header, body|
142
+ @loop.stop
143
+ end
144
+ @loop.run
145
+ end
146
+ end
147
+
148
+ class CommandClient
149
+ include Configurable
150
+
151
+ config_param :groonga, :string, :default => "groonga"
152
+ config_param :database, :string
153
+ config_param :arguments, :default => [] do |value|
154
+ Shellwords.split(value)
155
+ end
156
+
157
+ def initialize
158
+ super
159
+ end
160
+
161
+ def configure(conf)
162
+ super
163
+ end
164
+
165
+ def start
166
+ run_groonga
167
+ wrap_io
168
+ end
169
+
170
+ def shutdown
171
+ @groonga_input.close
172
+ @groonga_output.close
173
+ @groonga_error.close
174
+ Process.waitpid(@pid)
175
+ end
176
+
177
+ def send(command)
178
+ body = nil
179
+ if command.name == "load"
180
+ body = command.arguments.delete(:values)
181
+ end
182
+ @groonga_input.write("#{command.to_uri_format}\n")
183
+ if body
184
+ body.each_line do |line|
185
+ @groonga_input.write("#{line}\n")
186
+ end
187
+ end
188
+ @loop.run
189
+ end
190
+
191
+ private
192
+ def run_groonga
193
+ env = {}
194
+ @input = IO.pipe("ASCII-8BIT")
195
+ @output = IO.pipe("ASCII-8BIT")
196
+ @error = IO.pipe("ASCII-8BIT")
197
+ input_fd = @input[0].to_i
198
+ output_fd = @output[1].to_i
199
+ options = {
200
+ input_fd => input_fd,
201
+ output_fd => output_fd,
202
+ :err => @error[1],
203
+ }
204
+ arguments = @arguments
205
+ arguments += [
206
+ "--input-fd", input_fd.to_s,
207
+ "--output-fd", output_fd.to_s,
208
+ ]
209
+ unless File.exist?(@database)
210
+ FileUtils.mkdir_p(File.dirname(@database))
211
+ arguments << "-n"
212
+ end
213
+ arguments << @database
214
+ @pid = spawn(env, @groonga, *arguments, options)
215
+ @input[0].close
216
+ @output[1].close
217
+ @error[1].close
218
+ end
219
+
220
+ def wrap_io
221
+ @loop = Coolio::Loop.new
222
+
223
+ @groonga_input = Coolio::IO.new(@input[1])
224
+ on_write_complete = lambda do
225
+ @loop.stop
226
+ end
227
+ @groonga_input.on_write_complete do
228
+ on_write_complete.call
229
+ end
230
+ @groonga_output = Coolio::IO.new(@output[0])
231
+ @groonga_error = Coolio::IO.new(@error[0])
232
+
233
+ @loop.attach(@groonga_input)
234
+ @loop.attach(@groonga_output)
235
+ @loop.attach(@groonga_error)
236
+ end
237
+ end
238
+ end
239
+ end