tutorial

How to Monitor FunWithFlags with Vigilmon

Track feature flag state, persistence backend health, and rollout consistency in your Elixir/Phoenix app using FunWithFlags and Vigilmon uptime monitoring.

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

  1. Sign up at vigilmon.online
  2. Click New Monitor → HTTP
  3. Enter https://yourdomain.com/health
  4. Set check interval: 1 minute
  5. Enable keyword check: body must contain "status":"ok"
  6. 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:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 20 minutes
  3. Copy the ping URL
  4. Set it as VIGILMON_HEARTBEAT_URL in 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:

  1. In Vigilmon, go to Notifications → New Channel → Slack
  2. Paste your Slack incoming webhook URL
  3. 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:

  1. Go to Status Pages → New Status Page
  2. Add your HTTP monitor and heartbeat monitor
  3. 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.

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →