signalfx 1.0.2 → 2.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/Gemfile.lock +42 -13
- data/README.md +20 -0
- data/examples/signalflow.rb +32 -0
- data/lib/signalfx/conf.rb +2 -0
- data/lib/signalfx/signal_fx_client.rb +14 -0
- data/lib/signalfx/signalflow/binary.rb +62 -0
- data/lib/signalfx/signalflow/channel.rb +80 -0
- data/lib/signalfx/signalflow/client.rb +73 -0
- data/lib/signalfx/signalflow/computation.rb +243 -0
- data/lib/signalfx/signalflow/queue.rb +43 -0
- data/lib/signalfx/signalflow/websocket.rb +317 -0
- data/lib/signalfx/version.rb +1 -1
- data/signalfx.gemspec +5 -1
- metadata +67 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 65d54216c0f3f8e9bac2bed755772ac96dc3b415
|
4
|
+
data.tar.gz: e90eeddd906dfe039ef213c5d605e4c29afca0da
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 469077ec78a1414d3a44c6f8482a90d8e78ba2cf27a4a16d1708c87999baf7bf24bd761d819ca2a4f6a9d05b70c68e9f73302f509da9c50b130f16bb321f16f1
|
7
|
+
data.tar.gz: 183aa859b752dab6739d5a4d7e1668e1fa39b0c486197ab57c233ce159640b1f3d329653d9d3cd7d4a1be037e62e1d5f68b88ec8611962aff1efa18eb23aee97
|
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,44 +1,58 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
signalfx (
|
4
|
+
signalfx (2.0.1)
|
5
5
|
protobuf (~> 3.5.1, >= 3.5.1)
|
6
6
|
rest-client (~> 2.0)
|
7
|
+
websocket-client-simple (~> 0.3.0)
|
7
8
|
|
8
9
|
GEM
|
9
10
|
remote: https://rubygems.org/
|
10
11
|
specs:
|
11
|
-
activesupport (5.
|
12
|
+
activesupport (5.1.4)
|
12
13
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
13
14
|
i18n (~> 0.7)
|
14
15
|
minitest (~> 5.1)
|
15
16
|
tzinfo (~> 1.1)
|
16
17
|
addressable (2.4.0)
|
17
|
-
|
18
|
+
coderay (1.1.1)
|
19
|
+
concurrent-ruby (1.0.5)
|
18
20
|
crack (0.4.3)
|
19
21
|
safe_yaml (~> 1.0.0)
|
22
|
+
daemons (1.2.4)
|
20
23
|
diff-lcs (1.2.5)
|
21
24
|
docile (1.1.5)
|
22
|
-
domain_name (0.5.
|
25
|
+
domain_name (0.5.20170404)
|
23
26
|
unf (>= 0.0.5, < 1.0.0)
|
27
|
+
event_emitter (0.2.6)
|
28
|
+
eventmachine (1.2.5)
|
29
|
+
faye-websocket (0.10.7)
|
30
|
+
eventmachine (>= 0.12.0)
|
31
|
+
websocket-driver (>= 0.5.1)
|
24
32
|
hashdiff (0.3.0)
|
25
|
-
http-cookie (1.0.
|
33
|
+
http-cookie (1.0.3)
|
26
34
|
domain_name (~> 0.5)
|
27
|
-
i18n (0.
|
35
|
+
i18n (0.8.6)
|
28
36
|
json (1.8.3)
|
37
|
+
method_source (0.8.2)
|
29
38
|
middleware (0.1.0)
|
30
39
|
mime-types (3.1)
|
31
40
|
mime-types-data (~> 3.2015)
|
32
41
|
mime-types-data (3.2016.0521)
|
33
|
-
minitest (5.
|
42
|
+
minitest (5.10.3)
|
34
43
|
netrc (0.11.0)
|
35
44
|
protobuf (3.5.5)
|
36
45
|
activesupport (>= 3.2)
|
37
46
|
middleware
|
38
47
|
thor
|
39
48
|
thread_safe
|
49
|
+
pry (0.10.4)
|
50
|
+
coderay (~> 1.1.0)
|
51
|
+
method_source (~> 0.8.1)
|
52
|
+
slop (~> 3.4)
|
53
|
+
rack (2.0.3)
|
40
54
|
rake (10.4.2)
|
41
|
-
rest-client (2.0.
|
55
|
+
rest-client (2.0.2)
|
42
56
|
http-cookie (>= 1.0.2, < 2.0)
|
43
57
|
mime-types (>= 1.16, < 4.0)
|
44
58
|
netrc (~> 0.8)
|
@@ -61,28 +75,43 @@ GEM
|
|
61
75
|
json (~> 1.8)
|
62
76
|
simplecov-html (~> 0.10.0)
|
63
77
|
simplecov-html (0.10.0)
|
64
|
-
|
65
|
-
|
66
|
-
|
78
|
+
slop (3.6.0)
|
79
|
+
thin (1.7.2)
|
80
|
+
daemons (~> 1.0, >= 1.0.9)
|
81
|
+
eventmachine (~> 1.0, >= 1.0.4)
|
82
|
+
rack (>= 1, < 3)
|
83
|
+
thor (0.20.0)
|
84
|
+
thread_safe (0.3.6)
|
85
|
+
tzinfo (1.2.3)
|
67
86
|
thread_safe (~> 0.1)
|
68
87
|
unf (0.1.4)
|
69
88
|
unf_ext
|
70
|
-
unf_ext (0.0.7.
|
89
|
+
unf_ext (0.0.7.4)
|
71
90
|
webmock (2.1.0)
|
72
91
|
addressable (>= 2.3.6)
|
73
92
|
crack (>= 0.3.2)
|
74
93
|
hashdiff
|
94
|
+
websocket (1.2.4)
|
95
|
+
websocket-client-simple (0.3.0)
|
96
|
+
event_emitter
|
97
|
+
websocket
|
98
|
+
websocket-driver (0.6.5)
|
99
|
+
websocket-extensions (>= 0.1.0)
|
100
|
+
websocket-extensions (0.1.2)
|
75
101
|
|
76
102
|
PLATFORMS
|
77
103
|
ruby
|
78
104
|
|
79
105
|
DEPENDENCIES
|
80
106
|
bundler (~> 1.10)
|
107
|
+
faye-websocket (~> 0.10.7)
|
108
|
+
pry
|
81
109
|
rake (~> 10.0)
|
82
110
|
rspec (~> 3.3)
|
83
111
|
signalfx!
|
84
112
|
simplecov
|
113
|
+
thin (~> 1.7)
|
85
114
|
webmock (~> 2.1)
|
86
115
|
|
87
116
|
BUNDLED WITH
|
88
|
-
1.
|
117
|
+
1.15.4
|
data/README.md
CHANGED
@@ -155,6 +155,26 @@ client.send_event(
|
|
155
155
|
See `examples/generic_usecase.rb` for a complete code example for
|
156
156
|
sending events.
|
157
157
|
|
158
|
+
### SignalFlow
|
159
|
+
|
160
|
+
You can run SignalFlow computations as well. This library supports all of the
|
161
|
+
functionality described in our [API docs for
|
162
|
+
SignalFlow](https://developers.signalfx.com/reference#signalflowconnect). Right
|
163
|
+
now, the only supported transport mechanism is WebSockets.
|
164
|
+
|
165
|
+
To create a new SignalFlow client instance from an existing SignalFx client:
|
166
|
+
|
167
|
+
```ruby
|
168
|
+
signalflow = client.signalflow()
|
169
|
+
```
|
170
|
+
|
171
|
+
For the full API see [the RubyDocs for
|
172
|
+
the SignalFlow
|
173
|
+
client](http://www.rubydoc.info/github/signalfx/signalfx-ruby/master/SignalFlowClient/)
|
174
|
+
(the `signalflow` var above).
|
175
|
+
|
176
|
+
There is also [a demo script](./examples/signalflow.rb) that shows basic usage.
|
177
|
+
|
158
178
|
## License
|
159
179
|
|
160
180
|
Apache Software License v2. Copyright © 2015-2016
|
@@ -0,0 +1,32 @@
|
|
1
|
+
$LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
|
2
|
+
require './lib/signalfx'
|
3
|
+
|
4
|
+
token = ARGV[0] # Your SignalFx API access token
|
5
|
+
if token.nil? || token.empty?
|
6
|
+
puts '
|
7
|
+
SignalFx API access token not defined. Please specify token in command line.
|
8
|
+
$ ./signalflow.rb YOUR_TOKEN
|
9
|
+
|
10
|
+
'
|
11
|
+
exit 0
|
12
|
+
end
|
13
|
+
|
14
|
+
|
15
|
+
#create client instance with SignalFx API access token
|
16
|
+
client = SignalFx.new(token, enable_aws_unique_id: false, timeout: 3000)
|
17
|
+
|
18
|
+
puts 'SignalFlow demo:'
|
19
|
+
puts
|
20
|
+
|
21
|
+
signalflow = client.signalflow()
|
22
|
+
|
23
|
+
signalflow.execute("data('cpu.utilization').publish()").each_message do |msg, comp|
|
24
|
+
case msg[:type]
|
25
|
+
when "data"
|
26
|
+
puts "#{'Host'.center(40, ' ')} | cpu.utilization"
|
27
|
+
msg[:data].each do |tsid,value|
|
28
|
+
puts "#{comp.metadata[tsid][:host][0..40].center(40, ' ')} | #{value}"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
puts ""
|
32
|
+
end
|
data/lib/signalfx/conf.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
module RbConfig
|
4
4
|
# Default Parameters
|
5
5
|
DEFAULT_INGEST_ENDPOINT = 'https://ingest.signalfx.com'
|
6
|
+
DEFAULT_API_ENDPOINT = 'https://api.signalfx.com'
|
7
|
+
DEFAULT_STREAM_ENDPOINT = 'wss://stream.signalfx.com'
|
6
8
|
DEFAULT_BATCH_SIZE = 300 # Will wait for this many requests before posting
|
7
9
|
DEFAULT_TIMEOUT = 1
|
8
10
|
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative './version'
|
4
4
|
require_relative './conf'
|
5
|
+
require_relative './signalflow/client'
|
5
6
|
|
6
7
|
require 'net/http'
|
7
8
|
require 'uri'
|
@@ -30,12 +31,16 @@ class SignalFxClient
|
|
30
31
|
def initialize(api_token,
|
31
32
|
enable_aws_unique_id: false,
|
32
33
|
ingest_endpoint: RbConfig::DEFAULT_INGEST_ENDPOINT,
|
34
|
+
api_endpoint: RbConfig::DEFAULT_API_ENDPOINT,
|
35
|
+
stream_endpoint: RbConfig::DEFAULT_STREAM_ENDPOINT,
|
33
36
|
timeout: RbConfig::DEFAULT_TIMEOUT,
|
34
37
|
batch_size: RbConfig::DEFAULT_BATCH_SIZE,
|
35
38
|
user_agents: [])
|
36
39
|
|
37
40
|
@api_token = api_token
|
38
41
|
@ingest_endpoint = ingest_endpoint
|
42
|
+
@api_endpoint = api_endpoint
|
43
|
+
@stream_endpoint = stream_endpoint
|
39
44
|
@timeout = timeout
|
40
45
|
@batch_size = batch_size
|
41
46
|
@user_agents = user_agents
|
@@ -161,6 +166,15 @@ class SignalFxClient
|
|
161
166
|
post(build_event(data), @ingest_endpoint, EVENT_ENDPOINT_SUFFIX)
|
162
167
|
end
|
163
168
|
|
169
|
+
# Create a new SignalFlow client. A single client can execute multiple
|
170
|
+
# computations that will be multiplexed over the same WebSocket connection.
|
171
|
+
#
|
172
|
+
# @return [SignalFlowClient] a newly instantiated client, configured with the
|
173
|
+
# api token and endpoints from this class
|
174
|
+
def signalflow
|
175
|
+
SignalFlowClient.new(@api_token, @api_endpoint, @stream_endpoint)
|
176
|
+
end
|
177
|
+
|
164
178
|
protected
|
165
179
|
|
166
180
|
def get_queue
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# Copyright (C) 2017 SignalFx, Inc. All rights reserved.
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'zlib'
|
5
|
+
|
6
|
+
# Converts binary WebSocket messages into a hash
|
7
|
+
module BinaryMessageParser
|
8
|
+
# data should be a raw string
|
9
|
+
def parse(data)
|
10
|
+
# See https://developers.signalfx.com/v2/reference#section-binary-encoding-of-websocket-messages
|
11
|
+
version, message_type, flags, _, channel, payload = data.unpack("CCb8CZ16a*")
|
12
|
+
compressed = flags[0] == "1"
|
13
|
+
is_json = flags[1] == "1"
|
14
|
+
|
15
|
+
if version != 1
|
16
|
+
raise "Unsupported SignalFlow version #{version}"
|
17
|
+
end
|
18
|
+
|
19
|
+
if compressed
|
20
|
+
payload = Zlib::Inflate.new(16+Zlib::MAX_WBITS).inflate(payload)
|
21
|
+
end
|
22
|
+
|
23
|
+
raise "Unknown binary message type #{message_type}" if !is_json && message_type != 5
|
24
|
+
|
25
|
+
message = is_json ?
|
26
|
+
JSON.parse(payload, {:symbolize_names => true}) :
|
27
|
+
parse_data_payload(payload)
|
28
|
+
|
29
|
+
message.merge({:channel => channel})
|
30
|
+
end
|
31
|
+
module_function :parse
|
32
|
+
|
33
|
+
def parse_data_payload(payload)
|
34
|
+
# See https://developers.signalfx.com/v2/reference#section-binary-encoding-used-for-the-websocket
|
35
|
+
timestamp, element_count, tuples_raw = payload.unpack("Q>L>a*")
|
36
|
+
data_hash = (0..element_count-1).map do |i|
|
37
|
+
type, tsid, value_raw = tuples_raw[i*17..i*17+16].unpack("CQ>a8")
|
38
|
+
|
39
|
+
value = case type
|
40
|
+
when 1 # long
|
41
|
+
value_raw.unpack("q>")
|
42
|
+
when 2 # double
|
43
|
+
value_raw.unpack("G")
|
44
|
+
when 3 # int (32 bit)
|
45
|
+
value_raw.unpack("l>")
|
46
|
+
end
|
47
|
+
|
48
|
+
[
|
49
|
+
Base64.urlsafe_encode64([tsid].pack("Q>")).gsub("=", ""),
|
50
|
+
value[0],
|
51
|
+
]
|
52
|
+
end.to_h
|
53
|
+
|
54
|
+
{
|
55
|
+
:type => "data",
|
56
|
+
:logicalTimestampMs => timestamp,
|
57
|
+
:logicalTimestamp => Time.at(timestamp / 1000.0),
|
58
|
+
:data => data_hash,
|
59
|
+
}
|
60
|
+
end
|
61
|
+
module_function :parse_data_payload
|
62
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# Copyright (C) 2017 SignalFx, Inc. All rights reserved.
|
2
|
+
|
3
|
+
require_relative './queue'
|
4
|
+
|
5
|
+
class ChannelTimeout < Exception
|
6
|
+
end
|
7
|
+
|
8
|
+
# Channel represents a medium through which SignalFlow messages pass.
|
9
|
+
# The main method for it is {#each_message}, which is how you get messages from
|
10
|
+
# the channel. There can only be one user of a channel and they are NOT
|
11
|
+
# thread-safe.
|
12
|
+
#
|
13
|
+
# Channels are for one-time use only. Once a channel is detached from (either
|
14
|
+
# manually or due to the end of a computation) previous messages will be
|
15
|
+
# iterable but nothing new will show up.
|
16
|
+
class Channel
|
17
|
+
attr_accessor :name
|
18
|
+
attr_accessor :detached
|
19
|
+
|
20
|
+
def initialize(name, detach_cb)
|
21
|
+
@lock = Mutex.new
|
22
|
+
@detach_lock = Mutex.new
|
23
|
+
@detached = false
|
24
|
+
@name = name
|
25
|
+
@detach_from_transport = detach_cb
|
26
|
+
@messages = QueueWithTimeout.new
|
27
|
+
end
|
28
|
+
|
29
|
+
# Waits for and returns the next message in the channel.
|
30
|
+
#
|
31
|
+
# @param timeout_seconds [Float] Number of seconds to wait for a message.
|
32
|
+
#
|
33
|
+
# @return [Hash] The next message received by this channel. A return value
|
34
|
+
# of `nil` indicates that the channel has detected it is done and will not be
|
35
|
+
# receiving any more useful messages.
|
36
|
+
#
|
37
|
+
# @raise [ChannelTimeout] If the timeout is exceeded with no messages
|
38
|
+
def pop(timeout_seconds=nil)
|
39
|
+
raise "Channel #{@name} is detached" if @detached
|
40
|
+
|
41
|
+
msg = nil
|
42
|
+
begin
|
43
|
+
msg = @messages.pop_with_timeout(timeout_seconds)
|
44
|
+
rescue ThreadError
|
45
|
+
raise ChannelTimeout.new(
|
46
|
+
"Did not receive a message on channel #{@name} within #{timeout_seconds} seconds")
|
47
|
+
end
|
48
|
+
|
49
|
+
if msg[:event] == "END_OF_CHANNEL" || msg[:event] == "CONNECTION_CLOSED" || msg[:event] == "CHANNEL_ABORT"
|
50
|
+
# Mark this channel as detached and then return nil as an indicator that
|
51
|
+
# this channel is done
|
52
|
+
detach(false)
|
53
|
+
|
54
|
+
nil
|
55
|
+
else
|
56
|
+
msg
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
def detach(send_detach_to_server=true)
|
63
|
+
if !@detached
|
64
|
+
@detached = true
|
65
|
+
@detach_from_transport.call if send_detach_to_server
|
66
|
+
@detach_from_transport = nil
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def inject_message(msg)
|
71
|
+
# Since messages are injected by a separate websocket thread, they could
|
72
|
+
# come in after the user has detached manually from the channel. Just
|
73
|
+
# silently ignore them in that case.
|
74
|
+
return if @detached
|
75
|
+
raise 'Cannot inject nil message' if msg.nil?
|
76
|
+
|
77
|
+
@messages << msg
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# Copyright (C) 2017 SignalFx, Inc. All rights reserved.
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
|
5
|
+
require_relative "./websocket"
|
6
|
+
|
7
|
+
# A SignalFlow client that uses the WebSockets interface.
|
8
|
+
#
|
9
|
+
# See https://developers.signalfx.com/v2/reference#signalflowconnect for
|
10
|
+
# low-level API details and information on the SignalFlow language.
|
11
|
+
#
|
12
|
+
# See
|
13
|
+
# {https://github.com/signalfx/signalfx-ruby/blob/master/examples/signalflow.rb}
|
14
|
+
# for an example script that uses the client.
|
15
|
+
#
|
16
|
+
# The messages passed into the `computation.each_message*` blocks will be
|
17
|
+
# decoded forms of what is described in
|
18
|
+
# {https://developers.signalfx.com/v2/reference#information-messages-specification
|
19
|
+
# our API reference for SignalFlow}. Hash keys will be symbols instead of
|
20
|
+
# strings.
|
21
|
+
class SignalFlowClient
|
22
|
+
def initialize(api_token, api_endpoint, stream_endpoint)
|
23
|
+
@transport = SignalFlowWebsocketTransport.new(api_token, stream_endpoint)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Start a computation and attach to its output. If using WebSockets (the
|
27
|
+
# default), the channel name is handled internally so you do not need to
|
28
|
+
# supply it.
|
29
|
+
#
|
30
|
+
# See https://developers.signalfx.com/reference#section-execute-a-computation
|
31
|
+
#
|
32
|
+
# @option options [Fixnum] :start
|
33
|
+
# @option options [Fixnum] :stop
|
34
|
+
# @option options [Fixnum] :resolution
|
35
|
+
# @option options [Fixnum] :max_delay
|
36
|
+
# @option options [Boolean] :persistent
|
37
|
+
#
|
38
|
+
# @return [Computation] A {Computation} instance with an active channel
|
39
|
+
def execute(program, **options)
|
40
|
+
@transport.execute(program, **options)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Start and attach to a computation that tells how many times a detector
|
44
|
+
# would have fired in a time range between `start` and `stop`.
|
45
|
+
#
|
46
|
+
# See https://developers.signalfx.com/v2/reference#signalflowpreflight
|
47
|
+
#
|
48
|
+
# @param start [Fixnum]
|
49
|
+
# @param stop [Fixnum]
|
50
|
+
# @option options [Fixnum] :max_delay
|
51
|
+
#
|
52
|
+
# @return [Computation] A {Computation} instance with an active channel
|
53
|
+
def preflight(program, start, stop, **options)
|
54
|
+
@transport.preflight(program, start, stop, **options)
|
55
|
+
end
|
56
|
+
|
57
|
+
# Start a computation without attaching to it
|
58
|
+
#
|
59
|
+
# The `publish()` call in the program must specify a `metric` to publish the
|
60
|
+
# output to since you cannot currently attach to the output.
|
61
|
+
#
|
62
|
+
# Optional parameters are the same as {#execute}.
|
63
|
+
# @return [Computation] A {Computation} instance with a handle but without a
|
64
|
+
# channel
|
65
|
+
def start(program, **options)
|
66
|
+
@transport.start(program, **options)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Stop everything and close any open connections.
|
70
|
+
def close
|
71
|
+
@transport.close
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
# Copyright (C) 2017 SignalFx, Inc. All rights reserved.
|
2
|
+
|
3
|
+
|
4
|
+
STARTED_STATE = :started
|
5
|
+
ABORTED_STATE = :aborted
|
6
|
+
COMPLETED_STATE = :completed
|
7
|
+
DATA_STREAMING_STATE = :data_streaming
|
8
|
+
|
9
|
+
# Represents a SignalFlow computation/job. A computation can have a channel
|
10
|
+
# associated with it, but not necessarily (it could have been detached while
|
11
|
+
# the computation is still running or never attached in the first place). New
|
12
|
+
# instances should only be created by the client and a Computation MUST have a
|
13
|
+
# handle.
|
14
|
+
class Computation
|
15
|
+
attr_accessor :handle
|
16
|
+
attr_accessor :channel
|
17
|
+
attr_accessor :state
|
18
|
+
attr_accessor :metadata
|
19
|
+
attr_accessor :resolution
|
20
|
+
attr_accessor :input_timeseries_count
|
21
|
+
attr_accessor :last_timestamp_seen
|
22
|
+
|
23
|
+
def initialize(handle, attach_func, stop_func)
|
24
|
+
@handle = handle
|
25
|
+
@channel = nil
|
26
|
+
@attach_func = attach_func
|
27
|
+
@stop_func = stop_func
|
28
|
+
@metadata = {}
|
29
|
+
# We can't have a handle until the job is started so we must be at least
|
30
|
+
# at this state
|
31
|
+
@state = STARTED_STATE
|
32
|
+
|
33
|
+
@pending_messages = Queue.new
|
34
|
+
|
35
|
+
@batch_size_known = false
|
36
|
+
@expected_batch_size = 0
|
37
|
+
@current_batch_data = nil
|
38
|
+
@current_batch_size = nil
|
39
|
+
|
40
|
+
@last_timestamp_seen = nil
|
41
|
+
@resolution = nil
|
42
|
+
@input_timeseries_count = nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def channel=(channel)
|
46
|
+
@channel = channel
|
47
|
+
end
|
48
|
+
|
49
|
+
def attached?
|
50
|
+
!@channel.nil? && !@channel.detached
|
51
|
+
end
|
52
|
+
|
53
|
+
# Get the next message in this computation.
|
54
|
+
#
|
55
|
+
# @param timeout_seconds [Float] If a new message does not come within this
|
56
|
+
# interval, raises a {ChannelTimeout} exception. Note that this does not
|
57
|
+
# mean that this function will return within this interval since there may
|
58
|
+
# be messages received that are part of a larger batch. If nil, will block
|
59
|
+
# indefinitely.
|
60
|
+
def next_message(timeout_seconds=nil)
|
61
|
+
raise "Computation #{@handle} is not attached to a channel" unless @channel
|
62
|
+
|
63
|
+
msg = nil
|
64
|
+
while msg.nil? && !@channel.nil?
|
65
|
+
# process_message might return no messages if it is building up a batch
|
66
|
+
msg = process_message(@channel.pop(timeout_seconds))
|
67
|
+
end
|
68
|
+
return msg
|
69
|
+
end
|
70
|
+
|
71
|
+
# Iterates over the messages asynchronously for this computation. A convenience
|
72
|
+
# function if you want to fire off multiple computations simultaneously,
|
73
|
+
# though not terribly efficient since it starts a new thread that spends a
|
74
|
+
# lot of time waiting. However, since we don't have a way of "select"ing on
|
75
|
+
# computations, this is probably good enough for basic use.
|
76
|
+
#
|
77
|
+
# See {#each_message}.
|
78
|
+
def each_message_async(&block)
|
79
|
+
raise "Computation #{@handle} is not attached to a channel" unless @channel
|
80
|
+
|
81
|
+
Thread.new{ each_message(&block) }
|
82
|
+
return
|
83
|
+
end
|
84
|
+
|
85
|
+
# Call the given block with each message in the channel as they arrive. This
|
86
|
+
# method will not return until the channel is detached from (either manually
|
87
|
+
# or due to the computation ending).
|
88
|
+
#
|
89
|
+
# Messages are queued in the channel so that none will be lost if this method
|
90
|
+
# is not called immediately.
|
91
|
+
#
|
92
|
+
# @yield [msg, comp] Called when a message arrives that is relevant to the
|
93
|
+
# channel's computation. The `comp` param will be set to this computation
|
94
|
+
# instance for easy referencing of computation metadata and state. `comp`
|
95
|
+
# may be omitted if this reference to the computation is not needed.
|
96
|
+
def each_message(&block)
|
97
|
+
raise "Computation #{@handle} is not attached to a channel" unless @channel
|
98
|
+
|
99
|
+
while @channel
|
100
|
+
msg = next_message
|
101
|
+
block.call(msg, self)
|
102
|
+
end
|
103
|
+
|
104
|
+
return
|
105
|
+
end
|
106
|
+
|
107
|
+
# Process the given message
|
108
|
+
def process_message(msg)
|
109
|
+
# nil is like EOF for channels
|
110
|
+
if msg.nil?
|
111
|
+
@channel = nil
|
112
|
+
reset_current_batch
|
113
|
+
else
|
114
|
+
# Sniff messages and update computation
|
115
|
+
case msg[:type]
|
116
|
+
when "metadata"
|
117
|
+
@metadata[msg[:tsId]] = msg[:properties]
|
118
|
+
msg
|
119
|
+
|
120
|
+
when "expired-tsid"
|
121
|
+
@metadata.delete(msg[:tsId])
|
122
|
+
msg
|
123
|
+
|
124
|
+
when "control-message"
|
125
|
+
case msg[:event]
|
126
|
+
when "CHANNEL_ABORT"
|
127
|
+
@state = ABORTED_STATE
|
128
|
+
when "END_OF_CHANNEL"
|
129
|
+
@state = COMPLETED_STATE
|
130
|
+
end
|
131
|
+
|
132
|
+
msg
|
133
|
+
|
134
|
+
when "message"
|
135
|
+
# Don't let users see any messages of this type, but use them to update
|
136
|
+
# computation state that the user can access.
|
137
|
+
case msg[:messageCode]
|
138
|
+
when 'JOB_RUNNING_RESOLUTION'
|
139
|
+
@resolution = msg[:contents][:resolutionMs]
|
140
|
+
when 'FETCH_NUM_TIMESERIES'
|
141
|
+
@input_timeseries_count += msg[:numInputTimeSeries]
|
142
|
+
end
|
143
|
+
|
144
|
+
# The server guarantees that an initial batch of data will be sent before
|
145
|
+
# the first "message" message. Therefore, when we see a message of this
|
146
|
+
# type, we know we have determined the batch size.
|
147
|
+
@batch_size_known = true
|
148
|
+
# We also know that the current batch (if any) is done
|
149
|
+
reset_current_batch
|
150
|
+
|
151
|
+
when "data"
|
152
|
+
@state = DATA_STREAMING_STATE
|
153
|
+
|
154
|
+
# The expected batch size is the number of data messages received before
|
155
|
+
# either the first arrival of a "message" message or receiving two data
|
156
|
+
# messages with different logical timestamps.
|
157
|
+
if !@batch_size_known
|
158
|
+
@expected_batch_size += 1
|
159
|
+
end
|
160
|
+
|
161
|
+
out = nil
|
162
|
+
if @current_batch_data && msg.fetch(:logicalTimestampMs) != @current_batch_data.fetch(:logicalTimestampMs)
|
163
|
+
# Two data messages back to back with different timestamps before
|
164
|
+
# receiving the first "message" message indicate that the previous
|
165
|
+
# batch is done and our batch size is now whatever the total data
|
166
|
+
# messages seen up until this point.
|
167
|
+
@batch_size_known = true
|
168
|
+
out = reset_current_batch
|
169
|
+
add_to_current_batch(msg)
|
170
|
+
else
|
171
|
+
add_to_current_batch(msg)
|
172
|
+
if @batch_size_known && @current_batch_size == @expected_batch_size
|
173
|
+
out = reset_current_batch
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
out
|
178
|
+
|
179
|
+
when "error"
|
180
|
+
raise ComputationFailure.new(msg[:errors])
|
181
|
+
|
182
|
+
else
|
183
|
+
msg
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
private :process_message
|
188
|
+
|
189
|
+
# Add to the current batch, initializing the current batch if not already
|
190
|
+
# set.
|
191
|
+
def add_to_current_batch(msg)
|
192
|
+
if !@current_data_batch
|
193
|
+
@current_data_batch = msg
|
194
|
+
@current_batch_size = 1
|
195
|
+
else
|
196
|
+
@current_data_batch[:data].merge!(msg.fetch(:data))
|
197
|
+
@current_batch_size += 1
|
198
|
+
end
|
199
|
+
end
|
200
|
+
private :add_to_current_batch
|
201
|
+
|
202
|
+
# Resets the current batch, returning the previous value
|
203
|
+
def reset_current_batch
|
204
|
+
msg = @current_data_batch
|
205
|
+
@current_data_batch = nil
|
206
|
+
@current_batch_size = 0
|
207
|
+
@last_timestamp_seen = msg.fetch(:logicalTimestampMs) if !msg.nil?
|
208
|
+
msg
|
209
|
+
end
|
210
|
+
private :reset_current_batch
|
211
|
+
|
212
|
+
# Attach to an already running computation.
|
213
|
+
#
|
214
|
+
# *Not currently implemented on backend!*
|
215
|
+
#
|
216
|
+
# @return [Computation] This same computation instance with a now active
|
217
|
+
# channel attached to it.
|
218
|
+
def attach(**options)
|
219
|
+
raise "Computation #{@handle} is already attached!" if @channel
|
220
|
+
|
221
|
+
@channel = @attach_func.call(@handle, **options)
|
222
|
+
self
|
223
|
+
end
|
224
|
+
|
225
|
+
# Detach from this computation and remove reference to the channel to free up
|
226
|
+
# memory.
|
227
|
+
def detach
|
228
|
+
@channel.detach
|
229
|
+
@channel = nil
|
230
|
+
end
|
231
|
+
|
232
|
+
# Stop a computation
|
233
|
+
#
|
234
|
+
# See https://developers.signalfx.com/v2/reference#section-stop-a-computation
|
235
|
+
#
|
236
|
+
# @param reason [String] Reason for stopping the computation.
|
237
|
+
def stop(reason=nil)
|
238
|
+
@stop_func.call(@handle, reason)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
class ComputationFailure < Exception
|
243
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
# Borrowed from
|
4
|
+
# https://spin.atomicobject.com/2017/06/28/queue-pop-with-timeout-fixed/
|
5
|
+
|
6
|
+
class QueueWithTimeout
|
7
|
+
def initialize
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@queue = []
|
10
|
+
@received = ConditionVariable.new
|
11
|
+
end
|
12
|
+
|
13
|
+
def <<(x)
|
14
|
+
@mutex.synchronize do
|
15
|
+
@queue << x
|
16
|
+
@received.signal
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def pop(non_block = false)
|
21
|
+
pop_with_timeout(non_block ? 0 : nil)
|
22
|
+
end
|
23
|
+
|
24
|
+
def pop_with_timeout(timeout = nil)
|
25
|
+
@mutex.synchronize do
|
26
|
+
if timeout.nil?
|
27
|
+
# wait indefinitely until there is an element in the queue
|
28
|
+
while @queue.empty?
|
29
|
+
@received.wait(@mutex)
|
30
|
+
end
|
31
|
+
elsif @queue.empty? && timeout != 0
|
32
|
+
# wait for element or timeout
|
33
|
+
timeout_time = timeout + Time.now.to_f
|
34
|
+
while @queue.empty? && (remaining_time = timeout_time - Time.now.to_f) > 0
|
35
|
+
@received.wait(@mutex, remaining_time)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
#if we're still empty after the timeout, raise exception
|
39
|
+
raise ThreadError, "queue empty" if @queue.empty?
|
40
|
+
@queue.shift
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,317 @@
|
|
1
|
+
# Copyright (C) 2017 SignalFx, Inc. All rights reserved.
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'thread'
|
5
|
+
require 'websocket-client-simple'
|
6
|
+
require 'eventmachine'
|
7
|
+
|
8
|
+
require_relative './binary'
|
9
|
+
require_relative './channel'
|
10
|
+
require_relative './computation'
|
11
|
+
|
12
|
+
|
13
|
+
# A WebSocket transport for SignalFlow. This should not be used directly by
|
14
|
+
# end-users.
|
15
|
+
class SignalFlowWebsocketTransport
|
16
|
+
DETACHED = "DETACHED"
|
17
|
+
|
18
|
+
# A lower bound on the amount of time to wait for a computation to start
|
19
|
+
COMPUTATION_START_TIMEOUT_SECONDS = 30
|
20
|
+
|
21
|
+
def initialize(api_token, stream_endpoint)
|
22
|
+
@api_token = api_token
|
23
|
+
@stream_endpoint = stream_endpoint
|
24
|
+
@compress = true
|
25
|
+
|
26
|
+
@lock = Mutex.new
|
27
|
+
@close_reason = nil
|
28
|
+
reinit
|
29
|
+
end
|
30
|
+
|
31
|
+
def reinit
|
32
|
+
@ws = nil
|
33
|
+
@authenticated = false
|
34
|
+
@chan_callbacks = {}
|
35
|
+
|
36
|
+
name_lock = Mutex.new
|
37
|
+
num = 0
|
38
|
+
# Returns a unique channel name each time it is called
|
39
|
+
@channel_namer = ->{
|
40
|
+
name_lock.synchronize do
|
41
|
+
num += 1
|
42
|
+
"channel-#{num}"
|
43
|
+
end
|
44
|
+
}
|
45
|
+
end
|
46
|
+
private :reinit
|
47
|
+
|
48
|
+
# Starts a job (either execute or preflight) and waits until the JOB_START
|
49
|
+
# message is received with the computation handle arrives so that we can
|
50
|
+
# create a properly initialized computation object. Yields to the given
|
51
|
+
# block which should send the WS message to start the job.
|
52
|
+
def start_job
|
53
|
+
computation = nil
|
54
|
+
|
55
|
+
channel = make_new_channel
|
56
|
+
|
57
|
+
yield channel.name
|
58
|
+
|
59
|
+
while true
|
60
|
+
begin
|
61
|
+
msg = channel.pop(COMPUTATION_START_TIMEOUT_SECONDS)
|
62
|
+
rescue ChannelTimeout
|
63
|
+
raise "Computation did not start after at least #{COMPUTATION_START_TIMEOUT_SECONDS} seconds"
|
64
|
+
end
|
65
|
+
if msg[:type] == "error"
|
66
|
+
raise ComputationFailure.new(msg[:message])
|
67
|
+
end
|
68
|
+
|
69
|
+
# STREAM_START comes before this but contains no useful information
|
70
|
+
if msg[:event] == "JOB_START"
|
71
|
+
computation = Computation.new(msg[:handle], method(:attach), method(:stop))
|
72
|
+
computation.channel = channel
|
73
|
+
elsif msg[:type] == "computation-started"
|
74
|
+
computation = Computation.new(msg[:computationId], method(:attach), method(:stop))
|
75
|
+
# Start jobs only use the channel to get error messages and can
|
76
|
+
# detach from the channel once the job has started.
|
77
|
+
channel.detach
|
78
|
+
else
|
79
|
+
next
|
80
|
+
end
|
81
|
+
|
82
|
+
return computation
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def execute(program, start: nil, stop: nil, resolution: nil, max_delay: nil, persistent: nil)
|
87
|
+
start_job do |channel_name|
|
88
|
+
send_msg({
|
89
|
+
:type => "execute",
|
90
|
+
:channel => channel_name,
|
91
|
+
:program => program,
|
92
|
+
:start => start,
|
93
|
+
:stop => stop,
|
94
|
+
:resolution => resolution,
|
95
|
+
:max_delay => max_delay,
|
96
|
+
:persistent => persistent,
|
97
|
+
:compress => @compress,
|
98
|
+
}.reject!{|k,v| v.nil?}.to_json)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def preflight(program, start, stop, resolution: nil, max_delay: nil)
|
103
|
+
start_job do |channel_name|
|
104
|
+
send_msg({
|
105
|
+
:type => "preflight",
|
106
|
+
:channel => channel_name,
|
107
|
+
:program => program,
|
108
|
+
:start => start,
|
109
|
+
:stop => stop,
|
110
|
+
:resolution => resolution,
|
111
|
+
:max_delay => max_delay,
|
112
|
+
:compress => @compress,
|
113
|
+
}.reject!{|k,v| v.nil?}.to_json)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def start(program, start: nil, stop: nil, resolution: nil, max_delay: nil)
|
118
|
+
start_job do |channel_name|
|
119
|
+
send_msg({
|
120
|
+
:type => "start",
|
121
|
+
:channel => channel_name,
|
122
|
+
:program => program,
|
123
|
+
:start => start,
|
124
|
+
:stop => stop,
|
125
|
+
:resolution => resolution,
|
126
|
+
:max_delay => max_delay,
|
127
|
+
}.reject!{|k,v| v.nil?}.to_json)
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def stop(handle, reason)
|
132
|
+
send_msg({
|
133
|
+
:type => "stop",
|
134
|
+
:handle => handle,
|
135
|
+
:reason => reason,
|
136
|
+
}.reject!{|k,v| v.nil?}.to_json)
|
137
|
+
end
|
138
|
+
|
139
|
+
# This doesn't actually work on the backend yet
|
140
|
+
def attach(handle, filters: nil, resolution: nil)
|
141
|
+
channel = make_new_channel
|
142
|
+
|
143
|
+
send_msg({
|
144
|
+
:type => "attach",
|
145
|
+
:channel => channel.name,
|
146
|
+
:handle => handle,
|
147
|
+
:filters => filters,
|
148
|
+
:resolution => resolution,
|
149
|
+
:compress => @compress,
|
150
|
+
}.reject!{|k,v| v.nil?}.to_json)
|
151
|
+
|
152
|
+
channel
|
153
|
+
end
|
154
|
+
|
155
|
+
def detach(channel, reason=nil)
|
156
|
+
send_msg({
|
157
|
+
:type => "detach",
|
158
|
+
:channel => channel,
|
159
|
+
:reason => reason,
|
160
|
+
}.to_json)
|
161
|
+
|
162
|
+
# There is no response message from the server signifying detach complete
|
163
|
+
# and there could be messages coming in even after the detach request is
|
164
|
+
# sent. Therefore, use a sentinal value in place of the callback block so
|
165
|
+
# that the message receiver logic can distinguish this case from some
|
166
|
+
# anomolous case (say, due to bad logic in the code).
|
167
|
+
@chan_callbacks[channel] = DETACHED
|
168
|
+
end
|
169
|
+
|
170
|
+
def close
|
171
|
+
if @ws
|
172
|
+
@ws.close
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def send_msg(msg)
|
177
|
+
@lock.synchronize do
|
178
|
+
if @ws.nil?
|
179
|
+
startup_client
|
180
|
+
|
181
|
+
# Polling is the simplest and most robust way to handle blocking until
|
182
|
+
# authenticated. Using ConditionVariable requires more complex logic
|
183
|
+
# that gains very little in terms of efficiecy given how quick auth
|
184
|
+
# should be.
|
185
|
+
start_time = Time.now
|
186
|
+
while !@authenticated
|
187
|
+
# The socket will be closed by the server if auth isn't successful
|
188
|
+
# within 5 seconds so no point in waiting longer
|
189
|
+
if Time.now - start_time > 5 || @close_reason
|
190
|
+
raise "Could not authenticate to SignalFlow WebSocket: #{@close_reason}"
|
191
|
+
end
|
192
|
+
sleep 0.1
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
@ws.send(msg)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
private :send_msg
|
200
|
+
|
201
|
+
def on_close(msg)
|
202
|
+
@close_reason = "(#{msg.code}, #{msg.data})"
|
203
|
+
@chan_callbacks.keys.each do |channel_name|
|
204
|
+
invoke_callback_for_channel({ :event => "CONNECTION_CLOSED" }, channel_name)
|
205
|
+
end
|
206
|
+
|
207
|
+
reinit
|
208
|
+
end
|
209
|
+
|
210
|
+
def on_message(m)
|
211
|
+
begin
|
212
|
+
return if m.type == :ping
|
213
|
+
if m.type == :close
|
214
|
+
on_close(m)
|
215
|
+
return
|
216
|
+
end
|
217
|
+
|
218
|
+
message_received(m.data, m.type == :text)
|
219
|
+
rescue Exception => e
|
220
|
+
puts "Error processing SignalFlow message: #{e.backtrace.first}: #{e.message} (#{e.class})"
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def on_open
|
225
|
+
@ws.send({
|
226
|
+
:type => "authenticate",
|
227
|
+
:token => @api_token,
|
228
|
+
}.to_json)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Start up a new WS client in its own thread that runs an EventMachine
|
232
|
+
# reactor.
|
233
|
+
def startup_client
|
234
|
+
this = self
|
235
|
+
WebSocket::Client::Simple.connect("#{@stream_endpoint}/v2/signalflow/connect",
|
236
|
+
# Verification is disabled by default so this is essential
|
237
|
+
{verify_mode: OpenSSL::SSL::VERIFY_PEER}) do |ws|
|
238
|
+
@ws = ws
|
239
|
+
ws.on :error do |e|
|
240
|
+
puts "ERROR #{e.inspect}"
|
241
|
+
end
|
242
|
+
|
243
|
+
ws.on :close do |e|
|
244
|
+
this.on_close(e)
|
245
|
+
end
|
246
|
+
|
247
|
+
ws.on :message do |m|
|
248
|
+
this.on_message(m)
|
249
|
+
end
|
250
|
+
|
251
|
+
ws.on :open do
|
252
|
+
this.on_open
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
private :startup_client
|
257
|
+
|
258
|
+
def invoke_callback_for_channel(msg, channel_name)
|
259
|
+
chan = @chan_callbacks[channel_name]
|
260
|
+
|
261
|
+
raise "Callback for channel #{channel_name} is missing!" unless chan
|
262
|
+
|
263
|
+
if chan == DETACHED
|
264
|
+
return
|
265
|
+
else
|
266
|
+
chan.inject_message(msg)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
private :invoke_callback_for_channel
|
270
|
+
|
271
|
+
def message_received(raw_msg, is_text)
|
272
|
+
msg = add_parsed_timestamp!(parse_message(raw_msg, is_text))
|
273
|
+
|
274
|
+
if msg[:type] == "authenticated"
|
275
|
+
@authenticated = true
|
276
|
+
return
|
277
|
+
end
|
278
|
+
|
279
|
+
if msg[:channel]
|
280
|
+
invoke_callback_for_channel(msg, msg[:channel])
|
281
|
+
else
|
282
|
+
# Ignore keep-alives
|
283
|
+
if msg[:event] == "KEEP_ALIVE"
|
284
|
+
return
|
285
|
+
else
|
286
|
+
raise "Unknown SignalFlow message: #{msg}"
|
287
|
+
end
|
288
|
+
end
|
289
|
+
end
|
290
|
+
private :message_received
|
291
|
+
|
292
|
+
def parse_message(raw_msg, is_text)
|
293
|
+
if is_text
|
294
|
+
JSON.parse(raw_msg, {:symbolize_names => true})
|
295
|
+
else
|
296
|
+
BinaryMessageParser.parse(raw_msg)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
private :parse_message
|
300
|
+
|
301
|
+
def add_parsed_timestamp!(msg)
|
302
|
+
if msg.has_key?(:timestampMs)
|
303
|
+
msg[:timestamp] = Time.at(msg[:timestampMs] / 1000.0)
|
304
|
+
end
|
305
|
+
msg
|
306
|
+
end
|
307
|
+
private :add_parsed_timestamp!
|
308
|
+
|
309
|
+
def make_new_channel
|
310
|
+
name = @channel_namer.()
|
311
|
+
channel = Channel.new(name, ->(){ detach(name) })
|
312
|
+
@chan_callbacks[name] = channel
|
313
|
+
channel
|
314
|
+
end
|
315
|
+
private :make_new_channel
|
316
|
+
end
|
317
|
+
|
data/lib/signalfx/version.rb
CHANGED
data/signalfx.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["info@signalfx.com"]
|
11
11
|
|
12
12
|
spec.summary = "Ruby client library for SignalFx"
|
13
|
-
spec.description = "This is a programmatic interface in Ruby for SignalFx's metadata and ingest APIs. It is meant to provide a base for communicating with SignalFx APIs that can be easily leveraged by scripts and applications to interact with SignalFx or report metric and event data to SignalFx. Library supports Ruby 2.x versions"
|
13
|
+
spec.description = "This is a programmatic interface in Ruby for SignalFx's metadata and ingest APIs. It is meant to provide a base for communicating with SignalFx APIs that can be easily leveraged by scripts and applications to interact with SignalFx or report metric and event data to SignalFx. Library supports Ruby 2.2.x+ versions"
|
14
14
|
spec.homepage = "https://signalfx.com"
|
15
15
|
spec.license = "Apache Software License v2 © SignalFx"
|
16
16
|
|
@@ -33,6 +33,10 @@ Gem::Specification.new do |spec|
|
|
33
33
|
spec.add_development_dependency "rake", "~> 10.0"
|
34
34
|
spec.add_development_dependency "rspec", "~> 3.3"
|
35
35
|
spec.add_development_dependency "webmock", "~> 2.1"
|
36
|
+
spec.add_development_dependency "thin", "~> 1.7"
|
37
|
+
spec.add_development_dependency "pry"
|
38
|
+
spec.add_development_dependency "faye-websocket", "~> 0.10.7"
|
36
39
|
spec.add_dependency "protobuf", "~> 3.5.1", ">= 3.5.1"
|
37
40
|
spec.add_dependency "rest-client", "~> 2.0"
|
41
|
+
spec.add_dependency 'websocket-client-simple', "~> 0.3.0"
|
38
42
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: signalfx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- SignalFx, Inc
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-09-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -66,6 +66,48 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '2.1'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: thin
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.7'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.7'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: pry
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: faye-websocket
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.10.7
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.10.7
|
69
111
|
- !ruby/object:Gem::Dependency
|
70
112
|
name: protobuf
|
71
113
|
requirement: !ruby/object:Gem::Requirement
|
@@ -100,10 +142,24 @@ dependencies:
|
|
100
142
|
- - "~>"
|
101
143
|
- !ruby/object:Gem::Version
|
102
144
|
version: '2.0'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: websocket-client-simple
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - "~>"
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: 0.3.0
|
152
|
+
type: :runtime
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: 0.3.0
|
103
159
|
description: This is a programmatic interface in Ruby for SignalFx's metadata and
|
104
160
|
ingest APIs. It is meant to provide a base for communicating with SignalFx APIs
|
105
161
|
that can be easily leveraged by scripts and applications to interact with SignalFx
|
106
|
-
or report metric and event data to SignalFx. Library supports Ruby 2.x versions
|
162
|
+
or report metric and event data to SignalFx. Library supports Ruby 2.2.x+ versions
|
107
163
|
email:
|
108
164
|
- info@signalfx.com
|
109
165
|
executables: []
|
@@ -119,12 +175,19 @@ files:
|
|
119
175
|
- bin/console
|
120
176
|
- bin/setup
|
121
177
|
- examples/generic_usecase.rb
|
178
|
+
- examples/signalflow.rb
|
122
179
|
- lib/proto/signal_fx_protocol_buffers.pb.rb
|
123
180
|
- lib/signalfx.rb
|
124
181
|
- lib/signalfx/conf.rb
|
125
182
|
- lib/signalfx/json_signal_fx_client.rb
|
126
183
|
- lib/signalfx/protobuf_signal_fx_client.rb
|
127
184
|
- lib/signalfx/signal_fx_client.rb
|
185
|
+
- lib/signalfx/signalflow/binary.rb
|
186
|
+
- lib/signalfx/signalflow/channel.rb
|
187
|
+
- lib/signalfx/signalflow/client.rb
|
188
|
+
- lib/signalfx/signalflow/computation.rb
|
189
|
+
- lib/signalfx/signalflow/queue.rb
|
190
|
+
- lib/signalfx/signalflow/websocket.rb
|
128
191
|
- lib/signalfx/version.rb
|
129
192
|
- signalfx.gemspec
|
130
193
|
homepage: https://signalfx.com
|
@@ -147,7 +210,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
147
210
|
version: '0'
|
148
211
|
requirements: []
|
149
212
|
rubyforge_project:
|
150
|
-
rubygems_version: 2.
|
213
|
+
rubygems_version: 2.6.10
|
151
214
|
signing_key:
|
152
215
|
specification_version: 4
|
153
216
|
summary: Ruby client library for SignalFx
|