fiveruns-dash-ruby 0.7.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.
@@ -0,0 +1,24 @@
1
+ module Fiveruns::Dash::Store
2
+
3
+ module File
4
+
5
+ def store_file(*uris)
6
+ uris.each do |uri|
7
+ directory = uri.path
8
+ write_to filename(directory)
9
+ end
10
+ end
11
+
12
+ def write_to(path)
13
+ ::File.open(path, 'w') { |f| f.write @payload.to_json }
14
+ end
15
+
16
+ def filename(directory)
17
+ kind = payload.class.name =~ /(\w+)Payload/
18
+ name = kind ? $1 : 'unknown'
19
+ ::File.join(directory, "#{guid}.#{name}.json")
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,198 @@
1
+ require 'net/https'
2
+ #require 'resolv'
3
+ require 'ostruct'
4
+
5
+ if defined?(Socket)
6
+ Socket.do_not_reverse_lookup=true
7
+ end
8
+
9
+ module Fiveruns::Dash::Store
10
+
11
+ module HTTP
12
+
13
+ def resolved_hostnames
14
+ Thread.current[:resolved_hostnames] ||= {}
15
+ Thread.current[:resolved_hostnames]
16
+ end
17
+
18
+ def resolved_hostname(hostname)
19
+ if resolved_hostnames[hostname] && Time.now < resolved_hostnames[hostname].next_update
20
+ ip = resolved_hostnames[hostname].ip
21
+ else
22
+ ip = hostname == 'localhost' ? '127.0.0.1' : IPSocket.getaddress(hostname)
23
+ ip_struct = OpenStruct.new(:ip => ip, :next_update => Time.now + 23.hours + rand(60).minutes)
24
+ resolved_hostnames[hostname] = ip_struct
25
+ end
26
+ ip
27
+ end
28
+
29
+
30
+ def store_http(*uris)
31
+ Fiveruns::Dash.logger.info "Attempting to send #{payload.class}"
32
+ if (uri = uris.detect { |u| transmit_to(add_path_to(u)) })
33
+ Fiveruns::Dash.logger.info "Sent #{payload.class} to #{uri}"
34
+ uri
35
+ else
36
+ Fiveruns::Dash.logger.warn "Could not send #{payload.class}"
37
+ false
38
+ end
39
+ end
40
+
41
+ def transmit_to(uri)
42
+ response = nil
43
+ safely do
44
+ http = Net::HTTP.new(resolved_hostname(uri.host), uri.port)
45
+ http.use_ssl = true if uri.scheme == 'https'
46
+ http.open_timeout = 10
47
+ http.read_timeout = 10
48
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
49
+ extra_params = extra_params_for(payload)
50
+ multipart = Multipart.new(payload.io, payload.params.merge(extra_params))
51
+ response = http.post(uri.request_uri, multipart.to_s, "Content-Type" => multipart.content_type, "Host" => uri.host)
52
+ end
53
+ check_response_of response
54
+ end
55
+
56
+ def safely
57
+ yield
58
+ rescue Exception => e
59
+ Fiveruns::Dash.logger.error "Could not access Dash service: #{e.message}"
60
+ Fiveruns::Dash.logger.error e.backtrace.join("\n\t")
61
+ false
62
+ end
63
+
64
+ def check_response_of(response)
65
+ unless response
66
+ Fiveruns::Dash.logger.debug "Received no response from Dash service"
67
+ return false
68
+ end
69
+ case response.code.to_i
70
+ when 201
71
+ data = JSON.load(response.body)
72
+ set_trace_contexts(data)
73
+ true
74
+ when 400..499
75
+ Fiveruns::Dash.logger.warn "Could not access Dash service (#{response.code.to_i}, #{response.body.inspect})"
76
+ false
77
+ else
78
+ Fiveruns::Dash.logger.debug "Received unknown response from Dash service (#{response.inspect})"
79
+ false
80
+ end
81
+ rescue JSON::ParserError => e
82
+ puts response.body
83
+ Fiveruns::Dash.logger.error "Received non-JSON response (#{response.inspect})"
84
+ false
85
+ end
86
+
87
+ def set_trace_contexts(data)
88
+ trace_contexts = data['traces']
89
+ if trace_contexts.is_a?(Array)
90
+ Fiveruns::Dash.trace_contexts = trace_contexts
91
+ end
92
+ end
93
+
94
+ def add_path_to(uri)
95
+ returning uri.dup do |new_uri|
96
+ path = case payload
97
+ when Fiveruns::Dash::PingPayload
98
+ ::File.join('/apps', app_token, "ping")
99
+ when Fiveruns::Dash::InfoPayload
100
+ ::File.join('/apps', app_token, "processes.json")
101
+ when Fiveruns::Dash::DataPayload
102
+ ::File.join('/apps', app_token, "metrics.json")
103
+ when Fiveruns::Dash::TracePayload
104
+ ::File.join('/apps', app_token, "traces.json")
105
+ when Fiveruns::Dash::ExceptionsPayload
106
+ ::File.join('/apps', app_token, "exceptions.json")
107
+ else
108
+ raise ArgumentError, 'Unknown payload type: #{payload.class}'
109
+ end
110
+ new_uri.path = path
111
+ end
112
+ end
113
+
114
+ def extra_params_for(payload)
115
+ case payload
116
+ when Fiveruns::Dash::ExceptionsPayload
117
+ {:app_id => app_token}
118
+ else
119
+ Hash.new
120
+ end
121
+ end
122
+
123
+ def normalize_key(key)
124
+ key.to_a.flatten.map(&:to_s).sort
125
+ end
126
+
127
+ def app_token
128
+ ::Fiveruns::Dash.configuration.options[:app]
129
+ end
130
+
131
+ class Multipart
132
+
133
+ BOUNDARY_ROOT = 'B0UND~F0R~UPL0AD'
134
+
135
+ attr_reader :file, :params
136
+ def initialize(file, params={})
137
+ @file = file
138
+ @params = params
139
+ end
140
+
141
+ def content_type
142
+ %(multipart/form-data, boundary="#{boundary}")
143
+ end
144
+
145
+ def to_s
146
+ %(#{parts}\r\n#{separator}--)
147
+ end
148
+
149
+ #######
150
+ private
151
+ #######
152
+
153
+ def boundary
154
+ "#{BOUNDARY_ROOT}*#{nonce}"
155
+ end
156
+
157
+ def parts
158
+ params.merge(:file => file).map do |name, value|
159
+ [
160
+ separator,
161
+ headers_for(name, value)
162
+ ].flatten.join(crlf) + crlf + crlf + content_of(value)
163
+ end.flatten.join(crlf)
164
+ end
165
+
166
+ def separator
167
+ %(--#{boundary})
168
+ end
169
+
170
+ def crlf
171
+ @crlf ||= "\r\n"
172
+ end
173
+
174
+ def headers_for(name, value)
175
+ if value.respond_to?(:read)
176
+ [
177
+ %(Content-Disposition: form-data; name="#{name}"; filename="metrics.json.gz"),
178
+ %(Content-Transfer-Encoding: binary),
179
+ %(Content-Type: application/octet-stream)
180
+ ]
181
+ else
182
+ [ %(Content-Disposition: form-data; name="#{name}") ]
183
+ end
184
+ end
185
+
186
+ def nonce
187
+ @nonce ||= (Time.now.utc.to_f * 1000).to_i
188
+ end
189
+
190
+ def content_of(value)
191
+ value.respond_to?(:read) ? value.read : value.to_s
192
+ end
193
+
194
+ end
195
+
196
+ end
197
+
198
+ end
@@ -0,0 +1,24 @@
1
+ require 'thread'
2
+
3
+ module Fiveruns::Dash
4
+
5
+ module Threads
6
+
7
+ def self.included(base)
8
+ base.extend ClassMethods
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ def monitor
14
+ @monitor ||= Monitor.new
15
+ end
16
+
17
+ def sync(&block)
18
+ monitor.synchronize(&block)
19
+ end
20
+ end
21
+
22
+ end
23
+
24
+ end
@@ -0,0 +1,65 @@
1
+ module Fiveruns::Dash
2
+
3
+ class Trace
4
+
5
+ attr_reader :context, :data, :stack
6
+ def initialize(context)
7
+ @context = context
8
+ @stack = []
9
+ validate!
10
+ end
11
+
12
+ def step(&block)
13
+ s = Step.new
14
+ @stack.last.children << s if !@stack.empty?
15
+ @stack << s
16
+ result = yield
17
+ last_step = @stack.pop
18
+ @data = last_step if @stack.empty?
19
+ result
20
+ end
21
+
22
+ def add_data(metric, contexts, value)
23
+ unless @stack.empty?
24
+ @stack.last.metrics.push(
25
+ metric.key.merge({:value => value, :contexts => contexts})
26
+ )
27
+ end
28
+ end
29
+
30
+ def to_json
31
+ { :context => context,
32
+ :data => (@data || {})
33
+ }.to_json
34
+ end
35
+
36
+ private
37
+
38
+ def validate!
39
+ unless @context.is_a?(Array) && @context.size % 2 == 0
40
+ raise ArgumentError, 'Invalid context: #{@context.inspect} (must be an array with an even number of elements)'
41
+ end
42
+ end
43
+
44
+ class Step
45
+
46
+ def metrics
47
+ @metrics ||= []
48
+ end
49
+
50
+ def children
51
+ @children ||= []
52
+ end
53
+
54
+ def to_json
55
+ {
56
+ :metrics => metrics,
57
+ :children => children,
58
+ }.to_json
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+
65
+ end
@@ -0,0 +1,29 @@
1
+ module Fiveruns::Dash
2
+
3
+ module Typable
4
+
5
+ def self.included(base)
6
+ name = base.name.demodulize.underscore
7
+ base.class_eval %{
8
+ def self.#{name}_type
9
+ @#{name}_type ||= name.demodulize.underscore.sub(/_#{name}$/, '').to_sym
10
+ end
11
+ }
12
+ base.extend ClassMethods
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def inherited(klass)
18
+ types[klass.__send__("#{name.demodulize.underscore}_type")] = klass
19
+ end
20
+
21
+ def types
22
+ @types ||= {}
23
+ end
24
+
25
+ end
26
+
27
+ end
28
+
29
+ end
@@ -0,0 +1,215 @@
1
+ require 'zlib'
2
+
3
+ require 'dash/store/http'
4
+ require 'dash/store/file'
5
+
6
+ module Fiveruns::Dash
7
+
8
+ class Pinger
9
+
10
+ attr_reader :payload
11
+ def initialize(payload)
12
+ @payload = payload
13
+ end
14
+
15
+ def ping(*urls)
16
+ try_urls(urls) do |url|
17
+ send_ping(url, payload)
18
+ end
19
+ end
20
+
21
+ def send_ping(url, payload)
22
+ begin
23
+ http = Net::HTTP.new(url.host, url.port)
24
+ http.use_ssl = true if url.scheme == 'https'
25
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
26
+ multipart = Fiveruns::Dash::Store::HTTP::Multipart.new(payload.io, payload.params)
27
+ response = http.post("/apps/#{token}/ping", multipart.to_s, "Content-Type" => multipart.content_type)
28
+ case response.code.to_i
29
+ when 201
30
+ data = JSON.load(response.body)
31
+ [:success, "Found application '#{data['name']}'"]
32
+ else
33
+ # Error message
34
+ [:failed, response.body.to_s]
35
+ end
36
+ rescue => e
37
+ [:error, e.message]
38
+ end
39
+ end
40
+
41
+ def token
42
+ ::Fiveruns::Dash.configuration.options[:app]
43
+ end
44
+
45
+ def try_urls(urls)
46
+ results = urls.map do |u|
47
+ result = yield(URI.parse(u))
48
+ case result[0]
49
+ when :success
50
+ puts "OK: #{result[1]}"
51
+ true
52
+ when :failed
53
+ puts "Failed talking to #{u}: #{result[1]}"
54
+ false
55
+ when :error
56
+ puts "Error contacting #{u}: #{result[1]}"
57
+ false
58
+ end
59
+ end
60
+ results.all?
61
+ end
62
+ end
63
+
64
+ class Update
65
+
66
+ include Store::HTTP
67
+ include Store::File
68
+
69
+ attr_reader :payload, :handler
70
+ def initialize(payload, &handler)
71
+ @payload = payload
72
+ @handler = handler
73
+ end
74
+
75
+ def store(*urls)
76
+ uris_by_scheme(urls).each do |scheme, uris|
77
+ value = __send__("store_#{storage_method_for(scheme)}", *uris)
78
+ return value if value
79
+ end
80
+ return false
81
+ end
82
+
83
+ def ping(*urls)
84
+ Pinger.new(payload).ping(*urls)
85
+ end
86
+
87
+ def guid
88
+ @guid ||= timestamp << "_#{Process.pid}"
89
+ end
90
+
91
+ #######
92
+ private
93
+ #######
94
+
95
+ def timestamp
96
+ Time.now.utc.strftime('%Y%m%d%H%M%S')
97
+ end
98
+
99
+ def uris_by_scheme(urls)
100
+ urls.map { |url| safe_parse(url) }.group_by(&:scheme)
101
+ end
102
+
103
+ def storage_method_for(scheme)
104
+ scheme =~ /^http/ ? :http : :file
105
+ end
106
+
107
+ def safe_parse(url)
108
+ url.respond_to?(:scheme) ? url : URI.parse(url)
109
+ end
110
+
111
+ end
112
+
113
+ class Payload
114
+
115
+ def initialize(data)
116
+ @version = Fiveruns::Dash::Version::STRING
117
+ @data = data
118
+ end
119
+
120
+ def io
121
+ returning StringIO.new do |io|
122
+ io.write compressed
123
+ io.rewind
124
+ end
125
+ end
126
+
127
+ def params
128
+ {}
129
+ end
130
+
131
+ def to_json
132
+ @data.to_json
133
+ end
134
+
135
+ #######
136
+ private
137
+ #######
138
+
139
+ def timestamp
140
+ Time.now.utc.rfc2822
141
+ end
142
+
143
+ def compressed
144
+ Zlib::Deflate.deflate(to_json)
145
+ end
146
+
147
+ end
148
+
149
+ class InfoPayload < Payload
150
+ def initialize(data, started_at)
151
+ super(data)
152
+ @started_at = started_at
153
+ end
154
+ def params
155
+ @params ||= begin
156
+ params = {
157
+ :type => 'info',
158
+ :ip => Fiveruns::Dash.host.ip_address,
159
+ :hostname => Fiveruns::Dash.host.hostname,
160
+ :pid => Process.pid,
161
+ :os_name => Fiveruns::Dash.host.os_name,
162
+ :os_version => Fiveruns::Dash.host.os_version,
163
+ :arch => Fiveruns::Dash.host.architecture,
164
+ :dash_version => Fiveruns::Dash::Version::STRING,
165
+ :ruby_version => RUBY_VERSION,
166
+ :started_at => @started_at
167
+ }
168
+ if (scm = Fiveruns::Dash.scm)
169
+ params.update(
170
+ :scm_revision => scm.revision,
171
+ :scm_time => scm.time,
172
+ :scm_type => scm.class.scm_type,
173
+ :scm_url => scm.url
174
+ )
175
+ end
176
+ params
177
+ end
178
+ end
179
+
180
+ end
181
+
182
+ class PingPayload < InfoPayload
183
+ end
184
+
185
+ class ExceptionsPayload < Payload
186
+ def params
187
+ @params ||= {
188
+ :type => 'exceptions',
189
+ :collected_at => timestamp,
190
+ :hostname => Fiveruns::Dash.host.hostname,
191
+ }
192
+ end
193
+ end
194
+
195
+ class DataPayload < Payload
196
+ def params
197
+ @params ||= {
198
+ :type => 'data',
199
+ :collected_at => timestamp,
200
+ :hostname => Fiveruns::Dash.host.hostname,
201
+ }
202
+ end
203
+ end
204
+
205
+ class TracePayload < Payload
206
+ def params
207
+ @params ||= {
208
+ :type => 'trace',
209
+ :collected_at => timestamp,
210
+ :hostname => Fiveruns::Dash.host.hostname,
211
+ }
212
+ end
213
+ end
214
+
215
+ end