Bluepill is a simple process monitoring tool written in Ruby.
It's hosted on rubygems.org.
sudo gem install bluepill
Bluepill logs to syslog's local6 facility, as bluepilld. On a system with systemd, the journal keeps those messages, and journalctl -t bluepilld shows them. To also write them to a file with rsyslog, put a line for local6 in a file of its own, such as /etc/rsyslog.d/bluepill.conf, and restart rsyslog:
local6.* /var/log/bluepill.log
Important
Bluepill logs to syslog by default, which on Ruby 3.4 and newer needs the syslog gem (sudo gem install syslog), since Ruby no longer includes it. Or have bluepill write to a log file instead (see Logging).
To keep that file from growing forever, rotate it with logrotate, from a file of its own such as /etc/logrotate.d/bluepill:
/var/log/bluepill.log {
weekly
rotate 4
compress
missingok
notifempty
copytruncate
}
Bluepill keeps its pid and socket files in /var/run/bluepill when it runs as root, and in ~/.bluepill otherwise. It creates the directory itself.
Bluepill organizes processes into 3 levels: application -> group -> process. Each process has a few attributes that tell bluepill how to start, stop, and restart it, where to look or put the pid file, what process conditions to monitor and the options for each of those.
The minimum config file looks something like this:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
end
endNote that since we specified a PID file and start command, bluepill assumes the process will daemonize itself. If we wanted bluepill to daemonize it for us, we can do (note we still need to specify a PID file):
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.daemonize = true
end
endIf you don't specify a stop command, a TERM signal will be sent by default. Similarly, the default restart action is to issue stop and then start.
Bluepill only accepts the attributes it knows, which are the ones this README describes. A misspelled or unsupported attribute fails when the config loads, and the error names the attribute, the process, and the closest attribute bluepill knows. Bluepill prints the line of the pill file with the mistake before the error:
app.pill.rb:5: Unknown attribute 'start_comand' for process 'process_name'. Did you mean 'start_command'?
Now if we want to do something more meaningful, like actually monitor the process, we do:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.checks :cpu_usage, every: 10.seconds, below: 5, times: 3
end
endWe added a line that checks every 10 seconds to make sure the cpu usage of this process is below 5 percent. Three failed checks in a row restart the process. We can specify a two-element array for the times: option to say that 3 out of 5 failed attempts result in a restart. A check restarts the process by default, and fires: picks other events instead, from :start, :stop, :unmonitor and :restart, such as fires: :stop or fires: [:stop, :start].
To watch memory usage, we just add one more line:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.checks :cpu_usage, every: 10.seconds, below: 5, times: 3
process.checks :mem_usage, every: 10.seconds, below: 100.megabytes, times: [3,5]
end
endA process can have more than one check of a kind, such as two :http checks of different URLs.
To watch the modification time of a file, e.g. a log file to ensure the process is actually working add one more line:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.checks :cpu_usage, every: 10.seconds, below: 5, times: 3
process.checks :mem_usage, every: 10.seconds, below: 100.megabytes, times: [3,5]
process.checks :file_time, every: 60.seconds, below: 3.minutes, filename: "/tmp/some_file.log", times: 2
end
endTo restart process if it's running too long:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.checks :running_time, every: 10.minutes, below: 24.hours
end
endWe can tell bluepill to give a process some grace time to start/stop/restart before resuming monitoring:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.start_grace_time = 3.seconds
process.stop_grace_time = 5.seconds
process.restart_grace_time = 8.seconds
process.checks :cpu_usage, every: 10.seconds, below: 5, times: 3
process.checks :mem_usage, every: 10.seconds, below: 100.megabytes, times: [3,5]
end
endWe can group processes by name:
Bluepill.application("app_name") do |app|
5.times do |i|
app.process("process_name_#{i}") do |process|
process.group = "mongrels"
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file_#{i}.pid"
end
end
endIf you want to run the process as someone other than root:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.uid = "deploy"
process.gid = "deploy"
process.checks :cpu_usage, every: 10.seconds, below: 5, times: 3
process.checks :mem_usage, every: 10.seconds, below: 100.megabytes, times: [3,5]
end
endA process with a uid gets every group that the user belongs to, the same as logging in as the user, and the user's primary group unless gid names another. To add more groups to those:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.uid = "deploy"
process.gid = "deploy"
process.supplementary_groups = ['rvm']
process.checks :cpu_usage, every: 10.seconds, below: 5, times: 3
process.checks :mem_usage, every: 10.seconds, below: 100.megabytes, times: [3,5]
end
endYou can also set an app-wide uid, gid and supplementary groups:
Bluepill.application("app_name") do |app|
app.uid = "deploy"
app.gid = "deploy"
app.supplementary_groups = ['rvm']
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
end
endThese only apply when bluepill runs as root. Every command a process runs, such as its start_command, stop_command and pre_start_command, runs as its user and groups, and so do the commands of its children with monitor_children.
Warning
On JRuby, which can't fork, bluepill switches users and groups with setpriv, from util-linux, which most Linux distributions include. Without setpriv, as on macOS, a config that sets uid, gid or supplementary_groups fails to load when bluepill runs as root. Run it on CRuby to use them there.
To track resources of child processes, use :include_children:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.checks :mem_usage, every: 1.seconds, below: 5.megabytes, times: [3,5], include_children: true
end
endTo check for flapping:
process.checks :flapping, times: 2, within: 30.seconds, retry_in: 7.secondsTo check that a URL answers, use process.checks :http, url: "http://localhost:4567/", kind: :success, pattern: /html/, timeout: 3.seconds. For an https URL, bluepill verifies the server's certificate.
Caution
For a server with a self-signed certificate, add verify_ssl: false. It turns off certificate verification, so bluepill accepts a certificate from anyone. Use it only for a server you trust, on a network you trust.
To set the working directory to cd into when starting the command:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.working_dir = "/path/to/some_directory"
end
endYou can also have an app-wide working directory:
Bluepill.application("app_name") do |app|
app.working_dir = "/path/to/some_directory"
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
end
endNote
We also set the PWD in the environment to the working dir you specify. This is useful for when the working dir is a symlink. Unicorn in particular will cd into the environment variable in PWD when it re-execs to deal with a change in the symlink.
To run a command before every start, for example to clear a cache:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.pre_start_command = "/usr/bin/some_setup_command"
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
end
endBluepill waits for the pre_start_command to finish, for up to hook_timeout (30 seconds by default), and then starts the process either way. It logs a pre_start_command that exits with a non-zero status, fails to run, or runs too long, and kills one that runs too long, with anything it started. It runs in the same working_dir, with the same environment, uid, and gid, as the process.
Bluepill has four more hooks, named after systemd's ExecStartPost, ExecStop, and ExecStopPost:
Bluepill.application("app_name") do |app|
# Runs on every change of state, for every process in the app
app.notify_command = "/usr/local/bin/alert-on-call"
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
# Runs when the process comes up after a start or a restart
process.post_start_command = "renice 20 {{PID}}"
# Runs before the stop, which waits for it
process.pre_stop_command = "/usr/bin/leave-load-balancer"
# Runs after the process has stopped and bluepill has cleaned up
process.post_stop_command = "/usr/bin/remove-socket {{PID}}"
end
endpost_start_commandruns once the process reachesupafter a start or a restart, not as soon asstart_commandreturns, so a server that daemonizes itself has written its pid file by then.notify_commandruns on every change of state, such asuptodown, ordowntostarting. It gets the details in environment variables:BLUEPILL_APP,BLUEPILL_GROUP,BLUEPILL_PROCESS,BLUEPILL_FROMandBLUEPILL_TO(the old and new state),BLUEPILL_REASON(such as the check that failed,Flapping, oruser initiated), andBLUEPILL_PID. Use it to send an alert when bluepill restarts a process or gives up on one. Set it on the app for every process, or on one process.pre_stop_commandruns before the stop signals orstop_command, for example to take a server out of a load balancer or to drain a queue. The stop waits for it to finish, and then goes ahead either way.post_stop_commandruns after the process has stopped and bluepill has cleaned up, for example to remove a socket or a lock file.
{{PID}} in a hook is replaced with the process's pid, or, in post_stop_command, with the pid of the process that just stopped. Like pre_start_command, the hooks run in the process's working_dir, with its environment, uid, and gid, and without a shell. post_start_command, notify_command, and post_stop_command run in the background, so a slow hook never holds up monitoring. A hook that runs longer than hook_timeout (30 seconds by default) is killed, with anything it started. Bluepill logs a hook that fails or times out, but a hook never changes the process's state or stops bluepill from starting or stopping it:
process.hook_timeout = 10.secondsBy default, bluepill will send a SIGTERM to your process when stopping. To change the stop command:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.stop_command = "/user/bin/some_stop_command"
end
endIf you'd like to send a signal or signals to your process to stop it:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.stop_signals = [:quit, 30.seconds, :term, 5.seconds, :kill]
process.stop_grace_time = 45.seconds
end
endWe added a line that will send a SIGQUIT, wait 30 seconds and check to
see if the process is still up, send a SIGTERM, wait 5 seconds and check
to see if the process is still up, and finally send a SIGKILL. The
stop_grace_time has to be longer than the delays added up, here 35
seconds, so the last signal has time to work.
And lastly, to monitor child processes:
process.monitor_children do |child_process|
child_process.checks :cpu_usage, every: 10, below: 5, times: 3
child_process.checks :mem_usage, every: 10, below: 100.megabytes, times: [3, 5]
child_process.stop_command = "kill -QUIT {{PID}}"
endBluepill runs this block once when the pill file loads, to check it, and again for each child it finds. child_process.actual_pid is the child's pid, and nil during that first run. The block must set a stop_command.
Note {{PID}} will be substituted for the pid of process in both the stop and restart commands.
10.seconds and 100.megabytes are plain numbers of seconds and bytes, so every: 10.seconds and every: 10 mean the same thing. To make them work, bluepill adds seconds, minutes, hours, days, weeks, bytes, kilobytes, megabytes, gigabytes, and terabytes, and their singular forms such as 1.minute, to Ruby's Numeric. It's the only change bluepill makes to Ruby's own classes, and it applies to all the code that a pill file loads, not just the pill file.
If a pill file loads ActiveSupport, for example by loading a Rails app, ActiveSupport's versions of these methods win, whichever loads first. Bluepill accepts ActiveSupport's durations too.
A check is a subclass of Bluepill::ProcessConditions::ProcessCondition, in any module, named after the check in camel case, so checks :log_size uses a class named LogSize. Define it in the config file, before the application. It gets the check's options as keyword arguments, except every, times, fires, and include_children, which bluepill handles. Name the ones it needs, and pass the rest to super, which rejects any that no check takes. run reads a value, and check? returns whether the value is fine:
module Bluepill
module ProcessConditions
class LogSize < ProcessCondition
def initialize(path:, below:, **options)
super(**options)
@path = path
@below = below
end
def run(_pid, _include_children)
File.size?(@path).to_i
end
def check?(size)
logger.warning("#{@path} is #{size} bytes") unless size < @below
size < @below
end
end
end
end
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.pid_file = "/tmp/some_pid_file.pid"
process.checks :log_size, every: 1.minute, path: "/var/log/app.log", below: 1.gigabyte
end
endlogger writes to bluepill's log, with the app's and the process's names, like the built-in checks. The check gets it through the logger: keyword, which super stores, so call super first in initialize: a check that doesn't call super has no logger.
If run, check? or format_value raises, bluepill logs the error with its backtrace and skips the check until its next interval. It doesn't count toward times, and bluepill keeps monitoring. A check class must define both run and check?, or the pill file fails to load.
Important
Bluepill splits each command (start_command, stop_command, restart_command, and the hooks, such as pre_start_command) into words and runs it directly, without a shell. So shell syntax like cd ... &&, VAR=value, |, >, 2>&1, and * globs doesn't work.
Use the config options instead:
Bluepill.application("app_name") do |app|
app.process("process_name") do |process|
process.start_command = "/usr/bin/some_start_command"
process.working_dir = "/tmp/some_dir"
process.environment = {"SOME_VAR" => "1"}
process.stdout = process.stderr = "/tmp/server.log"
process.pid_file = "/tmp/some_pid_file.pid"
process.daemonize = true
end
endstdout, stderr, and stdin only apply to processes that bluepill daemonizes. A daemonized process starts in / unless you set working_dir, so use absolute paths or set working_dir.
If you really need a shell, run one explicitly, and use exec so bluepill monitors your server instead of the shell that started it:
process.start_command = "sh -c 'exec /usr/bin/some_start_command < /tmp/input'"bluepill [app_name] command [options]
For the "load" command, the app_name is specified in the config file, and must not be provided on the command line.
For all other commands, the app_name is optional if there is only one bluepill daemon running. Otherwise, the app_name must be provided, because the command will fail when there are multiple bluepill daemons running. The example commands below leaves out the app_name.
To start a bluepill daemon and load the config for an application:
sudo bluepill load /path/to/production.pill
To act on a process or group for an application:
sudo bluepill <start|stop|restart|unmonitor> <process_or_group_name>
To view process statuses for an application:
sudo bluepill status
To view the log for a process or group for an application:
sudo bluepill log <process_or_group_name>
To quit the bluepill daemon for an application:
sudo bluepill quit
Its processes keep running. The next bluepill load monitors them again, from their pid files, without restarting them, so you can upgrade bluepill this way.
bpsv runs a bluepill command on the process it's named after, so a
symlink to it works like an init script. For a process named unicorn:
sudo ln -s "$(which bpsv)" /usr/local/bin/unicorn
sudo unicorn restart
That runs bluepill restart unicorn. Like bluepill without an
app_name, it only works when one bluepill daemon is running.
examples/sample_app.rb monitors a small forking echo server, and stops any worker that uses too much memory. Its comments show how to run it. examples/runit.rb monitors a service managed by runit.
By default, bluepill uses syslog local6 facility as described in the installation section. But if for any reason you don't want to use syslog, you can use a log file. You can do this by setting the log_file: option in the config:
Bluepill.application("app_name", log_file: "/path/to/bluepill.log") do |app|
# ...
endTip
Set up log rotation for the log file too (described in the installation section), to keep it from growing huge.
Bluepill logs everything by default, including a line for every check it runs. To log less, set the log_level: option to the least severe priority you want to keep. Bluepill drops anything less severe, whether it logs to syslog, a log file, or stdout in foreground mode:
Bluepill.application("app_name", log_level: :warning) do |app|
# ...
endThe levels, from most to least severe, are emerg, alert, crit, err, warning, notice, info, and debug, the default. Bluepill logs each check's history at info, and process state changes at info and warning.
You can run bluepill in the foreground:
Bluepill.application("app_name", foreground: true) do |app|
# ...
endImportant
Define only one application per config when using foreground mode.
This library aims to support and is tested against the following Ruby implementations:
- Ruby 3.2 through 3.4
- Ruby 4.0
- JRuby 10.0 and 10.1, with
uid,gidandsupplementary_groupsonly wheresetprivis installed
If something doesn't work on one of these interpreters, please open an issue.
This library may inadvertently work (or seem to work) on other Ruby implementations, however support will only be provided for the versions listed above.
If you would like this library to support another Ruby version, you may volunteer to be a maintainer. Being a maintainer entails making sure all tests run and pass on that implementation. When something breaks on your implementation, you will be responsible for providing patches in a timely fashion. If critical issues for a particular implementation exist at the time of a major release, support for that Ruby version may be dropped.