Let's write an uptime monitor using Unison Cloud

I saw this repository by yorickpeterse where fish and podman were used to create an uptime notifier and thought it was a very cool, readable repository. And fish is a great shell as well, so double fun. It motivated me to try to do something similar with Unison Cloud and document parts of it.

First up: what does it do?

The fish script is a great read, so do check it out beforehand. From a high-level overview it's doing the following:

  • check for settings: IP address of the database and the database port.
    • looping through a list of hosts, and for each host:
      • do a HTTP GET request
      • save the timings of the request
      • save the timing results to the database
      • sleep a bit so we don't bother the same domain too much
      • For a random time between 100 and 300 seconds nap some more to catch outliers that can happen with a fixed interval schedule for checking the list of hosts.

We can start small with this!

Where to start?

I usually read and click around the documentation of a library on Share until I find some type that seems useful for my objective, then I go and play around with an implementation. I'll iterate on it until I get something I like, so here I'll do the same.

Since we want to schedule our monitor to run every so often, we need some kind of an abstraction(or data structure) that does that for us, and I think Daemon can be the one for us.

Daemon is a paid feature

Daemon currently(as of April 07 2026) is only for paid users, so you can't use it with the free/basic tier of Unison Cloud. Managing daemons also isn't fully there yet.

We'll also want a HTTP client, to make HTTP requests, and lucky for us, the Unison team maintains one right here on Share

Show me the code

We need some dependencies for our experiment first:

  • lib.install @unison/base - Unison's standard library
  • lib.install @unison/http - HTTP client
  • lib.install @unison/cloud - the data structures we'll be using.

Let's start small, with a function that pings a single host:

ping : URI ->{Http} Either Failure HttpResponse
ping uri = tryGet uri

Note that the function returns an Either. So we can do something depending on the ping returning a Failure or an HttpResponse.

We'll keep track of the hosts to ping in a list in memory for now.

hosts : [Text]
hosts = ["https://httpbin.org"]

Now let's glue it together to get the core of our small service:

Now is a good time to check out Daemon, because pingHosts is the function we would like to run every X seconds.

A daemon on Unison cloud has to return a Unit, and thus if we want to call it every so often, we have to use recursion. The Unit and recursion usage are the key difference between Daemons and other Unison Cloud types: Daemons are non-terminating programs.

pingDaemon = do
  go = do
    result = pingHosts()
    Remote.sleep (Remote.Duration.minutes 5)
    go()
  go()
  ()
todo "deploy pingDaemon and ping things"

The inner go function contains the business logic here for now. Let's also not forget to sleep for a random interval

pingDaemon = do
  go = do
    result = pingHosts()
    Remote.sleep (Remote.Duration.minutes 5)
    go()
  go()
  ()
todo "deploy pingDaemon and ping things"

You might have noticed that result isn't being used anywhere. We have a lot of freedom in how we'd handle an Failure and that's a good next step, however since we have the Daemon ready, why not look at deploying our little daemon service first, since Unison makes this easy:

deployDaemon : '{IO, Exception} DaemonHash
deployDaemon = main do
  env = Environment.default()
  db = named "funitor-db"
  log : LinearLog PingResult
  log = named db "funitor-log"
  storage = PingStorage db log
  daemon = named "funitor"
  Daemon.deploy daemon env (pingDaemon storage)

And that's it for the deploy! If you enter run deployDaemon in ucm you'll get a new service up and running.

You'll know it worked if you see the following:

run deployDaemon

DaemonHash "a-long-hash-here"

Doing more useful things

We aren't doing actual useful stuff just yet. We can start adding several features now:

  • Record the timings of the request.
  • Change the hard coded list of hosts to something more flexible.
  • Save the data of each run.

And a nice-to-have would be sending an email on failure.

Timings

One statistic that could be useful is to time how much time a ping took, so let's edit create a new function that does exactly that:

pingTimed : URI -> '{Http, Remote} PingResult
pingTimed uri = do
  use Duration -
  start = monotonic!
  result = ping uri
  end = monotonic!
  duration = end - start
  PingResult result duration

A monotonic! call before and after the call suffices for our use cases for now.

Persistence-ing

A big part of why I like using the Cloud option of Unison is because I'm not spending time on writing (de)serializing code or infrastructure code. Because Unison code can be persisted as-is! We can focus on the data structures and logic of our code.

So for saving a PingResult we're looking for some sort of data structure that's amenable to logging. I went around looking for this datatype that could fit this usecase(without dependencies), but couldn't find one, so I did the next best thing: asking around on Discord.

And thus pchiusano gave me my answer, introducing... LinearLog!

With this we have data structure we can save our runs; how you might ask? It's again a function:

saveRun : PingStorage -> PingResult ->{Exception, Storage} ()
saveRun storage pingResult =
  db = PingStorage.db storage
  log = runs storage
  transact db do tx log pingResult

Now the refactor for the daemon becomes very satisfying, because we only need to change up the logic in pingDaemon:

pingDaemon :
  PingStorage -> '{Exception, Http, Storage, Remote, Random} ()
pingDaemon storage =
  do
    go =
      do
        use List map
        hosts
          |> map URI.parse
          |> map (uri -> pingTimed uri () |> saveRun storage)
          |> ignore
        sleepTime = natIn 100 301
        Remote.sleep (Remote.Duration.seconds sleepTime)
        go()
    go()
    ()

If you run the deployDaemon code again in ucm, you'll have a refactored service that pings and persists!

run deployDaemon

That's it...for now?

Building on top of this, gets really cool because now you can tack on a Slackbot that alerts or sends a weekly email containing the weekly statistics.

Whatever it may be, it's nice to be able to do it while focusing on the business logic.