Skip to content

Commit

Permalink
Fieldreference replacement
Browse files Browse the repository at this point in the history
  • Loading branch information
colinsurprenant committed Mar 14, 2014
1 parent ef5af78 commit 6796736
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 108 deletions.
57 changes: 19 additions & 38 deletions lib/logstash/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require "date"
require "logstash/namespace"
require "logstash/util/fieldreference"
require "logstash/util/accessors"
require "logstash/time_addon"

# Use a custom serialization for jsonifying Time objects.
Expand Down Expand Up @@ -31,7 +32,7 @@ def inspect
# * "@version" - the version of the schema. Currently "1"
#
# They are prefixed with an "@" symbol to avoid clashing with your
# own custom fields.
# own custom fields.
#
# When serialized, this is represented in JSON. For example:
#
Expand All @@ -53,8 +54,10 @@ def initialize(data={})
@cancelled = false

@data = data
@accessors = LogStash::Util::Accessors.new(data)

data[VERSION] = VERSION_ONE if !@data.include?(VERSION)
if data.include?(TIMESTAMP)
if data.include?(TIMESTAMP)
t = data[TIMESTAMP]
if t.is_a?(String)
data[TIMESTAMP] = LogStash::Time.parse_iso8601(t)
Expand Down Expand Up @@ -113,59 +116,36 @@ def unix_timestamp
def ruby_timestamp
raise DeprecatedMethod
end # def unix_timestamp

# field-related access
public
def [](str)
if str[0,1] == CHAR_PLUS
# nothing?
else
return LogStash::Util::FieldReference.exec(str, @data)
# return LogStash::Util::FieldReference.exec(str, @data)
@accessors.get(str)
end
end # def []

public
def []=(str, value)
if str == TIMESTAMP && !value.is_a?(Time)
raise TypeError, "The field '@timestamp' must be a Time, not a #{value.class} (#{value})"
end

r = LogStash::Util::FieldReference.exec(str, @data) do |obj, key|
obj[key] = value
end

# The assignment can fail if the given field reference (str) does not exist
# In this case, we'll want to set the value manually.
if r.nil?
# TODO(sissel): Implement this in LogStash::Util::FieldReference
if str[0,1] != "["
return @data[str] = value
end

# No existing element was found, so let's set one.
*parents, key = str.scan(/(?<=\[)[^\]]+(?=\])/)
obj = @data
parents.each do |p|
if obj.include?(p)
obj = obj[p]
else
obj[p] = {}
obj = obj[p]
end
end
obj[key] = value
end
return value
# return LogStash::Util::FieldReference.set(str, value, @data)
@accessors.set(str, value)
end # def []=

public
def fields
raise DeprecatedMethod
end

public
def to_json(*args)
return @data.to_json(*args)
return @data.to_json(*args)
end # def to_json

def to_hash
Expand Down Expand Up @@ -197,13 +177,14 @@ def append(event)
# deleted
public
def remove(str)
return LogStash::Util::FieldReference.exec(str, @data) do |obj, key|
next obj.delete(key)
end
# return LogStash::Util::FieldReference.exec(str, @data) do |obj, key|
# next obj.delete(key)
# end
@accessors.del(str)
end # def remove

# sprintf. This could use a better method name.
# The idea is to take an event and convert it to a string based on
# The idea is to take an event and convert it to a string based on
# any format values, delimited by %{foo} where 'foo' is a field or
# metadata member.
#
Expand All @@ -216,7 +197,7 @@ def remove(str)
# If a %{name} value is an array, then we will join by ','
# If a %{name} value does not exist, then no substitution occurs.
#
# TODO(sissel): It is not clear what the value of a field that
# TODO(sissel): It is not clear what the value of a field that
# is an array (or hash?) should be. Join by comma? Something else?
public
def sprintf(format)
Expand Down
62 changes: 62 additions & 0 deletions lib/logstash/util/accessors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# encoding: utf-8
require "logstash/namespace"
require "logstash/util"


module LogStash::Util

# PathCache is a singleton which globally caches a parsed fields path for the path to the
# container hash and key in that hash.
module PathCache
extend self

def get(accessor)
@cache ||= {}
@cache[accessor] ||= parse(accessor)
end

def parse(accessor)
path = accessor.split(/[\[\]]/).select{|s| !s.empty?}
[path.pop, path]
end
end


# Accessors uses a lookup table to speedup access of an accessor field of the type
# "[hello][world]" to the underlying store hash into {"hello" => {"world" => "foo"}}
class Accessors

def initialize(store)
@store = store
@lut = {}
end

def get(accessor)
target, key = lookup(accessor)
target.is_a?(Array) ? target[key.to_i] : target[key]
end

def set(accessor, value)
target, key = lookup(accessor)
target[key] = value
end

def del(accessor)
target, key = lookup(accessor)
target.delete(key)
end

private

def lookup(accessor)
@lut[accessor] ||= store_path(accessor)
end

def store_path(accessor)
key, path = PathCache.get(accessor)
target = path.inject(@store) {|r, k| r[k] ||= {}}
[target, key]
end

end
end # module LogStash::Util::Accessors
58 changes: 38 additions & 20 deletions lib/logstash/util/fieldreference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,47 +3,65 @@
require "logstash/util"

module LogStash::Util::FieldReference
def compile(str)
if str[0,1] != '['

def compile(accessor)
if accessor[0,1] != '['
return <<-"CODE"
lambda do |e, &block|
return block.call(e, #{str.inspect}) unless block.nil?
return e[#{str.inspect}]
lambda do |store, &block|
return block.nil? ? store[#{accessor.inspect}] : block.call(store, #{accessor.inspect})
end
CODE
end

code = "lambda do |e, &block|\n"
selectors = str.scan(/(?<=\[).+?(?=\])/)
code = "lambda do |store, &block|\n"
selectors = accessor.scan(/(?<=\[).+?(?=\])/)
selectors.each_with_index do |tok, i|
last = (i == selectors.count() - 1)
code << " # [#{tok}]#{ last ? " (last selector)" : "" }\n"

if last
code << <<-"CODE"
return block.call(e, #{tok.inspect}) unless block.nil?
return block.call(store, #{tok.inspect}) unless block.nil?
CODE
end

code << <<-"CODE"
if e.is_a?(Array)
e = e[#{tok.to_i}]
else
e = e[#{tok.inspect}]
end
return e if e.nil?
store = store.is_a?(Array) ? store[#{tok.to_i}] : store[#{tok.inspect}]
return store if store.nil?
CODE

end
code << "return e\nend"
code << "return store\nend"
#puts code
return code
end # def compile

def exec(str, obj, &block)
def exec(accessor, store, &block)
@__fieldeval_cache ||= {}
@__fieldeval_cache[str] ||= eval(compile(str))
return @__fieldeval_cache[str].call(obj, &block)
@__fieldeval_cache[accessor] ||= eval(compile(accessor))
return @__fieldeval_cache[accessor].call(store, &block)
end

def set(accessor, value, store)
# The assignment can fail if the given field reference (accessor) does not exist
# In this case, we'll want to set the value manually.
if exec(accessor, store) { |hash, key| hash[key] = value }.nil?
return (store[accessor] = value) if accessor[0,1] != "["

# No existing element was found, so let's set one.
*parents, key = accessor.scan(/(?<=\[)[^\]]+(?=\])/)
parents.each do |p|
if store.include?(p)
store = store[p]
else
store[p] = {}
store = store[p]
end
end
store[key] = value
end

return value
end

extend self
Expand Down
Loading

0 comments on commit 6796736

Please sign in to comment.