marquise 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1 @@
1
+ Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org/'
2
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,12 @@
1
+ guard 'spork', :rspec_env => { 'RACK_ENV' => 'test' } do
2
+ watch('Gemfile')
3
+ watch('spec/spec_helper.rb') { :rspec }
4
+ end
5
+
6
+ guard 'rspec',
7
+ :cmd => "rspec --drb",
8
+ :all_on_start => true do
9
+ watch('lib/marquise/ffi.rb')
10
+ watch(%r{^spec/.+_spec\.rb$})
11
+ watch(%r{^lib/(.+)\.rb$}) { |m| Dir["spec/#{m[1].gsub('/', '_')}*_spec.rb"] }
12
+ end
data/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Marquise for Ruby
2
+
3
+ This package contains Ruby bindings for the
4
+ [Marquise](https://github.com/anchor/libmarquise) data vault insertion
5
+ library. You should use it if you want to send data points to
6
+ [Vaultaire](https://github.com/anchor/vaultaire) in Ruby code.
7
+
8
+
9
+ # Installation
10
+
11
+ You should make sure you have libmarquise installed correctly first -- this
12
+ package will not work properly without it.
13
+
14
+ Then, simply install the `marquise` gem from Rubygems:
15
+
16
+ gem install marquise
17
+
18
+ And you should be good to go.
19
+
20
+
21
+ # Usage
22
+
23
+ Simply create a `Marquise` object, with the URL of the zeroMQ broker you
24
+ wish to use:
25
+
26
+ s = Marquise.new('tcp://chateau.example.com:5560')
27
+
28
+ When you're finished with your instance, you should close it, to cleanup
29
+ file handles and such:
30
+
31
+ s.close
32
+
33
+ If you're someone who doesn't like to stuff around with manual resource
34
+ release, there's also the very Rubyish block form:
35
+
36
+ Marquise.open('tcp://chateau.example.com:5560') do |s|
37
+ # Do things with s
38
+ end
39
+
40
+ Which will create a new Marquise object, yield it to your block, then close
41
+ off the thing when you're done.
42
+
43
+
44
+ # Sending data points
45
+
46
+ You send data points through a `Marquise` instance by `tell`ing it; these
47
+ are the simplest possible forms for each type of data Vaultaire can store:
48
+
49
+ s.tell(42) # Send an integer
50
+ s.tell(Math::PI) # Send a float
51
+ s.tell("Hello World") # Send a string
52
+ s.tell("Hello World".encode("ASCII-8BIT") # Send a binary blob
53
+ s.tell # Send a counter increment
54
+
55
+
56
+ ## Types and type conversion
57
+
58
+ Vaultaire is able to store:
59
+
60
+ * UTF-8 Strings;
61
+ * Binary blobs;
62
+ * Integers, in the range -(2^63)+1 to (2^63)-1;
63
+ * Double-precision floating-point numbers;
64
+ * Counter increments.
65
+
66
+ Marquise will automatically determine the most appropriate type to store the
67
+ value in, given the type of the first argument passed to `#tell`, by
68
+ applying the following rules:
69
+
70
+ 1. If the first argument `is_a? Hash`, then store a counter increment with
71
+ a timestamp of `Time.now`;
72
+
73
+ 1. If the first argument `is_a? Time`, then store a counter increment with
74
+ a timestamp of the first argument;
75
+
76
+ 1. If the first argument has a `#to_str` method, then call that and store
77
+ the result as a binary (if the `#encoding` method returns `ASCII-8BIT`
78
+ or the string does not encode cleanly to UTF-8), or as a UTF-8 string
79
+ otherwise;
80
+
81
+ 1. If the first argument has a `#integer?` method, then call that, and
82
+ based on whether the result of that is `true` or `false`, store either
83
+ an integer or float.
84
+
85
+ 1. Otherwise, raise `ArgumentError`, as we were unable to determine how to
86
+ convert the provided argument into something that would be understood.
87
+
88
+
89
+ ## Timestamps
90
+
91
+ Vaultaire stores all data points with a timestamp. If you want to specify
92
+ the timestamp that should be associated with your data point, you should
93
+ pass a `Time` object to `Marquise#send` as the first non-value argument
94
+ (that is, the first argument if storing a counter increment, or the second
95
+ argument otherwise). If you do not specify a timestamp, then `Time.now`
96
+ will be called within the `#send` method, and that time used as the
97
+ timestamp.
98
+
99
+
100
+ ## Tagging data points
101
+
102
+ To differentiate data points from different series, Vaultaire allows the
103
+ specification of arbitrary key/value hashes, which you specify in the usual
104
+ Rubyesque fashion:
105
+
106
+ s.tell(42, :answer => 'ultimate')
107
+ s.tell(Math::PI, constant: 'yes', closest_to: 3)
108
+
109
+ Note that all keys and values will be converted into strings using `#to_s`,
110
+ because Vaultaire only supports strings in its tag hashes.
data/Rakefile ADDED
@@ -0,0 +1,39 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ task :default => :test
5
+
6
+ begin
7
+ Bundler.setup
8
+ rescue Bundler::BundlerError => e
9
+ $stderr.puts e.message
10
+ $stderr.puts "Run `bundle install` to install missing gems"
11
+ exit e.status_code
12
+ end
13
+
14
+ Bundler::GemHelper.install_tasks
15
+
16
+ require 'rdoc/task'
17
+
18
+ Rake::RDocTask.new do |rd|
19
+ rd.main = "README.md"
20
+ rd.title = 'marquise'
21
+ rd.rdoc_files.include("README.md", "lib/**/*.rb")
22
+ end
23
+
24
+ require 'git-version-bump/rake-tasks'
25
+
26
+ desc "Run guard"
27
+ task :guard do
28
+ Bundler.setup(:default, :test)
29
+ require 'guard'
30
+ ::Guard.start(:clear => true)
31
+ while ::Guard.running do
32
+ sleep 0.5
33
+ end
34
+ end
35
+
36
+ require 'rspec/core/rake_task'
37
+ RSpec::Core::RakeTask.new :test do |t|
38
+ t.pattern = "spec/**/*_spec.rb"
39
+ end
data/lib/marquise.rb ADDED
@@ -0,0 +1,212 @@
1
+ require 'marquise/ffi'
2
+ require 'ffi/dry/errno'
3
+
4
+ # A Vaultaire data point transport
5
+ #
6
+ # Instances of `Marquise` send data points that they are told about to a
7
+ # Vaultaire data store. It has a very simple interface that hides a lot
8
+ # of complexity.
9
+ class Marquise
10
+ include ::FFI::DRY::ErrnoHelper
11
+
12
+ # Open a Marquise consumer and (optionally) yield it to a block
13
+ #
14
+ # With no associated block, `Marquise.open` is a synonym for
15
+ # `Marquise.new`. If the optional block is given, a newly created
16
+ # `Marquise` object will be passed to the block, and will then be
17
+ # automatically closed when the block terminates, and the value of
18
+ # the block returned.
19
+ def self.open(zmq_url, batch_period = 5)
20
+ m = Marquise.new(zmq_url, batch_period)
21
+ rv = m
22
+
23
+ if block_given?
24
+ begin
25
+ rv = yield m
26
+ ensure
27
+ m.close
28
+ end
29
+ end
30
+
31
+ rv
32
+ end
33
+
34
+ # Create a new `Marquise` transport object
35
+ #
36
+ # `zmq_url` is the URL to your ZeroMQ broker associated with the
37
+ # Vaultaire system you're dumping data into. `batch_period` is optional
38
+ # (defaults to `5`) is the number of seconds between "flushes" of data
39
+ # points to ZeroMQ. It can be a floating-point number if you wish to
40
+ # have sub-second flushes. Increasing the `batch_period` increases the
41
+ # possibility of losing data points in the event of a spectacular
42
+ # failure, but also improves performance.
43
+ def initialize(zmq_url, batch_period = 5)
44
+ @consumer = Marquise::FFI.marquise_consumer_new(zmq_url, batch_period)
45
+
46
+ if @consumer.nil?
47
+ raise RuntimeError,
48
+ "libmarquise failed; check syslog (no, seriously)"
49
+ end
50
+
51
+ @connections = {}
52
+
53
+ @janitor = Janitor.new(@consumer, @connections)
54
+ ObjectSpace.define_finalizer(self, @janitor)
55
+ end
56
+
57
+ def tell(*args)
58
+ val, ts, opts = parse_tell_opts(args)
59
+
60
+ k, v, len = if opts.length == 0
61
+ [nil, nil, 0]
62
+ else
63
+ [
64
+ Marquise::FFI.pointer_list_from_string_array(opts.keys),
65
+ Marquise::FFI.pointer_list_from_string_array(opts.values),
66
+ opts.length
67
+ ]
68
+ end
69
+
70
+ rv = if val.nil?
71
+ Marquise::FFI.marquise_send_counter(
72
+ connection,
73
+ k,
74
+ v,
75
+ len,
76
+ ts.to_f * 1_000_000_000
77
+ )
78
+ elsif val.respond_to? :to_str and val.respond_to? :encoding
79
+ s = val.to_str
80
+ method = nil
81
+
82
+ if s.encoding.to_s == 'ASCII-8BIT' or !s.force_encoding('UTF-8').valid_encoding?
83
+ method = :marquise_send_binary
84
+ else
85
+ method = :marquise_send_text
86
+ s = s.encode('UTF-8')
87
+ end
88
+
89
+ Marquise::FFI.send(
90
+ method,
91
+ connection,
92
+ k,
93
+ v,
94
+ len,
95
+ s,
96
+ s.length,
97
+ ts.to_f * 1_000_000_000
98
+ )
99
+ elsif val.respond_to? :integer? and val.integer?
100
+ if val < -(2**63)+1 or val > (2**63)-1
101
+ raise ArgumentError,
102
+ "Integer out of range for Marquise#tell"
103
+ end
104
+
105
+ Marquise::FFI.marquise_send_int(
106
+ connection,
107
+ k,
108
+ v,
109
+ len,
110
+ val,
111
+ ts.to_f * 1_000_000_000
112
+ )
113
+ elsif val.respond_to? :integer? and !val.integer?
114
+ Marquise::FFI.marquise_send_real(
115
+ connection,
116
+ k,
117
+ v,
118
+ len,
119
+ val,
120
+ ts.to_f * 1_000_000_000
121
+ )
122
+ end
123
+
124
+ if rv == -1
125
+ raise errno_exception
126
+ end
127
+ end
128
+
129
+ # Close a Marquise instance
130
+ #
131
+ # This must be called when you are done with your Marquise instance, to
132
+ # avoid leaving memory and file descriptors.
133
+ def close
134
+ @janitor.call
135
+ ObjectSpace.undefine_finalizer(self)
136
+ @connections = {}
137
+ @consumer = nil
138
+ end
139
+
140
+ # :stopdoc:
141
+ # Initialize a connection
142
+ #
143
+ # You should rarely have to call this method yourself; Marquise will do
144
+ # it automatically for you when required.
145
+ def connect
146
+ th = Thread.current
147
+
148
+ return if @connections[th]
149
+
150
+ @connections[th] = Marquise::FFI.marquise_connect(@consumer)
151
+
152
+ if @connections[th].nil?
153
+ raise RuntimeError.new("marquise_connect() failed... consult syslog (no, seriously)")
154
+ end
155
+
156
+ nil
157
+ end
158
+
159
+ # Get the connection pointer for the current thread
160
+ def connection
161
+ self.connect
162
+
163
+ @connections[Thread.current]
164
+ end
165
+
166
+ # A helper class to cleanup Marquise consumers. We can't just do the
167
+ # obvious, which would be to create a Proc inside `Marquise.initialize`,
168
+ # because that would leave a reference to the object laying around and
169
+ # we'd never get GC'd. So we do this instead. I stole this technique
170
+ # from Tempfile; blame them if this is insane.
171
+ class Janitor
172
+ def initialize(ptr, conns)
173
+ @ptr = ptr
174
+ @conns = conns
175
+ end
176
+
177
+ def call(*args)
178
+ @conns.values.each { |c| Marquise::FFI.marquise_close(c) }
179
+ Marquise::FFI.marquise_consumer_shutdown(@ptr)
180
+ end
181
+ end
182
+
183
+ private
184
+ def parse_tell_opts(args)
185
+ orig_args = args.dup
186
+
187
+ val = nil
188
+ ts = Time.now
189
+ opts = {}
190
+
191
+ if ((args[0].respond_to? :to_str and args[0].respond_to? :encoding) or
192
+ args[0].respond_to? :integer?)
193
+ val = args.shift
194
+ end
195
+
196
+ if args[0].is_a? Time
197
+ ts = args.shift
198
+ end
199
+
200
+ if args[0].is_a? Hash
201
+ opts = args.shift
202
+ end
203
+
204
+ unless args.empty?
205
+ raise ArgumentError,
206
+ "Invalid call to Marquise#tell (you passed '#{orig_args.map(&:inspect).join(', ')}')"
207
+ end
208
+
209
+ [val, ts, opts]
210
+ end
211
+ # :startdoc:
212
+ end
@@ -0,0 +1,65 @@
1
+ require 'ffi'
2
+
3
+ class Marquise
4
+ module FFI
5
+ extend ::FFI::Library
6
+ ffi_lib 'libmarquise.so.1'
7
+
8
+ attach_function :marquise_consumer_new, [ :string, :double ], :pointer
9
+ attach_function :marquise_consumer_shutdown, [ :pointer ], :void
10
+
11
+ attach_function :marquise_connect, [ :pointer ], :pointer
12
+ attach_function :marquise_close, [ :pointer ], :void
13
+
14
+ attach_function :marquise_send_text, [ :pointer,
15
+ :pointer,
16
+ :pointer,
17
+ :size_t,
18
+ :string,
19
+ :size_t,
20
+ :uint64 ],
21
+ :int
22
+ attach_function :marquise_send_int, [ :pointer,
23
+ :pointer,
24
+ :pointer,
25
+ :size_t,
26
+ :int64,
27
+ :uint64 ],
28
+ :int
29
+ attach_function :marquise_send_real, [ :pointer,
30
+ :pointer,
31
+ :pointer,
32
+ :size_t,
33
+ :double,
34
+ :uint64 ],
35
+ :int
36
+ attach_function :marquise_send_counter, [ :pointer,
37
+ :pointer,
38
+ :pointer,
39
+ :size_t,
40
+ :uint64 ],
41
+ :int
42
+ attach_function :marquise_send_binary, [ :pointer,
43
+ :pointer,
44
+ :pointer,
45
+ :size_t,
46
+ :pointer,
47
+ :size_t,
48
+ :uint64 ],
49
+ :int
50
+
51
+ # Common helper to take an array of strings (or things that we'll turn
52
+ # into strings) and turn it into an FFI::MemoryPointer containing
53
+ # FFI::MemoryPointers for each string.
54
+ def self.pointer_list_from_string_array(ary)
55
+ ptrs = []
56
+ ary.each { |e| ptrs << ::FFI::MemoryPointer.from_string(e.to_s) }
57
+ ptrs << nil
58
+
59
+ ptr = ::FFI::MemoryPointer.new(:pointer, ptrs.length)
60
+ ptrs.each_with_index { |p, i| ptr[i].write_pointer(p) }
61
+
62
+ ptr
63
+ end
64
+ end
65
+ end