logstash-core-event 2.2.4.snapshot1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/logstash-core-event.rb +1 -0
- data/lib/logstash-core-event/logstash-core-event.rb +5 -0
- data/lib/logstash-core-event/version.rb +8 -0
- data/lib/logstash/event.rb +278 -0
- data/lib/logstash/string_interpolation.rb +150 -0
- data/lib/logstash/timestamp.rb +103 -0
- data/lib/logstash/util/accessors.rb +123 -0
- data/logstash-core-event.gemspec +23 -0
- data/spec/logstash/event_spec.rb +534 -0
- data/spec/logstash/timestamp_spec.rb +109 -0
- data/spec/logstash/util/accessors_spec.rb +179 -0
- metadata +59 -0
@@ -0,0 +1,103 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/environment"
|
3
|
+
require "logstash/json"
|
4
|
+
require "forwardable"
|
5
|
+
require "date"
|
6
|
+
require "time"
|
7
|
+
|
8
|
+
module LogStash
|
9
|
+
class TimestampParserError < StandardError; end
|
10
|
+
|
11
|
+
class Timestamp
|
12
|
+
extend Forwardable
|
13
|
+
include Comparable
|
14
|
+
|
15
|
+
def_delegators :@time, :tv_usec, :usec, :year, :iso8601, :to_i, :tv_sec, :to_f, :to_edn, :<=>, :+
|
16
|
+
|
17
|
+
attr_reader :time
|
18
|
+
|
19
|
+
ISO8601_STRFTIME = "%04d-%02d-%02dT%02d:%02d:%02d.%06d%+03d:00".freeze
|
20
|
+
ISO8601_PRECISION = 3
|
21
|
+
|
22
|
+
def initialize(time = Time.new)
|
23
|
+
@time = time.utc
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.at(*args)
|
27
|
+
epoch = args.first
|
28
|
+
if epoch.is_a?(BigDecimal)
|
29
|
+
# bug in JRuby prevents correcly parsing a BigDecimal fractional part, see https://github.com/elastic/logstash/issues/4565
|
30
|
+
Timestamp.new(::Time.at(epoch.to_i, epoch.frac.to_f * 1000000))
|
31
|
+
else
|
32
|
+
Timestamp.new(::Time.at(*args))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.parse(*args)
|
37
|
+
Timestamp.new(::Time.parse(*args))
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.now
|
41
|
+
Timestamp.new(::Time.now)
|
42
|
+
end
|
43
|
+
|
44
|
+
# coerce tries different strategies based on the time object class to convert into a Timestamp.
|
45
|
+
# @param [String, Time, Timestamp] time the time object to try coerce
|
46
|
+
# @return [Timestamp, nil] Timestamp will be returned if successful otherwise nil
|
47
|
+
# @raise [TimestampParserError] on String with invalid format
|
48
|
+
def self.coerce(time)
|
49
|
+
case time
|
50
|
+
when String
|
51
|
+
LogStash::Timestamp.parse_iso8601(time)
|
52
|
+
when LogStash::Timestamp
|
53
|
+
time
|
54
|
+
when Time
|
55
|
+
LogStash::Timestamp.new(time)
|
56
|
+
else
|
57
|
+
nil
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
if LogStash::Environment.jruby?
|
62
|
+
JODA_ISO8601_PARSER = org.joda.time.format.ISODateTimeFormat.dateTimeParser
|
63
|
+
UTC = org.joda.time.DateTimeZone.forID("UTC")
|
64
|
+
|
65
|
+
def self.parse_iso8601(t)
|
66
|
+
millis = JODA_ISO8601_PARSER.parseMillis(t)
|
67
|
+
LogStash::Timestamp.at(millis / 1000, (millis % 1000) * 1000)
|
68
|
+
rescue => e
|
69
|
+
raise(TimestampParserError, "invalid timestamp string #{t.inspect}, error=#{e.inspect}")
|
70
|
+
end
|
71
|
+
|
72
|
+
else
|
73
|
+
|
74
|
+
def self.parse_iso8601(t)
|
75
|
+
# warning, ruby's Time.parse is *really* terrible and slow.
|
76
|
+
LogStash::Timestamp.new(::Time.parse(t))
|
77
|
+
rescue => e
|
78
|
+
raise(TimestampParserError, "invalid timestamp string #{t.inspect}, error=#{e.inspect}")
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def utc
|
83
|
+
@time.utc # modifies the receiver
|
84
|
+
self
|
85
|
+
end
|
86
|
+
alias_method :gmtime, :utc
|
87
|
+
|
88
|
+
def to_json(*args)
|
89
|
+
# ignore arguments to respect accepted to_json method signature
|
90
|
+
"\"" + to_iso8601 + "\""
|
91
|
+
end
|
92
|
+
alias_method :inspect, :to_json
|
93
|
+
|
94
|
+
def to_iso8601
|
95
|
+
@iso8601 ||= @time.iso8601(ISO8601_PRECISION)
|
96
|
+
end
|
97
|
+
alias_method :to_s, :to_iso8601
|
98
|
+
|
99
|
+
def -(value)
|
100
|
+
@time - (value.is_a?(Timestamp) ? value.time : value)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "logstash/namespace"
|
3
|
+
require "logstash/util"
|
4
|
+
require "thread_safe"
|
5
|
+
|
6
|
+
module LogStash::Util
|
7
|
+
|
8
|
+
# PathCache is a singleton which globally caches the relation between a field reference and its
|
9
|
+
# decomposition into a [key, path array] tuple. For example the field reference [foo][bar][baz]
|
10
|
+
# is decomposed into ["baz", ["foo", "bar"]].
|
11
|
+
module PathCache
|
12
|
+
extend self
|
13
|
+
|
14
|
+
# requiring libraries and defining constants is thread safe in JRuby so
|
15
|
+
# PathCache::CACHE will be corretly initialized, once, when accessors.rb
|
16
|
+
# will be first required
|
17
|
+
CACHE = ThreadSafe::Cache.new
|
18
|
+
|
19
|
+
def get(field_reference)
|
20
|
+
# the "get_or_default(x, nil) || put(x, parse(x))" is ~2x faster than "get || put" because the get call is
|
21
|
+
# proxied through the JRuby JavaProxy op_aref method. the correct idiom here would be to use
|
22
|
+
# "compute_if_absent(x){parse(x)}" but because of the closure creation, it is ~1.5x slower than
|
23
|
+
# "get_or_default || put".
|
24
|
+
# this "get_or_default || put" is obviously non-atomic which is not really important here
|
25
|
+
# since all threads will set the same value and this cache will stabilize very quickly after the first
|
26
|
+
# few events.
|
27
|
+
CACHE.get_or_default(field_reference, nil) || CACHE.put(field_reference, parse(field_reference))
|
28
|
+
end
|
29
|
+
|
30
|
+
def parse(field_reference)
|
31
|
+
path = field_reference.split(/[\[\]]/).select{|s| !s.empty?}
|
32
|
+
[path.pop, path]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Accessors uses a lookup table to speedup access of a field reference of the form
|
37
|
+
# "[hello][world]" to the underlying store hash into {"hello" => {"world" => "foo"}}
|
38
|
+
class Accessors
|
39
|
+
|
40
|
+
# @param store [Hash] the backing data store field refereces point to
|
41
|
+
def initialize(store)
|
42
|
+
@store = store
|
43
|
+
|
44
|
+
# @lut is a lookup table between a field reference and a [target, key] tuple
|
45
|
+
# where target is the containing Hash or Array for key in @store.
|
46
|
+
# this allows us to directly access the containing object for key instead of
|
47
|
+
# walking the field reference path into the inner @store objects
|
48
|
+
@lut = {}
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param field_reference [String] the field reference
|
52
|
+
# @return [Object] the value in @store for this field reference
|
53
|
+
def get(field_reference)
|
54
|
+
target, key = lookup(field_reference)
|
55
|
+
return nil unless target
|
56
|
+
target.is_a?(Array) ? target[key.to_i] : target[key]
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param field_reference [String] the field reference
|
60
|
+
# @param value [Object] the value to set in @store for this field reference
|
61
|
+
# @return [Object] the value set
|
62
|
+
def set(field_reference, value)
|
63
|
+
target, key = lookup_or_create(field_reference)
|
64
|
+
target[target.is_a?(Array) ? key.to_i : key] = value
|
65
|
+
end
|
66
|
+
|
67
|
+
# @param field_reference [String] the field reference to remove
|
68
|
+
# @return [Object] the removed value in @store for this field reference
|
69
|
+
def del(field_reference)
|
70
|
+
target, key = lookup(field_reference)
|
71
|
+
return nil unless target
|
72
|
+
target.is_a?(Array) ? target.delete_at(key.to_i) : target.delete(key)
|
73
|
+
end
|
74
|
+
|
75
|
+
# @param field_reference [String] the field reference to test for inclusion in the store
|
76
|
+
# @return [Boolean] true if the store contains a value for this field reference
|
77
|
+
def include?(field_reference)
|
78
|
+
target, key = lookup(field_reference)
|
79
|
+
return false unless target
|
80
|
+
|
81
|
+
target.is_a?(Array) ? !target[key.to_i].nil? : target.include?(key)
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
# retrieve the [target, key] tuple associated with this field reference
|
87
|
+
# @param field_reference [String] the field referece
|
88
|
+
# @return [[Object, String]] the [target, key] tuple associated with this field reference
|
89
|
+
def lookup(field_reference)
|
90
|
+
@lut[field_reference] ||= find_target(field_reference)
|
91
|
+
end
|
92
|
+
|
93
|
+
# retrieve the [target, key] tuple associated with this field reference and create inner
|
94
|
+
# container objects if they do not exists
|
95
|
+
# @param field_reference [String] the field referece
|
96
|
+
# @return [[Object, String]] the [target, key] tuple associated with this field reference
|
97
|
+
def lookup_or_create(field_reference)
|
98
|
+
@lut[field_reference] ||= find_or_create_target(field_reference)
|
99
|
+
end
|
100
|
+
|
101
|
+
# find the target container object in store for this field reference
|
102
|
+
# @param field_reference [String] the field referece
|
103
|
+
# @return [Object] the target container object in store associated with this field reference
|
104
|
+
def find_target(field_reference)
|
105
|
+
key, path = PathCache.get(field_reference)
|
106
|
+
target = path.inject(@store) do |r, k|
|
107
|
+
return nil unless r
|
108
|
+
r[r.is_a?(Array) ? k.to_i : k]
|
109
|
+
end
|
110
|
+
target ? [target, key] : nil
|
111
|
+
end
|
112
|
+
|
113
|
+
# find the target container object in store for this field reference and create inner
|
114
|
+
# container objects if they do not exists
|
115
|
+
# @param field_reference [String] the field referece
|
116
|
+
# @return [Object] the target container object in store associated with this field reference
|
117
|
+
def find_or_create_target(accessor)
|
118
|
+
key, path = PathCache.get(accessor)
|
119
|
+
target = path.inject(@store) {|r, k| r[r.is_a?(Array) ? k.to_i : k] ||= {}}
|
120
|
+
[target, key]
|
121
|
+
end
|
122
|
+
end # class Accessors
|
123
|
+
end # module LogStash::Util
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'logstash-core-event/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.authors = ["Elastic"]
|
8
|
+
gem.email = ["info@elastic.co"]
|
9
|
+
gem.description = %q{The core event component of logstash, the scalable log and event management tool}
|
10
|
+
gem.summary = %q{logstash-core-event - The core event component of logstash}
|
11
|
+
gem.homepage = "http://www.elastic.co/guide/en/logstash/current/index.html"
|
12
|
+
gem.license = "Apache License (2.0)"
|
13
|
+
|
14
|
+
gem.files = Dir.glob(["logstash-core-event.gemspec", "lib/**/*.rb", "spec/**/*.rb"])
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.name = "logstash-core-event"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = LOGSTASH_CORE_EVENT_VERSION
|
19
|
+
|
20
|
+
if RUBY_PLATFORM == 'java'
|
21
|
+
gem.platform = RUBY_PLATFORM
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,534 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
require "spec_helper"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
describe LogStash::Event do
|
6
|
+
|
7
|
+
shared_examples "all event tests" do
|
8
|
+
context "[]=" do
|
9
|
+
it "should raise an exception if you attempt to set @timestamp to a value type other than a Time object" do
|
10
|
+
expect{subject["@timestamp"] = "crash!"}.to raise_error(TypeError)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should assign simple fields" do
|
14
|
+
expect(subject["foo"]).to be_nil
|
15
|
+
expect(subject["foo"] = "bar").to eq("bar")
|
16
|
+
expect(subject["foo"]).to eq("bar")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should overwrite simple fields" do
|
20
|
+
expect(subject["foo"]).to be_nil
|
21
|
+
expect(subject["foo"] = "bar").to eq("bar")
|
22
|
+
expect(subject["foo"]).to eq("bar")
|
23
|
+
|
24
|
+
expect(subject["foo"] = "baz").to eq("baz")
|
25
|
+
expect(subject["foo"]).to eq("baz")
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should assign deep fields" do
|
29
|
+
expect(subject["[foo][bar]"]).to be_nil
|
30
|
+
expect(subject["[foo][bar]"] = "baz").to eq("baz")
|
31
|
+
expect(subject["[foo][bar]"]).to eq("baz")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should overwrite deep fields" do
|
35
|
+
expect(subject["[foo][bar]"]).to be_nil
|
36
|
+
expect(subject["[foo][bar]"] = "baz").to eq("baz")
|
37
|
+
expect(subject["[foo][bar]"]).to eq("baz")
|
38
|
+
|
39
|
+
expect(subject["[foo][bar]"] = "zab").to eq("zab")
|
40
|
+
expect(subject["[foo][bar]"]).to eq("zab")
|
41
|
+
end
|
42
|
+
|
43
|
+
it "allow to set the @metadata key to a hash" do
|
44
|
+
subject["@metadata"] = { "action" => "index" }
|
45
|
+
expect(subject["[@metadata][action]"]).to eq("index")
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should add key when setting nil value" do
|
49
|
+
subject["[baz]"] = nil
|
50
|
+
expect(subject.to_hash).to include("baz" => nil)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context "#sprintf" do
|
55
|
+
it "should report a unix timestamp for %{+%s}" do
|
56
|
+
expect(subject.sprintf("%{+%s}")).to eq("1356998400")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should work if there is no fieldref in the string" do
|
60
|
+
expect(subject.sprintf("bonjour")).to eq("bonjour")
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should raise error when formatting %{+%s} when @timestamp field is missing" do
|
64
|
+
str = "hello-%{+%s}"
|
65
|
+
subj = subject.clone
|
66
|
+
subj.remove("[@timestamp]")
|
67
|
+
expect{ subj.sprintf(str) }.to raise_error(LogStash::Error)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should report a time with %{+format} syntax", :if => RUBY_ENGINE == "jruby" do
|
71
|
+
expect(subject.sprintf("%{+YYYY}")).to eq("2013")
|
72
|
+
expect(subject.sprintf("%{+MM}")).to eq("01")
|
73
|
+
expect(subject.sprintf("%{+HH}")).to eq("00")
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should support mixed string" do
|
77
|
+
expect(subject.sprintf("foo %{+YYYY-MM-dd} %{type}")).to eq("foo 2013-01-01 sprintf")
|
78
|
+
end
|
79
|
+
|
80
|
+
it "should raise error with %{+format} syntax when @timestamp field is missing", :if => RUBY_ENGINE == "jruby" do
|
81
|
+
str = "logstash-%{+YYYY}"
|
82
|
+
subj = subject.clone
|
83
|
+
subj.remove("[@timestamp]")
|
84
|
+
expect{ subj.sprintf(str) }.to raise_error(LogStash::Error)
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should report fields with %{field} syntax" do
|
88
|
+
expect(subject.sprintf("%{type}")).to eq("sprintf")
|
89
|
+
expect(subject.sprintf("%{message}")).to eq(subject["message"])
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should print deep fields" do
|
93
|
+
expect(subject.sprintf("%{[j][k1]}")).to eq("v")
|
94
|
+
expect(subject.sprintf("%{[j][k2][0]}")).to eq("w")
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should be able to take a non-string for the format" do
|
98
|
+
expect(subject.sprintf(2)).to eq("2")
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should allow to use the metadata when calling #sprintf" do
|
102
|
+
expect(subject.sprintf("super-%{[@metadata][fancy]}")).to eq("super-pants")
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should allow to use nested hash from the metadata field" do
|
106
|
+
expect(subject.sprintf("%{[@metadata][have-to-go][deeper]}")).to eq("inception")
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should return a json string if the key is a hash" do
|
110
|
+
expect(subject.sprintf("%{[j][k3]}")).to eq("{\"4\":\"m\"}")
|
111
|
+
end
|
112
|
+
|
113
|
+
it "should not strip last character" do
|
114
|
+
expect(subject.sprintf("%{type}%{message}|")).to eq("sprintfhello world|")
|
115
|
+
end
|
116
|
+
|
117
|
+
context "#encoding" do
|
118
|
+
it "should return known patterns as UTF-8" do
|
119
|
+
expect(subject.sprintf("%{message}").encoding).to eq(Encoding::UTF_8)
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should return unknown patterns as UTF-8" do
|
123
|
+
expect(subject.sprintf("%{unkown_pattern}").encoding).to eq(Encoding::UTF_8)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
context "#[]" do
|
129
|
+
it "should fetch data" do
|
130
|
+
expect(subject["type"]).to eq("sprintf")
|
131
|
+
end
|
132
|
+
it "should fetch fields" do
|
133
|
+
expect(subject["a"]).to eq("b")
|
134
|
+
expect(subject['c']['d']).to eq("f")
|
135
|
+
end
|
136
|
+
it "should fetch deep fields" do
|
137
|
+
expect(subject["[j][k1]"]).to eq("v")
|
138
|
+
expect(subject["[c][d]"]).to eq("f")
|
139
|
+
expect(subject['[f][g][h]']).to eq("i")
|
140
|
+
expect(subject['[j][k3][4]']).to eq("m")
|
141
|
+
expect(subject['[j][5]']).to eq(7)
|
142
|
+
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should be fast?", :performance => true do
|
146
|
+
count = 1000000
|
147
|
+
2.times do
|
148
|
+
start = Time.now
|
149
|
+
count.times { subject["[j][k1]"] }
|
150
|
+
duration = Time.now - start
|
151
|
+
puts "event #[] rate: #{"%02.0f/sec" % (count / duration)}, elapsed: #{duration}s"
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
context "#include?" do
|
157
|
+
it "should include existing fields" do
|
158
|
+
expect(subject.include?("c")).to eq(true)
|
159
|
+
expect(subject.include?("[c][d]")).to eq(true)
|
160
|
+
expect(subject.include?("[j][k4][0][nested]")).to eq(true)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "should include field with nil value" do
|
164
|
+
expect(subject.include?("nilfield")).to eq(true)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should include @metadata field" do
|
168
|
+
expect(subject.include?("@metadata")).to eq(true)
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should include field within @metadata" do
|
172
|
+
expect(subject.include?("[@metadata][fancy]")).to eq(true)
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should not include non-existing fields" do
|
176
|
+
expect(subject.include?("doesnotexist")).to eq(false)
|
177
|
+
expect(subject.include?("[j][doesnotexist]")).to eq(false)
|
178
|
+
expect(subject.include?("[tag][0][hello][yes]")).to eq(false)
|
179
|
+
end
|
180
|
+
|
181
|
+
it "should include within arrays" do
|
182
|
+
expect(subject.include?("[tags][0]")).to eq(true)
|
183
|
+
expect(subject.include?("[tags][1]")).to eq(false)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
context "#overwrite" do
|
188
|
+
it "should swap data with new content" do
|
189
|
+
new_event = LogStash::Event.new(
|
190
|
+
"type" => "new",
|
191
|
+
"message" => "foo bar",
|
192
|
+
)
|
193
|
+
subject.overwrite(new_event)
|
194
|
+
|
195
|
+
expect(subject["message"]).to eq("foo bar")
|
196
|
+
expect(subject["type"]).to eq("new")
|
197
|
+
|
198
|
+
["tags", "source", "a", "c", "f", "j"].each do |field|
|
199
|
+
expect(subject[field]).to be_nil
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context "#append" do
|
205
|
+
it "should append strings to an array" do
|
206
|
+
subject.append(LogStash::Event.new("message" => "another thing"))
|
207
|
+
expect(subject["message"]).to eq([ "hello world", "another thing" ])
|
208
|
+
end
|
209
|
+
|
210
|
+
it "should concatenate tags" do
|
211
|
+
subject.append(LogStash::Event.new("tags" => [ "tag2" ]))
|
212
|
+
# added to_a for when array is a Java Collection when produced from json input
|
213
|
+
# TODO: we have to find a better way to handle this in tests. maybe override
|
214
|
+
# rspec eq or == to do an explicit to_a when comparing arrays?
|
215
|
+
expect(subject["tags"].to_a).to eq([ "tag1", "tag2" ])
|
216
|
+
end
|
217
|
+
|
218
|
+
context "when event field is nil" do
|
219
|
+
it "should add single value as string" do
|
220
|
+
subject.append(LogStash::Event.new({"field1" => "append1"}))
|
221
|
+
expect(subject[ "field1" ]).to eq("append1")
|
222
|
+
end
|
223
|
+
it "should add multi values as array" do
|
224
|
+
subject.append(LogStash::Event.new({"field1" => [ "append1","append2" ]}))
|
225
|
+
expect(subject[ "field1" ]).to eq([ "append1","append2" ])
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
context "when event field is a string" do
|
230
|
+
before { subject[ "field1" ] = "original1" }
|
231
|
+
|
232
|
+
it "should append string to values, if different from current" do
|
233
|
+
subject.append(LogStash::Event.new({"field1" => "append1"}))
|
234
|
+
expect(subject[ "field1" ]).to eq([ "original1", "append1" ])
|
235
|
+
end
|
236
|
+
it "should not change value, if appended value is equal current" do
|
237
|
+
subject.append(LogStash::Event.new({"field1" => "original1"}))
|
238
|
+
expect(subject[ "field1" ]).to eq("original1")
|
239
|
+
end
|
240
|
+
it "should concatenate values in an array" do
|
241
|
+
subject.append(LogStash::Event.new({"field1" => [ "append1" ]}))
|
242
|
+
expect(subject[ "field1" ]).to eq([ "original1", "append1" ])
|
243
|
+
end
|
244
|
+
it "should join array, removing duplicates" do
|
245
|
+
subject.append(LogStash::Event.new({"field1" => [ "append1","original1" ]}))
|
246
|
+
expect(subject[ "field1" ]).to eq([ "original1", "append1" ])
|
247
|
+
end
|
248
|
+
end
|
249
|
+
context "when event field is an array" do
|
250
|
+
before { subject[ "field1" ] = [ "original1", "original2" ] }
|
251
|
+
|
252
|
+
it "should append string values to array, if not present in array" do
|
253
|
+
subject.append(LogStash::Event.new({"field1" => "append1"}))
|
254
|
+
expect(subject[ "field1" ]).to eq([ "original1", "original2", "append1" ])
|
255
|
+
end
|
256
|
+
it "should not append string values, if the array already contains it" do
|
257
|
+
subject.append(LogStash::Event.new({"field1" => "original1"}))
|
258
|
+
expect(subject[ "field1" ]).to eq([ "original1", "original2" ])
|
259
|
+
end
|
260
|
+
it "should join array, removing duplicates" do
|
261
|
+
subject.append(LogStash::Event.new({"field1" => [ "append1","original1" ]}))
|
262
|
+
expect(subject[ "field1" ]).to eq([ "original1", "original2", "append1" ])
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
end
|
267
|
+
|
268
|
+
it "timestamp parsing speed", :performance => true do
|
269
|
+
warmup = 10000
|
270
|
+
count = 1000000
|
271
|
+
|
272
|
+
data = { "@timestamp" => "2013-12-21T07:25:06.605Z" }
|
273
|
+
event = LogStash::Event.new(data)
|
274
|
+
expect(event["@timestamp"]).to be_a(LogStash::Timestamp)
|
275
|
+
|
276
|
+
duration = 0
|
277
|
+
[warmup, count].each do |i|
|
278
|
+
start = Time.now
|
279
|
+
i.times do
|
280
|
+
data = { "@timestamp" => "2013-12-21T07:25:06.605Z" }
|
281
|
+
LogStash::Event.new(data.clone)
|
282
|
+
end
|
283
|
+
duration = Time.now - start
|
284
|
+
end
|
285
|
+
puts "event @timestamp parse rate: #{"%02.0f/sec" % (count / duration)}, elapsed: #{duration}s"
|
286
|
+
end
|
287
|
+
|
288
|
+
context "acceptable @timestamp formats" do
|
289
|
+
subject { LogStash::Event.new }
|
290
|
+
|
291
|
+
formats = [
|
292
|
+
"YYYY-MM-dd'T'HH:mm:ss.SSSZ",
|
293
|
+
"YYYY-MM-dd'T'HH:mm:ss.SSSSSSZ",
|
294
|
+
"YYYY-MM-dd'T'HH:mm:ss.SSS",
|
295
|
+
"YYYY-MM-dd'T'HH:mm:ss",
|
296
|
+
"YYYY-MM-dd'T'HH:mm:ssZ",
|
297
|
+
]
|
298
|
+
formats.each do |format|
|
299
|
+
it "includes #{format}" do
|
300
|
+
time = subject.sprintf("%{+#{format}}")
|
301
|
+
begin
|
302
|
+
LogStash::Event.new("@timestamp" => time)
|
303
|
+
rescue => e
|
304
|
+
raise StandardError, "Time '#{time}' was rejected. #{e.class}: #{e.to_s}"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
context "from LOGSTASH-1738" do
|
310
|
+
it "does not error" do
|
311
|
+
LogStash::Event.new("@timestamp" => "2013-12-29T23:12:52.371240+02:00")
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context "from LOGSTASH-1732" do
|
316
|
+
it "does not error" do
|
317
|
+
LogStash::Event.new("@timestamp" => "2013-12-27T11:07:25+00:00")
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
context "timestamp initialization" do
|
323
|
+
let(:logger_mock) { double("logger") }
|
324
|
+
|
325
|
+
after(:each) do
|
326
|
+
LogStash::Event.logger = LogStash::Event::DEFAULT_LOGGER
|
327
|
+
end
|
328
|
+
|
329
|
+
it "should coerce timestamp" do
|
330
|
+
t = Time.iso8601("2014-06-12T00:12:17.114Z")
|
331
|
+
expect(LogStash::Event.new("@timestamp" => t).timestamp.to_i).to eq(t.to_i)
|
332
|
+
expect(LogStash::Event.new("@timestamp" => LogStash::Timestamp.new(t)).timestamp.to_i).to eq(t.to_i)
|
333
|
+
expect(LogStash::Event.new("@timestamp" => "2014-06-12T00:12:17.114Z").timestamp.to_i).to eq(t.to_i)
|
334
|
+
end
|
335
|
+
|
336
|
+
it "should assign current time when no timestamp" do
|
337
|
+
expect(LogStash::Event.new({}).timestamp.to_i).to be_within(1).of (Time.now.to_i)
|
338
|
+
end
|
339
|
+
|
340
|
+
it "should tag for invalid value" do
|
341
|
+
event = LogStash::Event.new("@timestamp" => "foo")
|
342
|
+
expect(event.timestamp.to_i).to be_within(1).of Time.now.to_i
|
343
|
+
expect(event["tags"]).to eq([LogStash::Event::TIMESTAMP_FAILURE_TAG])
|
344
|
+
expect(event[LogStash::Event::TIMESTAMP_FAILURE_FIELD]).to eq("foo")
|
345
|
+
|
346
|
+
event = LogStash::Event.new("@timestamp" => 666)
|
347
|
+
expect(event.timestamp.to_i).to be_within(1).of Time.now.to_i
|
348
|
+
expect(event["tags"]).to eq([LogStash::Event::TIMESTAMP_FAILURE_TAG])
|
349
|
+
expect(event[LogStash::Event::TIMESTAMP_FAILURE_FIELD]).to eq(666)
|
350
|
+
end
|
351
|
+
|
352
|
+
it "should warn for invalid value" do
|
353
|
+
LogStash::Event.logger = logger_mock
|
354
|
+
|
355
|
+
expect(logger_mock).to receive(:warn).twice
|
356
|
+
|
357
|
+
LogStash::Event.new("@timestamp" => :foo)
|
358
|
+
LogStash::Event.new("@timestamp" => 666)
|
359
|
+
end
|
360
|
+
|
361
|
+
it "should tag for invalid string format" do
|
362
|
+
event = LogStash::Event.new("@timestamp" => "foo")
|
363
|
+
expect(event.timestamp.to_i).to be_within(1).of Time.now.to_i
|
364
|
+
expect(event["tags"]).to eq([LogStash::Event::TIMESTAMP_FAILURE_TAG])
|
365
|
+
expect(event[LogStash::Event::TIMESTAMP_FAILURE_FIELD]).to eq("foo")
|
366
|
+
end
|
367
|
+
|
368
|
+
it "should warn for invalid string format" do
|
369
|
+
LogStash::Event.logger = logger_mock
|
370
|
+
|
371
|
+
expect(logger_mock).to receive(:warn)
|
372
|
+
LogStash::Event.new("@timestamp" => "foo")
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
context "to_json" do
|
377
|
+
it "should support to_json" do
|
378
|
+
new_event = LogStash::Event.new(
|
379
|
+
"@timestamp" => Time.iso8601("2014-09-23T19:26:15.832Z"),
|
380
|
+
"message" => "foo bar",
|
381
|
+
)
|
382
|
+
json = new_event.to_json
|
383
|
+
|
384
|
+
expect(JSON.parse(json)).to eq( JSON.parse("{\"@timestamp\":\"2014-09-23T19:26:15.832Z\",\"message\":\"foo bar\",\"@version\":\"1\"}"))
|
385
|
+
end
|
386
|
+
|
387
|
+
it "should support to_json and ignore arguments" do
|
388
|
+
new_event = LogStash::Event.new(
|
389
|
+
"@timestamp" => Time.iso8601("2014-09-23T19:26:15.832Z"),
|
390
|
+
"message" => "foo bar",
|
391
|
+
)
|
392
|
+
json = new_event.to_json(:foo => 1, :bar => "baz")
|
393
|
+
|
394
|
+
expect(JSON.parse(json)).to eq( JSON.parse("{\"@timestamp\":\"2014-09-23T19:26:15.832Z\",\"message\":\"foo bar\",\"@version\":\"1\"}"))
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
context "metadata" do
|
399
|
+
context "with existing metadata" do
|
400
|
+
subject { LogStash::Event.new("hello" => "world", "@metadata" => { "fancy" => "pants" }) }
|
401
|
+
|
402
|
+
it "should not include metadata in to_hash" do
|
403
|
+
expect(subject.to_hash.keys).not_to include("@metadata")
|
404
|
+
|
405
|
+
# 'hello', '@timestamp', and '@version'
|
406
|
+
expect(subject.to_hash.keys.count).to eq(3)
|
407
|
+
end
|
408
|
+
|
409
|
+
it "should still allow normal field access" do
|
410
|
+
expect(subject["hello"]).to eq("world")
|
411
|
+
end
|
412
|
+
end
|
413
|
+
|
414
|
+
context "with set metadata" do
|
415
|
+
let(:fieldref) { "[@metadata][foo][bar]" }
|
416
|
+
let(:value) { "bar" }
|
417
|
+
subject { LogStash::Event.new("normal" => "normal") }
|
418
|
+
before do
|
419
|
+
# Verify the test is configured correctly.
|
420
|
+
expect(fieldref).to start_with("[@metadata]")
|
421
|
+
|
422
|
+
# Set it.
|
423
|
+
subject[fieldref] = value
|
424
|
+
end
|
425
|
+
|
426
|
+
it "should still allow normal field access" do
|
427
|
+
expect(subject["normal"]).to eq("normal")
|
428
|
+
end
|
429
|
+
|
430
|
+
it "should allow getting" do
|
431
|
+
expect(subject[fieldref]).to eq(value)
|
432
|
+
end
|
433
|
+
|
434
|
+
it "should be hidden from .to_json" do
|
435
|
+
require "json"
|
436
|
+
obj = JSON.parse(subject.to_json)
|
437
|
+
expect(obj).not_to include("@metadata")
|
438
|
+
end
|
439
|
+
|
440
|
+
it "should be hidden from .to_hash" do
|
441
|
+
expect(subject.to_hash).not_to include("@metadata")
|
442
|
+
end
|
443
|
+
|
444
|
+
it "should be accessible through #to_hash_with_metadata" do
|
445
|
+
obj = subject.to_hash_with_metadata
|
446
|
+
expect(obj).to include("@metadata")
|
447
|
+
expect(obj["@metadata"]["foo"]["bar"]).to eq(value)
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
context "with no metadata" do
|
452
|
+
subject { LogStash::Event.new("foo" => "bar") }
|
453
|
+
it "should have no metadata" do
|
454
|
+
expect(subject["@metadata"]).to be_empty
|
455
|
+
end
|
456
|
+
it "should still allow normal field access" do
|
457
|
+
expect(subject["foo"]).to eq("bar")
|
458
|
+
end
|
459
|
+
|
460
|
+
it "should not include the @metadata key" do
|
461
|
+
expect(subject.to_hash_with_metadata).not_to include("@metadata")
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
context "signal events" do
|
467
|
+
it "should define the shutdown event" do
|
468
|
+
# the SHUTDOWN and FLUSH constants are part of the plugin API contract
|
469
|
+
# if they are changed, all plugins must be updated
|
470
|
+
expect(LogStash::SHUTDOWN).to be_a(LogStash::ShutdownEvent)
|
471
|
+
expect(LogStash::FLUSH).to be_a(LogStash::FlushEvent)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
|
476
|
+
let(:event_hash) do
|
477
|
+
{
|
478
|
+
"@timestamp" => "2013-01-01T00:00:00.000Z",
|
479
|
+
"type" => "sprintf",
|
480
|
+
"message" => "hello world",
|
481
|
+
"tags" => [ "tag1" ],
|
482
|
+
"source" => "/home/foo",
|
483
|
+
"a" => "b",
|
484
|
+
"c" => {
|
485
|
+
"d" => "f",
|
486
|
+
"e" => {"f" => "g"}
|
487
|
+
},
|
488
|
+
"f" => { "g" => { "h" => "i" } },
|
489
|
+
"j" => {
|
490
|
+
"k1" => "v",
|
491
|
+
"k2" => [ "w", "x" ],
|
492
|
+
"k3" => {"4" => "m"},
|
493
|
+
"k4" => [ {"nested" => "cool"} ],
|
494
|
+
5 => 6,
|
495
|
+
"5" => 7
|
496
|
+
},
|
497
|
+
"nilfield" => nil,
|
498
|
+
"@metadata" => { "fancy" => "pants", "have-to-go" => { "deeper" => "inception" } }
|
499
|
+
}
|
500
|
+
end
|
501
|
+
|
502
|
+
describe "using normal hash input" do
|
503
|
+
it_behaves_like "all event tests" do
|
504
|
+
subject{LogStash::Event.new(event_hash)}
|
505
|
+
end
|
506
|
+
end
|
507
|
+
|
508
|
+
describe "using hash input from deserialized json" do
|
509
|
+
# this is to test the case when JrJackson deserialises Json and produces
|
510
|
+
# native Java Collections objects for efficiency
|
511
|
+
it_behaves_like "all event tests" do
|
512
|
+
subject{LogStash::Event.new(LogStash::Json.load(LogStash::Json.dump(event_hash)))}
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
|
517
|
+
describe "#to_s" do
|
518
|
+
let(:timestamp) { LogStash::Timestamp.new }
|
519
|
+
let(:event1) { LogStash::Event.new({ "@timestamp" => timestamp, "host" => "foo", "message" => "bar"}) }
|
520
|
+
let(:event2) { LogStash::Event.new({ "host" => "bar", "message" => "foo"}) }
|
521
|
+
|
522
|
+
it "should cache only one template" do
|
523
|
+
LogStash::StringInterpolation.clear_cache
|
524
|
+
expect {
|
525
|
+
event1.to_s
|
526
|
+
event2.to_s
|
527
|
+
}.to change { LogStash::StringInterpolation.cache_size }.by(1)
|
528
|
+
end
|
529
|
+
|
530
|
+
it "return the string containing the timestamp, the host and the message" do
|
531
|
+
expect(event1.to_s).to eq("#{timestamp.to_iso8601} #{event1["host"]} #{event1["message"]}")
|
532
|
+
end
|
533
|
+
end
|
534
|
+
end
|