fiveruns-dash-ruby 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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