How to Monitor FunWithFlags with Vigilmon
FunWithFlags is the go-to feature flag library for Elixir and Phoenix. It supports boolean flags, actor-based targeting, percentage rollouts, and group-based switching — all persisted to Ecto or Redis so flags survive restarts. But when the persistence backend is down, FunWithFlags falls back silently, and a flag that should be on for 50% of users may return the wrong value for everyone.
External monitoring catches what your flag library can't self-report. This tutorial covers:
- A health endpoint that validates FunWithFlags connectivity and backend state
- HTTP uptime monitoring with Vigilmon
- Heartbeat monitoring for background flag-sync jobs
- Slack alerts and a status page
Step 1: Add FunWithFlags to your project
# mix.exs
defp deps do
[
{:fun_with_flags, "~> 1.12"},
{:fun_with_flags_ui, "~> 1.0"}, # optional web UI
{:req, "~> 0.4"} # for heartbeat pings
]
end
Configure the persistence backend (Ecto example):
# config/config.exs
config :fun_with_flags, :persistence,
adapter: FunWithFlags.Store.Persistent.Ecto,
repo: MyApp.Repo
config :fun_with_flags, :cache,
enabled: true,
ttl: 900 # 15 minutes
For Redis:
config :fun_with_flags, :persistence,
adapter: FunWithFlags.Store.Persistent.Redis,
database: 0
config :fun_with_flags, :redis,
host: "localhost",
port: 6379
Run the migration if using Ecto:
mix fun_with_flags.create_table --repo MyApp.Repo
mix ecto.migrate
Step 2: Build a health endpoint for FunWithFlags
# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
import Plug.Conn
require Logger
def init(opts), do: opts
def call(%Plug.Conn{request_path: "/health"} = conn, _opts) do
checks = run_checks()
status = if checks.status == "ok", do: 200, else: 503
conn
|> put_resp_content_type("application/json")
|> send_resp(status, Jason.encode!(checks))
|> halt()
end
def call(conn, _opts), do: conn
defp run_checks do
flags = flag_check()
overall = flags.status == "ok"
%{
status: if(overall, do: "ok", else: "degraded"),
feature_flags: flags
}
end
defp flag_check do
start = System.monotonic_time(:millisecond)
# Write and read a probe flag to verify backend round-trip
probe = :"health_probe_#{System.unique_integer([:positive])}"
result =
with :ok <- FunWithFlags.enable(probe),
{:ok, true} <- FunWithFlags.enabled?(probe),
:ok <- FunWithFlags.disable(probe) do
elapsed = System.monotonic_time(:millisecond) - start
{:ok, elapsed}
else
err -> {:error, inspect(err)}
end
case result do
{:ok, latency_ms} ->
%{status: "ok", backend_latency_ms: latency_ms}
{:error, reason} ->
Logger.error("[FunWithFlags] health check failed: #{reason}")
%{status: "error", reason: reason}
end
end
end
Register the plug before the router:
# lib/my_app_web/endpoint.ex
plug MyAppWeb.Plugs.HealthCheck
plug MyAppWeb.Router
Test it:
mix phx.server
curl http://localhost:4000/health | jq .
# {
# "status": "ok",
# "feature_flags": {
# "status": "ok",
# "backend_latency_ms": 4
# }
# }
Step 3: Track flag-specific state in your health response
For critical flags, expose their current value so you can alert when a flag is unexpectedly off or on:
defp run_checks do
flags = flag_check()
critical = critical_flag_check()
overall = flags.status == "ok" and critical.status == "ok"
%{
status: if(overall, do: "ok", else: "degraded"),
feature_flags: flags,
critical_flags: critical
}
end
defp critical_flag_check do
critical_flags = Application.get_env(:my_app, :critical_flags, [])
states =
Enum.map(critical_flags, fn flag ->
case FunWithFlags.enabled?(flag) do
{:ok, enabled} -> %{flag: flag, enabled: enabled, status: "ok"}
{:error, reason} -> %{flag: flag, status: "error", reason: inspect(reason)}
end
end)
has_error = Enum.any?(states, &(&1.status == "error"))
%{status: if(has_error, do: "error", else: "ok"), flags: states}
end
Configure which flags to expose:
# config/config.exs
config :my_app, :critical_flags, [:new_checkout, :dark_mode, :beta_api]
Step 4: Monitor the health endpoint with Vigilmon
- Sign up at vigilmon.online
- Click New Monitor → HTTP
- Enter
https://yourdomain.com/health - Set check interval: 1 minute
- Enable keyword check: body must contain
"status":"ok" - Save
The keyword check matters here: if FunWithFlags' Ecto or Redis backend is unreachable, your endpoint returns HTTP 200 but with "status":"degraded". The keyword match catches that before users see inconsistent flag behavior.
Step 5: Heartbeat monitoring for flag sync jobs
If you run periodic jobs that sync flags from a central config store or reset experiment flags, a heartbeat confirms they ran:
# lib/my_app/workers/flag_sync_worker.ex
defmodule MyApp.Workers.FlagSyncWorker do
use GenServer
require Logger
@interval :timer.minutes(10)
def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
def init(state) do
schedule()
{:ok, state}
end
def handle_info(:sync, state) do
sync_flags()
schedule()
{:noreply, state}
end
defp sync_flags do
# Example: reset any expired experiment flags
case FunWithFlags.all_flags() do
{:ok, _flags} ->
ping_vigilmon()
{:error, reason} ->
Logger.error("[FlagSyncWorker] failed to list flags: #{inspect(reason)}")
end
end
defp ping_vigilmon do
url = Application.get_env(:my_app, :vigilmon)[:heartbeat_url]
if url do
case Req.get(url, receive_timeout: 5_000) do
{:ok, %{status: 200}} -> :ok
err -> Logger.warning("[FlagSyncWorker] heartbeat ping failed: #{inspect(err)}")
end
end
end
defp schedule, do: Process.send_after(self(), :sync, @interval)
end
Add it to your supervision tree:
# lib/my_app/application.ex
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
MyApp.Workers.FlagSyncWorker
]
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat
- Set expected interval: 20 minutes
- Copy the ping URL
- Set it as
VIGILMON_HEARTBEAT_URLin your environment
If the sync worker crashes or the backend goes down, the heartbeat stops — and Vigilmon alerts you within one missed interval.
Step 6: Slack alerts and status page
Slack alerts:
- In Vigilmon, go to Notifications → New Channel → Slack
- Paste your Slack incoming webhook URL
- Enable the channel on your FunWithFlags health monitor and heartbeat monitor
When the backend goes down:
🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
1 minute ago
When the flag sync heartbeat misses:
🔴 HEARTBEAT MISSED: FunWithFlags Sync
Expected ping within 20 minutes — none received
Last seen: 23 minutes ago
Status page:
- Go to Status Pages → New Status Page
- Add your HTTP monitor and heartbeat monitor
- Share the URL with your team
What you've built
| What | How |
|------|-----|
| Health endpoint | Plug with FunWithFlags backend probe |
| Keyword check | Vigilmon keyword match on "status":"ok" |
| Uptime monitoring | Vigilmon HTTP monitor → /health |
| Flag backend liveness | Backend round-trip probe with latency |
| Critical flag exposure | Named flags reported in health response |
| Sync job monitoring | Heartbeat GenServer + Vigilmon heartbeat monitor |
| Slack alerts | Vigilmon Slack notification channel |
| Status page | Vigilmon public status page |
FunWithFlags manages your feature releases safely. Vigilmon tells you when the flag store can't be trusted.
Next steps
- Add percentage rollout tracking — log what fraction of users see a flag as enabled and alert when the ratio drifts unexpectedly
- Monitor Redis latency separately if you use the Redis backend, since a slow Redis will slow every flag evaluation
- Use Vigilmon's response time history to detect gradual backend degradation before it affects flag consistency
Get started free at vigilmon.online.