logstash-output-zabbix 0.1.4 → 1.0.0

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.
@@ -54,13 +54,28 @@ class LogStash::Outputs::Zabbix < LogStash::Outputs::Base
54
54
  # the @metadata field.
55
55
  config :zabbix_host, :validate => :string, :required => true
56
56
 
57
- # The field name which holds the Zabbix key. This can be a sub-field of
58
- # the @metadata field.
59
- config :zabbix_key, :validate => :string, :required => true
57
+ # A single field name which holds the value you intend to use as the Zabbix
58
+ # item key. This can be a sub-field of the @metadata field.
59
+ # This directive will be ignored if using `multi_value`
60
+ config :zabbix_key, :validate => :string
60
61
 
61
62
  # The field name which holds the value you want to send.
63
+ # This directive will be ignored if using `multi_value`
62
64
  config :zabbix_value, :validate => :string, :default => "message"
63
65
 
66
+ # Use the `multi_value` directive to send multiple key/value pairs.
67
+ # This can be thought of as an array, like:
68
+ #
69
+ # `[ zabbix_key1, zabbix_value1, zabbix_key2, zabbix_value2, ... zabbix_keyN, zabbix_valueN ]`
70
+ #
71
+ # ...where `zabbix_key1` is an instance of `zabbix_key`, and `zabbix_value1`
72
+ # is an instance of `zabbix_value`. If the field referenced by any
73
+ # `zabbix_key` or `zabbix_value` does not exist, that entry will be ignored.
74
+ #
75
+ # This directive cannot be used in conjunction with the single-value directives
76
+ # `zabbix_key` and `zabbix_value`.
77
+ config :multi_value, :validate => :array
78
+
64
79
  # The number of seconds to wait before giving up on a connection to the Zabbix
65
80
  # server. This number should be very small, otherwise delays in delivery of
66
81
  # other outputs could result.
@@ -68,31 +83,71 @@ class LogStash::Outputs::Zabbix < LogStash::Outputs::Base
68
83
 
69
84
  public
70
85
  def register
86
+ if !@zabbix_key.nil? && !@multi_value.nil?
87
+ @logger.warn("Cannot use multi_value in conjunction with zabbix_key/zabbix_value. Ignoring zabbix_key.")
88
+ end
89
+
90
+ # We're only going to use @multi_value in the end, so let's build it from
91
+ # @zabbix_key and @zabbix_value if it is empty (single value configuration).
92
+ if @multi_value.nil?
93
+ @multi_value = [ @zabbix_key, @zabbix_value ]
94
+ end
95
+ if @multi_value.length % 2 == 1
96
+ raise LogStash::ConfigurationError, I18n.t("logstash.agent.configuration.invalid_plugin_register",
97
+ :plugin => "output", :type => "zabbix",
98
+ :error => "Invalid zabbix configuration #{@multi_value}. multi_value requires an even number of elements as ['zabbix_key1', 'zabbix_value1', 'zabbix_key2', 'zabbix_value2']")
99
+ end
71
100
  end # def register
72
101
 
73
102
  public
74
103
  def field_check(event, fieldname)
75
104
  if !event[fieldname]
76
- @logger.warn("Skipping zabbix output; field referenced by #{fieldname} is missing")
105
+ @logger.warn("Field referenced by #{fieldname} is missing")
77
106
  false
78
107
  else
79
108
  true
80
109
  end
81
110
  end
82
111
 
112
+ public
113
+ def kv_check(event, key_field, value_field)
114
+ errors = 0
115
+ for field in [key_field, value_field]
116
+ errors += 1 unless field_check(event, field)
117
+ end
118
+ errors < 1 ? true : false
119
+ end # kv_check
120
+
121
+ public
122
+ def validate_fields(event)
123
+ found = []
124
+ (0..@multi_value.length-1).step(2) do |idx|
125
+ if kv_check(event, @multi_value[idx], @multi_value[idx+1])
126
+ found << @multi_value[idx]
127
+ found << @multi_value[idx+1]
128
+ end
129
+ end
130
+ found
131
+ end # validate_fields
132
+
83
133
  public
84
134
  def format_request(event)
85
135
  # The nested `clock` value is the event timestamp
86
136
  # The ending `clock` value is "now" so Zabbix knows it's not receiving stale
87
137
  # data.
138
+ validated = validate_fields(event)
139
+ data = []
140
+ (0..validated.length-1).step(2) do |idx|
141
+ data << {
142
+ "host" => event[@zabbix_host],
143
+ "key" => event[validated[idx]],
144
+ "value" => event[validated[idx+1]],
145
+ "clock" => event["@timestamp"].to_i
146
+ }
147
+ end
88
148
  {
89
149
  "request" => "sender data",
90
- "data" => [{
91
- "host" => event[@zabbix_host],
92
- "key" => event[@zabbix_key],
93
- "value" => event[@zabbix_value].to_s,
94
- "clock" => event["@timestamp"].to_i
95
- }],
150
+ "data" => data,
96
151
  "clock" => Time.now.to_i,
97
152
  }
98
153
  end
@@ -125,16 +180,12 @@ class LogStash::Outputs::Zabbix < LogStash::Outputs::Base
125
180
  total = info[5].to_i
126
181
  if failed == total
127
182
  @logger.warn("Zabbix server at #{@zabbix_server_host} rejected all items sent.",
128
- :zabbix_host => event[@zabbix_host],
129
- :zabbix_key => event[@zabbix_key],
130
- :zabbix_value => event[@zabbix_value]
183
+ :zabbix_host => event[@zabbix_host]
131
184
  )
132
185
  false
133
186
  elsif failed > 0
134
187
  @logger.warn("Zabbix server at #{@zabbix_server_host} rejected #{info[3]} item(s).",
135
- :zabbix_host => event[@zabbix_host],
136
- :zabbix_key => event[@zabbix_key],
137
- :zabbix_value => event[@zabbix_value]
188
+ :zabbix_host => event[@zabbix_host]
138
189
  )
139
190
  false
140
191
  elsif failed == 0 && total > 0
@@ -184,9 +235,7 @@ class LogStash::Outputs::Zabbix < LogStash::Outputs::Base
184
235
  public
185
236
  def receive(event)
186
237
  return unless output?(event)
187
- for field in [@zabbix_host, @zabbix_key, @zabbix_value]
188
- return unless field_check(event, field)
189
- end
238
+ return unless field_check(event, @zabbix_host)
190
239
  send_to_zabbix(event)
191
240
  end # def event
192
241
 
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'logstash-output-zabbix'
3
- s.version = "0.1.4"
3
+ s.version = "1.0.0"
4
4
  s.licenses = ["Apache License (2.0)"]
5
5
  s.summary = "This output sends key/value pairs to a Zabbix server."
6
6
  s.description = "This gem is a logstash plugin required to be installed on top of the Logstash core pipeline using $LS_HOME/bin/plugin install gemname. This gem is not a stand-alone program"
@@ -23,5 +23,5 @@ Gem::Specification.new do |s|
23
23
  s.add_runtime_dependency "logstash-codec-plain"
24
24
  s.add_development_dependency "logstash-devutils", ">= 0.0.12"
25
25
  s.add_development_dependency "logstash-filter-mutate"
26
- s.add_development_dependency "docker-api"
26
+ s.add_development_dependency "longshoreman"
27
27
  end
@@ -0,0 +1,69 @@
1
+ module ZabbixHelper
2
+
3
+ # This is used to ensure the (Docker) Zabbix port is up and running
4
+ def port_open?(ip, port, seconds=1)
5
+ Timeout::timeout(seconds) do
6
+ begin
7
+ TCPSocket.new(ip, port).close
8
+ true
9
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
10
+ false
11
+ end
12
+ end
13
+ rescue Timeout::Error
14
+ false
15
+ end
16
+
17
+ def port_responding?(ip, port)
18
+ # Try for up to 10 seconds to get a response
19
+ 10.times do
20
+ if port_open?(ip, port)
21
+ return true
22
+ else
23
+ sleep 1
24
+ end
25
+ end
26
+ false
27
+ end
28
+
29
+ def zabbix_server_up?(zabbix_ip, port)
30
+ data = {
31
+ "request" => "sender data",
32
+ "data" => [{
33
+ "host" => "zabbix.example.com",
34
+ "key" => "zabbix.key",
35
+ "value" => "This is a log entry.",
36
+ "clock" => 1429100000,
37
+ }],
38
+ "clock" => Time.now.to_i,
39
+ }
40
+ if port_responding?(zabbix_ip, port)
41
+ ###
42
+ ### This is a hacky way to guarantee that Zabbix is responsive, because the
43
+ ### port check alone is insufficient. TCP tests say the port is open, but
44
+ ### it can take another 2 to 5 seconds (depending on the machine) before it
45
+ ### is responding in the way we need for these tests.
46
+ ###
47
+ resp = ""
48
+ # Try for up to 10 seconds to get a response.
49
+ 10.times do
50
+ TCPSocket.open(zabbix_ip, port) do |sock|
51
+ sock.print ZabbixProtocol.dump(data)
52
+ resp = sock.read
53
+ end
54
+ if resp.length == 0
55
+ sleep 1
56
+ else
57
+ return true
58
+ end
59
+ end
60
+ if resp.length == 0
61
+ puts "Zabbix server or db is unreachable"
62
+ end
63
+ else
64
+ puts "Unable to reach Zabbix server on #{zabbix_ip}:#{port}"
65
+ end
66
+ false
67
+ end
68
+
69
+ end
@@ -5,97 +5,21 @@ require "logstash/event"
5
5
  require "zabbix_protocol"
6
6
  require "socket"
7
7
  require "timeout"
8
- require "docker"
8
+ require "longshoreman"
9
+ require_relative "../helpers/zabbix_helper"
9
10
 
10
-
11
- CONTAINER_NAME = "zabbix_container_" + rand(99).to_s
12
- IMAGE = "untergeek/logstash_output_zabbix_rspec"
13
- TAG = "zabbix_v2.2.2"
14
-
15
- # This is used to ensure the Docker Zabbix port is up and running
16
- def port_open?(ip, port, seconds=1)
17
- Timeout::timeout(seconds) do
18
- begin
19
- TCPSocket.new(ip, port).close
20
- true
21
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH
22
- false
23
- end
24
- end
25
- rescue Timeout::Error
26
- false
27
- end
28
-
29
- def get_docker_ip
30
- # Let the crazy one-liner definition begin:
31
- # Docker.url.split(':')[1][2..-1]
32
- # Docker.url = tcp://192.168.123.205:2375
33
- # split(':') = ["tcp", "//192.168.123.205", "2375"]
34
- # [1] = "//192.168.123.205"
35
- # [2..-1] = "192.168.123.205"
36
- # This last bit prunes the leading //
37
- url = Docker.url
38
- ip = "127.0.0.1"
39
- case url.split(':')[0]
40
- when 'unix'
41
- ip = "127.0.0.1"
42
- when 'tcp'
43
- ip = url.split(':')[1][2..-1]
44
- end
45
- ip
11
+ RSpec.configure do |c|
12
+ c.include ZabbixHelper
46
13
  end
47
14
 
48
- def port_responding?(ip, port)
49
- # Try for up to 10 seconds to get a response
50
- 10.times do
51
- if port_open?(ip, port)
52
- return true
53
- else
54
- sleep 1
55
- end
56
- end
57
- false
58
- end
59
-
60
- def zabbix_server_up?
61
- zabbix_ip = get_docker_ip
62
- port = 10051
63
- test_cfg = { "zabbix_server_host" => zabbix_ip, "zabbix_host" => "zabhost", "zabbix_key" => "zabkey", "zabbix_value" => "message" }
64
- test_event = LogStash::Event.new({ "message" => "This is a log entry.", "zabhost" => "zabbix.example.com", "zabkey" => "zabbix.key" })
65
- test_out = LogStash::Outputs::Zabbix.new(test_cfg)
66
- data = test_out.format_request(test_event)
67
- if port_responding?(zabbix_ip, port)
68
- ###
69
- ### This is a hacky way to guarantee that Zabbix is responsive, because the
70
- ### port check alone is insufficient. TCP tests say the port is open, but
71
- ### it can take another 2 to 5 seconds (depending on the machine) before it
72
- ### is responding in the way we need for these tests.
73
- ###
74
- resp = ""
75
- # Try for up to 10 seconds to get a response.
76
- 10.times do
77
- TCPSocket.open(zabbix_ip, port) do |sock|
78
- sock.print ZabbixProtocol.dump(data)
79
- resp = sock.read
80
- end
81
- if resp.length == 0
82
- sleep 1
83
- else
84
- return true
85
- end
86
- end
87
- if resp.length == 0
88
- puts "Zabbix server or db is unreachable"
89
- end
90
- else
91
- puts "Unable to reach Zabbix server on #{zabbix_ip}:#{port}"
92
- end
93
- false
94
- end
15
+ NAME = "logstash-output-zabbix-#{rand(999).to_s}"
16
+ IMAGE = "untergeek/logstash_output_zabbix_rspec"
17
+ TAG = "latest"
18
+ ZABBIX_PORT = 10051
95
19
 
96
20
  describe LogStash::Outputs::Zabbix do
97
21
  # Building block "lets"
98
- let(:port) { 10051 }
22
+ let(:port) { ZABBIX_PORT }
99
23
  let(:host) { "127.0.0.1" }
100
24
  let(:timeout) { 1 }
101
25
  let(:timestamp) { "2015-04-15T15:39:42Z" }
@@ -113,13 +37,33 @@ describe LogStash::Outputs::Zabbix do
113
37
  "zabkey" => zabkey,
114
38
  }
115
39
  }
40
+ let(:multi_value) {
41
+ {
42
+ "@timestamp" => timestamp,
43
+ "message" => message,
44
+ "zabhost" => zabhost,
45
+ "key1" => "multi1",
46
+ "val1" => "value1",
47
+ "key2" => "multi2",
48
+ "val2" => "value2",
49
+ }
50
+ }
116
51
  let(:zabout) {
117
52
  {
118
53
  "zabbix_server_host" => host,
119
54
  "zabbix_server_port" => port,
120
- "zabbix_host" => "zabhost",
121
- "zabbix_key" => "zabkey",
122
- "timeout" => timeout
55
+ "zabbix_host" => "zabhost",
56
+ "zabbix_key" => "zabkey",
57
+ "timeout" => timeout,
58
+ }
59
+ }
60
+ let(:mzabout) {
61
+ {
62
+ "zabbix_server_host" => host,
63
+ "zabbix_server_port" => port,
64
+ "zabbix_host" => "zabhost",
65
+ "multi_value" => ["key1", "val1", "key2", "val2"],
66
+ "timeout" => timeout,
123
67
  }
124
68
  }
125
69
 
@@ -132,22 +76,27 @@ describe LogStash::Outputs::Zabbix do
132
76
  end
133
77
 
134
78
  describe "Unit Tests" do
79
+
135
80
  describe "#field_check" do
81
+
136
82
  context "when expected field not found" do
137
83
  subject { output.field_check(string_event, "not_appearing") }
138
84
  it "should return false" do
139
85
  expect(subject).to eq(false)
140
86
  end
141
87
  end
88
+
142
89
  context "when expected field found" do
143
90
  subject { output.field_check(string_event, "zabhost") }
144
91
  it "should return true" do
145
92
  expect(subject).to eq(true)
146
93
  end
147
94
  end
148
- end
149
95
 
150
- describe "#format_request" do
96
+ end # "#field_check"
97
+
98
+ describe "#format_request (single key/value pair)" do
99
+
151
100
  context "when it receives an event" do
152
101
  # {
153
102
  # "request" => "sender data",
@@ -177,9 +126,52 @@ describe LogStash::Outputs::Zabbix do
177
126
  expect(diff).to be < 3 # It should take less than 3 seconds for this.
178
127
  end
179
128
  end
180
- end
129
+
130
+ end # "#format_request (single key/value pair)"
131
+
132
+ describe "#format_request (multiple key/value pairs)" do
133
+ let(:multival_event) { LogStash::Event.new(multi_value) }
134
+ let(:m_output) { LogStash::Outputs::Zabbix.new(mzabout) }
135
+
136
+ context "when it receives an event and is configured for multiple values" do
137
+ subject { m_output.format_request(multival_event) }
138
+ it "should return a Zabbix sender data object with a data array" do
139
+ expect(subject['data'].length).to eq(2)
140
+ end
141
+ it "should return a Zabbix sender data object with the correct host [0]" do
142
+ expect(subject['data'][0]['host']).to eq(zabhost)
143
+ end
144
+ it "should return a Zabbix sender data object with the correct key [0]" do
145
+ expect(subject['data'][0]['key']).to eq('multi1')
146
+ end
147
+ it "should return a Zabbix sender data object with the correct value [0]" do
148
+ expect(subject['data'][0]['value']).to eq('value1')
149
+ end
150
+ it "should return a Zabbix sender data object with the correct clock from @timestamp [0]" do
151
+ expect(subject['data'][0]['clock']).to eq(epoch)
152
+ end
153
+ it "should return a Zabbix sender data object with the correct host [1]" do
154
+ expect(subject['data'][1]['host']).to eq(zabhost)
155
+ end
156
+ it "should return a Zabbix sender data object with the correct key [1]" do
157
+ expect(subject['data'][1]['key']).to eq('multi2')
158
+ end
159
+ it "should return a Zabbix sender data object with the correct value [1]" do
160
+ expect(subject['data'][1]['value']).to eq('value2')
161
+ end
162
+ it "should return a Zabbix sender data object with the correct clock from @timestamp [1]" do
163
+ expect(subject['data'][1]['clock']).to eq(epoch)
164
+ end
165
+ it "should return a Zabbix sender data object with the correct clock" do
166
+ diff = Time.now.to_i - subject['clock']
167
+ expect(diff).to be < 3 # It should take less than 3 seconds for this.
168
+ end
169
+ end
170
+
171
+ end # "#format_request (multiple key/value pairs)"
181
172
 
182
173
  describe "#response_check" do
174
+
183
175
  context "when it receives a success value" do
184
176
  subject { output.response_check(string_event, { "response" => "success" } ) }
185
177
  it "should return true" do
@@ -200,9 +192,11 @@ describe LogStash::Outputs::Zabbix do
200
192
  expect(subject).to eq(false)
201
193
  end
202
194
  end
203
- end
195
+
196
+ end # "#response_check"
204
197
 
205
198
  describe "#info_check" do
199
+
206
200
  context "when it receives a success value" do
207
201
  let(:success_msg) { {"response" => "success", "info" => "processed 1; Failed 0; Total 1; seconds spent: 0.000018"} }
208
202
  subject { output.info_check(string_event, success_msg) }
@@ -225,45 +219,33 @@ describe LogStash::Outputs::Zabbix do
225
219
  expect(subject).to eq(false)
226
220
  end
227
221
  end
228
- end
229
222
 
230
- end
223
+ end # "#info_check"
224
+
225
+ end # "Unit Tests"
231
226
 
232
227
  describe "Integration Tests", :integration => true do
233
- let(:host) { get_docker_ip }
234
228
 
235
- # Only open the container once for all tests.
236
229
  before(:all) do
237
- zabbix_ip = get_docker_ip
238
- port = 10051
239
- container = Docker::Container.create(
240
- "name" => CONTAINER_NAME,
241
- "Cmd" => ["run"],
242
- "Image" => "#{IMAGE}:#{TAG}",
243
- "ExposedPorts" => { "#{port}/tcp" => {} },
244
- "Tty" => true,
245
- "HostConfig" => {
246
- "PortBindings" => {
247
- "#{port}/tcp" => [
248
- {
249
- "HostIp" => "",
250
- "HostPort" => "#{port}"
251
- }
252
- ]
253
- }
254
- }
230
+ @zabbix = Longshoreman.new(
231
+ "#{IMAGE}:#{TAG}", # Image to use
232
+ NAME, # Container name
233
+ {
234
+ "Cmd" => ["run"],
235
+ "Tty" => true,
236
+ } # Extra options, if any
255
237
  )
256
- container.start
257
- zabbix_server_up?
238
+ zabbix_server_up?(@zabbix.ip, @zabbix.container.rport(ZABBIX_PORT))
258
239
  end
259
240
 
260
241
  after(:all) do
261
- container = Docker::Container.get(CONTAINER_NAME)
262
- container.stop
263
- container.delete(:force => true)
242
+ @zabbix.cleanup
264
243
  end
265
244
 
266
245
  describe "#tcp_send", :integration => true do
246
+ let(:port) { @zabbix.container.rport(ZABBIX_PORT) }
247
+ let(:host) { @zabbix.ip }
248
+
267
249
  context "when the Zabbix server responds with 'success'" do
268
250
  subject { output.tcp_send(string_event) }
269
251
  it "should return true" do
@@ -279,9 +261,24 @@ describe LogStash::Outputs::Zabbix do
279
261
  end
280
262
  end
281
263
 
282
- end
264
+ context "when multiple values are sent" do
265
+ let(:port) { @zabbix.container.rport(ZABBIX_PORT) }
266
+ let(:host) { @zabbix.ip }
267
+ let(:multival_event) { LogStash::Event.new(multi_value) }
268
+ let(:m_output) { LogStash::Outputs::Zabbix.new(mzabout) }
269
+
270
+ subject { m_output.tcp_send(multival_event) }
271
+ it "should return true" do
272
+ expect(subject).to eq(true)
273
+ end
274
+ end
275
+
276
+ end # "#tcp_send", :integration => true
283
277
 
284
278
  describe "#send_to_zabbix", :integration => true do
279
+ let(:port) { @zabbix.container.rport(ZABBIX_PORT) }
280
+ let(:host) { @zabbix.ip }
281
+
285
282
  context "when an event is sent successfully" do
286
283
  subject { output.send_to_zabbix(string_event) }
287
284
  it "should return true" do
@@ -298,8 +295,8 @@ describe LogStash::Outputs::Zabbix do
298
295
  end
299
296
  end
300
297
 
301
- end
298
+ end # "#send_to_zabbix", :integration => true
302
299
 
303
- end
300
+ end # "Integration Tests", :integration => true
304
301
 
305
- end
302
+ end # describe LogStash::Outputs::Zabbix