spriteCloud/teelogger

View on GitHub
lib/teelogger/filter.rb

Summary

Maintainability
B
4 hrs
Test Coverage
#
# TeeLogger
# https://github.com/spriteCloud/teelogger
#
# Copyright (c) 2014,2015 spriteCloud B.V. and other TeeLogger contributors.
# All rights reserved.
#
require 'require_all'

module TeeLogger
  module Filter
    ##
    # The default words to filter. It's up to each individual filter to decide
    # what to do when they encounter a word, but these are the words the filters
    # should process.
    # Note that they can be strings or regular expressions. Regular expressions
    # should by and large not be anchored to the beginning or end of strings.
    DEFAULT_FILTER_WORDS = [
      /password[a-z\-_]*/,
      /salt[a-z\-_]*/,
    ]

    ##
    # Word to use in place of original values
    REDACTED_WORD = "[REDACTED]"

    ##
    # Filter words
    def filter_words
      @filter_words ||= DEFAULT_FILTER_WORDS
      return @filter_words
    end

    def filter_words=(arg)
      # Coerce into array
      begin
        arr = []
        arg.each do |item|
          arr << item
        end
        @filter_words = arr
      rescue NameError, NoMethodError
        raise "Can't set filter words, not iterable: #{arg}"
      end
    end


    ##
    # Load all built-in filters.
    def load_filters(*args)
      require_rel 'filters'
      ::TeeLogger::Filter.constants.collect {|const_sym|
        ::TeeLogger::Filter.const_get(const_sym)
      }.each do |filter|
        begin
          register_filter(filter)
          if not ENV['TEELOGGER_VERBOSE'].nil? and ENV['TEELOGGER_VERBOSE'].to_i > 0
            puts "Registered filter #{filter}."
          end
        rescue StandardError => err
          if not ENV['TEELOGGER_VERBOSE'].nil? and ENV['TEELOGGER_VERBOSE'].to_i > 0
            puts "Not registering filter: #{err}"
          end
        end
      end
    end

    ##
    # Returns all registered filters.
    def registered_filters
      # Initialize if it doesn't exist
      @filters ||= {}
      return @filters
    end

    ##
    # Expects a class, registers the class for use by the filter function
    def register_filter(filter)
      # Sanity checks/register filter
      if filter.class != Class
        raise "Ignoring '#{filter}', not a class."
      end

      if not filter < FilterBase
        raise "Class '#{filter}' is not derived from FilterBase."
      end

      begin
        window = filter::WINDOW_SIZE.to_i
        window_filters = registered_filters.fetch(window, {})

        filter::FILTER_TYPES.each do |type|
          type_filters = window_filters.fetch(type, [])
          type_filters.push(filter) unless type_filters.include?(filter)
          window_filters[type] = type_filters
        end

        registered_filters[window] = window_filters
      rescue NameError, NoMethodError
        raise "Class '#{filter}' is missing a FILTER_TYPES Array or a WINDOW_SIZE Integer."
      end
    end

    ##
    # Applies all registered filters.
    def apply_filters(*args)
      # Pre-process filter words: we need to have regular expressions everywhere
      words = []
      filter_words.each do |word|
        if word.is_a? Regexp
          words << word
        else
          words << Regexp.new(word.to_s)
        end
      end

      # We instanciate each filter once per application, and store the instnaces
      # in a cache for that duration.
      filter_cache = {}

      # Pass state on to apply_filters_internal
      state = {
        :words => words,
        :filter_cache => filter_cache,
        :filters => self,
      }
      return apply_filters_internal(state, *args)
    end


    ##
    # Implementation of apply_filters that doesn't initialize state, but carries
    # it over. Used internally only.
    def apply_filters_internal(state, *args)
      filtered_args = args

      # Iterate through filters
      registered_filters.each do |window, window_filters|
        # Determine actual window size
        window_size = [window, filtered_args.size].min

        # Process each window so that elements are updated in-place. This
        # means we'll start at index 0 and process up to window_size elements.
        idx = 0
        while (idx + window_size - 1) < filtered_args.size
          # We need to use *one* argument to determine whether the filter
          # type applies. The current strategy is to match the first argument
          # only, and let the filter cast to other types if necessary.
          first_arg = filtered_args[idx]

          window_filters.each do |class_match, type_filters|
            # We process with these type filters if first_arg matches the
            # class_match.
            if not first_arg.is_a? class_match
              next
            end

            # Now process with the given filters.
            type_filters.each do |filter|
              # XXX Do not turn this into a one-liner, or we'll instanciate
              #     filters without using them.
              filter_instance = state[:filter_cache].fetch(filter, nil)
              if filter_instance.nil?
                filter_instance = filter.new(state)
                state[:filter_cache][filter] = filter_instance
              end

              # Single item windows need to be processed a bit differently from
              # multi-item windows.
              tuple = filtered_args[idx..idx + window_size - 1]
              filtered = filter_instance.process(*tuple)

              # Sanity check result
              if filtered.size != tuple.size
                raise "Filter #{filter} added or removed items to the log; don't know how to process!"
              end

              filtered.each_with_index do |item, offset|
                filtered_args[idx + offset] = item
              end
            end # type_filters.each
          end # window_filters.each

          # Advance to the next window
          idx += 1
        end # each window
      end # all registered filters

      return filtered_args
    end


    ##
    # Any filter implementations must derive from this
    class FilterBase
      # Define FILTER_TYPES = [class, class] to declare what types this filter
      # applies to.
      # Define WINDOW_SIZE = int to declare how many parameters the filter
      # processes at a time. It will be a sliding window of arguments.
      # Note that filters may receive fewer arguments if there are less than
      # WINDOW_SIZE in total.

      ##
      # Initialize with filter words
      attr_accessor :run_data

      def initialize(run_data)
        @run_data = run_data
      end

      ##
      # Base filter leaves the argument untouched.
      def process(*args)
        args
      end

    end # class FilterBase
  end # end module Filter
end # end module TeeLogger