vayacondios-client 0.1.2 → 0.1.6

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.
@@ -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