DEV Community

bakenator
bakenator

Posted on

Web Monitor with Email Notifications in Elixir

Why Build This In Elixir?

First and foremost this app is a tool to learn more about writing elixir code outside of Phoenix.

But! Elixir is an excellent language for a website monitoring tool. By using a supervisor and outside mailing service, we get a highly resiliant tool from only a few lines of code.

The Monitor

To check our website we will use a single function module that performs the following steps:

  • checks the response from the website
  • sends an email out depending on whether or not the response was ok
  • waits 1 hr
  • starts the function over

I also added a limit on the successful case so that emails are only sent once per day if the website is up. However when the site is down, email notifications will go out every hour.

One of the most interesting things to note is how we can recur the same function with a timer. In most other languages you would have to use a cron job to get a scheduled recurring function. Elixir can due this thanks to the power of its tail call optimization.

Here is the full code for the module

defmodule WebMonitor.SimpleMonitor do
  def start_link() do
    # boilerplate to prepare for :httpc.request
    Application.ensure_all_started(:inets)
    Application.ensure_all_started(:ssl)

    IO.inspect "Monitor Started"
    # Could use the Task module here, but spawn_link used for educational reasons
    pid = spawn_link(WebMonitor.SimpleMonitor, :check_site, [])
    {:ok, pid}
  end

  def check_site() do
    # make web request and set timeout to 10 seconds
    case :httpc.request(:get, {'https://bakerbaker.dev', []}, [{:timeout, 10000}], []) do
      {:ok, {{'HTTP/1.1', 200, 'OK'}, _headers, _body}} -> 
        if Time.utc_now().hour == 6 do
          # send email for alive website 
          WebMonitor.Mailer.send_good_email()
        end
      _ -> 
        # send email for down website
        WebMonitor.Mailer.send_bad_email()
    end
    # sleep for 1 hr
    :timer.sleep(60 * 60 * 1000)
    # run check function again
    check_site()
  end

  # returning informational object to the supervisor
  def child_spec([]) do
     %{
        id: __MODULE__,
        start: {__MODULE__, :start_link, []}
      }
  end
end

There are several places in this module where we could beef up our code. One way would be to use a Genserver and submit recursive calls to the check_site function. I chose the explicit spawn_link here to provide an example of manually supervising an independent process.

We could have also used a Task instead of spawn_link to get better debugging/error handling capabilities.

Finally I used the :httpc module over HTTPoison as a chance to learn. I was inspired by this blog post: https://virviil.github.io/2018/04/06/elixir-do-you-have-http-requests-you-are-doing-them-wrong/

Sending Emails

In the SimpleMonitor code I use a Mailer module for sending the emails. This module was made with the very easy to use MailGun lib at https://github.com/chrismccord/mailgun

Setting it up requires creating a free mailgun account. For free accounts you have to confirm each recipient, but since I am the only recipient this is not a problem.

I followed the set up tutorial and used the default Mailer module in the library as a guide. Once simplified it ended up as this for my app

defmodule WebMonitor.Mailer do
  # the :web_monitor atom is specific to this app's name
  @config domain: Application.get_env(:web_monitor, :mailgun_domain),
          key: Application.get_env(:web_monitor, :mailgun_key)

  use Mailgun.Client, @config

  @from "site_checker@bakerbaker.dev"

  def send_good_email() do
    send_email to: "andrew@bakerbaker.dev",
               from: @from,
               subject: "Site is up",
               html: "<strong>The site is good!</strong>"
  end

  def send_bad_email() do
    send_email to: "andrew@bakerbaker.dev",
               from: @from,
               subject: "Site is DOWN!",
               html: "<strong>The site is DOWN!</strong>"
  end
end

Pretty much all of the work is done by the Mailgun library through macros.

Starting the App

The SimpleMonitor needs to be added to the supervision tree so that it will be called when the app starts.

Here is the full Application code

defmodule WebMonitor.Application do
  @moduledoc false

  use Application

  def start(_type, _args) do
    # List all child processes to be supervised
    children = [
      {WebMonitor.SimpleMonitor, []}
    ]

    opts = [strategy: :one_for_one, name: WebMonitor.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Now we can run test the app with iex -S mix or create a Release to run permanently on a server

Going Further

I wanted to provide this app as an example for anyone looking to move from Phoenix to more manual control of an Elixir app. With automated email and a timed function, this app could be used as a start for building many other notifier apps. For example an application that checks a web API on a recurring schedule and emails the results.

Hope you enjoyed and learned something! Coding in Elixir is fun!

Top comments (0)