fluent-plugin-groonga 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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