cronitor 1.1.4 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 45f4d1fee0102bb736fe0034bccfef77b137302e
4
- data.tar.gz: 66ee857b37e9ebb1a87f0e4f9ba81bb47c633cca
2
+ SHA256:
3
+ metadata.gz: 0d5c6dc2547c52ed2ccee489b2ea5428c6badd902f996ea076cd196e6c4bb1c3
4
+ data.tar.gz: 57b37c91645625096fbe19004d181a6ed375517e02379b6d8365676c4d33839b
5
5
  SHA512:
6
- metadata.gz: 5ae75d0eba8956a41e612571dfdfb3acac7496274f3fa4007e1f90c25d57323ed5960784ca7f5d23bbec62c558b38da0543ade1048675b0a42e4621e1a2b67a3
7
- data.tar.gz: ece447cea65c579d744bc2104e5eb703531867d02dfa0c7e90569118dbdc760b36f618ecc9b1c6403f9777bb7615c3e7a8e5da51a2cf58bf3d677bcc639c59ed
6
+ metadata.gz: 349711cfbefe7cb0519e872862f9f54fd24d877850ac63714db59b62f4d7f7008bc39008fb74bca22eebb85c72d1e90e0fdf7c1a5fcb5ace205af63611440579
7
+ data.tar.gz: 10e8d151d5bd52f464aa92dcd737f1e29354699b66832f17f977b5608b4716532406ba21ac0d83e439732abad9558f46feb0fc0037e06b7ce79169622a36a283
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: Publish
3
+
4
+ on:
5
+ push:
6
+ tags:
7
+ - '*'
8
+
9
+ jobs:
10
+ publish:
11
+ needs:
12
+ - lint
13
+ - test
14
+
15
+ runs-on: ubuntu-latest
16
+
17
+ steps:
18
+ - uses: actions/checkout@v2
19
+ - name: Set up Ruby 2.7
20
+ uses: actions/setup-ruby@v1
21
+ with:
22
+ ruby-version: 2.7.x
23
+ - name: Build and test with Rake
24
+ run: |
25
+ gem install bundler
26
+ bundle install --jobs 4 --retry 3
27
+ bundle exec rake release
28
+ with:
29
+ api_key: ${{secrets.RUBYGEMS_API_KEY}}
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: Test
3
+
4
+ on:
5
+ push:
6
+ branches:
7
+ - master
8
+ pull_request:
9
+ branches:
10
+ - master
11
+
12
+ jobs:
13
+ lint:
14
+ strategy:
15
+ fail-fast: false
16
+ matrix:
17
+ ruby:
18
+ - '2.7'
19
+ - '3.0'
20
+
21
+ runs-on: ubuntu-latest
22
+
23
+ steps:
24
+ - uses: actions/checkout@v2
25
+ - name: Set up Ruby 2.7
26
+ uses: ruby/setup-ruby@v1
27
+ with:
28
+ ruby-version: ${{ matrix.ruby }}
29
+ bundler-cache: true
30
+ - name: Run Rubocop
31
+ run: bundle exec rake rubocop
32
+
33
+ spec:
34
+ strategy:
35
+ fail-fast: false
36
+ matrix:
37
+ ruby:
38
+ - '2.7'
39
+ - '3.0'
40
+
41
+ runs-on: ubuntu-latest
42
+
43
+ steps:
44
+ - uses: actions/checkout@v2
45
+ - name: Set up Ruby 2.7
46
+ uses: ruby/setup-ruby@v1
47
+ with:
48
+ ruby-version: ${{ matrix.ruby }}
49
+ bundler-cache: true
50
+ - name: Build and test with Rake
51
+ run: |
52
+ gem install bundler
53
+ bundle install --jobs 4 --retry 3
54
+ bundle exec rake
@@ -0,0 +1,41 @@
1
+ ---
2
+ require:
3
+ - rubocop-rake
4
+ - rubocop-rspec
5
+
6
+ inherit_mode:
7
+ merge:
8
+ - Exclude
9
+
10
+ AllCops:
11
+ NewCops: enable
12
+ Exclude:
13
+ - 'spec/cronitor_spec.rb'
14
+
15
+ Metrics/AbcSize:
16
+ Enabled: false
17
+
18
+ Metrics/ClassLength:
19
+ Enabled: false
20
+
21
+ Metrics/BlockLength:
22
+ Enabled: false
23
+
24
+ Metrics/CyclomaticComplexity:
25
+ Enabled: false
26
+
27
+ Metrics/PerceivedComplexity:
28
+ Enabled: false
29
+
30
+ Metrics/MethodLength:
31
+ Enabled: false
32
+
33
+ Style/Documentation:
34
+ Enabled: false
35
+
36
+ Style/RaiseArgs:
37
+ EnforcedStyle: compact
38
+
39
+ Style/EachWithObject:
40
+ Enabled: false
41
+
data/Gemfile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source 'https://rubygems.org'
2
4
 
3
5
  # Specify your gem's dependencies in cronitor.gemspec
data/README.md CHANGED
@@ -1,104 +1,230 @@
1
- # Cronitor
1
+ # Cronitor Ruby Library
2
2
 
3
- [![Travis](https://img.shields.io/travis/evertrue/cronitor.svg)](https://github.com/evertrue/cronitor)
3
+ ![Test](https://github.com/cronitorio/cronitor-ruby/workflows/Test/badge.svg)
4
4
  [![Gem Version](https://badge.fury.io/rb/cronitor.svg)](https://badge.fury.io/rb/cronitor)
5
5
 
6
- [Cronitor](https://cronitor.io/) is a service for heartbeat-style monitoring of just about anything that can send an HTTP request.
7
6
 
8
- This gem provides a simple abstraction for the creation and pinging of a Cronitor monitor. For a better understanding of the API this gem talks to, please see [How Cronitor Works](https://cronitor.io/help/how-cronitor-works).
7
+ [Cronitor](https://cronitor.io/) provides dead simple monitoring for cron jobs, daemons, data pipelines, queue workers, and anything else that can send or receive an HTTP request. The Cronitor Ruby library provides convenient access to the Cronitor API from applications written in Ruby.
8
+
9
+ ## Documentation
10
+ See our [API docs](https://cronitor.io/docs/api) for a detailed reference information about the APIs this library uses for configuring monitors and sending telemetry pings.
9
11
 
10
12
  ## Installation
11
13
 
12
- Add this line to your application's Gemfile:
14
+ ```
15
+ gem install cronitor
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ The package needs to be configured with your account's `API key`, which is available on the [account settings](https://cronitor.io/settings) page. You can also optionally specify an `API Version` (default: account default) and `Environment` (default: account default).
21
+
22
+ These can be supplied using the environment variables `CRONITOR_API_KEY`, `CRONITOR_API_VERSION`, `CRONITOR_ENVIRONMENT` or set directly on the cronitor object.
13
23
 
14
24
  ```ruby
15
- gem 'cronitor'
25
+ require 'cronitor'
26
+
27
+ Cronitor.api_key = 'apiKey123'
28
+ Cronitor.api_version = '2020-10-01'
29
+ Cronitor.environment = 'staging'
16
30
  ```
17
31
 
18
- And then execute:
32
+ You can also use a YAML config file to manage all of your monitors (_see Create and Update Monitors section below_). The path to this file can be supplied using the enviroment variable `CRONITOR_CONFIG` or call `cronitor.read_config()`.
19
33
 
20
- $ bundle
34
+ ```ruby
35
+ require 'cronitor'
21
36
 
22
- Or install it yourself as:
37
+ Cronitor.read_config('./path/to/cronitor.yaml')
38
+ ```
23
39
 
24
- $ gem install cronitor
25
40
 
26
- ## Usage
41
+ ### Monitor Any Block
27
42
 
28
- ### Creating a Monitor
43
+ The quickest way to start using this library is to wrap a block of code with the `#job` helper. It will report the start time, end time, and exit state to Cronitor. If an exception is raised, the stack trace will be included in the failure message.
29
44
 
30
- A Cronitor monitor (hereafter referred to only as a monitor for brevity) is created if it does not already exist, and its ID returned.
45
+ ```ruby
46
+ require 'cronitor'
47
+
48
+ Cronitor.job 'warehouse-replenishmenth-report' do
49
+ ReplenishmentReport.new(Date.today).run()
50
+ end
51
+ ```
52
+
53
+ ### Sending Telemetry Events
54
+
55
+ If you want finer control over when/how [telemetry pings](https://cronitor.io/docs/telemetry-api) are sent,
56
+ you can instantiate a monitor and call `#ping`.
31
57
 
32
58
  ```ruby
33
59
  require 'cronitor'
34
60
 
35
- monitor_options = {
36
- name: 'My Fancy Monitor',
37
- notifications: {
38
- emails: ['test@example.com'],
39
- slack: [],
40
- pagerduty: [],
41
- phones: [],
42
- webhooks: []
43
- },
44
- rules: [
45
- {
46
- rule_type: 'not_run_in',
47
- duration: 5
48
- rime_unit: 'seconds'
49
- }
50
- ],
51
- note: 'A human-friendly description of this monitor'
52
- }
53
- my_monitor = Cronitor.new token: 'api_token', opts: monitor_options
61
+
62
+ monitor = Cronitor::Monitor.new('heartbeat-monitor')
63
+
64
+ monitor.ping # a basic heartbeat event
65
+
66
+ # optional params can be passed as kwargs
67
+ # complete list - https://cronitor.io/docs/telemetry-api#parameters
68
+
69
+ monitor.ping(state: 'run', env: 'staging') # a job/process has started in a staging environment
70
+
71
+ # a job/process has completed - include metrics for cronitor to record
72
+ monitor.ping(state: 'complete', metrics: {count: 1000, error_count: 17})
54
73
  ```
55
74
 
56
- You may optionally specify a `:human_readable` value for your rule(s), otherwise one will be crafted for you:
75
+ ### Pause, Reset, Delete
57
76
 
58
77
  ```ruby
59
- monitor_options = {
60
- rules: [
61
- {
62
- rule_type: 'not_run_in',
63
- duration: 5
64
- time_unit: 'seconds',
65
- human_readable: 'not_run_in 5 seconds'
66
- }
67
- ],
78
+ require 'cronitor'
79
+
80
+ monitor = Cronitor::Monitor.new('heartbeat-monitor')
81
+
82
+ monitor.pause(24) # pause alerting for 24 hours
83
+ monitor.unpause # alias for .pause(0)
84
+ monitor.ok # manually reset to a passing state alias for monitor.ping({state: ok})
85
+ monitor.delete # destroy the monitor
68
86
  ```
69
87
 
70
- ### Pinging a Monitor
88
+ ## Create and Update Monitors
89
+
90
+ You can create monitors programatically using the `Monitor` object.
91
+ For details on all of the attributes that can be set see the [Monitor API](https://cronitor.io/docs/monitor-api) documentation.
71
92
 
72
- Once you’ve created a monitor, you can continue to use the existing instance of the object to ping the monitor that your task status: `run`, `complete`, or `fail`.
73
93
 
74
94
  ```ruby
75
- my_monitor.ping 'run'
76
- my_monitor.ping 'complete'
77
- my_monitor.ping 'fail', 'A short description of the failure'
95
+ require 'cronitor'
96
+
97
+ monitors = Cronitor::Monitor.put([
98
+ {
99
+ type: 'job',
100
+ key: 'send-customer-invoices',
101
+ schedule: '0 0 * * *',
102
+ assertions: [
103
+ 'metric.duration < 5 min'
104
+ ],
105
+ notify: ['devops-alerts-slack']
106
+ },
107
+ {
108
+ type: 'synthetic',
109
+ key: 'Orders Api Uptime',
110
+ schedule: 'every 45 seconds',
111
+ assertions: [
112
+ 'response.code = 200',
113
+ 'response.time < 1.5s',
114
+ 'response.json "open_orders" < 2000'
115
+ ]
116
+ }
117
+ ])
78
118
  ```
79
119
 
80
- ### Pinging a monitor when you have a Cronitor code
120
+ You can also manage all of your monitors via a YAML config file.
121
+ This can be version controlled and synced to Cronitor as part of
122
+ a deployment process or system update.
81
123
 
82
- You may already have the code for a monitor, in which case, the expense of `Cronitor.new` may seem unnecessary (since it makes an HTTP request to check if a monitor exists, and you already know it does).
124
+ ```ruby
125
+ require 'cronitor'
83
126
 
84
- In that case:
127
+ # read config file and set credentials (if included).
128
+ Cronitor.read_config('./cronitor.yaml')
85
129
 
86
- ```ruby
87
- my_monitor = Cronitor.new code: 'abcd'
130
+ # sync config file's monitors to Cronitor.
131
+ Cronitor.apply_config
132
+
133
+ # send config file's monitors to Cronitor to validate correctness.
134
+ # monitors will not be saved.
135
+ Cronitor.validate_config
88
136
  ```
89
137
 
90
- The aforementioned ping methods can now be used.
91
138
 
92
- ## Development
139
+ The `cronitor.yaml` file accepts the following attributes:
140
+
141
+ ```yaml
142
+ api_key: 'optionally read Cronitor api_key from here'
143
+ api_version: 'optionally read Cronitor api_version from here'
144
+ environment: 'optionally set an environment for telemetry pings'
145
+
146
+ # configure all of your monitors with type "job"
147
+ # you may omit the type attribute and the key
148
+ # of each object will be set as the monitor key
149
+ jobs:
150
+ nightly-database-backup:
151
+ schedule: 0 0 * * *
152
+ notify:
153
+ - devops-alert-pagerduty
154
+ assertions:
155
+ - metric.duration < 5 minutes
156
+
157
+ send-welcome-email:
158
+ schedule: every 10 minutes
159
+ assertions:
160
+ - metric.count > 0
161
+ - metric.duration < 30 seconds
162
+
163
+ # configure all of your monitors with type "synthetic"
164
+ synthetics:
165
+ cronitor-homepage:
166
+ request:
167
+ url: https://cronitor.io
168
+ regions:
169
+ - us-east-1
170
+ - eu-central-1
171
+ - ap-northeast-1
172
+ assertions:
173
+ - response.code = 200
174
+ - response.time < 2s
175
+
176
+ cronitor-telemetry-api:
177
+ request:
178
+ url: https://cronitor.link/ping
179
+ assertions:
180
+ - response.body contains ok
181
+ - response.time < .25s
182
+
183
+ events:
184
+ production-deploy:
185
+ notify:
186
+ alerts: ['deploys-slack']
187
+ events: true # send alert when the event occurs
93
188
 
94
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
189
+ ```
95
190
 
96
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
97
191
 
98
192
  ## Contributing
99
193
 
100
- 1. Fork it ( https://github.com/evertrue/cronitor/fork )
101
- 2. Create your feature branch (`git checkout -b my-new-feature`)
102
- 3. Commit your changes (`git commit -am 'Add some feature'`)
103
- 4. Push to the branch (`git push origin my-new-feature`)
104
- 5. Create a new Pull Request
194
+ Pull requests and features are happily considered! By participating in this project you agree to abide by the [Code of Conduct](http://contributor-covenant.org/version/2/0).
195
+
196
+ ### To contribute
197
+
198
+ Fork, then clone the repo:
199
+
200
+ git clone git@github.com:your-username/cronitor-ruby.git
201
+
202
+
203
+ Set up your machine:
204
+
205
+ bin/setup
206
+
207
+
208
+ Make sure the tests pass:
209
+
210
+ rake spec
211
+
212
+
213
+ Make your change. You can experiment using:
214
+
215
+ bin/console
216
+
217
+
218
+ Add tests for your change. Make the tests pass:
219
+
220
+ rake spec
221
+
222
+ Push to your fork and [submit a pull request]( https://github.com/cronitorio/cronitor-ruby/compare/)
223
+
224
+
225
+ ## Release a new version
226
+
227
+ The bump gem makes this easy:
228
+
229
+ 1. `rake bump:(major|minor|patch|pre)`
230
+ 2. `rake release`
data/Rakefile CHANGED
@@ -1,7 +1,15 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rspec/core/rake_task'
3
5
  require 'bump/tasks'
6
+ require 'rubocop/rake_task'
4
7
 
5
8
  RSpec::Core::RakeTask.new(:spec)
6
9
 
10
+ RuboCop::RakeTask.new do |task|
11
+ task.requires << 'rubocop-rake'
12
+ task.requires << 'rubocop-rspec'
13
+ end
14
+
7
15
  task default: :spec
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
4
  require 'bundler/setup'
4
5
  require 'cronitor'
@@ -1,15 +1,19 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+
3
+ lib = File.expand_path('lib', __dir__)
2
4
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
5
  require 'cronitor/version'
4
6
 
5
7
  Gem::Specification.new do |spec|
6
8
  spec.name = 'cronitor'
7
9
  spec.version = Cronitor::VERSION
8
- spec.authors = ['Jeff Byrnes']
9
- spec.email = ['thejeffbyrnes@gmail.com']
10
+ spec.authors = ['Jeff Byrnes', 'August Flanagan']
11
+ spec.email = ['thejeffbyrnes@gmail.com', 'august@cronitor.io']
10
12
 
11
13
  spec.summary = 'An interface for the Cronitor API'
12
- spec.homepage = 'https://github.com/evertrue/cronitor'
14
+ spec.homepage = 'https://github.com/cronitorio/cronitor-ruby'
15
+
16
+ spec.required_ruby_version = '>= 2.4'
13
17
 
14
18
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
15
19
  f.match(%r{^(test|spec|features)/})
@@ -18,14 +22,16 @@ Gem::Specification.new do |spec|
18
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
23
  spec.require_paths = ['lib']
20
24
 
21
- spec.add_dependency 'unirest', '~> 1.1'
22
- spec.add_dependency 'hashie', '~> 3.4'
25
+ spec.add_runtime_dependency 'httparty'
23
26
 
24
- spec.add_development_dependency 'bundler', '~> 1.10'
25
- spec.add_development_dependency 'rake', '~> 10.0'
26
- spec.add_development_dependency 'rspec', '~> 3.3'
27
- spec.add_development_dependency 'pry', '~> 0.10'
28
- spec.add_development_dependency 'webmock', '~> 1.21'
29
- spec.add_development_dependency 'sinatra', '~> 1.4'
30
27
  spec.add_development_dependency 'bump', '~> 0.1'
28
+ spec.add_development_dependency 'bundler'
29
+ spec.add_development_dependency 'pry', '~> 0.10'
30
+ spec.add_development_dependency 'rake'
31
+ spec.add_development_dependency 'rspec', '~> 3.3'
32
+ spec.add_development_dependency 'rubocop', '~> 1.8'
33
+ spec.add_development_dependency 'rubocop-rake', '~> 0.5.1'
34
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.1'
35
+ spec.add_development_dependency 'sinatra', '~> 2.0'
36
+ spec.add_development_dependency 'webmock', '~> 3.1'
31
37
  end
@@ -1,107 +1,87 @@
1
- require 'cronitor/version'
2
- require 'cronitor/error'
3
- require 'net/http'
4
- require 'unirest'
5
- require 'hashie'
6
-
7
- Unirest.default_header 'Accept', 'application/json'
8
- Unirest.default_header 'Content-Type', 'application/json'
1
+ # frozen_string_literal: true
9
2
 
10
- class Cronitor
11
- attr_accessor :token, :opts, :code
12
- API_URL = 'https://cronitor.io/v1'
13
- PING_URL = 'https://cronitor.link'
3
+ require 'logger'
4
+ require 'json'
5
+ require 'httparty'
6
+ require 'socket'
7
+ require 'time'
8
+ require 'yaml'
14
9
 
15
- def initialize(token: nil, opts: {}, code: nil)
16
- @token = token
17
- @opts = opts
18
- @code = code
19
-
20
- if @token.nil? && @code.nil?
21
- fail(
22
- Cronitor::Error,
23
- 'Either a Cronitor API token or an existing monitor code must be ' \
24
- 'provided'
10
+ require 'cronitor/config'
11
+ require 'cronitor/error'
12
+ require 'cronitor/version'
13
+ require 'cronitor/monitor'
14
+
15
+ module Cronitor
16
+ def self.read_config(path = nil, output: false)
17
+ Cronitor.config = path || Cronitor.config
18
+ unless Cronitor.config
19
+ raise ConfigurationError.new(
20
+ "Must include a path by setting Cronitor.config or passing a path to read_config e.g. \
21
+ Cronitor.read_config('./cronitor.yaml')"
25
22
  )
26
23
  end
27
24
 
28
- if @opts
29
- Hashie.symbolize_keys! @opts
30
- exists? @opts[:name] if @opts.key? :name
31
- human_readable @opts[:rules] if @opts.key? :rules
25
+ conf = YAML.safe_load(File.read(Cronitor.config))
26
+ conf.each do |k, _v|
27
+ raise ConfigurationError.new("Invalid configuration variable: #{k}") unless Cronitor::YAML_KEYS.include?(k)
32
28
  end
33
29
 
34
- create if @code.nil?
35
- end
36
-
37
- def create
38
- response = Unirest.post(
39
- "#{API_URL}/monitors",
40
- auth: { user: token },
41
- parameters: opts.to_json
42
- )
30
+ Cronitor.api_key = conf[:api_key] if conf[:api_key]
31
+ Cronitor.api_version = conf[:api_version] if conf[:api_version]
32
+ Cronitor.environment = conf[:environment] if conf[:environment]
43
33
 
44
- @code = response.body['code'] if valid? response
45
- end
34
+ return unless output
46
35
 
47
- def exists?(name)
48
- response = Unirest.get(
49
- "#{API_URL}/monitors/#{URI.escape(name).gsub('[', '%5B').gsub(']', '%5D')}",
50
- auth: { user: token }
51
- )
52
- return false unless response.code == 200
36
+ monitors = []
37
+ Cronitor::MONITOR_TYPES.each do |t|
38
+ plural_t = "#{t}s"
39
+ to_parse = conf[t] || conf[plural_t] || nil
40
+ next unless to_parse
53
41
 
54
- @code = response.body['code']
55
-
56
- true
57
- end
58
-
59
- def ping(type, msg = nil)
60
- url = "#{PING_URL}/#{code}/#{type}"
61
- url += "?msg=#{URI.escape msg}" if type == 'fail' && !msg.nil?
62
-
63
- response = Unirest.get url
64
- valid? response
65
- end
42
+ unless to_parse.is_a?(Hash)
43
+ raise ConfigurationError.new('A Hash with keys corresponding to monitor keys is expected.')
44
+ end
66
45
 
67
- def human_readable(rules)
68
- rules.each do |rule|
69
- unless rule[:human_readable]
70
- rule[:human_readable] = "#{rule[:rule_type]} #{rule[:duration]} " \
71
- "#{rule[:time_unit]}"
46
+ to_parse.each do |key, m|
47
+ m['key'] = key
48
+ m['type'] = t
49
+ monitors << m
72
50
  end
73
51
  end
52
+ conf['monitors'] = monitors
53
+ conf
74
54
  end
75
55
 
76
- private
77
-
78
- def valid?(response)
79
- return true if [200, 201].include? response.code
80
- server_error? response
56
+ def self.apply_config(rollback: false)
57
+ conf = read_config(output: true)
58
+ monitors = Monitor.put(monitors: conf.fetch('monitors', []), rollback: rollback)
59
+ puts("#{monitors.length} monitors #{rollback ? 'validated' : 'synced to Cronitor'}.")
60
+ rescue ValidationError => e
61
+ Cronitor.logger.error(e)
62
+ end
81
63
 
82
- fail Cronitor::Error, error_msg(response.body)
64
+ def self.validate_config
65
+ apply_config(rollback: true)
83
66
  end
84
67
 
85
- def error_msg(body, msg = [])
86
- body.each do |opt, value|
87
- if value.respond_to? 'each'
88
- value.each do |error_msg|
89
- msg << "#{opt}: #{error_msg}"
90
- end
91
- else
92
- msg << "#{opt}: #{value}"
93
- end
68
+ def self.job(key, &block)
69
+ monitor = Monitor.new(key)
70
+ series = Time.now.to_f
71
+ monitor.ping(state: 'run', series: series)
72
+
73
+ begin
74
+ block.call
75
+ monitor.ping(state: 'complete', series: series)
76
+ rescue StandardError => e
77
+ monitor.ping(state: 'fail', message: e.message[[0, e.message.length - 1600].max..-1], series: series)
78
+ raise e
94
79
  end
95
-
96
- msg.join ' '
97
80
  end
98
81
 
99
- def server_error?(response)
100
- return unless [301, 302, 404, 500, 502, 503, 504].include? response.code
101
-
102
- fail(
103
- Cronitor::Error,
104
- "Something else has gone awry. HTTP status: #{response.code}"
105
- )
82
+ def self.monitor_api_url
83
+ 'https://cronitor.io/api/monitors'
106
84
  end
107
85
  end
86
+
87
+ Cronitor.read_config(Cronitor.config) unless Cronitor.config.nil?
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cronitor
4
+ TYPE_JOB = 'job'
5
+ TYPE_EVENT = 'event'
6
+ TYPE_SYNTHETIC = 'synthetic'
7
+ MONITOR_TYPES = [TYPE_JOB, TYPE_EVENT, TYPE_SYNTHETIC].freeze
8
+ YAML_KEYS = %w[
9
+ api_key
10
+ api_version
11
+ environment
12
+ ] + MONITOR_TYPES.map { |t| "#{t}s" }
13
+
14
+ class << self
15
+ attr_accessor :api_key, :api_version, :environment, :logger, :config, :_headers
16
+
17
+ def configure(&block)
18
+ block.call(self)
19
+ end
20
+ end
21
+
22
+ self.api_key = ENV['CRONITOR_API_KEY']
23
+ self.api_version = ENV['CRONITOR_API_VERSION']
24
+ self.environment = ENV['CRONITOR_ENVIRONMENT']
25
+ self.config = ENV['CRONITOR_CONFIG']
26
+ self.logger = Logger.new($stdout)
27
+ logger.level = Logger::INFO
28
+ self._headers = {
29
+ 'Content-Type': 'application/json',
30
+ 'User-Agent': 'cronitor-ruby',
31
+ 'Cronitor-Version': Cronitor.api_version
32
+ }
33
+ end
@@ -1,4 +1,12 @@
1
- class Cronitor
1
+ # frozen_string_literal: true
2
+
3
+ module Cronitor
2
4
  class Error < StandardError
3
5
  end
6
+
7
+ class ValidationError < Error
8
+ end
9
+
10
+ class ConfigurationError < Error
11
+ end
4
12
  end
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Cronitor
4
+ class Monitor
5
+ attr_reader :key, :api_key, :api_version, :env
6
+
7
+ PING_RETRY_THRESHOLD = 5
8
+
9
+ def self.put(opts = {})
10
+ rollback = opts[:rollback] || false
11
+ opts.delete(:rollback)
12
+
13
+ monitors = opts[:monitors] || [opts]
14
+
15
+ resp = HTTParty.put(
16
+ Cronitor.monitor_api_url,
17
+ basic_auth: {
18
+ username: Cronitor.api_key,
19
+ password: ''
20
+ },
21
+ body: {
22
+ monitors: monitors,
23
+ rollback: rollback
24
+ }.to_json,
25
+ headers: Cronitor._headers,
26
+ timeout: 10
27
+ )
28
+
29
+ case resp.code
30
+ when 200
31
+ out = []
32
+ data = JSON.parse(resp.body)
33
+
34
+ (data['monitors'] || []).each do |md|
35
+ m = Monitor.new(md['key'])
36
+ m.data = Cronitor.symbolize_keys(md)
37
+ out << m
38
+ end
39
+ out.length == 1 ? out[0] : out
40
+ when 400
41
+ raise ValidationError.new(resp.body)
42
+ else
43
+ raise Error.new("Error connecting to Cronitor: #{resp.code}")
44
+ end
45
+ end
46
+
47
+ def self.delete(key)
48
+ resp = HTTParty.delete(
49
+ "#{Cronitor.monitor_api_url}/#{key}",
50
+ timeout: 10,
51
+ basic_auth: {
52
+ username: Cronitor.api_key,
53
+ password: ''
54
+ },
55
+ headers: Cronitor._headers
56
+ )
57
+ if resp.code != 204
58
+ Cronitor.logger.error("Error deleting monitor: #{key}")
59
+ return false
60
+ end
61
+ true
62
+ end
63
+
64
+ def initialize(key, api_key: nil, env: nil)
65
+ @key = key
66
+ @api_key = api_key || Cronitor.api_key
67
+ @env = env || Cronitor.environment
68
+ end
69
+
70
+ def data
71
+ return @data if defined?(@data)
72
+
73
+ @data = fetch
74
+ @data
75
+ end
76
+
77
+ def data=(data)
78
+ @data = Cronitor.symbolize_keys(data)
79
+ end
80
+
81
+ def ping(params = {})
82
+ retry_count = params[:retry_count] || 0
83
+ if api_key.nil?
84
+ Cronitor.logger.error('No API key detected. Set Cronitor.api_key or initialize Monitor with an api_key:')
85
+ return false
86
+ end
87
+
88
+ begin
89
+ ping_url = ping_api_url
90
+ ping_url = fallback_ping_api_url if retry_count > (PING_RETRY_THRESHOLD / 2)
91
+
92
+ response = HTTParty.get(
93
+ ping_url,
94
+ query: clean_params(params),
95
+ timeout: 5,
96
+ headers: Cronitor._headers,
97
+ query_string_normalizer: lambda do |query|
98
+ query.compact!
99
+ metrics = query[:metric]
100
+ query.delete(:metric)
101
+ out = query.map { |k, v| "#{k}=#{v}" }
102
+ out += metrics.map { |m| "metric=#{m}" } unless metrics.nil?
103
+ out.join('&')
104
+ end
105
+ # query_string_normalizer for metrics. instead of metric[]=foo:1 we want metric=foo:1
106
+ )
107
+
108
+ if response.code != 200
109
+ Cronitor.logger.error("Cronitor Telemetry Error: #{response.code}")
110
+ return false
111
+ end
112
+ true
113
+ rescue StandardError => e
114
+ # rescue instances of StandardError i.e. Timeout::Error, SocketError, etc
115
+ Cronitor.logger.error("Cronitor Telemetry Error: #{e}")
116
+ return false if retry_count >= Monitor::PING_RETRY_THRESHOLD
117
+
118
+ # apply a backoff before sending the next ping
119
+ sleep(retry_count * 1.5 * rand)
120
+ ping(params.merge(retry_count: params[:retry_count] + 1))
121
+ end
122
+ end
123
+
124
+ def ok
125
+ ping(state: 'ok')
126
+ end
127
+
128
+ def pause(hours = nil)
129
+ pause_url = "#{monitor_api_url}/pause"
130
+ pause_url += "/#{hours}" unless hours.nil?
131
+
132
+ resp = HTTParty.get(
133
+ pause_url,
134
+ timeout: 5,
135
+ headers: Cronitor._headers,
136
+ basic_auth: {
137
+ username: api_key,
138
+ password: ''
139
+ }
140
+ )
141
+ puts(resp.code)
142
+ resp.code == 200
143
+ end
144
+
145
+ def unpause
146
+ pause(0)
147
+ end
148
+
149
+ def ping_api_url
150
+ "https://cronitor.link/p/#{api_key}/#{key}"
151
+ end
152
+
153
+ def fallback_ping_api_url
154
+ "https://cronitor.io/p/#{api_key}/#{key}"
155
+ end
156
+
157
+ def monitor_api_url
158
+ "#{Cronitor.monitor_api_url}/#{key}"
159
+ end
160
+
161
+ private
162
+
163
+ def fetch
164
+ unless api_key
165
+ Cronitor.logger.error(
166
+ 'No API key detected. Set Cronitor.api_key or initialize Monitor with the api_key kwarg'
167
+ )
168
+ return
169
+ end
170
+
171
+ HTTParty.get(monitor_api_url, timeout: 10, headers: Cronitor._headers, format: :json)
172
+ end
173
+
174
+ def clean_params(params)
175
+ {
176
+ state: params.fetch(:state, nil),
177
+ message: params.fetch(:message, nil),
178
+ series: params.fetch(:series, nil),
179
+ host: params.fetch(:host, Socket.gethostname),
180
+ metric: params[:metrics] ? params[:metrics].map { |k, v| "#{k}:#{v}" } : nil,
181
+ stamp: Time.now.to_f,
182
+ env: params.fetch(:env, env)
183
+ }
184
+ end
185
+ end
186
+
187
+ def self.symbolize_keys(obj)
188
+ obj.inject({}) do |memo, (k, v)|
189
+ memo[k.to_sym] = v
190
+ memo
191
+ end
192
+ end
193
+ end
@@ -1,3 +1,5 @@
1
- class Cronitor
2
- VERSION = '1.1.4'
1
+ # frozen_string_literal: true
2
+
3
+ module Cronitor
4
+ VERSION = '4.0.0'
3
5
  end
metadata CHANGED
@@ -1,71 +1,86 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cronitor
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Byrnes
8
- autorequire:
8
+ - August Flanagan
9
+ autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2015-10-26 00:00:00.000000000 Z
12
+ date: 2021-01-22 00:00:00.000000000 Z
12
13
  dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
- name: unirest
15
+ name: httparty
15
16
  requirement: !ruby/object:Gem::Requirement
16
17
  requirements:
17
- - - "~>"
18
+ - - ">="
18
19
  - !ruby/object:Gem::Version
19
- version: '1.1'
20
+ version: '0'
20
21
  type: :runtime
21
22
  prerelease: false
22
23
  version_requirements: !ruby/object:Gem::Requirement
23
24
  requirements:
24
- - - "~>"
25
+ - - ">="
25
26
  - !ruby/object:Gem::Version
26
- version: '1.1'
27
+ version: '0'
27
28
  - !ruby/object:Gem::Dependency
28
- name: hashie
29
+ name: bump
29
30
  requirement: !ruby/object:Gem::Requirement
30
31
  requirements:
31
32
  - - "~>"
32
33
  - !ruby/object:Gem::Version
33
- version: '3.4'
34
- type: :runtime
34
+ version: '0.1'
35
+ type: :development
35
36
  prerelease: false
36
37
  version_requirements: !ruby/object:Gem::Requirement
37
38
  requirements:
38
39
  - - "~>"
39
40
  - !ruby/object:Gem::Version
40
- version: '3.4'
41
+ version: '0.1'
41
42
  - !ruby/object:Gem::Dependency
42
43
  name: bundler
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ - !ruby/object:Gem::Dependency
57
+ name: pry
43
58
  requirement: !ruby/object:Gem::Requirement
44
59
  requirements:
45
60
  - - "~>"
46
61
  - !ruby/object:Gem::Version
47
- version: '1.10'
62
+ version: '0.10'
48
63
  type: :development
49
64
  prerelease: false
50
65
  version_requirements: !ruby/object:Gem::Requirement
51
66
  requirements:
52
67
  - - "~>"
53
68
  - !ruby/object:Gem::Version
54
- version: '1.10'
69
+ version: '0.10'
55
70
  - !ruby/object:Gem::Dependency
56
71
  name: rake
57
72
  requirement: !ruby/object:Gem::Requirement
58
73
  requirements:
59
- - - "~>"
74
+ - - ">="
60
75
  - !ruby/object:Gem::Version
61
- version: '10.0'
76
+ version: '0'
62
77
  type: :development
63
78
  prerelease: false
64
79
  version_requirements: !ruby/object:Gem::Requirement
65
80
  requirements:
66
- - - "~>"
81
+ - - ">="
67
82
  - !ruby/object:Gem::Version
68
- version: '10.0'
83
+ version: '0'
69
84
  - !ruby/object:Gem::Dependency
70
85
  name: rspec
71
86
  requirement: !ruby/object:Gem::Requirement
@@ -81,72 +96,89 @@ dependencies:
81
96
  - !ruby/object:Gem::Version
82
97
  version: '3.3'
83
98
  - !ruby/object:Gem::Dependency
84
- name: pry
99
+ name: rubocop
85
100
  requirement: !ruby/object:Gem::Requirement
86
101
  requirements:
87
102
  - - "~>"
88
103
  - !ruby/object:Gem::Version
89
- version: '0.10'
104
+ version: '1.8'
90
105
  type: :development
91
106
  prerelease: false
92
107
  version_requirements: !ruby/object:Gem::Requirement
93
108
  requirements:
94
109
  - - "~>"
95
110
  - !ruby/object:Gem::Version
96
- version: '0.10'
111
+ version: '1.8'
97
112
  - !ruby/object:Gem::Dependency
98
- name: webmock
113
+ name: rubocop-rake
114
+ requirement: !ruby/object:Gem::Requirement
115
+ requirements:
116
+ - - "~>"
117
+ - !ruby/object:Gem::Version
118
+ version: 0.5.1
119
+ type: :development
120
+ prerelease: false
121
+ version_requirements: !ruby/object:Gem::Requirement
122
+ requirements:
123
+ - - "~>"
124
+ - !ruby/object:Gem::Version
125
+ version: 0.5.1
126
+ - !ruby/object:Gem::Dependency
127
+ name: rubocop-rspec
99
128
  requirement: !ruby/object:Gem::Requirement
100
129
  requirements:
101
130
  - - "~>"
102
131
  - !ruby/object:Gem::Version
103
- version: '1.21'
132
+ version: '2.1'
104
133
  type: :development
105
134
  prerelease: false
106
135
  version_requirements: !ruby/object:Gem::Requirement
107
136
  requirements:
108
137
  - - "~>"
109
138
  - !ruby/object:Gem::Version
110
- version: '1.21'
139
+ version: '2.1'
111
140
  - !ruby/object:Gem::Dependency
112
141
  name: sinatra
113
142
  requirement: !ruby/object:Gem::Requirement
114
143
  requirements:
115
144
  - - "~>"
116
145
  - !ruby/object:Gem::Version
117
- version: '1.4'
146
+ version: '2.0'
118
147
  type: :development
119
148
  prerelease: false
120
149
  version_requirements: !ruby/object:Gem::Requirement
121
150
  requirements:
122
151
  - - "~>"
123
152
  - !ruby/object:Gem::Version
124
- version: '1.4'
153
+ version: '2.0'
125
154
  - !ruby/object:Gem::Dependency
126
- name: bump
155
+ name: webmock
127
156
  requirement: !ruby/object:Gem::Requirement
128
157
  requirements:
129
158
  - - "~>"
130
159
  - !ruby/object:Gem::Version
131
- version: '0.1'
160
+ version: '3.1'
132
161
  type: :development
133
162
  prerelease: false
134
163
  version_requirements: !ruby/object:Gem::Requirement
135
164
  requirements:
136
165
  - - "~>"
137
166
  - !ruby/object:Gem::Version
138
- version: '0.1'
139
- description:
167
+ version: '3.1'
168
+ description:
140
169
  email:
141
170
  - thejeffbyrnes@gmail.com
171
+ - august@cronitor.io
142
172
  executables: []
143
173
  extensions: []
144
174
  extra_rdoc_files: []
145
175
  files:
146
176
  - ".editorconfig"
177
+ - ".github/workflows/publish.yml"
178
+ - ".github/workflows/test.yml"
147
179
  - ".gitignore"
148
180
  - ".rspec"
149
- - ".travis.yml"
181
+ - ".rubocop.yml"
150
182
  - Gemfile
151
183
  - LICENSE.txt
152
184
  - README.md
@@ -155,12 +187,14 @@ files:
155
187
  - bin/setup
156
188
  - cronitor.gemspec
157
189
  - lib/cronitor.rb
190
+ - lib/cronitor/config.rb
158
191
  - lib/cronitor/error.rb
192
+ - lib/cronitor/monitor.rb
159
193
  - lib/cronitor/version.rb
160
- homepage: https://github.com/evertrue/cronitor
194
+ homepage: https://github.com/cronitorio/cronitor-ruby
161
195
  licenses: []
162
196
  metadata: {}
163
- post_install_message:
197
+ post_install_message:
164
198
  rdoc_options: []
165
199
  require_paths:
166
200
  - lib
@@ -168,16 +202,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
202
  requirements:
169
203
  - - ">="
170
204
  - !ruby/object:Gem::Version
171
- version: '0'
205
+ version: '2.4'
172
206
  required_rubygems_version: !ruby/object:Gem::Requirement
173
207
  requirements:
174
208
  - - ">="
175
209
  - !ruby/object:Gem::Version
176
210
  version: '0'
177
211
  requirements: []
178
- rubyforge_project:
179
- rubygems_version: 2.4.6
180
- signing_key:
212
+ rubygems_version: 3.1.4
213
+ signing_key:
181
214
  specification_version: 4
182
215
  summary: An interface for the Cronitor API
183
216
  test_files: []
@@ -1,4 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.2
4
- before_install: gem install bundler -v 1.10