class LogStash::Filters::Xml

XML filter. Takes a field that contains XML and expands it into an actual datastructure.

Constants

XMLPARSEFAILURE_TAG

Public Instance Methods

filter(event) click to toggle source
# File lib/logstash/filters/xml.rb, line 124
def filter(event)
  matched = false

  @logger.debug? && @logger.debug("Running xml filter", :event => event)

  value = event.get(@source)
  return unless value

  if value.is_a?(Array)
    if value.length != 1
      event.tag(XMLPARSEFAILURE_TAG)
      @logger.warn("XML filter expects single item array", :source => @source, :value => value)
      return
    end

    value = value.first
  end

  unless value.is_a?(String)
    event.tag(XMLPARSEFAILURE_TAG)
    @logger.warn("XML filter expects a string but received a #{value.class}", :source => @source, :value => value)
    return
  end

  # Do nothing with an empty string.
  return if value.strip.empty?

  if @xpath
    begin
      doc = Nokogiri::XML::Document.parse(value, nil, value.encoding.to_s, xml_parse_options)
    rescue => e
      event.tag(XMLPARSEFAILURE_TAG)
      @logger.warn("Error parsing xml", :source => @source, :value => value, :exception => e, :backtrace => e.backtrace)
      return
    else
      doc.errors.any? && @logger.debug? && @logger.debug("Parsed xml with #{doc.errors.size} errors")
    end
    doc.remove_namespaces! if @remove_namespaces

    @xpath.each do |xpath_src, xpath_dest|
      nodeset = @namespaces.empty? ? doc.xpath(xpath_src) : doc.xpath(xpath_src, @namespaces)

      # If asking xpath for a String, like "name(/*)", we get back a
      # String instead of a NodeSet.  We normalize that here.
      normalized_nodeset = nodeset.kind_of?(Nokogiri::XML::NodeSet) ? nodeset : [nodeset]

      # Initialize empty resultset
      data = []

      normalized_nodeset.each do |value|
        # some XPath functions return empty arrays as string
        next if value.is_a?(Array) && value.length == 0

        if value
          matched = true
          data << value.to_s
        end

      end
      # set the destination attribute, if it's an array with a bigger size than one, leave as is. otherwise make it a string. added force_array param to provide same functionality as writing it in an xml target
      if data.size == 1 && !@force_array
        event.set(xpath_dest, data[0])
      else
        event.set(xpath_dest, data) unless data.nil? || data.empty? 
      end
    end
  end

  if @store_xml
    begin
      xml_options = {"ForceArray" => @force_array, "ForceContent" => @force_content}
      xml_options["SuppressEmpty"] = true if @suppress_empty
      event.set(@target, XmlSimple.xml_in(value, xml_options))
      matched = true
    rescue => e
      event.tag(XMLPARSEFAILURE_TAG)
      @logger.warn("Error parsing xml with XmlSimple", :source => @source, :value => value, :exception => e, :backtrace => e.backtrace)
      return
    end
  end

  filter_matched(event) if matched
  @logger.debug? && @logger.debug("Event after xml filter", :event => event)
rescue => e
  event.tag(XMLPARSEFAILURE_TAG)

  log_payload = { :exception => e.message, :source => @source }
  log_payload[:value] = value unless value.nil?
  log_payload[:backtrace] = e.backtrace if @logger.debug?

  @logger.warn("XML Parse Error", log_payload)
end
register() click to toggle source
# File lib/logstash/filters/xml.rb, line 109
def register
  require "nokogiri"
  require "xmlsimple"

  if @store_xml && (!@target || @target.empty?)
    raise LogStash::ConfigurationError, I18n.t(
      "logstash.runner.configuration.invalid_plugin_register",
      :plugin => "filter",
      :type => "xml",
      :error => "When the 'store_xml' configuration option is true, 'target' must also be set"
    )
  end
  xml_parse_options # validates parse_options => ...
end

Private Instance Methods

xml_parse_options() click to toggle source
# File lib/logstash/filters/xml.rb, line 219
def xml_parse_options
  return Nokogiri::XML::ParseOptions::DEFAULT_XML unless @parse_options # (RECOVER | NONET)
  @xml_parse_options ||= begin
    parse_options = @parse_options.split(/,|\|/).map do |opt|
      name = opt.strip.tr('_', '').upcase
      if name.empty?
        nil
      else
        begin
          Nokogiri::XML::ParseOptions.const_get(name)
        rescue NameError
          raise LogStash::ConfigurationError, "unsupported parse option: #{opt.inspect}"
        end
      end
    end
    parse_options.compact.inject(0, :|) # e.g. NOERROR | NOWARNING
  end
end