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
- 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 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:
- Click New Monitor → Heartbeat
- Set expected interval: 10 minutes
- Copy the ping URL
- Set it as
VIGILMON_HEARTBEAT_URLin 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:
- In Vigilmon, go to Notifications → New Channel → Slack
- Paste your Slack incoming webhook URL
- 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:
- 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 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.