proby 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.swp
2
+ doc
3
+ .yardoc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source :rubygems
2
+
3
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,36 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ proby (2.0.0)
5
+ chronic (~> 0.6.7)
6
+ httparty (~> 0.8.1)
7
+ multi_json (~> 1.2.0)
8
+
9
+ GEM
10
+ remote: http://rubygems.org/
11
+ specs:
12
+ bluecloth (2.1.0)
13
+ chronic (0.6.7)
14
+ fakeweb (1.3.0)
15
+ httparty (0.8.3)
16
+ multi_json (~> 1.0)
17
+ multi_xml
18
+ json (1.6.6)
19
+ multi_json (1.2.0)
20
+ multi_xml (0.4.4)
21
+ rake (0.9.2.2)
22
+ shoulda (2.11.3)
23
+ yard (0.6.8)
24
+
25
+ PLATFORMS
26
+ ruby
27
+
28
+ DEPENDENCIES
29
+ bluecloth (~> 2.1.0)
30
+ bundler (>= 1.0.0)
31
+ fakeweb (~> 1.3.0)
32
+ json (~> 1.6.6)
33
+ proby!
34
+ rake (~> 0.9.0)
35
+ shoulda (~> 2.11.3)
36
+ yard (~> 0.6.4)
data/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # Proby
2
+ A simple library for working with the Proby task monitoring application.
3
+
4
+
5
+ Installation
6
+ ------------
7
+
8
+ ### RubyGems ###
9
+ Proby can be installed using RubyGems
10
+
11
+ gem install proby
12
+
13
+ Inside your script, be sure to
14
+
15
+ require "rubygems"
16
+ require "proby"
17
+
18
+ ### Bundler ###
19
+ If you're using Bundler, add the following to your Gemfile
20
+
21
+ gem "proby"
22
+
23
+ and then run
24
+
25
+ bundle install
26
+
27
+
28
+ Setup
29
+ -----
30
+ Before notifications can be sent, you must tell Proby your API key. This only needs to be done once,
31
+ and should ideally be done inside your apps initialization code.
32
+
33
+ Proby.api_key = "b4fe1200c105012efde3482a1411a947"
34
+
35
+ In addition, you can optionally give Proby a logger to use.
36
+
37
+ Proby.logger = Rails.logger
38
+
39
+
40
+ Sending Notifications
41
+ ---------------------
42
+ To send a start notification
43
+
44
+ Proby.send_start_notification(task_api_id)
45
+
46
+ To send a finish notification
47
+
48
+ Proby.send_finish_notification(task_api_id)
49
+
50
+ Specifying the `task_api_id` when calling the notification methods is optional. If it is not provided,
51
+ Proby will use the value of the `PROBY_TASK_ID` environment variable. If no task id is specified
52
+ in the method call, and no value is set in the `PROBY_TASK_ID` environment variable, then no notification
53
+ will be sent.
54
+
55
+
56
+ The Resque Plugin
57
+ -----------------
58
+ The Resque plugin will automatically send start and finish notifications to Proby when your job
59
+ starts and finishes. Simply `extend Proby::ResquePlugin` in your Resque job. The task id
60
+ can either be pulled from the `PROBY_TASK_ID` environment variable, or specified in the job itself
61
+ by setting the `@proby_id` attribute to the task id.
62
+
63
+ class SomeJob
64
+ extend Proby::ResquePlugin
65
+ @proby_id = 'abc123' # Or simply let it use the value in the PROBY_TASK_ID environment variable
66
+
67
+ self.perform
68
+ do_stuff
69
+ end
70
+ end
71
+
72
+
73
+ Managing Tasks
74
+ --------------
75
+ The Proby::ProbyTask class can be used to create, read, update, delete, pause, and unpause your
76
+ tasks on Proby.
77
+
78
+ my_tasks = Proby::ProbyTask.find(:all)
79
+ a_specific_task = Proby::ProbyTask.find("the_proby_task_id")
80
+
81
+ task = Proby::ProbyTask.create(:name => 'Task name', :crontab => '* * * * *')
82
+
83
+ task.name = "New name"
84
+ task.save
85
+
86
+ task.pause
87
+ task.unpause
88
+
89
+ task.delete
90
+
91
+
92
+ API Doc
93
+ -------
94
+ [http://rdoc.info/github/signal/proby-ruby/master/frames](http://rdoc.info/github/signal/proby-ruby/master/frames)
95
+
data/Rakefile ADDED
@@ -0,0 +1,38 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+
4
+ Bundler::GemHelper.install_tasks
5
+
6
+ begin
7
+ Bundler.setup(:default, :development)
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
+ require 'rake'
15
+ require 'rake/testtask'
16
+ require 'yard'
17
+
18
+ task :default => :test
19
+
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = ENV['TEST'] || "test/**/*_test.rb"
23
+ test.verbose = true
24
+ end
25
+
26
+ YARD::Rake::YardocTask.new do |t|
27
+ t.files = ['lib/**/*.rb']
28
+ end
29
+
30
+ desc 'Delete yard, and other generated files'
31
+ task :clobber => [:clobber_yard]
32
+
33
+ desc 'Delete yard generated files'
34
+ task :clobber_yard do
35
+ puts 'rm -rf doc .yardoc'
36
+ FileUtils.rm_rf ['doc', '.yardoc']
37
+ end
38
+
@@ -0,0 +1,13 @@
1
+ module Proby
2
+ # Exception raised when a request to Proby fails
3
+ class ApiException < StandardError; end
4
+
5
+ # Exception raised when the api key is not properly set
6
+ class InvalidApiKeyException < StandardError; end
7
+
8
+ # Authentication to Proby failed. Make sure your API key is correct.
9
+ class AuthFailedException < StandardError; end
10
+
11
+ # An invalid parameter was passed to the given method
12
+ class InvalidParameterException < StandardError; end
13
+ end
@@ -0,0 +1,28 @@
1
+ module Proby
2
+ class Notifier < ProbyHttpApi
3
+
4
+ def self.send_notification(type, proby_task_id, options={})
5
+ if Proby.api_key.nil?
6
+ Proby.logger.warn "Proby: No notification sent because API key is not set"
7
+ return nil
8
+ end
9
+
10
+ proby_task_id = ENV['PROBY_TASK_ID'] if blank?(proby_task_id)
11
+ if blank?(proby_task_id)
12
+ Proby.logger.warn "Proby: No notification sent because task ID was not specified"
13
+ return nil
14
+ end
15
+
16
+ response = post("/api/v1/tasks/#{proby_task_id}/#{type}.json",
17
+ :body => MultiJson.encode(options),
18
+ :format => :json,
19
+ :headers => default_headers)
20
+ response.code
21
+ rescue Exception => e
22
+ Proby.logger.error "Proby: Proby notification failed: #{e.message}"
23
+ Proby.logger.error e.backtrace
24
+ end
25
+
26
+ end
27
+ end
28
+
@@ -0,0 +1,28 @@
1
+ module Proby
2
+ class ProbyHttpApi
3
+ include HTTParty
4
+ base_uri "https://proby.signalhq.com"
5
+ default_timeout 5
6
+
7
+ protected
8
+
9
+ def self.handle_api_failure(response)
10
+ if response.code == 401
11
+ raise AuthFailedException.new("Authentication to Proby failed. Make sure your API key is correct.")
12
+ else
13
+ message = "API request failed with a response code of #{response.code}. Respone body: #{response.body}"
14
+ Proby.logger.error message
15
+ raise ApiException.new(message)
16
+ end
17
+ end
18
+
19
+ def self.default_headers
20
+ { 'api_key' => Proby.api_key, 'Content-Type' => 'application/json' }
21
+ end
22
+
23
+ def self.blank?(s)
24
+ s.nil? || s.strip.empty?
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,278 @@
1
+ module Proby
2
+
3
+ # Represents the status of a Proby task
4
+ class ProbyTaskStatus
5
+ # The description of the task (OK, ERROR, PAUSED)
6
+ attr_reader :description
7
+
8
+ # Any details (why the task is in the ERROR state)
9
+ attr_reader :details
10
+
11
+ def initialize(attributes={})
12
+ @description = attributes['description']
13
+ @details = attributes['details']
14
+ end
15
+ end
16
+
17
+ # Represents a task in Proby
18
+ class ProbyTask < ProbyHttpApi
19
+ # The name of the task
20
+ attr_accessor :name
21
+
22
+ # The schedule for the task, specified in crontab format
23
+ attr_accessor :crontab
24
+
25
+ # The time zone of the machine executing the task
26
+ attr_accessor :time_zone
27
+
28
+ # The name of the machine that is responsible for running this task
29
+ attr_accessor :machine
30
+
31
+ # Should finish alarms be sent when the task runs longer than expected?
32
+ attr_accessor :finish_alarms_enabled
33
+
34
+ # The maximum amount of time the task is allowed to run before Proby sends
35
+ # a finish alarm. If not specified, Proby will determine when an alarm should
36
+ # be sent based on past run times
37
+ attr_accessor :maximum_run_time
38
+
39
+ # The number of minutes to wait for a task to send its start notification after it
40
+ # should have started before sending an alarm
41
+ attr_accessor :start_notification_grace_period
42
+
43
+ # The number of consecutive tasks that must fail before an alarm is sent
44
+ attr_accessor :consecutive_alarmed_tasks_required_to_trigger_alarm
45
+
46
+ # The API Task ID of the task
47
+ attr_reader :api_id
48
+
49
+ # Is the task currently paused?
50
+ attr_reader :paused
51
+
52
+ # The number of consecutive times this task has triggered an alarm
53
+ attr_reader :consecutive_alarmed_tasks
54
+
55
+ # The date and time this task was created
56
+ attr_reader :created_at
57
+
58
+ # The date and time this task was updated
59
+ attr_reader :updated_at
60
+
61
+ # The current status of the task, represented as a ProbyTaskStatus
62
+ attr_reader :status
63
+
64
+ # <b>Should not be called directly</b>
65
+ def initialize(attributes={})
66
+ @name = attributes['name']
67
+ @api_id = attributes['api_id']
68
+ @crontab = attributes['crontab']
69
+ @paused = attributes['paused']
70
+ @time_zone = attributes['time_zone']
71
+ @machine = attributes['machine']
72
+ @finish_alarms_enabled = attributes['finish_alarms_enabled']
73
+ @maximum_run_time = attributes['maximum_run_time']
74
+ @start_notification_grace_period = attributes['start_notification_grace_period']
75
+ @consecutive_alarmed_tasks = attributes['consecutive_alarmed_tasks']
76
+ @consecutive_alarmed_tasks_required_to_trigger_alarm = attributes['consecutive_alarmed_tasks_required_to_trigger_alarm']
77
+ @created_at = Chronic.parse attributes['created_at']
78
+ @updated_at = Chronic.parse attributes['updated_at']
79
+ @status = ProbyTaskStatus.new(attributes['status']) if attributes['status']
80
+ end
81
+
82
+ # Get a single, or all tasks from Proby.
83
+ #
84
+ # @param [Object] param :all if you are fetching all tasks, or the api_id of the Proby task you would like to fetch.
85
+ #
86
+ # @return If requesting all tasks, an [Array<ProbyTask>] will be returned. If an api_id was provided, the
87
+ # ProbyTask with that api_id will be returned if it exists, or nil if it could not be found.
88
+ #
89
+ # @example
90
+ # all_of_my_tasks = ProbyTask.find(:all)
91
+ # my_task = ProbyTask.find('my_proby_task_api_id')
92
+ def self.find(param)
93
+ ensure_api_key_set
94
+ param == :all ? list : fetch(param)
95
+ end
96
+
97
+ # Create a new Proby task.
98
+ #
99
+ # @param [Hash] attributes The attributes for your task.
100
+ # @option attributes [String] :name A name for your task.
101
+ # @option attributes [String] :crontab The schedule of the task, specified in cron format.
102
+ # @option attributes [String] :time_zone <b>(Optional)</b> The time zone of the machine executing the task.
103
+ # @option attributes [String] :machine <b>(Optional)</b> The name of the machine that is responsible for running this task.
104
+ # Will default to the default time zone configured in Proby if not specified.
105
+ # @option attributes [Boolean] :finish_alarms_enabled <b>(Optional)</b> true if you would like to receive finish alarms for
106
+ # this task, false otherwise (default: true).
107
+ # @option attributes [Fixnum] :maximum_run_time <b>(Optional)</b> The maximum amount of time the task is allowed to run before
108
+ # Proby sends a finish alarm. If not specified, Proby will determine when an alarm should be
109
+ # sent based on past run times.
110
+ # @option attributes [Fixnum] :start_notification_grace_period <b>(Optional)</b> The number of minutes to wait for a task to
111
+ # send its start notification after it should have started before sending an alarm.
112
+ # @option attributes [Fixnum] :consecutive_alarmed_tasks_required_to_trigger_alarm <b>(Optional)</b> The number of consecutive
113
+ # tasks that must fail before an alarm is sent.
114
+ #
115
+ # @return [ProbyTask] The task that was created.
116
+ #
117
+ # @example
118
+ # proby_task = ProbyTask.create(:name => "My new task", :crontab => "* * * * *")
119
+ def self.create(attributes={})
120
+ ensure_api_key_set
121
+ raise InvalidParameterException.new("attributes are required") if attributes.nil? || attributes.empty?
122
+ raise InvalidParameterException.new("name is required") unless !blank?(attributes[:name]) || !blank?(attributes['name'])
123
+ raise InvalidParameterException.new("crontab is required") unless !blank?(attributes[:crontab]) || !blank?(attributes['crontab'])
124
+
125
+ Proby.logger.info "Creating task with attributes: #{attributes.inspect}"
126
+ response = post("/api/v1/tasks.json",
127
+ :format => :json,
128
+ :body => MultiJson.encode(:task => attributes),
129
+ :headers => default_headers)
130
+
131
+ if response.code == 201
132
+ new(response.parsed_response['task'])
133
+ else
134
+ handle_api_failure(response)
135
+ end
136
+ end
137
+
138
+ # Saves the task in Proby, updating all attributes to the values stored in the object. Only the attributes specified in
139
+ # the ProbyTask.create documentation can be updated.
140
+ #
141
+ # @example
142
+ # proby_task = ProbyTask.get('my_proby_task_api_id')
143
+ # proby_task.name = "Some other name"
144
+ # proby_task.crontab = "1 2 3 4 5"
145
+ # proby_task.save
146
+ def save
147
+ self.class.ensure_api_key_set
148
+ raise InvalidParameterException.new("name is required") if self.class.blank?(@name)
149
+ raise InvalidParameterException.new("crontab is required") if self.class.blank?(@crontab)
150
+
151
+ attributes = {
152
+ :name => @name,
153
+ :crontab => @crontab,
154
+ :time_zone => @time_zone,
155
+ :machine => @machine,
156
+ :finish_alarms_enabled => @finish_alarms_enabled,
157
+ :maximum_run_time => @maximum_run_time,
158
+ :start_notification_grace_period => @start_notification_grace_period,
159
+ :consecutive_alarmed_tasks_required_to_trigger_alarm => @consecutive_alarmed_tasks_required_to_trigger_alarm
160
+ }
161
+
162
+ Proby.logger.info "Updating task #{@api_id} with attributes: #{attributes.inspect}"
163
+ response = self.class.put("/api/v1/tasks/#{@api_id}.json",
164
+ :format => :json,
165
+ :body => MultiJson.encode(:task => attributes),
166
+ :headers => self.class.default_headers)
167
+
168
+ if response.code == 200
169
+ true
170
+ else
171
+ self.class.handle_api_failure(response)
172
+ end
173
+ end
174
+
175
+ # Delete a Proby task. The object will be frozen after the delete.
176
+ #
177
+ # @example
178
+ # proby_task = ProbyTask.get('my_proby_task_api_id')
179
+ # proby_task.delete
180
+ def delete
181
+ self.class.ensure_api_key_set
182
+
183
+ Proby.logger.info "Deleting task #{@api_id}"
184
+ response = self.class.delete("/api/v1/tasks/#{@api_id}.json",
185
+ :format => :json,
186
+ :headers => self.class.default_headers)
187
+
188
+ if response.code == 200
189
+ self.freeze
190
+ true
191
+ else
192
+ self.class.handle_api_failure(response)
193
+ end
194
+ end
195
+
196
+ # Pause a Proby task.
197
+ #
198
+ # @example
199
+ # proby_task = ProbyTask.get('my_proby_task_api_id')
200
+ # proby_task.pause
201
+ def pause
202
+ self.class.ensure_api_key_set
203
+
204
+ Proby.logger.info "Pausing task #{@api_id}"
205
+ response = self.class.post("/api/v1/tasks/#{@api_id}/pause.json",
206
+ :format => :json,
207
+ :headers => self.class.default_headers)
208
+
209
+ if response.code == 200
210
+ @paused = true
211
+ true
212
+ else
213
+ self.class.handle_api_failure(response)
214
+ end
215
+ end
216
+
217
+ # Unpause a Proby task.
218
+ #
219
+ # @example
220
+ # proby_task = ProbyTask.get('my_proby_task_api_id')
221
+ # proby_task.unpause
222
+ def unpause
223
+ self.class.ensure_api_key_set
224
+
225
+ Proby.logger.info "Unpausing task #{@api_id}"
226
+ response = self.class.post("/api/v1/tasks/#{@api_id}/unpause.json",
227
+ :format => :json,
228
+ :headers => self.class.default_headers)
229
+
230
+ if response.code == 200
231
+ @paused = false
232
+ true
233
+ else
234
+ self.class.handle_api_failure(response)
235
+ end
236
+ end
237
+
238
+ private
239
+
240
+ def self.list
241
+ Proby.logger.info "Getting the list of tasks"
242
+ response = get('/api/v1/tasks.json',
243
+ :format => :json,
244
+ :headers => default_headers)
245
+
246
+ if response.code == 200
247
+ data = response.parsed_response['tasks']
248
+ data.map { |task_data| new(task_data) }
249
+ else
250
+ handle_api_failure(response)
251
+ end
252
+ end
253
+
254
+ def self.fetch(api_id)
255
+ raise InvalidParameterException.new("api_id is required") if api_id.nil? || api_id.strip.empty?
256
+
257
+ Proby.logger.info "Fetching task from Proby: #{api_id}"
258
+ response = get("/api/v1/tasks/#{api_id}.json",
259
+ :format => :json,
260
+ :headers => default_headers)
261
+
262
+ if response.code == 200
263
+ new(response.parsed_response['task'])
264
+ elsif response.code == 404
265
+ nil
266
+ else
267
+ handle_api_failure(response)
268
+ end
269
+ end
270
+
271
+ def self.ensure_api_key_set
272
+ if Proby.api_key.nil? || Proby.api_key.strip.empty?
273
+ raise InvalidApiKeyException.new("Your Proby API key has not been set. Set it using Proby.api_key = 'my_api_key'")
274
+ end
275
+ end
276
+
277
+ end
278
+ end
@@ -0,0 +1,65 @@
1
+ module Proby
2
+ # Automatically notifies Proby when this job starts and finishes.
3
+ #
4
+ # class SomeJob
5
+ # extend Proby::ResquePlugin
6
+ #
7
+ # self.perform
8
+ # do_stuff
9
+ # end
10
+ # end
11
+ #
12
+ # The Proby Task ID can be set in one of two ways. The most common way is to
13
+ # put the ID in an ENV variable that is set in your crontab. This ID will be
14
+ # transparently passed to the Resque job via Redis.
15
+ #
16
+ # 0 0 * * * PROBY_TASK_ID=abc123 ./queue_some_job
17
+ #
18
+ # Alternatively, if you're not using cron and therefore don't want that
19
+ # support, you can just set the @proby_id ivar in the class, like so.
20
+ #
21
+ # class SomeJob
22
+ # extend Proby::ResquePlugin
23
+ # @proby_id = 'abc123'
24
+ #
25
+ # self.perform
26
+ # do_stuff
27
+ # end
28
+ # end
29
+ #
30
+ # Setting the @proby_id variable will take precendence over the ENV variable.
31
+ #
32
+ module ResquePlugin
33
+ def proby_id_bucket(*args)
34
+ "proby_id:#{name}-#{args.to_s}"
35
+ end
36
+
37
+ def before_enqueue_proby(*args)
38
+ return true if @proby_id
39
+
40
+ env_proby_id = ENV['PROBY_TASK_ID']
41
+ Resque.redis.setex(proby_id_bucket(*args), 24.hours, env_proby_id)
42
+ return true
43
+ end
44
+
45
+ def proby_id(*args)
46
+ @proby_id || Resque.redis.get(proby_id_bucket(*args))
47
+ end
48
+
49
+ def around_perform_proby(*args)
50
+ failed = false
51
+ error_message = nil
52
+ _proby_id = proby_id(*args)
53
+ Proby.send_start_notification(_proby_id)
54
+ yield
55
+ rescue Exception => e
56
+ failed = true
57
+ error_message = "#{e.class.name}: #{e.message}"
58
+ error_message << "\n#{e.backtrace.join("\n")}" if e.backtrace
59
+ raise e
60
+ ensure
61
+ Proby.send_finish_notification(_proby_id, :failed => failed, :error_message => error_message)
62
+ end
63
+ end
64
+ end
65
+
@@ -0,0 +1,3 @@
1
+ module Proby
2
+ VERSION = "2.0.0"
3
+ end
data/lib/proby.rb ADDED
@@ -0,0 +1,75 @@
1
+ require 'logger'
2
+ require 'httparty'
3
+ require 'chronic'
4
+
5
+ require 'proby/exceptions'
6
+ require 'proby/proby_http_api'
7
+ require 'proby/proby_task'
8
+ require 'proby/notifier'
9
+ require 'proby/resque_plugin'
10
+
11
+ module Proby
12
+
13
+ # A simple library for working with the Proby task monitoring application.
14
+ class << self
15
+
16
+ # Set your Proby API key.
17
+ #
18
+ # @param [String] api_key Your Proby API key
19
+ #
20
+ # @example
21
+ # Proby.api_key = '1234567890abcdefg'
22
+ def api_key=(api_key)
23
+ @api_key = api_key
24
+ end
25
+
26
+ # Get the api key.
27
+ def api_key
28
+ @api_key
29
+ end
30
+
31
+ # Set the logger to be used by Proby.
32
+ #
33
+ # @param [Logger] logger The logger you would like Proby to use
34
+ #
35
+ # @example
36
+ # Proby.logger = Rails.logger
37
+ # Proby.logger = Logger.new(STDERR)
38
+ def logger=(logger)
39
+ @logger = logger
40
+ end
41
+
42
+ # Get the logger used by Proby.
43
+ def logger
44
+ @logger ||= Logger.new("/dev/null")
45
+ end
46
+
47
+ # Send a start notification for this task to Proby.
48
+ #
49
+ # @param [String] proby_task_id The id of the task to be notified. If nil, the
50
+ # value of the +PROBY_TASK_ID+ environment variable will be used.
51
+ #
52
+ # @return [Fixnum] The HTTP status code that was returned from Proby.
53
+ def send_start_notification(proby_task_id=nil)
54
+ Notifier.send_notification('start', proby_task_id)
55
+ end
56
+
57
+ # Send a finish notification for this task to Proby
58
+ #
59
+ # @param [String] proby_task_id The id of the task to be notified. If nil, the
60
+ # value of the +PROBY_TASK_ID+ environment variable will be used.
61
+ # @param [Hash] options The options for the finish notification
62
+ # @option options [Boolean] :failed true if this task run resulted in some sort of failure. Setting
63
+ # this parameter to true will trigger a notification to be sent to
64
+ # the alarms configured for the given task. Defaults to false.
65
+ # @option options [String] :error_message A string message describing the failure that occurred.
66
+ # 1,000 character limit.
67
+ #
68
+ # @return [Fixnum] The HTTP status code that was returned from Proby.
69
+ def send_finish_notification(proby_task_id=nil, options={})
70
+ Notifier.send_notification('finish', proby_task_id, options)
71
+ end
72
+
73
+ end
74
+ end
75
+