Monitoring Uvicorn using Monit

In this blog post, I will share my take of solving a specific issue faced while setting up monitoring for a Uvicorn server using Monit.

At Qxf2, we have a web application built with ReactJS, Neo4j and FastAPI, implemented for an internal use-case. It runs on an EC2 instance. The application, silently going down has been troublesome. Hence, we looked for a monitoring and self-healing solution. Monit deemed fit. I started off by setting up the Monit daemon, on the respective EC2 instance and enabling Monit’s web interface. Monitoring and maintaining applications using Monit, is pretty straight-forward for any daemon implementations like nginx. However, I hit a tricky spot while setting up monitoring using Monit.

Issue faced

The web application uses FastAPI as the backend framework which is run using an ASGI server program called Uvicorn. The FastAPI application’s run command `nohup uvicorn main:app –host hostIP –port portNumber &` could however not be called out as it is in the Monit’s start program command. One of the reasons being difference in shells between Monit and the terminal program. To run the application, Monit has to activate the Python virtual environment and move into the respective application folder.

My solution

I resolved this issue by implementing a shell script and calling it in `monitrc` (Monit’s config file, by default, located at /etc/monit/monitrc with the monit install). Sharing sample snippets of my Monit implementation.

1. Write a shell script

start_uvicorn ()
 . /home/ubuntu/<application_dir_name>/venv/bin/activate
 cd /home/ubuntu/<application_dir_name>/backend/app
 uvicorn main:app --reload --host --port <port_no>
if [ "$1" = "start" ]; then
    echo "Starting application!"
elif [ "$1" = "stop" ]; then
    echo "Terminating application!"
    kill $(ps aux | grep uvicorn| grep -v grep| pgrep uvicorn)
    echo "Command not found!"

Note: Replace placeholders marked with <> appropriately.

Let me quickly go through the script. The shell script compares the first command line argument passed and acts accordingly. If it is `start`, then start_uvicorn(), a user-defined function is called. This function starts the web application by first activating the Python virtual environment and moving into the respective application folder. (.) dot command is source command’s equivalent, to activate the virtual environment. If the first command line argument to the shell script is `stop`, then the application’s process ID is extracted and passed on to kill command to terminate the application execution.

2. Update Monit configuration file


check process uvicorn
      matching "uvicorn"
   start program = "/bin/bash -c '/bin/sh /home/ubuntu/ start'" with timeout 100 seconds
   stop program = "/bin/bash -c '/bin/sh /home/ubuntu/ stop'" with timeout 60 seconds
   if cpu usage > 80% for 5 cycles then alert
   if failed host port <port_no> then restart
   if 5 restarts within 5 cycles then timeout

Note: Replace placeholders marked with <> appropriately.

Call the shell script written in step 1 with the appropriate parameter, in Monit’s start and stop program commands.

3. Restart and Verify

Finally, post every Monit config file update, remember to restart Monit service. Run `service monit restart` command to restart Monit with root privileges. Attempt stopping, starting or restarting application using Monit’s web interface. Post performing any Monit action, verify application status by running `monit status` or `monit summary`. Alternatively, monit logs located at `/var/log/monit.log` or in the web interface, can also be looked at for more detailed information.

Output of monit summary:
monit summary

This approach helped me solve the issue. Also, we were able to do a health check of our web-application using Monit’s web interface with ease. I believe, you find the solution that I had discussed in this blog useful. I would be glad to hear from you, on your solutions, to similar issues with Monit.

Leave a Reply

Your email address will not be published.