jobtick 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e1a77c2ba34367f7c639da880b7a12daf835f113396548163cc5b3606b4d038a
4
- data.tar.gz: 5e2f95bab354b989cc682e2be8818bac35c65c613dc664ea102b0cbaba03af5c
3
+ metadata.gz: e01eac085feb7343b0410a4d7f4de783df293e27853ac8c1aae3428db83975b3
4
+ data.tar.gz: 76beab20811235bf0f47bd36015b3185d1b9515fb2d28fe9e465e518653481ee
5
5
  SHA512:
6
- metadata.gz: e663d7882568c102d137d2a95eab98bc95a8d0dbf2dd8848b72bf8e985fba5cf4ad7cda189207ae3bb7bb184bd2dbec42bf68a668d83d1b9b0c526fca5a03d52
7
- data.tar.gz: bca231c8c8ec3a5644dcc9d6f8815ad51ea70eb2a18f59967de1290824bb3ecba84a1c6fca803065818cf9c821a092d887f7b0f10ed9c9877a5bbe4da35a1a3d
6
+ metadata.gz: d3107ba0af4b2a1eb40ee86ccfd4ca92e08852467cdc7d0879b9ce772ba2ebf238c116759af69a7084f97513b441453e4920aa9e4f9ed63a5b67efaa9bf1b501
7
+ data.tar.gz: 3787f52f4ce0a05a6c78c4af25377526d51a4b8a0fa8e829917a308b51fe8e1c3ba63034bd24983a4f7a30f7fe2fa62c45b5bb761571a26987049e5b0294369d
data/README.md CHANGED
@@ -1,39 +1,144 @@
1
- # Jobtick
1
+ # JobTick
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ **Rails job monitoring for Whenever, Solid Queue, and Sidekiq.**
4
+ Know when your scheduled jobs stop running — before your users do.
4
5
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/jobtick`. To experiment with that code, run `bin/console` for an interactive prompt.
6
+ ---
7
+
8
+ ## The problem
9
+
10
+ Your error monitor catches exceptions. Mission Control shows you failed jobs.
11
+ Neither tells you when a recurring job **simply stops running**.
12
+
13
+ No exception. No alert. Nothing.
14
+
15
+ The Solid Queue scheduler process dies at 2am. Your nightly invoice job hasn't run in three days. Your users notice before you do.
16
+
17
+ JobTick solves this. It watches every scheduled job in your Rails app and alerts you the moment one goes silent — even when no error is raised.
18
+
19
+ ---
20
+
21
+ ## Why JobTick over Healthchecks.io or Cronitor?
22
+
23
+ Those tools work — but they require you to manually add a heartbeat ping to every job. Touch 15 job files. Name each monitor by hand. Keep names in sync when jobs are renamed or removed.
24
+
25
+ JobTick reads your existing config files and registers everything automatically.
26
+
27
+ ```
28
+ config/schedule.rb → Whenever jobs, auto-discovered
29
+ config/recurring.yml → Solid Queue recurring tasks, auto-discovered
30
+ sidekiq.yml → Sidekiq periodic jobs, auto-discovered
31
+ ```
32
+
33
+ Add the gem. Add your API key. Deploy. Done.
34
+
35
+ ---
6
36
 
7
37
  ## Installation
8
38
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
39
+ Add to your Gemfile:
40
+
41
+ ```ruby
42
+ gem 'jobtick'
43
+ ```
44
+
45
+ Create an initializer:
46
+
47
+ ```ruby
48
+ # config/initializers/jobtick.rb
49
+ JobTick.configure do |config|
50
+ config.api_key = ENV['JOBTICK_API_KEY']
51
+ end
52
+ ```
53
+
54
+ That's it. On next deploy, JobTick reads your schedule config, registers a monitor for every job, and starts tracking.
10
55
 
11
- Install the gem and add to the application's Gemfile by executing:
56
+ No changes to individual job files. No manual monitor creation. No names to keep in sync.
12
57
 
13
- ```bash
14
- bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
58
+ ---
59
+
60
+ ## What gets monitored
61
+
62
+ ### Whenever (`config/schedule.rb`)
63
+
64
+ ```ruby
65
+ # Your existing schedule.rb — no changes needed
66
+ every 1.day, at: '2:00 am' do
67
+ runner 'InvoiceJob.perform_later'
68
+ end
69
+
70
+ every :hour do
71
+ runner 'SyncInventoryJob.perform_later'
72
+ end
73
+ ```
74
+
75
+ JobTick reads this file at deploy time and creates a monitor for each job automatically.
76
+
77
+ ### Solid Queue (`config/recurring.yml`)
78
+
79
+ ```yaml
80
+ # Your existing recurring.yml — no changes needed
81
+ nightly_report:
82
+ class: NightlyReportJob
83
+ schedule: every day at 3am
84
+
85
+ sync_exchange_rates:
86
+ class: ExchangeRateJob
87
+ schedule: every hour
15
88
  ```
16
89
 
17
- If bundler is not being used to manage dependencies, install the gem by executing:
90
+ Each entry becomes a monitor. If `NightlyReportJob` doesn't run within its expected window, you get an alert.
91
+
92
+ ### Sidekiq periodic jobs
18
93
 
19
- ```bash
20
- gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
94
+ ```ruby
95
+ # Your existing Sidekiq config — no changes needed
96
+ Sidekiq.configure_server do |config|
97
+ config.periodic do |mgr|
98
+ mgr.register('0 * * * *', HourlyDigestWorker)
99
+ mgr.register('0 2 * * *', NightlyCleanupWorker)
100
+ end
101
+ end
21
102
  ```
22
103
 
23
- ## Usage
104
+ ---
24
105
 
25
- TODO: Write usage instructions here
106
+ ## What you get
26
107
 
27
- ## Development
108
+ **Silent failure detection** — alerts when a job stops running entirely, not just when it raises an exception. The failure mode your error monitor misses.
28
109
 
29
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
110
+ **Auto-sync on deploy** add a job to your schedule, it appears in your dashboard at next deploy. Remove a job, its monitor is automatically retired.
30
111
 
31
- 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
112
+ **Run history** see every execution: start time, duration, exit status. Spot when a job starts getting slower before it becomes a problem.
32
113
 
33
- ## Contributing
114
+ **Maintenance windows** — deploying at 3am? Snooze any monitor for a set period so you don't get paged for expected downtime.
34
115
 
35
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/jobtick.
116
+ **Team alerts** — email and Slack notifications. On-call rotation support on higher plans.
117
+
118
+ ---
119
+
120
+ ## Requirements
121
+
122
+ - Ruby >= 3.2
123
+ - Rails >= 7.0
124
+ - One or more of: Whenever, Solid Queue, Sidekiq
125
+
126
+ ---
127
+
128
+ ## Status
129
+
130
+ > **JobTick is currently in development.**
131
+ > Sign up for early access at [jobtick.app](https://jobtick.app).
132
+ > Launching June 2026.
133
+
134
+ If you want to follow along or give early feedback, open an issue or watch the repo.
135
+
136
+ ---
36
137
 
37
138
  ## License
38
139
 
39
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
140
+ MIT. See [LICENSE](LICENSE).
141
+
142
+ ---
143
+
144
+ *Built by [Clearstack Labs](https://clearstacklabs.com)*
data/Rakefile CHANGED
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- require "minitest/test_task"
4
+ require "rspec/core/rake_task"
5
5
 
6
- Minitest::TestTask.create
6
+ RSpec::Core::RakeTask.new(:spec)
7
7
 
8
8
  require "rubocop/rake_task"
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[test rubocop]
12
+ task default: %i[spec rubocop]
data/jobtick-0.0.1.gem ADDED
Binary file
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module JobTick
8
+ class Client
9
+ TIMEOUT = 5
10
+
11
+ def ping(monitor_key, status:, duration: nil, message: nil)
12
+ return unless JobTick.config.enabled
13
+ return if JobTick.config.api_key.nil?
14
+
15
+ payload = { status: status }
16
+ payload[:duration] = duration.round(3) if duration
17
+ payload[:message] = message if message
18
+
19
+ post("/ping/#{monitor_key}", payload)
20
+ end
21
+
22
+ def register(monitors)
23
+ return unless JobTick.config.enabled
24
+ return if JobTick.config.api_key.nil?
25
+
26
+ post("/monitors/sync", { monitors: monitors })
27
+ end
28
+
29
+ private
30
+
31
+ def post(path, body)
32
+ uri = URI("#{JobTick.config.endpoint}#{path}")
33
+ http = Net::HTTP.new(uri.host, uri.port)
34
+ http.use_ssl = uri.scheme == "https"
35
+ http.open_timeout = TIMEOUT
36
+ http.read_timeout = TIMEOUT
37
+
38
+ request = Net::HTTP::Post.new(uri)
39
+ request["Content-Type"] = "application/json"
40
+ request["Authorization"] = "Bearer #{JobTick.config.api_key}"
41
+ request["User-Agent"] = "jobtick-ruby/#{JobTick::VERSION}"
42
+ request.body = body.to_json
43
+
44
+ http.request(request)
45
+ rescue StandardError => e
46
+ JobTick.logger.warn("[JobTick] HTTP request failed (#{path}): #{e.message}")
47
+ nil
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobTick
4
+ class Configuration
5
+ attr_accessor :api_key, :endpoint, :environment, :enabled
6
+
7
+ def initialize
8
+ @endpoint = "https://api.jobtick.app/v1"
9
+ @environment = ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "production"
10
+ @enabled = @environment == "production"
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobTick
4
+ class Monitor
5
+ def self.run(key)
6
+ return yield unless JobTick.config.enabled
7
+
8
+ started_at = Time.now
9
+ JobTick.client.ping(key, status: :started)
10
+ result = yield
11
+ duration = Time.now - started_at
12
+ JobTick.client.ping(key, status: :completed, duration: duration)
13
+ result
14
+ rescue StandardError => e
15
+ JobTick.client.ping(key, status: :failed, message: e.message)
16
+ raise
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobTick
4
+ module Parsers
5
+ class Sidekiq
6
+ def self.parse
7
+ return [] unless defined?(::Sidekiq)
8
+ return parse_cron_jobs if defined?(::Sidekiq::Cron::Job)
9
+ return parse_periodic_jobs if defined?(::Sidekiq::Periodic::LoopSet)
10
+
11
+ []
12
+ rescue StandardError => e
13
+ JobTick.logger.warn("[JobTick] Sidekiq parser failed: #{e.message}")
14
+ []
15
+ end
16
+
17
+ def self.parse_cron_jobs
18
+ ::Sidekiq::Cron::Job.all.map do |job|
19
+ { key: "sidekiq.#{slugify(job.name)}", schedule: job.cron, source: "sidekiq", task: job.klass }
20
+ end
21
+ end
22
+ private_class_method :parse_cron_jobs
23
+
24
+ def self.parse_periodic_jobs
25
+ periodic = sidekiq_periodic_config
26
+ (periodic || []).map do |klass, opts|
27
+ { key: "sidekiq.#{slugify(klass.to_s)}", schedule: opts[:cron] || opts[:every].to_s,
28
+ source: "sidekiq", task: klass.to_s }
29
+ end
30
+ end
31
+ private_class_method :parse_periodic_jobs
32
+
33
+ def self.slugify(str)
34
+ str.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_+|_+\z/, "")
35
+ end
36
+ private_class_method :slugify
37
+
38
+ def self.sidekiq_periodic_config
39
+ if ::Sidekiq.respond_to?(:default_configuration)
40
+ ::Sidekiq.default_configuration[:periodic]
41
+ else
42
+ ::Sidekiq.options[:periodic]
43
+ end
44
+ end
45
+ private_class_method :sidekiq_periodic_config
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module JobTick
6
+ module Parsers
7
+ class SolidQueue
8
+ RECURRING_FILE = "config/recurring.yml"
9
+
10
+ def self.parse
11
+ return [] unless File.exist?(RECURRING_FILE)
12
+
13
+ yaml = YAML.load_file(RECURRING_FILE, aliases: true)
14
+ env = JobTick.config.environment
15
+
16
+ tasks = yaml[env] || yaml["default"] || yaml
17
+ return [] unless tasks.is_a?(Hash)
18
+
19
+ tasks.map do |key, config|
20
+ next unless config.is_a?(Hash)
21
+
22
+ {
23
+ key: "solid_queue.#{key}",
24
+ schedule: config["schedule"],
25
+ source: "solid_queue",
26
+ task: config["class"]
27
+ }
28
+ end.compact
29
+ rescue StandardError => e
30
+ JobTick.logger.warn("[JobTick] Solid Queue parser failed: #{e.message}")
31
+ []
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobTick
4
+ module Parsers
5
+ class Whenever
6
+ SCHEDULE_FILE = "config/schedule.rb"
7
+
8
+ def self.parse
9
+ return [] unless defined?(::Whenever)
10
+ return [] unless File.exist?(SCHEDULE_FILE)
11
+
12
+ schedule = ::Whenever::JobList.new(file: SCHEDULE_FILE)
13
+ schedule.jobs.flat_map do |period, jobs|
14
+ jobs.map do |job|
15
+ {
16
+ key: job_key(job),
17
+ schedule: period.to_s,
18
+ source: "whenever",
19
+ task: job[:task].to_s.strip
20
+ }
21
+ end
22
+ end
23
+ rescue StandardError => e
24
+ JobTick.logger.warn("[JobTick] Whenever parser failed: #{e.message}")
25
+ []
26
+ end
27
+
28
+ def self.job_key(job)
29
+ task = job[:task].to_s.strip
30
+ slug = task.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/\A_+|_+\z/, "")
31
+ "whenever.#{slug}"
32
+ end
33
+ private_class_method :job_key
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobTick
4
+ class Railtie < Rails::Railtie
5
+ initializer "jobtick.sync_registry" do
6
+ ActiveSupport.on_load(:after_initialize) do
7
+ next unless JobTick.config.enabled
8
+
9
+ JobTick::Registry.sync
10
+ end
11
+ end
12
+
13
+ rake_tasks do
14
+ load File.expand_path("../tasks/jobtick.rake", __dir__)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JobTick
4
+ class Registry
5
+ def self.sync
6
+ monitors = [
7
+ Parsers::Whenever.parse,
8
+ Parsers::SolidQueue.parse,
9
+ Parsers::Sidekiq.parse
10
+ ].flatten.compact
11
+
12
+ return [] if monitors.empty?
13
+
14
+ JobTick.client.register(monitors)
15
+ monitors
16
+ end
17
+ end
18
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Jobtick
4
- VERSION = "0.0.1"
3
+ module JobTick
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/jobtick.rb CHANGED
@@ -1,8 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "logger"
3
4
  require_relative "jobtick/version"
5
+ require_relative "jobtick/configuration"
6
+ require_relative "jobtick/client"
7
+ require_relative "jobtick/monitor"
8
+ require_relative "jobtick/parsers/whenever"
9
+ require_relative "jobtick/parsers/solid_queue"
10
+ require_relative "jobtick/parsers/sidekiq"
11
+ require_relative "jobtick/registry"
12
+ require_relative "jobtick/railtie" if defined?(Rails::Railtie)
4
13
 
5
- module Jobtick
14
+ module JobTick
6
15
  class Error < StandardError; end
7
- # Your code goes here...
16
+
17
+ class << self
18
+ def configure
19
+ yield config
20
+ end
21
+
22
+ def config
23
+ @config ||= Configuration.new
24
+ end
25
+
26
+ def client
27
+ @client ||= Client.new
28
+ end
29
+
30
+ def logger
31
+ defined?(Rails) ? Rails.logger : Logger.new($stdout)
32
+ end
33
+
34
+ def reset!
35
+ @config = nil
36
+ @client = nil
37
+ end
38
+ end
8
39
  end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ namespace :jobtick do
4
+ desc "Sync discovered jobs with jobtick.app"
5
+ task sync: :environment do
6
+ monitors = JobTick::Registry.sync
7
+ count = monitors&.length || 0
8
+ puts "[JobTick] Synced #{count} monitor(s)"
9
+ end
10
+
11
+ namespace :whenever do
12
+ desc "Print Whenever job_type wrappers to add to config/schedule.rb for heartbeat injection"
13
+ task :setup do
14
+ endpoint = JobTick.config.endpoint
15
+ puts <<~RUBY
16
+ # Add to config/schedule.rb to enable JobTick heartbeat injection for Whenever jobs:
17
+
18
+ job_type :jobtick_runner, %(curl -sf "#{endpoint}/ping/:monitor_key/started" ; ) \\
19
+ %(bundle exec rails runner ':task' :output && ) \\
20
+ %(curl -sf "#{endpoint}/ping/:monitor_key/completed" || ) \\
21
+ %(curl -sf "#{endpoint}/ping/:monitor_key/failed")
22
+
23
+ job_type :jobtick_rake, %(curl -sf "#{endpoint}/ping/:monitor_key/started" ; ) \\
24
+ %(bundle exec rake :task :output && ) \\
25
+ %(curl -sf "#{endpoint}/ping/:monitor_key/completed" || ) \\
26
+ %(curl -sf "#{endpoint}/ping/:monitor_key/failed")
27
+
28
+ # Then use jobtick_runner / jobtick_rake instead of runner / rake, e.g.:
29
+ # every 1.hour do
30
+ # jobtick_runner "InvoiceJob.perform_later", monitor_key: "invoice_job"
31
+ # end
32
+ RUBY
33
+ end
34
+ end
35
+ end
metadata CHANGED
@@ -1,14 +1,28 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jobtick
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Clearstack Labs
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: logger
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.6'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '1.6'
12
26
  description: Auto-discovers and monitors all scheduled jobs in your Rails app. Zero
13
27
  configuration per job.
14
28
  email:
@@ -21,8 +35,18 @@ files:
21
35
  - LICENSE.txt
22
36
  - README.md
23
37
  - Rakefile
38
+ - jobtick-0.0.1.gem
24
39
  - lib/jobtick.rb
40
+ - lib/jobtick/client.rb
41
+ - lib/jobtick/configuration.rb
42
+ - lib/jobtick/monitor.rb
43
+ - lib/jobtick/parsers/sidekiq.rb
44
+ - lib/jobtick/parsers/solid_queue.rb
45
+ - lib/jobtick/parsers/whenever.rb
46
+ - lib/jobtick/railtie.rb
47
+ - lib/jobtick/registry.rb
25
48
  - lib/jobtick/version.rb
49
+ - lib/tasks/jobtick.rake
26
50
  - sig/jobtick.rbs
27
51
  homepage: https://jobtick.app
28
52
  licenses:
@@ -30,8 +54,9 @@ licenses:
30
54
  metadata:
31
55
  allowed_push_host: https://rubygems.org
32
56
  homepage_uri: https://jobtick.app
33
- source_code_uri: https://github.com/clearstacklabs/jobtick
34
- changelog_uri: https://github.com/clearstacklabs/jobtick/blob/main/CHANGELOG.md
57
+ source_code_uri: https://github.com/clearstack-labs/jobtick
58
+ changelog_uri: https://github.com/clearstack-labs/jobtick/blob/main/CHANGELOG.md
59
+ rubygems_mfa_required: 'true'
35
60
  rdoc_options: []
36
61
  require_paths:
37
62
  - lib
@@ -39,7 +64,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
39
64
  requirements:
40
65
  - - ">="
41
66
  - !ruby/object:Gem::Version
42
- version: 3.2.0
67
+ version: 3.3.0
43
68
  required_rubygems_version: !ruby/object:Gem::Requirement
44
69
  requirements:
45
70
  - - ">="