diff --git a/lib/rackstash/filters.rb b/lib/rackstash/filters.rb index 7673d80..1963300 100644 --- a/lib/rackstash/filters.rb +++ b/lib/rackstash/filters.rb @@ -6,6 +6,8 @@ # of the MIT license. See the LICENSE.txt file for details. require 'rackstash/filters/clear_color' +require 'rackstash/filters/default_fields' +require 'rackstash/filters/default_tags' require 'rackstash/filters/rename' require 'rackstash/filters/replace' require 'rackstash/filters/skip_event' diff --git a/lib/rackstash/filters/default_fields.rb b/lib/rackstash/filters/default_fields.rb new file mode 100644 index 0000000..630f514 --- /dev/null +++ b/lib/rackstash/filters/default_fields.rb @@ -0,0 +1,60 @@ +# 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. + +module Rackstash + module Filters + # The {DefaultFields} filter allows to define fields which should be added + # to an event if they aren't already explicitly set. + # + # Fields can be given either as a `Hash` or a `Proc` which in turn returns a + # `Hash` on `call`. The `Hash` can be nested arbitrarily deep. + # + # Each `Hash` value can again optionally be a `Proc` which is expected to + # return a field value on `call`. You can set nested Hashes or Arrays and + # define nested Procs which in turn are resolved recursively when applying + # the filter. That way, you can set lazy-evaluated values which are only + # resolved at the time the filter is applied to a logged event. + # + # @example + # Rackstash::Flow.new(STDOUT) do + # # All three defined filters set the same default fields + # filter :default_fields, 'beep' => 'boop' + # filter :default_fields, 'beep' => -> { 'boop' } + # filter :default_fields, -> { { 'beep' => 'boop' } } + # end + class DefaultFields + # @param default_fields [Hash<#to_s => Object>, Proc] a `Hash` specifying + # default values for the named keys. You can either give a literal + # `Hash` object or a `Proc` which returns such a `Hash`. + def initialize(default_fields) + @default_fields = default_fields + end + + # Add the defined `default_fields` to the event hash, retaining all + # existing values. + # + # @param event [Hash] an event hash + # @return [Hash] the given `event` with the fields renamed + def call(event) + resolver = lambda do |key, old_val, new_val| + if old_val.nil? + new_val + elsif old_val.is_a?(Hash) && new_val.is_a?(Hash) + old_val.merge(new_val, &resolver) + elsif old_val.is_a?(Array) && new_val.is_a?(Array) + old_val | new_val + else + old_val + end + end + + fields = Rackstash::Fields::Hash(@default_fields).to_h + event.merge!(fields, &resolver) + end + end + end +end diff --git a/lib/rackstash/filters/default_tags.rb b/lib/rackstash/filters/default_tags.rb new file mode 100644 index 0000000..e117ad5 --- /dev/null +++ b/lib/rackstash/filters/default_tags.rb @@ -0,0 +1,54 @@ +# 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. + +module Rackstash + module Filters + # The {DefaultTags} filter allows to define tags which should be added + # to an event if they aren't already explicitly set there. All existing tags + # on the event are retained. + # + # The default tags are added to the `"tags"` field of the event `Hash`. They + # can be given either as an `Array` of `String`s or a `Proc` which in turn + # returns an `Array` of `String`s on `call`. + # + # Each value of the Array can again optionally be a Proc which in turn is + # expected to return a String on `call`. All the (potentially nested) procs + # are called recursively when applying the filter. That way, you can set + # lazy-evaluated values which are only resolved at the time the filter is + # applied to a logged event. + # + # @example + # Rackstash::Flow.new(STDOUT) do + # # All three defined filters set the same default tags + # filter :default_tags, ['important', 'request'] + # filter :default_tags, -> { ['important', 'request'] } + # filter :default_tags, ['important', -> { 'request' }] + # end + class DefaultTags + # @param default_tags [Array<#to_s>, Set<#to_s>, Proc] an `Array` + # specifying default tags for each event. You can either give a literal + # `Array` containing Strings or a `Proc` which returns such an `Array`. + def initialize(*default_tags) + @default_tags = default_tags + end + + # Add the defined `default_tags` to the event hash, retaining all + # existing tags. The `"tags"` field on the event will be normalized to a + # plain `Array` containing only `String`s. + # + # @param event [Hash] an event hash + # @return [Hash] the given `event` with the fields renamed + def call(event) + tags = Rackstash::Fields::Tags(event[FIELD_TAGS]) + tags.merge!(@default_tags) + + event[FIELD_TAGS] = tags.to_a + event + end + end + end +end diff --git a/spec/rackstash/filters/default_fields_spec.rb b/spec/rackstash/filters/default_fields_spec.rb new file mode 100644 index 0000000..87cd93e --- /dev/null +++ b/spec/rackstash/filters/default_fields_spec.rb @@ -0,0 +1,67 @@ +# 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 'spec_helper' + +require 'rackstash/filters/default_fields' + +describe Rackstash::Filters::DefaultFields do + let(:event) { + { + 'foo' => 'v1', + 'bar' => 'v2' + } + } + + def filter!(default_fields) + described_class.new(default_fields).call(event) + end + + it 'adds missing normalized fields' do + filter! 'new' => 'boing', 123 => :number + + expect(event).to eql({ + 'foo' => 'v1', + 'bar' => 'v2', + 'new' => 'boing', + '123' => 'number' + }) + end + + it 'retains existing fields' do + filter! foo: 'ignored' + + expect(event).to eql({ + 'foo' => 'v1', + 'bar' => 'v2' + }) + end + + it 'deep_merges fields' do + event['deep'] = { 'key' => [42, { 'foo' => 'bar' }, nil], 'new' => nil } + filter! 'deep' => { key: [123], new: 'new' } + + expect(event).to eql({ + 'foo' => 'v1', + 'bar' => 'v2', + 'deep' => { + 'key' => [42, { 'foo' => 'bar' }, nil, 123], + 'new' => 'new' + } + }) + end + + it 'resolves Procs' do + filter! -> { { 'beep' => -> { 'boop' } } } + + expect(event).to eql({ + 'foo' => 'v1', + 'bar' => 'v2', + 'beep' => 'boop' + }) + end +end diff --git a/spec/rackstash/filters/default_tags_spec.rb b/spec/rackstash/filters/default_tags_spec.rb new file mode 100644 index 0000000..40940fb --- /dev/null +++ b/spec/rackstash/filters/default_tags_spec.rb @@ -0,0 +1,47 @@ +# 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 'spec_helper' + +require 'rackstash/filters/default_tags' + +describe Rackstash::Filters::DefaultTags do + let(:event) { + { + 'key' => 'value' + } + } + + def filter!(*default_tags) + described_class.new(*default_tags).call(event) + end + + it 'adds missing tags' do + filter! 'foo', 'bar' + expect(event['tags']).to eql ['foo', 'bar'] + end + + it 'retains existing tags' do + event['tags'] = ['tag'] + filter! 'foo', 'bar' + + expect(event['tags']).to eql ['tag', 'foo', 'bar'] + end + + it 'flattens and normalizes tags' do + event['tags'] = 'bare' + filter! [:foo, [[123]]] + + expect(event['tags']).to eql ['bare', 'foo', '123'] + end + + it 'resolves Procs' do + filter! -> { ['beep', -> { ['boop'] }] } + + expect(event['tags']).to eql ['beep', 'boop'] + end +end