vayacondios-client 0.1.2 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -16,5 +16,7 @@ require 'gorillib/string/constantize'
16
16
  require 'gorillib/string/inflections'
17
17
 
18
18
  require 'vayacondios/client/http_client'
19
+ require 'vayacondios/client/cube_client'
20
+ require 'vayacondios/client/zabbix_client'
19
21
  require 'vayacondios/client/notifier'
20
22
  require 'vayacondios/client/configliere'
@@ -0,0 +1,39 @@
1
+ class Vayacondios
2
+ class CubeClient
3
+ include Gorillib::Model
4
+
5
+ field :host, String, :default => 'localhost'
6
+ field :port, Integer, :default => 6000
7
+
8
+ class Error < StandardError; end
9
+
10
+ def uri
11
+ return @uri if @uri
12
+
13
+ uri_str = "http://#{host}:#{port}/1.0"
14
+ @uri ||= URI(uri_str)
15
+ end
16
+
17
+ def event(topic, document = {})
18
+ request(:post, File.join(uri.path, 'event'), MultiJson.dump(document))
19
+ end
20
+
21
+ private
22
+
23
+ def request(method, path, document=nil)
24
+ http = Net::HTTP.new(uri.host, uri.port)
25
+
26
+ params = [method.to_sym, path]
27
+ params += [document, {'Content-Type' => 'application/json'}] unless document.nil?
28
+
29
+ response = http.send *params
30
+
31
+ if Net::HTTPSuccess === response
32
+ MultiJson.load(response.body) rescue response.body
33
+ else
34
+ raise Error.new("Error (#{response.code}) while #{method.to_s == 'get' ? 'fetching' : 'inserting'} document: " + response.body)
35
+ end
36
+ end
37
+ end
38
+ end
39
+
@@ -4,42 +4,29 @@ require 'multi_json'
4
4
  class Vayacondios
5
5
  class Client
6
6
  class ItemSet
7
- def initialize host, port, organization, topic, id
8
- @host = host
9
- @port = port
10
-
11
- @path = "/v1/#{organization}/itemset/#{topic}/#{id}"
7
+ def initialize host, port, organization=nil, topic=nil, id=nil
8
+ @host = host
9
+ @port = port
10
+ @organization = organization
11
+ @topic = topic
12
+ @id = id
12
13
  end
13
14
 
14
- # Exposed for testing.
15
- def _req type, ary = nil
16
- case type
17
- when :fetch then
18
- req = Net::HTTP::Get.new(@path)
19
- when :create then
20
- (req = Net::HTTP::Put.new(@path)).body = MultiJson.encode(ary)
21
- when :update then
22
- (req = Net::HTTP::Put.new(@path, {"x-method" => "PATCH"})).body = MultiJson.encode(ary)
23
- when :remove then
24
- (req = Net::HTTP::Delete.new(@path)).body = MultiJson.encode(ary)
25
- end
26
- req
15
+ def fetch organization=nil, topic=nil, id=nil
16
+ resp = execute_request(_req(:fetch, nil, organization, topic, id)) and
17
+ resp["contents"]
27
18
  end
28
19
 
29
- def fetch
30
- execute_request(_req(:fetch))
20
+ def update ary, organization=nil, topic=nil, id=nil
21
+ execute_request(_req(:update, ary, organization, topic, id))
31
22
  end
32
23
 
33
- def update ary
34
- execute_request(_req(:update, ary))
24
+ def create ary, organization=nil, topic=nil, id=nil
25
+ execute_request(_req(:create, ary, organization, topic, id))
35
26
  end
36
27
 
37
- def create ary
38
- execute_request(_req(:create, ary))
39
- end
40
-
41
- def remove ary
42
- execute_request(_req(:remove, ary))
28
+ def remove ary, organization=nil, topic=nil, id=nil
29
+ execute_request(_req(:remove, ary, organization, topic, id))
43
30
  end
44
31
 
45
32
 
@@ -52,6 +39,34 @@ class Vayacondios
52
39
  result = MultiJson.decode(resp) unless resp.nil? or resp.empty?
53
40
  (result.respond_to?(:has_key?) and result.has_key? "error") ? nil : result
54
41
  end
42
+
43
+ def path organization, topic, id
44
+ if ((the_organization = (organization || @organization)).nil? ||
45
+ (the_topic = (topic || @topic )).nil? ||
46
+ (the_id = (id || @id )).nil?)
47
+ raise ArgumentError.new("must provide organization, topic, and id!")
48
+ end
49
+
50
+ ['/v1', the_organization, 'itemset', the_topic, the_id].join("/")
51
+ end
52
+
53
+ # This is the only private method that is tested.
54
+ def _req type, ary=nil, organization=nil, topic=nil, id=nil
55
+
56
+ the_path = path(organization, topic, id)
57
+ headers = {"content-type" => "application/json"}
58
+ headers.merge!("x-method" => "PATCH") if type == :update
59
+
60
+ case type
61
+ when :fetch then Net::HTTP::Get
62
+ when :create then Net::HTTP::Put
63
+ when :update then Net::HTTP::Put
64
+ when :remove then Net::HTTP::Delete
65
+ else raise ArgumentError.new("invalid type: #{type}")
66
+ end.new(the_path, headers).tap do |req|
67
+ req.body = MultiJson.encode(contents: ary) unless type == :fetch
68
+ end
69
+ end
55
70
  end
56
71
  end
57
72
  end
@@ -56,11 +56,34 @@ class Vayacondios
56
56
  end
57
57
  end
58
58
 
59
+ class CubeNotifier < Notifier
60
+ def initialize(options={})
61
+ @client = Vayacondios::CubeClient.receive(options)
62
+ end
63
+ def notify topic, cargo={}
64
+ prepped = prepare(cargo)
65
+ client.event(topic, prepped)
66
+ nil
67
+ end
68
+ end
69
+
70
+ class ZabbixNotifier < Notifier
71
+ def initialize options={}
72
+ @client = Vayacondios::ZabbixClient.receive(options)
73
+ end
74
+ def notify(topic, cargo={})
75
+ prepped = prepare(cargo)
76
+ client.insert(topic, prepped)
77
+ end
78
+ end
79
+
59
80
  class NotifierFactory
60
81
  def self.receive(attrs = {})
61
82
  type = attrs[:type]
62
83
  case type
63
84
  when 'http' then HttpNotifier.new(attrs)
85
+ when 'cube' then CubeNotifier.new(attrs)
86
+ when 'zabbix' then ZabbixNotifier.new(attrs)
64
87
  when 'log' then LogNotifier.new(attrs)
65
88
  when 'none','null' then NullNotifier.new(attrs)
66
89
  else
@@ -80,7 +103,7 @@ class Vayacondios
80
103
  def self.included klass
81
104
  if klass.ancestors.include? Gorillib::Model
82
105
  klass.class_eval do
83
- field :notifier, Vayacondios::NotifierFactory, default: Vayacondios.default_notifier
106
+ field :notifier, Vayacondios::NotifierFactory, default: Vayacondios.default_notifier, :doc => "Notifier used to notify out of band data"
84
107
 
85
108
  def receive_notifier params
86
109
  params.merge!(log: try(:log)) if params[:type] == 'log'
@@ -0,0 +1,148 @@
1
+ require 'socket'
2
+
3
+ class Vayacondios
4
+
5
+ # Used for sending events to a Zabbix server.
6
+ #
7
+ # An 'event' from Vayacondios' perspective is an arbitrary Hash.
8
+ #
9
+ # An 'event' from Zabbix's perspective is a tuple of values:
10
+ #
11
+ # * time
12
+ # * host
13
+ # * key
14
+ # * value
15
+ #
16
+ # This client will accept a Vayacondios event and internally
17
+ # translate it into a set of Zabbix events.
18
+ #
19
+ # @example A CPU monitoring notification
20
+ #
21
+ # notify "foo-server.example.com", cpu: {
22
+ # util: {
23
+ # user: 0.20,
24
+ # idle: 0.70,
25
+ # sys: 0.10
26
+ # },
27
+ # load: 1.3
28
+ # }
29
+ #
30
+ # would get turned into the following events when written to Zabbix:
31
+ #
32
+ # @example The CPU monitoring notification translated to Zabbix events
33
+ #
34
+ # [
35
+ # { host: "foo-server.example.com", key: "cpu.util.user", value: 0.20 }
36
+ # { host: "foo-server.example.com", key: "cpu.util.idle", value: 0.70 },
37
+ # { host: "foo-server.example.com", key: "cpu.util.sys", value: 0.10 },
38
+ # { host: "foo-server.example.com", key: "cpu.load", value: 1.3 }
39
+ # ]
40
+ #
41
+ # Zabbix will interpret the time as the time it receives each event.
42
+ #
43
+ # The following links provide details on the protocol used by Zabbix
44
+ # to receive events:
45
+ #
46
+ # * https://www.zabbix.com/forum/showthread.php?t=20047&highlight=sender
47
+ # * https://gist.github.com/1170577
48
+ # * http://spin.atomicobject.com/2012/10/30/collecting-metrics-from-ruby-processes-using-zabbix-trappers/?utm_source=rubyflow&utm_medium=ao&utm_campaign=collecting-metrics-zabix
49
+ class ZabbixClient
50
+ include Gorillib::Builder
51
+
52
+ attr_accessor :socket
53
+
54
+ field :host, String, :default => 'localhost', :doc => "Host for the Zabbix server"
55
+ field :port, Integer, :default => 10051, :doc => "Port for the Zabbix server"
56
+
57
+ # Insert events to a Zabbix server.
58
+ #
59
+ # The `topic` will be used as the name of the Zabbix host to
60
+ # associate event data to.
61
+ #
62
+ # As per the documentation for the [Zabbix sender
63
+ # protocol](https://www.zabbix.com/wiki/doc/tech/proto/zabbixsenderprotocol),
64
+ # a new TCP connection will be created for each event.
65
+ #
66
+ # @param [String] topic
67
+ # @param [Hash] cargo
68
+ # Array<Hash>] text
69
+ def insert topic, cargo={}
70
+ self.socket = TCPSocket.new(host, port)
71
+ send_request(topic, cargo)
72
+ handle_response
73
+ self.socket.close
74
+ end
75
+
76
+ private
77
+
78
+ # :nodoc
79
+ def send_request topic, cargo
80
+ socket.write(payload(topic, cargo))
81
+ end
82
+
83
+ # :nodoc
84
+ def handle_response
85
+ header = socket.recv(5)
86
+ if header == "ZBXD\1"
87
+ data_header = socket.recv(8)
88
+ length = data_header[0,4].unpack("i")[0]
89
+ response = MultiJson.load(socket.recv(length))
90
+ puts response["info"]
91
+ else
92
+ puts "Invalid response: #{header}"
93
+ end
94
+ end
95
+
96
+ # :nodoc
97
+ def payload topic, cargo={}
98
+ body = body_for(topic, cargo)
99
+ header_for(body) + body
100
+ end
101
+
102
+ # :nodoc
103
+ def body_for topic, cargo={}
104
+ MultiJson.dump({request: "sender data", data: zabbix_events_from(topic, cargo) })
105
+ end
106
+
107
+ # :nodoc
108
+ def header_for body
109
+ length = body.bytesize
110
+ "ZBXD\1".encode("ascii") + [length].pack("i") + "\x00\x00\x00\x00"
111
+ end
112
+
113
+ # :nodoc
114
+ def zabbix_events_from topic, cargo, scope=''
115
+ events = []
116
+ case cargo
117
+ when Hash
118
+ cargo.each_pair do |key, value|
119
+ events += zabbix_events_from(topic, value, new_scope(scope, key))
120
+ end
121
+ when Array
122
+ cargo.each_with_index do |item, index|
123
+ events += zabbix_events_from(topic, item, new_scope(scope, index))
124
+ end
125
+ else
126
+ events << event_body(topic, scope, cargo)
127
+ end
128
+ events
129
+ end
130
+
131
+ # :nodoc
132
+ def new_scope(current_scope, new_scope)
133
+ [current_scope, new_scope].map(&:to_s).reject(&:empty?).join('.')
134
+ end
135
+
136
+ # :nodoc
137
+ def event_body topic, scope, cargo
138
+ value = case cargo
139
+ when Hash then cargo[:value]
140
+ when Array then cargo.first
141
+ else cargo
142
+ end
143
+ { host: topic, key: scope, value: value }
144
+ end
145
+
146
+ end
147
+ end
148
+
@@ -16,28 +16,28 @@ describe Vayacondios::Client::ItemSet do
16
16
  itemset = Vayacondios::Client::ItemSet.new("foohost", 9999, "fooorg", "footopic", "fooid")
17
17
  ary = ["foo", "bar", "baz"]
18
18
 
19
- # Actually testing internals here to avoid
19
+ # testing internals here to avoid shimming up HTTP libraries.
20
20
 
21
21
  it "generates a put request without a patch header when asked to create" do
22
- req = itemset._req :create, ary
22
+ req = itemset.instance_eval{_req(:create, ary)}
23
23
 
24
24
  req.method.should eql('PUT')
25
- req.body.should eql(ary.to_json)
25
+ req.body.should eql(MultiJson.encode(contents: ary))
26
26
  req.path.should eql('/v1/fooorg/itemset/footopic/fooid')
27
27
  req.each_header.to_a.should_not include(["x_method", "PATCH"])
28
28
  end
29
29
 
30
30
  it "generates a put request with a patch header when asked to update" do
31
- req = itemset._req :update, ary
31
+ req = itemset.instance_eval{_req(:update, ary)}
32
32
 
33
33
  req.method.should eql('PUT')
34
- req.body.should eql(ary.to_json)
34
+ req.body.should eql(MultiJson.encode(contents: ary))
35
35
  req.path.should eql('/v1/fooorg/itemset/footopic/fooid')
36
36
  req.each_header.to_a.should include(["x-method", "PATCH"])
37
37
  end
38
38
 
39
39
  it "generates a get request when asked to fetch" do
40
- req = itemset._req :fetch
40
+ req = itemset.instance_eval{_req(:fetch)}
41
41
 
42
42
  req.method.should eql('GET')
43
43
  req.body.should be_nil
@@ -45,10 +45,10 @@ describe Vayacondios::Client::ItemSet do
45
45
  end
46
46
 
47
47
  it "generates a delete request when asked to remove" do
48
- req = itemset._req :remove, ary
48
+ req = itemset.instance_eval{_req(:remove, ary)}
49
49
 
50
50
  req.method.should eql('DELETE')
51
- req.body.should eql(ary.to_json)
51
+ req.body.should eql(MultiJson.encode(contents: ary))
52
52
  req.path.should eql('/v1/fooorg/itemset/footopic/fooid')
53
53
  end
54
54
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vayacondios-client
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.6
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2012-12-17 00:00:00.000000000 Z
15
+ date: 2013-03-06 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: configliere
@@ -35,23 +35,23 @@ dependencies:
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  none: false
37
37
  requirements:
38
- - - ~>
38
+ - - ! '>='
39
39
  - !ruby/object:Gem::Version
40
- version: '1.1'
40
+ version: 1.3.6
41
41
  type: :runtime
42
42
  prerelease: false
43
43
  version_requirements: !ruby/object:Gem::Requirement
44
44
  none: false
45
45
  requirements:
46
- - - ~>
46
+ - - ! '>='
47
47
  - !ruby/object:Gem::Version
48
- version: '1.1'
48
+ version: 1.3.6
49
49
  - !ruby/object:Gem::Dependency
50
50
  name: gorillib
51
51
  requirement: !ruby/object:Gem::Requirement
52
52
  none: false
53
53
  requirements:
54
- - - ~>
54
+ - - ! '>='
55
55
  - !ruby/object:Gem::Version
56
56
  version: 0.4.2
57
57
  type: :runtime
@@ -59,7 +59,7 @@ dependencies:
59
59
  version_requirements: !ruby/object:Gem::Requirement
60
60
  none: false
61
61
  requirements:
62
- - - ~>
62
+ - - ! '>='
63
63
  - !ruby/object:Gem::Version
64
64
  version: 0.4.2
65
65
  - !ruby/object:Gem::Dependency
@@ -119,9 +119,11 @@ extra_rdoc_files: []
119
119
  files:
120
120
  - lib/vayacondios-client.rb
121
121
  - lib/vayacondios/client/configliere.rb
122
+ - lib/vayacondios/client/cube_client.rb
122
123
  - lib/vayacondios/client/http_client.rb
123
124
  - lib/vayacondios/client/itemset.rb
124
125
  - lib/vayacondios/client/notifier.rb
126
+ - lib/vayacondios/client/zabbix_client.rb
125
127
  - spec/client/itemset_spec.rb
126
128
  - spec/client/notifier_spec.rb
127
129
  homepage: https://github.com/infochimps-labs/vayacondios
@@ -138,7 +140,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
138
140
  version: '0'
139
141
  segments:
140
142
  - 0
141
- hash: -2836890391198069767
143
+ hash: -1918215973120450598
142
144
  required_rubygems_version: !ruby/object:Gem::Requirement
143
145
  none: false
144
146
  requirements:
@@ -147,10 +149,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
147
149
  version: '0'
148
150
  segments:
149
151
  - 0
150
- hash: -2836890391198069767
152
+ hash: -1918215973120450598
151
153
  requirements: []
152
154
  rubyforge_project:
153
- rubygems_version: 1.8.24
155
+ rubygems_version: 1.8.25
154
156
  signing_key:
155
157
  specification_version: 3
156
158
  summary: Data goes in. The right thing happens