diff --git a/lib/rackstash.rb b/lib/rackstash.rb index c08a84f..7779e81 100644 --- a/lib/rackstash.rb +++ b/lib/rackstash.rb @@ -183,7 +183,7 @@ require 'rackstash/filter/anonymize_ip_mask' require 'rackstash/filter/clear_color' require 'rackstash/filter/default_fields' require 'rackstash/filter/default_tags' -require 'rackstash/filter/drop_if' +require 'rackstash/filter/drop' require 'rackstash/filter/rename' require 'rackstash/filter/replace' require 'rackstash/filter/truncate_message' diff --git a/lib/rackstash/filter/drop.rb b/lib/rackstash/filter/drop.rb new file mode 100644 index 0000000..af2cd2a --- /dev/null +++ b/lib/rackstash/filter/drop.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true +# +# Copyright 2017 Holger Just +# +# This software may be modified and distributed under the terms +# of the MIT license. See the LICENSE.txt file for details. + +require 'rackstash/filter' + +module Rackstash + module Filter + # This filter skips a certain percentage of events passed through it. It + # does not change the `event` hash in any way on its own. + # + # You can select the events to be filtered be using an `:if` or `:unless` + # guard when adding the filter to the {FilterChain}. + # + # @example + # Rackstash::Flow.new(STDOUT) do + # # Drop half of all the events which have a 'debug' tag + # filter :drop, percent: 50, if: ->(event) { event['tags'].include?('debug') } + # end + class Drop + # @return [Integer] the percentage of events dropped by this filter + attr_reader :percent + + # @param percent [Integer] the percentage of events passed through this + # filter which are dropped. Can be an integer between 0 and 100 + # (inclusive). + def initialize(percent: 100) + @percent = Integer(percent) + unless percent.between?(0, 100) + raise ArgumentError, "percent must be an Integer between 0 and 100" + end + + @rand = Random.new + end + + # Run the filter against the passed `event` hash. + # + # We drop a defined percentage of log events passing through the filter. + # If an `event` is selected to be dropped, we return `false`, else we just + # return the passed event. + # + # @param event [Hash] an event hash + # @return [Hash, false] the given `event` or `false` if the event is + # dropped + def call(event) + return false if drop? + event + end + + private + + # @return [Bool] `true` is the event should be dropped based on the + # defined drop percentage, `false` otherwise. + def drop? + return true if @percent == 100 + return true if random_percentage < @percent + + false + end + + # @return [Integer] a random number between 0 and 99 (inclusive). + def random_percentage + @rand.rand(100) + end + end + + register Drop, :drop + end +end diff --git a/lib/rackstash/filter/drop_if.rb b/lib/rackstash/filter/drop_if.rb deleted file mode 100644 index 9b4e4bf..0000000 --- a/lib/rackstash/filter/drop_if.rb +++ /dev/null @@ -1,58 +0,0 @@ -# frozen_string_literal: true -# -# Copyright 2017 Holger Just -# -# This software may be modified and distributed under the terms -# of the MIT license. See the LICENSE.txt file for details. - -require 'rackstash/filter' - -module Rackstash - module Filter - # Skip the further processing of the event if the provided condition is - # truethy. In that case, the event will be dropped and not be written to the - # log adapter. - # - # This filter is a basic example of how you can write filters which abort - # further processing of an event. You can write your own filters which - # provide similar (but probably more useful) behavior. - # - # @example - # Rackstash::Flow.new(STDOUT) do - # # Drop the event if it has the 'debug' tag - # filter :drop_if, ->(event) { event['tags'].include?('debug') } - # end - class DropIf - # @param drop_if [#call] a callable object (e.g. a `Proc`) which returns a - # truethy or falsey value on `call` with an `event` hash. If it returns - # something truethy, we abort any further processing of the event. If the - # `drop_if` filter is not given, we expect a block to be provided which - # is used instead. - def initialize(drop_if = nil, &block) - if drop_if.respond_to?(:call) - @drop_if = drop_if - elsif block_given? - @drop_if = block - else - raise ArgumentError, 'must provide a condition when to drop the event' - end - end - - # Run the filter against the passed `event` hash. - # - # We will call the `drop_if` object with the passed event. If the return - # value is truethy, we abort any further processing of the event. This - # filter does not change the `event` hash in any way on its own. - # - # @param event [Hash] an event hash - # @return [Hash, false] the given `event` or `false` if the `drop_if` - # condition was evaluated - def call(event) - return false if @drop_if.call(event) - event - end - end - - register DropIf, :drop_if - end -end diff --git a/spec/rackstash/filter/drop_if_spec.rb b/spec/rackstash/filter/drop_if_spec.rb deleted file mode 100644 index 74c820b..0000000 --- a/spec/rackstash/filter/drop_if_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true -# -# Copyright 2017 - 2018 Holger Just -# -# This software may be modified and distributed under the terms -# of the MIT license. See the LICENSE.txt file for details. - -require 'spec_helper' - -require 'rackstash/filter/drop_if' - -RSpec.describe Rackstash::Filter::DropIf do - describe '#initialize' do - it 'expects a condition' do - expect { described_class.new }.to raise_error ArgumentError - end - - it 'accepts a callable object' do - expect { described_class.new(->(event) {}) }.not_to raise_error - end - - it 'accepts a block' do - expect { described_class.new {} }.not_to raise_error - end - end - - describe '#call' do - it 'returns the event if the condition is falsey' do - event = { 'foo' => 'bar' } - - expect(described_class.new(->(_event) { false }).call(event)).to equal event - expect(described_class.new(->(_event) { nil }).call(event)).to equal event - expect(described_class.new { |_event| false }.call(event)).to equal event - expect(described_class.new { |_event| nil }.call(event)).to equal event - end - - it 'returns false if the condition is truethy' do - event = { 'foo' => 'bar' } - - expect(described_class.new(->(_event) { true }).call(event)).to be false - expect(described_class.new(->(_event) { event }).call(event)).to be false - expect(described_class.new { |_event| true }.call(event)).to be false - expect(described_class.new { |_event| event }.call(event)).to be false - end - end -end diff --git a/spec/rackstash/filter/drop_spec.rb b/spec/rackstash/filter/drop_spec.rb new file mode 100644 index 0000000..ed7c2c8 --- /dev/null +++ b/spec/rackstash/filter/drop_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true +# +# Copyright 2017 - 2018 Holger Just +# +# This software may be modified and distributed under the terms +# of the MIT license. See the LICENSE.txt file for details. + +require 'spec_helper' + +require 'rackstash/filter/drop' + +RSpec.describe Rackstash::Filter::Drop do + let(:event) { + {what: :ever} + } + + describe '#initialize' do + it 'accepts a percentage' do + filter = described_class.new(percent: 23) + expect(filter.percent).to eql 23 + end + + it 'defaults to 100%' do + expect(described_class.new.percent).to eql 100 + end + + it 'only accepts valid percentages' do + expect { described_class.new(percent: -1) }.to raise_error ArgumentError + expect { described_class.new(percent: 101) }.to raise_error ArgumentError + expect { described_class.new(percent: 'value') }.to raise_error ArgumentError + expect { described_class.new(percent: :value) }.to raise_error TypeError + expect { described_class.new(percent: false) }.to raise_error TypeError + end + + end + + describe '#call' do + it 'always returns the event with percent: 0' do + drop_all = described_class.new(percent: 0) + + expect(1_000.times.count { drop_all.call(event) == event }).to eql 1_000 + end + + it 'drops about half of the events with percent: 50' do + drop_half = described_class.new(percent: 50) + expect(drop_half).to receive(:random_percentage) + .and_return(*[0, 99] * 500) + + expect(1_000.times.count { drop_half.call(event) == event }) + .to eql 500 + end + + it 'drops 99% of the events with percent: 99' do + drop_most = described_class.new(percent: 99) + expect(drop_most).to receive(:random_percentage) + .and_return(*(0..99).to_a.shuffle * 10) + + expect(1_000.times.count { drop_most.call(event) == event }) + .to eql 10 + end + + it 'always returns false with percent: 100' do + drop_all = described_class.new(percent: 100) + + expect(1_000.times.count { drop_all.call(event) == event }).to eql 0 + end + end +end