cronitor 3.1.0 → 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
2
  SHA256:
3
- metadata.gz: f4b5bc9e1efd7fb5247f7f473195589fb4cb822c52a1ee92f1b6c784086016f7
4
- data.tar.gz: f617431b175f6c2e9263fc48fece1af7d7bff634285d703e5d3d57d548f70236
3
+ metadata.gz: 0d5c6dc2547c52ed2ccee489b2ea5428c6badd902f996ea076cd196e6c4bb1c3
4
+ data.tar.gz: 57b37c91645625096fbe19004d181a6ed375517e02379b6d8365676c4d33839b
5
5
  SHA512:
6
- metadata.gz: 88a42cddedfea16c32fe0ab3716684343f7c6e8adcfa9cd5018a43e28c6e1e86ba9ced38cfe361e1ecc184dde146f2b8b222320ce17c2e8e1194c4d698d55e20
7
- data.tar.gz: 5ecb82ea549bd5a5ffb5d4298f493e40d3e2569d1517365156315a30f965fbe8b46629daf3bcae9cd029b93ad14c8363d6f06e0f543d8b26a0e270567446dc61
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
@@ -1,15 +1,41 @@
1
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
+
2
15
  Metrics/AbcSize:
3
16
  Enabled: false
4
17
 
18
+ Metrics/ClassLength:
19
+ Enabled: false
20
+
5
21
  Metrics/BlockLength:
6
22
  Enabled: false
7
23
 
8
24
  Metrics/CyclomaticComplexity:
9
25
  Enabled: false
10
26
 
27
+ Metrics/PerceivedComplexity:
28
+ Enabled: false
29
+
11
30
  Metrics/MethodLength:
12
31
  Enabled: false
13
32
 
14
33
  Style/Documentation:
15
34
  Enabled: false
35
+
36
+ Style/RaiseArgs:
37
+ EnforcedStyle: compact
38
+
39
+ Style/EachWithObject:
40
+ Enabled: false
41
+
data/README.md CHANGED
@@ -1,135 +1,230 @@
1
- # Cronitor
1
+ # Cronitor Ruby Library
2
2
 
3
- [![Travis](https://img.shields.io/travis/evertrue/cronitor.svg)](https://travis-ci.org/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.
9
8
 
10
- ## Installation
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.
11
11
 
12
- Add this line to your application's Gemfile:
12
+ ## Installation
13
13
 
14
- ```ruby
15
- gem 'cronitor'
14
+ ```
15
+ gem install cronitor
16
16
  ```
17
17
 
18
- And then execute:
18
+ ## Usage
19
19
 
20
- $ bundle
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
21
 
22
- Or install it yourself as:
22
+ These can be supplied using the environment variables `CRONITOR_API_KEY`, `CRONITOR_API_VERSION`, `CRONITOR_ENVIRONMENT` or set directly on the cronitor object.
23
23
 
24
- $ gem install cronitor
24
+ ```ruby
25
+ require 'cronitor'
25
26
 
26
- ## Usage
27
+ Cronitor.api_key = 'apiKey123'
28
+ Cronitor.api_version = '2020-10-01'
29
+ Cronitor.environment = 'staging'
30
+ ```
31
+
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()`.
27
33
 
34
+ ```ruby
35
+ require 'cronitor'
28
36
 
29
- ### Configure
37
+ Cronitor.read_config('./path/to/cronitor.yaml')
38
+ ```
30
39
 
31
- You need to set Cronitor Token in order to create a monitor
32
40
 
33
- #### Using configure
41
+ ### Monitor Any Block
42
+
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.
34
44
 
35
45
  ```ruby
36
46
  require 'cronitor'
37
47
 
38
- Cronitor.configure do |cronitor|
39
- cronitor.default_token = 'token' # default token to be re-used by cronitor
48
+ Cronitor.job 'warehouse-replenishmenth-report' do
49
+ ReplenishmentReport.new(Date.today).run()
40
50
  end
41
51
  ```
42
52
 
43
- #### Using ENV
53
+ ### Sending Telemetry Events
44
54
 
45
- ```
46
- # .env
47
- CRONITOR_TOKEN: token
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`.
57
+
58
+ ```ruby
59
+ require 'cronitor'
48
60
 
49
- # bash
50
- export CRONITOR_TOKEN='token'
51
- ```
52
61
 
53
- ### Creating a Monitor
62
+ monitor = Cronitor::Monitor.new('heartbeat-monitor')
54
63
 
55
- A Cronitor monitor (hereafter referred to only as a monitor for brevity) is created if it does not already exist, and its ID returned.
64
+ monitor.ping # a basic heartbeat event
56
65
 
57
- Please see the [Cronitor Monitor API docs](https://cronitor.io/docs/monitor-api) for details of all the possible monitor options.
66
+ # optional params can be passed as kwargs
67
+ # complete list - https://cronitor.io/docs/telemetry-api#parameters
58
68
 
59
- Example of creating a heartbeat monitor:
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})
73
+ ```
74
+
75
+ ### Pause, Reset, Delete
60
76
 
61
77
  ```ruby
62
78
  require 'cronitor'
63
79
 
64
- monitor_options = {
65
- name: 'My Fancy Monitor',
66
- type: 'heartbeat', # Optional: the gem defaults to this; the other value, 'healthcheck', is not yet supported by this gem
67
- notifications: {
68
- emails: ['test@example.com'],
69
- slack: [],
70
- pagerduty: [],
71
- phones: [],
72
- webhooks: []
73
- },
74
- rules: [
75
- {
76
- rule_type: 'run_ping_not_received',
77
- value: 5,
78
- time_unit: 'seconds'
79
- }
80
- ],
81
- note: 'A human-friendly description of this monitor'
82
- }
83
-
84
- # The token parameter is optional; if omittted, ENV['CRONITOR_TOKEN'] will be used if not configured
85
- my_monitor = Cronitor.new token: 'api_token', opts: monitor_options
86
- ```
80
+ monitor = Cronitor::Monitor.new('heartbeat-monitor')
87
81
 
88
- ### Updating an existing monitor
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
86
+ ```
89
87
 
90
- Currently this gem does not support updating or deleting an existing monitor.
88
+ ## Create and Update Monitors
91
89
 
92
- ### Pinging a Monitor
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.
93
92
 
94
- 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`.
95
93
 
96
94
  ```ruby
97
- my_monitor.ping 'run'
98
- my_monitor.ping 'complete'
99
- 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
+ ])
100
118
  ```
101
119
 
102
- ### 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.
103
123
 
104
- You may already have the code for a monitor, in which case, the expense of `Cronitor.create` 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'
105
126
 
106
- Cronitor does not require a token for pinging a monitor unless you have enabled Ping API authentication in your account settings. At the moment, this gem does not support Ping API auth.
127
+ # read config file and set credentials (if included).
128
+ Cronitor.read_config('./cronitor.yaml')
107
129
 
108
- In that case:
130
+ # sync config file's monitors to Cronitor.
131
+ Cronitor.apply_config
109
132
 
110
- ```ruby
111
- my_monitor = Cronitor.new code: 'abcd'
133
+ # send config file's monitors to Cronitor to validate correctness.
134
+ # monitors will not be saved.
135
+ Cronitor.validate_config
112
136
  ```
113
137
 
114
- The aforementioned ping methods can now be used.
115
138
 
116
- ## 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
117
188
 
118
- 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
+ ```
119
190
 
120
- 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).
121
191
 
122
192
  ## Contributing
123
193
 
124
- 1. Fork it ( https://github.com/evertrue/cronitor/fork )
125
- 2. Create your feature branch (`git checkout -b my-new-feature`)
126
- 3. Commit your changes (`git commit -am 'Add some feature'`)
127
- 4. Push to the branch (`git push origin my-new-feature`)
128
- 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
+
129
224
 
130
225
  ## Release a new version
131
226
 
132
- The `bump` gem makes this easy:
227
+ The bump gem makes this easy:
133
228
 
134
229
  1. `rake bump:(major|minor|patch|pre)`
135
- 2. `rake release`
230
+ 2. `rake release`
data/Rakefile CHANGED
@@ -3,7 +3,13 @@
3
3
  require 'bundler/gem_tasks'
4
4
  require 'rspec/core/rake_task'
5
5
  require 'bump/tasks'
6
+ require 'rubocop/rake_task'
6
7
 
7
8
  RSpec::Core::RakeTask.new(:spec)
8
9
 
10
+ RuboCop::RakeTask.new do |task|
11
+ task.requires << 'rubocop-rake'
12
+ task.requires << 'rubocop-rspec'
13
+ end
14
+
9
15
  task default: :spec
@@ -7,11 +7,13 @@ require 'cronitor/version'
7
7
  Gem::Specification.new do |spec|
8
8
  spec.name = 'cronitor'
9
9
  spec.version = Cronitor::VERSION
10
- spec.authors = ['Jeff Byrnes']
11
- spec.email = ['thejeffbyrnes@gmail.com']
10
+ spec.authors = ['Jeff Byrnes', 'August Flanagan']
11
+ spec.email = ['thejeffbyrnes@gmail.com', 'august@cronitor.io']
12
12
 
13
13
  spec.summary = 'An interface for the Cronitor API'
14
- spec.homepage = 'https://github.com/evertrue/cronitor'
14
+ spec.homepage = 'https://github.com/cronitorio/cronitor-ruby'
15
+
16
+ spec.required_ruby_version = '>= 2.4'
15
17
 
16
18
  spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
19
  f.match(%r{^(test|spec|features)/})
@@ -20,11 +22,16 @@ Gem::Specification.new do |spec|
20
22
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
23
  spec.require_paths = ['lib']
22
24
 
25
+ spec.add_runtime_dependency 'httparty'
26
+
23
27
  spec.add_development_dependency 'bump', '~> 0.1'
24
28
  spec.add_development_dependency 'bundler'
25
29
  spec.add_development_dependency 'pry', '~> 0.10'
26
30
  spec.add_development_dependency 'rake'
27
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'
28
35
  spec.add_development_dependency 'sinatra', '~> 2.0'
29
36
  spec.add_development_dependency 'webmock', '~> 3.1'
30
37
  end
@@ -1,138 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'cronitor/version'
4
- require 'cronitor/error'
3
+ require 'logger'
5
4
  require 'json'
6
- require 'net/http'
7
- require 'uri'
8
-
9
- class Cronitor
10
- attr_accessor :token, :opts, :code
11
- API_URL = 'https://cronitor.io/v3'
12
- PING_URL = 'https://cronitor.link'
13
-
14
- class << self
15
- attr_accessor :default_token
16
-
17
- def configure(&block)
18
- block.call(self)
19
- end
20
- end
21
-
22
- def initialize(token: ENV['CRONITOR_TOKEN'], opts: {}, code: nil)
23
- @token = token || self.class.default_token
24
- @opts = opts
25
- @code = code
5
+ require 'httparty'
6
+ require 'socket'
7
+ require 'time'
8
+ require 'yaml'
26
9
 
27
- if @token.nil? && @code.nil?
28
- raise(
29
- Cronitor::Error,
30
- 'Either a Cronitor API token or an existing monitor code must be ' \
31
- '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')"
32
22
  )
33
23
  end
34
24
 
35
- if @opts
36
- @opts = symbolize_keys @opts
37
-
38
- exists? @opts[:name] if @opts.key? :name
39
-
40
- # README: Per Cronitor API v2, we need to specify a type. The "heartbeat"
41
- # type corresponds to what the v1 API offered by default
42
- # We allow other values to be injected, and let the API handle
43
- # any errors.
44
- @opts[:type] = 'heartbeat' unless @opts[:type]
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)
45
28
  end
46
29
 
47
- create if @code.nil?
48
- end
49
-
50
- def create
51
- uri = URI.parse "#{API_URL}/monitors"
52
-
53
- http = Net::HTTP.new uri.host, uri.port
54
- http.use_ssl = uri.scheme == 'https'
55
-
56
- request = Net::HTTP::Post.new uri.path, default_headers
57
- request.basic_auth token, nil
58
- request.content_type = 'application/json'
59
- request.body = JSON.generate opts
60
-
61
- response = http.request request
62
-
63
- @code = JSON.parse(response.body).fetch 'code' if valid? response
64
- end
65
-
66
- def exists?(name)
67
- uri = URI.parse "#{API_URL}/monitors/#{URI.escape name}"
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]
68
33
 
69
- http = Net::HTTP.new uri.host, uri.port
70
- http.use_ssl = uri.scheme == 'https'
34
+ return unless output
71
35
 
72
- request = Net::HTTP::Get.new uri.path, default_headers
73
- request.basic_auth token, nil
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
74
41
 
75
- response = http.request request
76
-
77
- return false unless response.is_a? Net::HTTPSuccess
78
-
79
- @code = JSON.parse(response.body).fetch 'code'
80
- @opts = JSON.parse(response.body)
81
-
82
- true
83
- end
42
+ unless to_parse.is_a?(Hash)
43
+ raise ConfigurationError.new('A Hash with keys corresponding to monitor keys is expected.')
44
+ end
84
45
 
85
- def ping(type, msg = nil)
86
- uri = URI.parse "#{PING_URL}/#{URI.escape code}/#{URI.escape type}"
87
- if %w[run complete fail].include?(type) && !msg.nil?
88
- uri.query = URI.encode_www_form 'msg' => msg
46
+ to_parse.each do |key, m|
47
+ m['key'] = key
48
+ m['type'] = t
49
+ monitors << m
50
+ end
89
51
  end
90
-
91
- http = Net::HTTP.new uri.host, uri.port
92
- http.use_ssl = uri.scheme == 'https'
93
-
94
- request = Net::HTTP::Get.new uri, default_headers
95
-
96
- response = http.request request
97
-
98
- valid? response
52
+ conf['monitors'] = monitors
53
+ conf
99
54
  end
100
55
 
101
- private
102
-
103
- def valid?(response)
104
- return true if response.is_a? Net::HTTPSuccess
105
-
106
- msg = if response.content_type.match? 'json'
107
- error_msg JSON.parse(response.body)
108
- else
109
- "Something else has gone awry. HTTP status: #{response.code}"
110
- end
111
-
112
- raise Cronitor::Error, msg
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)
113
62
  end
114
63
 
115
- def error_msg(body, msg = [])
116
- body.each do |opt, value|
117
- if value.respond_to? 'each'
118
- value.each do |error_msg|
119
- msg << "#{opt}: #{error_msg}"
120
- end
121
- else
122
- msg << "#{opt}: #{value}"
123
- end
124
- end
125
-
126
- msg.join ' '
64
+ def self.validate_config
65
+ apply_config(rollback: true)
127
66
  end
128
67
 
129
- def default_headers
130
- { 'Accept' => 'application/json' }
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
79
+ end
131
80
  end
132
81
 
133
- def symbolize_keys(hash)
134
- hash.each_with_object({}) do |(k, v), h|
135
- h[k.to_sym] = v.is_a?(Hash) ? symbolize_keys(v) : v
136
- end
82
+ def self.monitor_api_url
83
+ 'https://cronitor.io/api/monitors'
137
84
  end
138
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,6 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Cronitor
3
+ module Cronitor
4
4
  class Error < StandardError
5
5
  end
6
+
7
+ class ValidationError < Error
8
+ end
9
+
10
+ class ConfigurationError < Error
11
+ end
6
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,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Cronitor
4
- VERSION = '3.1.0'
3
+ module Cronitor
4
+ VERSION = '4.0.0'
5
5
  end
metadata CHANGED
@@ -1,15 +1,30 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cronitor
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeff Byrnes
8
+ - August Flanagan
8
9
  autorequire:
9
10
  bindir: exe
10
11
  cert_chain: []
11
- date: 2020-10-23 00:00:00.000000000 Z
12
+ date: 2021-01-22 00:00:00.000000000 Z
12
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: httparty
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '0'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '0'
13
28
  - !ruby/object:Gem::Dependency
14
29
  name: bump
15
30
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +95,48 @@ dependencies:
80
95
  - - "~>"
81
96
  - !ruby/object:Gem::Version
82
97
  version: '3.3'
98
+ - !ruby/object:Gem::Dependency
99
+ name: rubocop
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: '1.8'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: '1.8'
112
+ - !ruby/object:Gem::Dependency
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
128
+ requirement: !ruby/object:Gem::Requirement
129
+ requirements:
130
+ - - "~>"
131
+ - !ruby/object:Gem::Version
132
+ version: '2.1'
133
+ type: :development
134
+ prerelease: false
135
+ version_requirements: !ruby/object:Gem::Requirement
136
+ requirements:
137
+ - - "~>"
138
+ - !ruby/object:Gem::Version
139
+ version: '2.1'
83
140
  - !ruby/object:Gem::Dependency
84
141
  name: sinatra
85
142
  requirement: !ruby/object:Gem::Requirement
@@ -111,15 +168,17 @@ dependencies:
111
168
  description:
112
169
  email:
113
170
  - thejeffbyrnes@gmail.com
171
+ - august@cronitor.io
114
172
  executables: []
115
173
  extensions: []
116
174
  extra_rdoc_files: []
117
175
  files:
118
176
  - ".editorconfig"
177
+ - ".github/workflows/publish.yml"
178
+ - ".github/workflows/test.yml"
119
179
  - ".gitignore"
120
180
  - ".rspec"
121
181
  - ".rubocop.yml"
122
- - ".travis.yml"
123
182
  - Gemfile
124
183
  - LICENSE.txt
125
184
  - README.md
@@ -128,9 +187,11 @@ files:
128
187
  - bin/setup
129
188
  - cronitor.gemspec
130
189
  - lib/cronitor.rb
190
+ - lib/cronitor/config.rb
131
191
  - lib/cronitor/error.rb
192
+ - lib/cronitor/monitor.rb
132
193
  - lib/cronitor/version.rb
133
- homepage: https://github.com/evertrue/cronitor
194
+ homepage: https://github.com/cronitorio/cronitor-ruby
134
195
  licenses: []
135
196
  metadata: {}
136
197
  post_install_message:
@@ -141,7 +202,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
141
202
  requirements:
142
203
  - - ">="
143
204
  - !ruby/object:Gem::Version
144
- version: '0'
205
+ version: '2.4'
145
206
  required_rubygems_version: !ruby/object:Gem::Requirement
146
207
  requirements:
147
208
  - - ">="
@@ -1,5 +0,0 @@
1
- ---
2
- language: ruby
3
- rvm:
4
- - 2
5
- - 2.4