tutorial

How to Monitor Decorator with Vigilmon

Add uptime monitoring, function-level timing instrumentation, and alerts to your Elixir Decorator-powered application — so caching failures, retry storms, and decorated function regressions don't slip past you.

How to Monitor Decorator with Vigilmon

The Decorator library brings Python-style function decorators to Elixir. With a single @decorate annotation you can layer caching, retry logic, timing instrumentation, and authorization checks onto any function — without touching its body. But decorators are invisible at runtime: if a caching decorator silently stops populating the cache, or a retry decorator triggers a storm of repeated calls, your application keeps running while performance degrades.

External monitoring surfaces what Decorator can't self-report. This tutorial covers:

  • A health endpoint that validates decorated function behavior
  • HTTP uptime monitoring with Vigilmon
  • Heartbeat monitoring to detect when decorated background tasks stall
  • Slack alerts and a status page

Step 1: Add Decorator to your project

# mix.exs
defp deps do
  [
    {:decorator, "~> 1.4"},
    {:req, "~> 0.4"}   # for heartbeat pings
  ]
end

Define a timing decorator that records function duration:

# lib/my_app/decorators/timing.ex
defmodule MyApp.Decorators.Timing do
  use Decorator.Define, timed: 0

  def timed(body, context) do
    quote do
      start = System.monotonic_time(:microsecond)
      result = unquote(body)
      elapsed_us = System.monotonic_time(:microsecond) - start

      :telemetry.execute(
        [:my_app, :decorated, :call],
        %{duration_us: elapsed_us},
        %{function: unquote(context.name), module: unquote(context.module)}
      )

      result
    end
  end
end

Use it on any function:

defmodule MyApp.Reports do
  use MyApp.Decorators.Timing

  @decorate timed()
  def generate_summary(user_id) do
    # expensive work
  end
end

Step 2: Build a health endpoint that validates decorator behavior

Add a plug that exercises a decorated function and reports its latency:

# lib/my_app_web/plugs/health_check.ex
defmodule MyAppWeb.Plugs.HealthCheck do
  import Plug.Conn

  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
    decorator_check = probe_decorator()

    %{
      status: if(decorator_check.status == "ok", do: "ok", else: "degraded"),
      decorator: decorator_check
    }
  end

  defp probe_decorator do
    start = System.monotonic_time(:millisecond)

    result =
      try do
        # Exercise a lightweight decorated function to confirm the decorator chain works
        MyApp.Decorators.Probe.noop()
        elapsed = System.monotonic_time(:millisecond) - start
        %{status: "ok", latency_ms: elapsed}
      rescue
        e -> %{status: "error", reason: Exception.message(e)}
      end

    result
  end
end

Define a probe module with a trivial decorated function:

# lib/my_app/decorators/probe.ex
defmodule MyApp.Decorators.Probe do
  use MyApp.Decorators.Timing

  @decorate timed()
  def noop, do: :ok
end

Register the plug in your endpoint:

# 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",
#   "decorator": {"status": "ok", "latency_ms": 0}
# }

Step 3: Collect decorator telemetry

Attach a telemetry handler to aggregate timing across all decorated calls:

# lib/my_app/telemetry.ex
defmodule MyApp.Telemetry do
  require Logger

  @slow_threshold_ms 500

  def setup do
    :telemetry.attach(
      "decorator-timing-logger",
      [:my_app, :decorated, :call],
      &handle_event/4,
      nil
    )
  end

  def handle_event(_event, %{duration_us: duration_us}, %{function: fn_name, module: mod}, _cfg) do
    duration_ms = duration_us / 1_000

    if duration_ms >= @slow_threshold_ms do
      Logger.warning(
        "[Decorator slow call] #{mod}.#{fn_name} took #{Float.round(duration_ms, 1)}ms"
      )
    end
  end
end

Call MyApp.Telemetry.setup() in application.ex:

def start(_type, _args) do
  MyApp.Telemetry.setup()
  # ...
end

Slow decorated calls now appear in your logs, and you can ship those logs to an aggregator for trend analysis.


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 ensures that a misconfigured decorator module — one that panics on use Decorator.Define misuse — surfaces as an alert rather than a silent degradation.


Step 5: Heartbeat monitoring for decorated background tasks

If you use Decorator on scheduled workers — caching pre-warmer, report scheduler, ETL pipeline — a heartbeat tells you the job ran and the decorated functions completed:

# lib/my_app/workers/decorator_heartbeat_worker.ex
defmodule MyApp.Workers.DecoratorHeartbeatWorker do
  use GenServer
  require Logger

  @interval :timer.minutes(5)

  def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__)

  def init(state) do
    schedule()
    {:ok, state}
  end

  def handle_info(:run, state) do
    run_check()
    schedule()
    {:noreply, state}
  end

  defp run_check do
    try do
      MyApp.Decorators.Probe.noop()
      ping_vigilmon()
    rescue
      e -> Logger.error("Decorator heartbeat check failed: #{Exception.message(e)}")
    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
        error -> Logger.warning("Vigilmon heartbeat ping failed: #{inspect(error)}")
      end
    end
  end

  defp schedule, do: Process.send_after(self(), :run, @interval)
end

Add to your supervision tree:

children = [
  MyAppWeb.Endpoint,
  MyApp.Workers.DecoratorHeartbeatWorker
]

In Vigilmon, create a Heartbeat Monitor:

  1. Click New Monitor → Heartbeat
  2. Set expected interval: 10 minutes
  3. Copy the ping URL
  4. Set it as VIGILMON_HEARTBEAT_URL in your environment

If the decorator macro expansion breaks after an upgrade, the probe function raises and the heartbeat stops — Vigilmon alerts you within one missed interval.


Step 6: Expose decorator call counts in the health response

Track how many decorated calls have been made and expose the count as a health signal:

# lib/my_app/decorator_stats.ex
defmodule MyApp.DecoratorStats do
  use Agent

  def start_link(_), do: Agent.start_link(fn -> %{calls: 0, slow_calls: 0} end, name: __MODULE__)

  def increment_calls, do: Agent.update(__MODULE__, &Map.update!(&1, :calls, fn n -> n + 1 end))

  def increment_slow, do: Agent.update(__MODULE__, &Map.update!(&1, :slow_calls, fn n -> n + 1 end))

  def stats, do: Agent.get(__MODULE__, & &1)
end

Update the telemetry handler to record counts, and add them to your health response:

# In health check plug
defp decorator_stats_check do
  stats = MyApp.DecoratorStats.stats()
  %{status: "ok", total_calls: stats.calls, slow_calls: stats.slow_calls}
end

This gives Vigilmon's keyword check more signal: if slow_calls spikes, a cache decorator may have stopped serving from cache.


Step 7: 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 health monitor and heartbeat monitor

When the decorator probe fails:

🔴 DOWN: yourdomain.com/health
Keyword "status":"ok" not found
Detected: EU-West
2 minutes ago

When the heartbeat times out:

🔴 HEARTBEAT MISSED: Decorator Background Worker
Expected ping within 10 minutes — none received
Last seen: 14 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 that exercises a decorated probe function | | Keyword check | Vigilmon keyword match on "status":"ok" | | Uptime monitoring | Vigilmon HTTP monitor → /health | | Decorator liveness | Heartbeat GenServer + Vigilmon heartbeat monitor | | Slow call detection | Telemetry handler with threshold logging | | Call count tracking | Agent-based stats exposed in health response | | Slack alerts | Vigilmon Slack notification channel | | Status page | Vigilmon public status page |

Decorator keeps cross-cutting concerns clean. Vigilmon tells you when they stop working.


Next steps

  • Add a @decorate cached() decorator and include cache hit/miss ratio in the health response
  • Alert on per-function latency regressions using Vigilmon's response time history
  • Chain decorator probes for every decorator type your app uses (caching, retry, auth) to isolate failures

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 →