fluent-plugin-gcloud-pubsub-universe-domain 2.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,273 @@
1
+ require 'json'
2
+ require 'webrick'
3
+
4
+ require 'fluent/plugin/compressable'
5
+ require 'fluent/plugin/input'
6
+ require 'fluent/plugin/parser'
7
+
8
+ require 'fluent/plugin/gcloud_pubsub/client'
9
+
10
+ module Fluent::Plugin
11
+ class GcloudPubSubInput < Input
12
+ include Fluent::Plugin::Compressable
13
+
14
+ Fluent::Plugin.register_input('gcloud_pubsub', self)
15
+
16
+ helpers :compat_parameters, :parser, :thread
17
+
18
+ DEFAULT_PARSER_TYPE = 'json'
19
+
20
+ class FailedParseError < StandardError
21
+ end
22
+
23
+ desc 'Set tag of messages.'
24
+ config_param :tag, :string
25
+ desc 'Set key to be used as tag.'
26
+ config_param :tag_key, :string, default: nil
27
+ desc 'Set your GCP project.'
28
+ config_param :project, :string, default: nil
29
+ desc 'Set your credential file path.'
30
+ config_param :key, :string, default: nil
31
+ desc 'Set topic name to pull.'
32
+ config_param :topic, :string, default: nil
33
+ desc 'Set subscription name to pull.'
34
+ config_param :subscription, :string
35
+ desc 'Pulling messages by intervals of specified seconds.'
36
+ config_param :pull_interval, :float, default: 5.0
37
+ desc 'Max messages pulling at once.'
38
+ config_param :max_messages, :integer, default: 100
39
+ desc 'Setting `true`, keepalive connection to wait for new messages.'
40
+ config_param :return_immediately, :bool, default: true
41
+ desc 'Set number of threads to pull messages.'
42
+ config_param :pull_threads, :integer, default: 1
43
+ desc 'Specify the key of the attribute to be acquired as a record'
44
+ config_param :attribute_keys, :array, default: []
45
+ desc 'Set error type when parsing messages fails.'
46
+ config_param :parse_error_action, :enum, default: :exception, list: [:exception, :warning]
47
+ # for HTTP RPC
48
+ desc 'If `true` is specified, HTTP RPC to stop or start pulling message is enabled.'
49
+ config_param :enable_rpc, :bool, default: false
50
+ desc 'Bind IP address for HTTP RPC.'
51
+ config_param :rpc_bind, :string, default: '0.0.0.0'
52
+ desc 'Port for HTTP RPC.'
53
+ config_param :rpc_port, :integer, default: 24680
54
+ desc 'Decompress messages'
55
+ config_param :decompression, :string, default: nil
56
+ desc 'Set universe domain'
57
+ config_param :universe_domain, :string, default: nil
58
+
59
+ config_section :parse do
60
+ config_set_default :@type, DEFAULT_PARSER_TYPE
61
+ end
62
+
63
+ class RPCServlet < WEBrick::HTTPServlet::AbstractServlet
64
+ class Error < StandardError; end
65
+
66
+ def initialize(server, plugin)
67
+ super
68
+ @plugin = plugin
69
+ end
70
+
71
+ def do_GET(req, res)
72
+ begin
73
+ code, header, body = process(req, res)
74
+ rescue
75
+ code, header, body = render_json(500, {
76
+ 'ok' => false,
77
+ 'message' => 'Internal Server Error',
78
+ 'error' => "#{$!}",
79
+ 'backtrace'=> $!.backtrace
80
+ })
81
+ end
82
+
83
+ res.status = code
84
+ header.each_pair {|k,v|
85
+ res[k] = v
86
+ }
87
+ res.body = body
88
+ end
89
+
90
+ def render_json(code, obj)
91
+ [code, {'Content-Type' => 'application/json'}, obj.to_json]
92
+ end
93
+
94
+ def process(req, res)
95
+ ret = {'ok' => true}
96
+ case req.path_info
97
+ when '/stop'
98
+ @plugin.stop_pull
99
+ when '/start'
100
+ @plugin.start_pull
101
+ when '/status'
102
+ ret['status'] = @plugin.status_of_pull
103
+ else
104
+ raise Error.new "Invalid path_info: #{req.path_info}"
105
+ end
106
+ render_json(200, ret)
107
+ end
108
+ end
109
+
110
+ def configure(conf)
111
+ compat_parameters_convert(conf, :parser)
112
+ super
113
+ @rpc_srv = nil
114
+ @rpc_thread = nil
115
+ @stop_pull = false
116
+
117
+ @extract_tag = if @tag_key.nil?
118
+ method(:static_tag)
119
+ else
120
+ method(:dynamic_tag)
121
+ end
122
+
123
+ @parser = parser_create
124
+ @decompress = if @decompression == 'gzip'
125
+ method(:gzip_decompress)
126
+ else
127
+ method(:no_decompress)
128
+ end
129
+ end
130
+
131
+ def start
132
+ super
133
+ start_rpc if @enable_rpc
134
+
135
+ @subscriber = Fluent::GcloudPubSub::Subscriber.new @project, @key, @topic, @subscription, @universe_domain
136
+ log.debug "connected subscription:#{@subscription} in project #{@project}"
137
+
138
+ @emit_guard = Mutex.new
139
+ @stop_subscribing = false
140
+ @subscribe_threads = []
141
+ @pull_threads.times do |idx|
142
+ @subscribe_threads.push thread_create("in_gcloud_pubsub_subscribe_#{idx}".to_sym, &method(:subscribe))
143
+ end
144
+ end
145
+
146
+ def shutdown
147
+ if @rpc_srv
148
+ @rpc_srv.shutdown
149
+ @rpc_srv = nil
150
+ end
151
+ if @rpc_thread
152
+ @rpc_thread = nil
153
+ end
154
+ @stop_subscribing = true
155
+ @subscribe_threads.each(&:join)
156
+ super
157
+ end
158
+
159
+ def stop_pull
160
+ @stop_pull = true
161
+ log.info "stop pull from subscription:#{@subscription}"
162
+ end
163
+
164
+ def start_pull
165
+ @stop_pull = false
166
+ log.info "start pull from subscription:#{@subscription}"
167
+ end
168
+
169
+ def status_of_pull
170
+ @stop_pull ? 'stopped' : 'started'
171
+ end
172
+
173
+ private
174
+
175
+ def static_tag(record)
176
+ @tag
177
+ end
178
+
179
+ def dynamic_tag(record)
180
+ record.delete(@tag_key) || @tag
181
+ end
182
+
183
+ def gzip_decompress(message)
184
+ decompress message
185
+ end
186
+
187
+ def no_decompress(message)
188
+ message
189
+ end
190
+
191
+ def start_rpc
192
+ log.info "listening http rpc server on http://#{@rpc_bind}:#{@rpc_port}/"
193
+ @rpc_srv = WEBrick::HTTPServer.new(
194
+ {
195
+ BindAddress: @rpc_bind,
196
+ Port: @rpc_port,
197
+ Logger: WEBrick::Log.new(STDERR, WEBrick::Log::FATAL),
198
+ AccessLog: []
199
+ }
200
+ )
201
+ @rpc_srv.mount('/api/in_gcloud_pubsub/pull/', RPCServlet, self)
202
+ @rpc_thread = thread_create(:in_gcloud_pubsub_rpc_thread){
203
+ @rpc_srv.start
204
+ }
205
+ end
206
+
207
+ def subscribe
208
+ until @stop_subscribing
209
+ _subscribe unless @stop_pull
210
+
211
+ if @return_immediately || @stop_pull
212
+ sleep @pull_interval
213
+ end
214
+ end
215
+ rescue => ex
216
+ log.error "unexpected error", error_message: ex.to_s, error_class: ex.class.to_s
217
+ log.error_backtrace ex.backtrace
218
+ end
219
+
220
+ def _subscribe
221
+ messages = @subscriber.pull @return_immediately, @max_messages
222
+ if messages.length == 0
223
+ log.debug "no messages are pulled"
224
+ return
225
+ end
226
+
227
+ process messages
228
+ @subscriber.acknowledge messages
229
+
230
+ log.debug "#{messages.length} message(s) processed"
231
+ rescue Fluent::GcloudPubSub::RetryableError => ex
232
+ log.warn "Retryable error occurs. Fluentd will retry.", error_message: ex.to_s, error_class: ex.class.to_s
233
+ rescue => ex
234
+ log.error "unexpected error", error_message: ex.to_s, error_class: ex.class.to_s
235
+ log.error_backtrace ex.backtrace
236
+ end
237
+
238
+ def process(messages)
239
+ event_streams = Hash.new do |hsh, key|
240
+ hsh[key] = Fluent::MultiEventStream.new
241
+ end
242
+
243
+ messages.each do |m|
244
+ line = @decompress.call(m.message.data).chomp
245
+ attributes = m.attributes
246
+ @parser.parse(line) do |time, record|
247
+ if time && record
248
+ @attribute_keys.each do |key|
249
+ record[key] = attributes[key]
250
+ end
251
+
252
+ event_streams[@extract_tag.call(record)].add(time, record)
253
+ else
254
+ case @parse_error_action
255
+ when :exception
256
+ raise FailedParseError.new "pattern not match: #{line}"
257
+ else
258
+ log.warn 'pattern not match', record: line
259
+ end
260
+ end
261
+ end
262
+ end
263
+
264
+ event_streams.each do |tag, es|
265
+ # There are some output plugins not to supposed to be called with multi-threading.
266
+ # Maybe remove in the future.
267
+ @emit_guard.synchronize do
268
+ router.emit_stream(tag, es)
269
+ end
270
+ end
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,161 @@
1
+ require 'fluent/plugin/compressable'
2
+ require 'fluent/plugin/output'
3
+ require 'fluent/plugin/gcloud_pubsub/client'
4
+ require 'fluent/plugin_helper/inject'
5
+
6
+ module Fluent::Plugin
7
+ class GcloudPubSubOutput < Output
8
+ include Fluent::Plugin::Compressable
9
+ include Fluent::PluginHelper::Inject
10
+
11
+ Fluent::Plugin.register_output('gcloud_pubsub', self)
12
+
13
+ helpers :compat_parameters, :formatter
14
+
15
+ DEFAULT_BUFFER_TYPE = "memory"
16
+ DEFAULT_FORMATTER_TYPE = "json"
17
+
18
+ desc 'Set your GCP project.'
19
+ config_param :project, :string, :default => nil
20
+ desc 'Set your credential file path.'
21
+ config_param :key, :string, :default => nil
22
+ desc 'Set topic name to publish.'
23
+ config_param :topic, :string
24
+ desc "Set your dest GCP project if publishing cross project"
25
+ config_param :dest_project, :string, :default => nil
26
+ desc "If set to `true`, specified topic will be created when it doesn't exist."
27
+ config_param :autocreate_topic, :bool, :default => false
28
+ desc 'Publishing messages count per request to Cloud Pub/Sub.'
29
+ config_param :max_messages, :integer, :default => 1000
30
+ desc 'Publishing messages bytesize per request to Cloud Pub/Sub.'
31
+ config_param :max_total_size, :integer, :default => 9800000 # 9.8MB
32
+ desc 'Limit bytesize per message.'
33
+ config_param :max_message_size, :integer, :default => 4000000 # 4MB
34
+ desc 'Publishing the set field as an attribute created from input message'
35
+ config_param :attribute_keys, :array, :default => []
36
+ desc 'Publishing the set field as an attribute created from input config params'
37
+ config_param :attribute_key_values, :hash, :default => {}
38
+ desc 'Set service endpoint'
39
+ config_param :endpoint, :string, :default => nil
40
+ desc 'Compress messages'
41
+ config_param :compression, :string, :default => nil
42
+ desc 'Set default emulator_host to use in publish requests'
43
+ config_param :emulator_host, :string, :default => nil
44
+ desc 'Set default timeout to use in publish requests'
45
+ config_param :timeout, :integer, :default => nil
46
+ desc 'Set universe domain'
47
+ config_param :universe_domain, :string, :default => nil
48
+
49
+ config_section :buffer do
50
+ config_set_default :@type, DEFAULT_BUFFER_TYPE
51
+ end
52
+
53
+ config_section :format do
54
+ config_set_default :@type, DEFAULT_FORMATTER_TYPE
55
+ end
56
+
57
+ def configure(conf)
58
+ compat_parameters_convert(conf, :buffer, :formatter)
59
+ super
60
+ placeholder_validate!(:topic, @topic)
61
+ @formatter = formatter_create
62
+ @compress = if @compression == 'gzip'
63
+ method(:gzip_compress)
64
+ else
65
+ method(:no_compress)
66
+ end
67
+ end
68
+
69
+ def start
70
+ super
71
+ @publisher = Fluent::GcloudPubSub::Publisher.new @project, @key, @autocreate_topic, @dest_project, @endpoint, @timeout, @emulator_host, @universe_domain
72
+ end
73
+
74
+ def format(tag, time, record)
75
+ record = inject_values_to_record(tag, time, record)
76
+ record = encode_utf8(record)
77
+ attributes = {}
78
+ @attribute_keys.each do |key|
79
+ attributes[key] = record.delete(key)
80
+ end
81
+ attributes.merge! @attribute_key_values
82
+ [@compress.call(@formatter.format(tag, time, record)), attributes].to_msgpack
83
+ end
84
+
85
+ # Recursively convert ASCII-8BIT encoded strings to UTF-8 so the JSON
86
+ # formatter can serialize them (JSON.generate raises on ASCII-8BIT strings
87
+ # containing multibyte characters).
88
+ def encode_utf8(value)
89
+ case value
90
+ when String
91
+ if value.encoding == Encoding::ASCII_8BIT
92
+ value.dup.force_encoding("UTF-8")
93
+ else
94
+ value
95
+ end
96
+ when Hash
97
+ value.transform_values { |v| encode_utf8(v) }
98
+ when Array
99
+ value.map { |v| encode_utf8(v) }
100
+ else
101
+ value
102
+ end
103
+ end
104
+
105
+ def formatted_to_msgpack_binary?
106
+ true
107
+ end
108
+
109
+ def multi_workers_ready?
110
+ true
111
+ end
112
+
113
+ def write(chunk)
114
+ topic = extract_placeholders(@topic, chunk.metadata)
115
+
116
+ messages = []
117
+ size = 0
118
+
119
+ chunk.msgpack_each do |msg, attr|
120
+ msg = Fluent::GcloudPubSub::Message.new(msg, attr)
121
+ if msg.bytesize > @max_message_size
122
+ log.warn 'Drop a message because its size exceeds `max_message_size`', size: msg.bytesize
123
+ next
124
+ end
125
+ if messages.length + 1 > @max_messages || size + msg.bytesize > @max_total_size
126
+ publish(topic, messages)
127
+ messages = []
128
+ size = 0
129
+ end
130
+ messages << msg
131
+ size += msg.bytesize
132
+ end
133
+
134
+ if messages.length > 0
135
+ publish(topic, messages)
136
+ end
137
+ rescue Fluent::GcloudPubSub::RetryableError => ex
138
+ log.warn "Retryable error occurs. Fluentd will retry.", error_message: ex.to_s, error_class: ex.class.to_s
139
+ raise ex
140
+ rescue => ex
141
+ log.error "unexpected error", error_message: ex.to_s, error_class: ex.class.to_s
142
+ log.error_backtrace
143
+ raise ex
144
+ end
145
+
146
+ private
147
+
148
+ def publish(topic, messages)
149
+ log.debug "send message topic:#{topic} length:#{messages.length} size:#{messages.map(&:bytesize).inject(:+)}"
150
+ @publisher.publish(topic, messages)
151
+ end
152
+
153
+ def gzip_compress(message)
154
+ compress message
155
+ end
156
+
157
+ def no_compress(message)
158
+ message
159
+ end
160
+ end
161
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ begin
4
+ Bundler.setup(:default, :development)
5
+ rescue Bundler::BundlerError => e
6
+ $stderr.puts e.message
7
+ $stderr.puts "Run `bundle install` to install missing gems"
8
+ exit e.status_code
9
+ end
10
+ require 'test/unit'
11
+ require "test/unit/rr"
12
+
13
+ $LOAD_PATH.unshift(File.expand_path("../../", __FILE__))
14
+
15
+ require "fluent/test"
16
+ require "fluent/test/driver/output"
17
+ require "fluent/test/helpers"
18
+
19
+ require 'fluent/plugin/in_gcloud_pubsub'
20
+ require 'fluent/plugin/out_gcloud_pubsub'
21
+
22
+ Test::Unit::TestCase.include(Fluent::Test::Helpers)
23
+ Test::Unit::TestCase.extend(Fluent::Test::Helpers)