How to Monitor TypeCheck with Vigilmon
TypeCheck is a runtime type checking library for Elixir that validates values against your declared typespecs at the moment they are used, not just at compile time. Where Dialyzer performs static analysis and finds potential issues at compile time, TypeCheck verifies actual runtime values — catching malformed data from external APIs, database rows, or user input that Dialyzer cannot see. When a value violates its declared type, TypeCheck raises a detailed error describing exactly which constraint failed and why.
Runtime type validation is only useful if you know when it fires. A type contract violation that happens once an hour in a background job may be silent for days without monitoring. When TypeCheck starts raising on data that previously conformed — because an upstream API changed its response shape, or because new code paths produce values with unexpected structure — you need to know immediately.
Vigilmon closes that gap: track violation rates by type and call site, alert when violation patterns indicate interface drift, and catch data contract breaks before they cause downstream failures.
Why Monitor TypeCheck at Runtime?
TypeCheck violations in production are a class of error distinct from logic bugs:
- Interface contract drift — an external service changes its response schema; TypeCheck violations flag this immediately rather than letting malformed data corrupt your database or crash a downstream consumer
- Data migration side effects — schema migrations that alter existing rows can cause TypeCheck violations in read paths that previously passed — violations surface exactly which old records have the wrong shape
- Performance overhead visibility — TypeCheck validation adds CPU cost at every check point; without metrics, you can't see whether type checking is impacting latency in hot code paths
- Violation clustering — a spike in violations may pinpoint a single bad actor (one endpoint, one background job, one upstream) rather than a systemic problem
- Gradual type erosion — some violations happen rarely and only on edge-case inputs; without counters, you never know they exist
Key Metrics to Track
| Metric | What it tells you | |--------|-------------------| | Violation count by type name | Which type contracts are failing most often | | Violation count by call site (module/function) | Where in your code violations occur | | Violation rate over time | Whether violations are increasing, stable, or isolated | | Check duration distribution | Performance overhead of type checking per code path | | Unique violation signatures | Whether many distinct inputs are failing or one bad input repeats | | Violation-to-call ratio | What fraction of type-checked calls actually violate the contract |
Step 1: Add TypeCheck to Your Project
# mix.exs
defp deps do
[
{:type_check, "~> 0.13"}
]
end
Define types and checked functions:
# lib/my_app/user.ex
defmodule MyApp.User do
use TypeCheck
@type! t :: %__MODULE__{
id: pos_integer(),
email: String.t(),
role: :admin | :user | :guest,
created_at: DateTime.t()
}
defstruct [:id, :email, :role, :created_at]
end
# lib/my_app/accounts.ex
defmodule MyApp.Accounts do
use TypeCheck
@spec! get_user(pos_integer()) :: {:ok, MyApp.User.t()} | {:error, :not_found}
def get_user(id) do
case MyApp.Repo.get(MyApp.User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
@spec! create_user(String.t(), :admin | :user | :guest) :: {:ok, MyApp.User.t()} | {:error, term()}
def create_user(email, role) do
%MyApp.User{email: email, role: role, created_at: DateTime.utc_now()}
|> MyApp.Repo.insert()
end
end
Step 2: Capture TypeCheck Violations with Telemetry
TypeCheck raises TypeCheck.TypeError on violations. Wrap your type-checked calls to capture these:
# lib/my_app/type_check_monitor.ex
defmodule MyApp.TypeCheckMonitor do
@moduledoc """
Captures TypeCheck.TypeError exceptions and emits telemetry before re-raising.
Attach this handler to any process that calls type-checked functions.
"""
require Logger
@doc """
Execute a function and emit telemetry for any TypeCheck violations.
Re-raises the TypeError so normal error handling still applies.
"""
def checked(type_name, call_site, fun) do
start = System.monotonic_time()
result = fun.()
duration = System.monotonic_time() - start
:telemetry.execute(
[:my_app, :type_check, :call],
%{duration: duration, count: 1},
%{type: type_name, call_site: call_site, result: :ok}
)
result
rescue
e in TypeCheck.TypeError ->
duration = System.monotonic_time() - start
violation_summary = summarize_violation(e)
Logger.warning("TypeCheck violation",
type: type_name,
call_site: call_site,
violation: violation_summary,
message: Exception.message(e)
)
:telemetry.execute(
[:my_app, :type_check, :violation],
%{duration: duration, count: 1},
%{
type: type_name,
call_site: call_site,
violation_kind: violation_summary.kind
}
)
reraise e, __STACKTRACE__
end
defp summarize_violation(%TypeCheck.TypeError{} = e) do
message = Exception.message(e)
kind =
cond do
String.contains?(message, "is not a") -> :wrong_type
String.contains?(message, "does not match") -> :pattern_mismatch
String.contains?(message, "is not one of") -> :invalid_enum
true -> :unknown
end
%{kind: kind, raw: message}
end
end
Usage in your code:
defmodule MyApp.UserService do
def process_external_user(raw_data) do
MyApp.TypeCheckMonitor.checked(MyApp.User, "UserService.process_external_user", fn ->
user = struct!(MyApp.User, raw_data)
MyApp.Accounts.create_user(user.email, user.role)
end)
end
end
Step 3: Configure TypeCheck Error Reporting
For non-critical paths where you want to observe violations without crashing, use TypeCheck's overrides to replace raises with logging:
# config/runtime.exs
if config_env() == :prod do
config :type_check,
enable_runtime_checks: true
end
# lib/my_app/soft_type_check.ex
defmodule MyApp.SoftTypeCheck do
@moduledoc """
Soft type checking for non-critical paths.
Logs violations and emits telemetry but returns the unchecked value
instead of raising, allowing the call to proceed.
"""
require Logger
def check(type_module, value, call_site) do
case TypeCheck.conforms(value, type_module.t()) do
:ok ->
:telemetry.execute(
[:my_app, :type_check, :soft_check],
%{count: 1},
%{type: type_module, call_site: call_site, result: :ok}
)
{:ok, value}
{:error, error} ->
Logger.warning("Soft TypeCheck violation (non-raising)",
type: type_module,
call_site: call_site,
error: inspect(error)
)
:telemetry.execute(
[:my_app, :type_check, :soft_check],
%{count: 1},
%{type: type_module, call_site: call_site, result: :violation}
)
{:violation, value}
end
end
end
Step 4: Define Telemetry Metrics
# lib/my_app/telemetry.ex
def metrics do
[
# Total TypeCheck calls (pass + fail)
counter("my_app.type_check.call.count",
tags: [:type, :call_site, :result]
),
# Duration of each type check
distribution("my_app.type_check.call.duration",
unit: {:native, :microsecond},
reporter_options: [buckets: [10, 50, 100, 500, 1000, 5000]],
tags: [:type, :call_site]
),
# Violations (raised TypeErrors)
counter("my_app.type_check.violation.count",
tags: [:type, :call_site, :violation_kind]
),
# Soft check outcomes (non-raising observation mode)
counter("my_app.type_check.soft_check.count",
tags: [:type, :call_site, :result]
)
]
end
Step 5: Track Violation Rate per Type
Build a rolling violation rate detector to spot type contract erosion:
# lib/my_app/type_violation_tracker.ex
defmodule MyApp.TypeViolationTracker do
use GenServer
require Logger
@window_seconds 600
@alert_threshold 10
@poll_interval :timer.seconds(60)
def start_link(_), do: GenServer.start_link(__MODULE__, %{violations: []}, name: __MODULE__)
def init(state) do
:telemetry.attach(
"type-violation-tracker",
[:my_app, :type_check, :violation],
&handle_violation/4,
nil
)
schedule()
{:ok, state}
end
def handle_violation(_event, %{count: 1}, %{type: type, violation_kind: kind}, _) do
GenServer.cast(__MODULE__, {:record, type, kind, System.system_time(:second)})
end
def handle_cast({:record, type, kind, ts}, state) do
entry = {type, kind, ts}
{:noreply, %{state | violations: [entry | state.violations]}}
end
def handle_info(:check, state) do
cutoff = System.system_time(:second) - @window_seconds
recent =
state.violations
|> Enum.filter(fn {_, _, ts} -> ts > cutoff end)
recent
|> Enum.group_by(fn {type, _, _} -> type end)
|> Enum.each(fn {type, entries} ->
count = length(entries)
:telemetry.execute(
[:my_app, :type_check, :violation_rate],
%{count: count},
%{type: type, window_seconds: @window_seconds}
)
if count > @alert_threshold do
Logger.error("TypeCheck violation spike",
type: type,
count: count,
window_seconds: @window_seconds
)
end
end)
pruned = Enum.filter(state.violations, fn {_, _, ts} -> ts > cutoff end)
schedule()
{:noreply, %{state | violations: pruned}}
end
defp schedule, do: Process.send_after(self(), :check, @poll_interval)
end
Step 6: Health Endpoint
# lib/my_app_web/controllers/health_controller.ex
defmodule MyAppWeb.HealthController do
use MyAppWeb, :controller
def index(conn, _params) do
type_check_status = check_type_validation()
checks = %{
type_check: type_check_status,
database: check_db()
}
status = if type_check_status.status == "ok", do: 200, else: 503
conn
|> put_status(status)
|> json(%{
status: if(status == 200, do: "ok", else: "degraded"),
checks: checks
})
end
defp check_type_validation do
probe = %MyApp.User{
id: 1,
email: "health@probe.local",
role: :user,
created_at: DateTime.utc_now()
}
case MyApp.SoftTypeCheck.check(MyApp.User, probe, "health_check") do
{:ok, _} -> %{status: "ok"}
{:violation, _} -> %{status: "error", reason: "health probe violated User type contract"}
end
rescue
e -> %{status: "error", reason: Exception.message(e)}
end
defp check_db do
case Ecto.Adapters.SQL.query(MyApp.Repo, "SELECT 1", []) do
{:ok, _} -> %{status: "ok"}
_ -> %{status: "error"}
end
rescue
_ -> %{status: "error"}
end
end
Step 7: Create Monitors in Vigilmon
HTTP monitor for the health endpoint:
- Sign in at vigilmon.online
- Click New Monitor → HTTP
- Set URL to
https://your-app.example.com/health - Set interval to 60 seconds
- Alert condition: status
200and body contains"type_check":{"status":"ok"}
Heartbeat monitor for the type-checking subsystem:
# lib/my_app/type_check_heartbeat.ex
defmodule MyApp.TypeCheckHeartbeat do
use GenServer
@heartbeat_url System.get_env("VIGILMON_TYPECHECK_HEARTBEAT_URL")
@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(:ping, state) do
probe = %MyApp.User{
id: 999,
email: "heartbeat@probe.local",
role: :guest,
created_at: DateTime.utc_now()
}
case MyApp.SoftTypeCheck.check(MyApp.User, probe, "heartbeat") do
{:ok, _} -> ping_vigilmon()
{:violation, _} -> :skip
end
schedule()
{:noreply, state}
end
defp ping_vigilmon do
if @heartbeat_url do
:httpc.request(:get, {String.to_charlist(@heartbeat_url), []}, [{:timeout, 3000}], [])
end
end
defp schedule, do: Process.send_after(self(), :ping, @interval)
end
Create a Heartbeat monitor in Vigilmon with an 8-minute expected interval.
Step 8: Alerting
In Vigilmon, configure Notifications → New Channel:
Slack for TypeCheck violation spike:
⚠️ DEGRADED: TypeCheck Violations — MyApp
Type: MyApp.User
Violations: 23 in last 10 minutes (threshold: 10)
Action: Check User type contract — possible upstream API schema change
PagerDuty for critical type check failure:
Page on-call if User, Order, or Payment type violations exceed 50 per minute — this indicates data corruption risk.
Grafana alert for sustained violation rate:
- alert: TypeCheckViolationSpike
expr: |
sum(rate(my_app_type_check_violation_count[5m])) by (type) > 0.5
for: 2m
annotations:
summary: "TypeCheck violations spiking for {{ $labels.type }} — possible interface contract break"
Common TypeCheck Issues and Fixes
Expensive type checks in hot paths:
# TypeCheck validation runs on every call — measure the overhead
# and move checks to boundary entry points rather than inner loops
defmodule MyApp.DataProcessor do
def process_batch(items) when is_list(items) do
# Validate once at the boundary
Enum.each(items, fn item ->
MyApp.SoftTypeCheck.check(MyApp.Item, item, "DataProcessor.process_batch")
end)
# Process without per-item type checking inside the loop
Enum.map(items, &process_item/1)
end
end
Violations from external API responses:
# Normalize API responses before type checking to avoid spurious violations
defmodule MyApp.ExternalAPI do
def fetch_user(id) do
with {:ok, raw} <- HTTPoison.get("https://api.example.com/users/#{id}"),
{:ok, body} <- Jason.decode(raw.body),
normalized = normalize_user(body),
{:ok, user} <- MyApp.SoftTypeCheck.check(MyApp.User, normalized, "ExternalAPI.fetch_user") do
{:ok, user}
end
end
defp normalize_user(%{"id" => id, "email" => email, "role" => role}) do
%MyApp.User{
id: id,
email: email,
role: String.to_existing_atom(role),
created_at: DateTime.utc_now()
}
rescue
_ -> %{}
end
end
Violations hiding behind catch-all rescue:
# Don't silence TypeCheck.TypeError in generic rescue blocks
def risky_call(data) do
process(data)
rescue
e in TypeCheck.TypeError ->
# Re-emit telemetry, then decide: re-raise or return error tuple
MyApp.TypeCheckMonitor.record_violation(e, "risky_call")
{:error, :type_violation}
e ->
{:error, Exception.message(e)}
end
What You've Built
| What | How | |------|-----| | Instrumented type check wrapper | Telemetry on every check with duration and outcome | | Soft check mode | Non-raising type observation for non-critical paths | | Violation rate tracker | Rolling-window counter with GenServer alerting | | Structured health endpoint | JSON health with type validation probe result | | Liveness heartbeat | GenServer pinging Vigilmon after successful type check | | Real-time alerting | Vigilmon HTTP monitor + PagerDuty on violation spikes |
TypeCheck makes Elixir's type guarantees visible at runtime. Vigilmon makes those runtime violations visible to you — before malformed data propagates through your system and causes failures that are much harder to diagnose.
Next Steps
- Use TypeCheck with Ecto changesets at database boundaries to enforce type contracts on all persisted data
- Add per-type violation dashboards in Grafana showing violation kinds (wrong type, pattern mismatch, invalid enum) over time
- Integrate TypeCheck violation telemetry with your distributed tracing system to correlate violations with specific request traces
- Set up Vigilmon monitors for each external API integration to detect interface drift the moment an upstream schema changes
Get started free at vigilmon.online.