lib/fluent/plugin/out_kinesis.rb (253 lines of code) (raw):

# # Copyright 2014-2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. # # Licensed under the Amazon Software License (the "License"). # You may not use this file except in compliance with the License. # A copy of the License is located at # # http://aws.amazon.com/asl/ # # or in the "license" file accompanying this file. This file is distributed # on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either # express or implied. See the License for the specific language governing # permissions and limitations under the License. require 'aws-sdk' require 'yajl' require 'logger' require 'securerandom' require 'zlib' require 'fluent_plugin_kinesis/version' module FluentPluginKinesis class OutputFilter < Fluent::BufferedOutput include Fluent::DetachMultiProcessMixin include Fluent::SetTimeKeyMixin include Fluent::SetTagKeyMixin USER_AGENT_NAME = 'fluent-plugin-kinesis-output-filter' PROC_BASE_STR = 'proc {|record| %s }' PUT_RECORDS_MAX_COUNT = 500 PUT_RECORD_MAX_DATA_SIZE = 1024 * 1024 PUT_RECORDS_MAX_DATA_SIZE = 1024 * 1024 * 5 Fluent::Plugin.register_output('kinesis',self) config_set_default :include_time_key, true config_set_default :include_tag_key, true config_param :aws_key_id, :string, default: nil, :secret => true config_param :aws_sec_key, :string, default: nil, :secret => true # The 'region' parameter is optional because # it may be set as an environment variable. config_param :region, :string, default: nil config_param :ensure_stream_connection, :bool, default: true config_param :profile, :string, :default => nil config_param :credentials_path, :string, :default => nil config_param :role_arn, :string, :default => nil config_param :external_id, :string, :default => nil config_param :stream_name, :string config_param :random_partition_key, :bool, default: false config_param :partition_key, :string, default: nil config_param :partition_key_expr, :string, default: nil config_param :explicit_hash_key, :string, default: nil config_param :explicit_hash_key_expr, :string, default: nil config_param :order_events, :bool, default: false config_param :retries_on_putrecords, :integer, default: 3 config_param :use_yajl, :bool, default: false config_param :zlib_compression, :bool, default: false config_param :debug, :bool, default: false config_param :http_proxy, :string, default: nil def configure(conf) log.warn("Deprecated warning: out_kinesis is no longer supported after v1.0.0. Please check out_kinesis_streams out.") super validate_params if @detach_process or (@num_threads > 1) @parallel_mode = true if @detach_process @use_detach_multi_process_mixin = true end else @parallel_mode = false end if @parallel_mode if @order_events log.warn 'You have set "order_events" to true, however this configuration will be ignored due to "detach_process" and/or "num_threads".' end @order_events = false end if @partition_key_expr partition_key_proc_str = sprintf( PROC_BASE_STR, @partition_key_expr ) @partition_key_proc = eval(partition_key_proc_str) end if @explicit_hash_key_expr explicit_hash_key_proc_str = sprintf( PROC_BASE_STR, @explicit_hash_key_expr ) @explicit_hash_key_proc = eval(explicit_hash_key_proc_str) end @dump_class = @use_yajl ? Yajl : JSON end def start detach_multi_process do super load_client if @ensure_stream_connection check_connection_to_stream end end end def format(tag, time, record) data = { data: @dump_class.dump(record), partition_key: get_key(:partition_key,record) } if @explicit_hash_key or @explicit_hash_key_proc data[:explicit_hash_key] = get_key(:explicit_hash_key,record) end data.to_msgpack end def write(chunk) data_list = chunk.to_enum(:msgpack_each).map{|record| build_data_to_put(record) }.find_all{|record| unless record_exceeds_max_size?(record[:data]) true else log.error sprintf('Record exceeds the %.3f KB(s) per-record size limit and will not be delivered: %s', PUT_RECORD_MAX_DATA_SIZE / 1024.0, record[:data]) false end } if @order_events put_record_for_order_events(data_list) else records_array = build_records_array_to_put(data_list) records_array.each{|records| put_records_with_retry(records) } end end private def validate_params unless @random_partition_key or @partition_key or @partition_key_expr raise Fluent::ConfigError, "'random_partition_key' or 'partition_key' or 'partition_key_expr' is required" end end def load_client user_agent_suffix = "#{USER_AGENT_NAME}/#{FluentPluginKinesis::VERSION}" options = { user_agent_suffix: user_agent_suffix } if @region options[:region] = @region end if @aws_key_id && @aws_sec_key options.update( access_key_id: @aws_key_id, secret_access_key: @aws_sec_key, ) elsif @profile credentials_opts = {:profile_name => @profile} credentials_opts[:path] = @credentials_path if @credentials_path credentials = Aws::SharedCredentials.new(credentials_opts) options[:credentials] = credentials elsif @role_arn credentials = Aws::AssumeRoleCredentials.new( client: Aws::STS::Client.new(options), role_arn: @role_arn, role_session_name: "aws-fluent-plugin-kinesis", external_id: @external_id, duration_seconds: 60 * 60 ) options[:credentials] = credentials end if @debug options.update( logger: Logger.new(log.out), log_level: :debug ) # XXX: Add the following options, if necessary # :http_wire_trace => true end if @http_proxy options[:http_proxy] = @http_proxy end @client = Aws::Kinesis::Client.new(options) end def check_connection_to_stream @client.describe_stream(stream_name: @stream_name) end def get_key(name, record) if @random_partition_key SecureRandom.uuid else key = instance_variable_get("@#{name}") key_proc = instance_variable_get("@#{name}_proc") value = key ? record[key] : record if key_proc value = key_proc.call(value) end value.to_s end end def build_data_to_put(data) if @zlib_compression Hash[data.map{|k, v| [k.to_sym, k=="data" ? Zlib::Deflate.deflate(v) : v] }] else Hash[data.map{|k, v| [k.to_sym, v] }] end end def put_record_for_order_events(data_list) sequence_number_for_ordering = nil data_list.each do |data_to_put| if sequence_number_for_ordering data_to_put.update( sequence_number_for_ordering: sequence_number_for_ordering ) end data_to_put.update( stream_name: @stream_name ) result = @client.put_record(data_to_put) sequence_number_for_ordering = result[:sequence_number] end end def build_records_array_to_put(data_list) records_array = [] records = [] records_payload_length = 0 data_list.each{|data_to_put| payload = data_to_put[:data] partition_key = data_to_put[:partition_key] if records.length >= PUT_RECORDS_MAX_COUNT or (records_payload_length + payload.length + partition_key.length) >= PUT_RECORDS_MAX_DATA_SIZE records_array.push(records) records = [] records_payload_length = 0 end records.push(data_to_put) records_payload_length += (payload.length + partition_key.length) } records_array.push(records) unless records.empty? records_array end def put_records_with_retry(records,retry_count=0) response = @client.put_records( records: records, stream_name: @stream_name ) if response[:failed_record_count] && response[:failed_record_count] > 0 failed_records = [] response[:records].each_with_index{|record,index| if record[:error_code] failed_records.push({body: records[index], error_code: record[:error_code]}) end } if(retry_count < @retries_on_putrecords) sleep(calculate_sleep_duration(retry_count)) retry_count += 1 log.warn sprintf('Retrying to put records. Retry count: %d', retry_count) put_records_with_retry( failed_records.map{|record| record[:body]}, retry_count ) else failed_records.each{|record| log.error sprintf( 'Could not put record, Error: %s, Record: %s', record[:error_code], @dump_class.dump(record[:body]) ) } end end end def calculate_sleep_duration(current_retry) Array.new(@retries_on_putrecords){|n| ((2 ** n) * scaling_factor)}[current_retry] end def scaling_factor 0.5 + Kernel.rand * 0.1 end def record_exceeds_max_size?(record_string) return record_string.length > PUT_RECORD_MAX_DATA_SIZE end end end