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.
- data/Gemfile +20 -0
- data/README.md +103 -0
- data/doc/text/lgpl-2.1.txt +502 -0
- data/doc/text/news.md +5 -0
- data/fluent-plugin-groonga.gemspec +42 -0
- data/lib/fluent/plugin/in_groonga.rb +264 -0
- data/lib/fluent/plugin/out_groonga.rb +239 -0
- data/sample/command.conf +14 -0
- data/sample/gqtp.conf +15 -0
- data/sample/http.conf +15 -0
- metadata +168 -0
data/doc/text/news.md
ADDED
@@ -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
|