NatureWhistle

NatureWhistle Banner

Nature Whistle CI Hex.pm version Hex.pm downloads Hex.pm License

Let your system whisper its troubles before they become screams.

NatureWhistle is a telemetry-driven alerting library for Elixir applications. It listens to :telemetry events, evaluates them against alert rules stored in ETS, and sends notifications to Slack, Microsoft Teams, generic webhooks, or the console.

It is designed for simple setup and low runtime overhead:

How It Works

flowchart LR
T[:telemetry.execute] --> H[EventHandler]
H --> G[EventGuard]
G -->|breach allowed| S[State update]
S --> N[Notification]
N --> Q[Task.Supervisor]
Q --> C[Console / Slack / Teams / Webhook]
S --> B[BackgroundCleaner timer]
B -->|resolution reached| C2[Calm notification]
B -->|cleanup sweep| R[Prune stale ETS buckets]
H --> F[FailureTracker]
F -->|aggregate threshold reached| S
Beam[BEAM Pack] --> BC[BEAM Collector]
BC -->|Telemetry events| H

When a telemetry event arrives:

  1. NatureWhistle.EventHandler looks up all alerts for that telemetry event.
  2. The measurement value is extracted from the telemetry payload.
  3. NatureWhistle.EventGuard applies rate-limit and sliding-window checks.
  4. Aggregate alerts can pass repeated failures through NatureWhistle.FailureTracker to determine whether the configured failure threshold has been reached within an aggregation window.
  5. If the breach is actionable, the alert state is marked as breached and an alert notification is queued.
  6. NatureWhistle.BackgroundCleaner later resolves the alert and sends the calm notification once the resolution timer expires.
  7. Runtime alerts can be added or removed without restarting the application.

Features

Alert Primitives

NatureWhistle supports three alert primitives:

Metric

A metric alert fires when a numeric measurement crosses a configured threshold.

%{
id: :api_latency,
event: [:my_app, :request, :stop],
condition: :metric,
measurement_key: :duration,
threshold: 500,
alert_message: "⚠️ Slow request: %{value}",
calm_message: "✅ Request latency recovered: %{value}",
notifiers: [:console]
}

Event

An event alert reacts to the occurrence of a telemetry event. It is useful when the event itself is the signal rather than a numeric threshold.

%{
id: :worker_crashed,
event: [:my_app, :worker, :crash],
condition: :event,
alert_message: "🚨 Worker crash detected",
notifiers: [:console]
}

Aggregate

An aggregate alert turns repeated failures into an actionable signal. Failures must reach the configured threshold within the aggregation window.

%{
id: :repeated_failures,
event: [:my_app, :job, :failure],
condition: {:aggregate, %{failures: 5, within_ms: 60_000}},
alert_message: "🚨 Repeated failures: %{value}",
calm_message: "✅ Failure rate recovered",
notifiers: [:console]
}

NatureWhistle.FailureTracker is responsible only for determining when repeated failures become significant. Once the aggregate threshold is reached, the result flows through the same normal alert state and notification machinery used by the other alert primitives.

🚀 Installation & Setup

Add nature_whistle to your mix.exs dependencies:

defp deps do
[
{:nature_whistle, "~> 0.4.1"}
]
end

Then add NatureWhistle.Application to your supervision tree:

def start(_type, _args) do
children = [
MyApp.Repo,
MyAppWeb.Endpoint,
NatureWhistle.Application
]
Supervisor.start_link(children, strategy: :one_for_one)
end

Configuration

Configure NatureWhistle from config/config.exs:

config :nature_whistle,
retry: [
max_attempts: 5,
base_delay_ms: 1_000,
max_delay_ms: 60_000
],
packs: [
{NatureWhistle.Packs.Beam,
thresholds: [
memory: 1_073_741_824,
process_count: 50_000,
run_queue: 4
]}
],
notifiers_config: [
%{name: :console, service: :console, config: %{}},
%{
name: :slack_primary,
service: :slack,
config: %{webhook_url: "https://hooks.slack.com/services/T00/B00/X00"}
},
%{
name: :ops_webhook,
service: :webhook,
config: %{
webhook_url: "https://api.example.com/alerts",
method: :post,
headers: [{"x-api-key", "secret"}],
payload: %{source: "nature_whistle"}
}
}
],
alerts: [
%{
id: :high_cpu_load,
event: [:vm, :total_run_queue_lengths, :total],
measurement_key: :total,
threshold: 4,
alert_message: "🚨 High CPU load: run queue is %{value}",
calm_message: "✅ CPU load back to normal: %{value}",
resolution_ms: 60_000,
rate_limit: [window_ms: 60_000, max_events: 10],
sliding_window: [window_ms: 30_000, max_events: 3],
notifiers: [:console]
},
%{
id: :api_latency,
event: [:my_app, :request, :stop],
measurement_key: :duration,
threshold: 500,
formatter: fn duration -> "#{div(duration, 1_000)} ms" end,
alert_message: "⚠️ Slow request: %{value}",
calm_message: "✅ Request latency recovered: %{value}",
resolution_ms: 30_000,
notifiers: [:slack_primary, :ops_webhook]
}
]

Runtime Alert Registration

Alerts do not have to be known when the application starts. You can register and unregister alerts while the BEAM is running.

Register an alert

NatureWhistle.register_alert(%{
id: :manual_test_alert,
event: [:nature_whistle, :manual_test],
condition: :metric,
measurement_key: :duration,
threshold: 1_000,
alert_message: "🚨 Manual test alert: %{value}ms",
calm_message: "✅ Manual test alert recovered: %{value}ms",
notifiers: [:console]
})

A successful registration returns:

{:ok, alert}

The alert is immediately available to the runtime alert registry and its telemetry event is wired into the normal NatureWhistle event handling path.

Unregister an alert

NatureWhistle.unregister_alert(:manual_test_alert)

This removes the alert and its associated runtime state.

Runtime alerts are ephemeral. They live in memory and are lost when the BEAM/application restarts. Use application configuration for alerts that should be restored automatically after a restart.

Runtime registration behavior

Alert Reference

Field Required Description
id Yes Unique alert identifier used for ETS state and runtime registration.
event Yes Telemetry event name, for example [:vm, :memory, :total].
condition No, defaults to :metric Alert primitive: :metric, :event, or aggregate configuration.
measurement_key No, defaults to :value Key in the telemetry measurements map that holds the numeric value.
threshold Depends on condition Threshold/value used by the alert condition.
alert_message No Message used when the alert becomes actionable. Supports %{value}.
calm_message No Message used when the alert returns to normal. Supports %{value}.
formatter No Optional one-argument function for custom value formatting.
resolution_ms No, defaults to 60_000 How long the active alert remains in its breached lifecycle before recovery.
notifiers No, defaults to [:console] List of notifier profile names to use for this alert.
rate_limit No, defaults to nil Optional traffic cap that blocks repeated dispatches within window_ms.
sliding_window No, defaults to nil Optional breach-density gate for recent breaches.
aggregate No Aggregation settings used by aggregate alerts.
correlation No Correlation configuration for alerts that depend on related telemetry events.

Important note on timing

The current runtime uses resolution_ms as the active alert lifecycle timer. The alert remains in a breached state until that timer expires or is extended by another breach. debounce_ms is stored in the loaded alert config, but it is not part of the active runtime decision path yet.

Aggregation window vs notification window

These are different concepts:

NatureWhistle.FailureTracker tracks aggregation state independently for each alert ID and failure key.

Notifier Profiles

notifiers_config defines the actual delivery endpoints, and each alert chooses from those profiles by name.

Console

%{name: :console, service: :console, config: %{}}

Slack

%{
name: :slack_primary,
service: :slack,
config: %{webhook_url: "https://hooks.slack.com/services/..."}
}

Teams

%{
name: :teams_primary,
service: :teams,
config: %{webhook_url: "https://outlook.office.com/webhook/..."}
}

Generic webhook

%{
name: :ops_webhook,
service: :webhook,
config: %{
webhook_url: "https://your.service/hook",
method: :post,
headers: [{"x-api-key", "abc"}],
payload: %{source: "nature_whistle"}
}
}

Public API

The main runtime API is intentionally small:

The HexDocs module pages are the authoritative API-level reference for these functions and their configuration options.

Built-in Behavior

Built-in Packs

NatureWhistle can generate alerts for supported integrations through packs.

BEAM

The BEAM pack monitors runtime-level signals directly from the Erlang VM.

By default, when :alerts is not configured, NatureWhistle loads the built-in BEAM alerts. The pack currently covers:

Alert Metric Default threshold
:high_memory Total VM memory 1_073_741_824 bytes
:high_process_memory Process memory 536_870_912 bytes
:high_ets_memory ETS memory 268_435_456 bytes
:high_binary_memory Binary memory 268_435_456 bytes
:high_process_count Process count 50_000
:high_atom_count Atom count 800_000
:high_port_count Port count 5_000
:high_cpu Total run queue 4 per scheduler

The total run-queue threshold is scaled by the number of schedulers online, so a configured threshold of 4 becomes 4 × schedulers_online at runtime.

The BEAM collector is metric-driven: it only samples metrics required by the active BEAM alerts.

Configuring BEAM thresholds

Thresholds are configured by metric name. A numeric value overrides the default threshold, while false disables that metric.

config :nature_whistle,
alerts: [],
packs: [
{NatureWhistle.Packs.Beam,
thresholds: [
memory: 2_147_483_648,
process_memory: 1_073_741_824,
process_count: 100_000,
atom_count: false
]}
]

In this example:

Set alerts: [] when you want the BEAM pack to be the source of the built-in BEAM alerts with customized thresholds. Pack-generated alert IDs must remain unique across the complete alert configuration.

Ecto

The Ecto pack can generate alerts for slow database operations based on Ecto telemetry measurements, including:

Thresholds are configured in milliseconds, and individual metrics can be disabled with false. This lets you use a pack while keeping only the alerts that matter to your application.

For example, you can disable slow_queue while keeping the other Ecto alerts:

config :nature_whistle,
packs: [
{NatureWhistle.Packs.Ecto,
repo: MyApp.Repo,
thresholds: [
slow_query: 1_000,
slow_queue: false,
slow_db_execution: 500
]}
]

Set any supported alert threshold to false to disable that alert from the pack.

Oban

The Oban pack provides alert definitions for Oban-related operational signals, including repeated job failures and slow jobs/queues where configured.

Pack-generated alerts use the same alert primitives and notification pipeline as manually configured alerts.

Default Alerts

If you do not define :alerts, NatureWhistle loads the built-in BEAM pack alerts using the default thresholds above.

These alerts monitor:

All built-in BEAM alerts use the console notifier by default. The run-queue threshold is scaled by the number of schedulers online.

If you want to customize or selectively disable the built-in BEAM metrics, configure NatureWhistle.Packs.Beam explicitly and set alerts: [] so the customized pack definitions are used instead of the implicit defaults.

Telemetry Example

Emit your own telemetry event like this:

:telemetry.execute(
[:my_app, :db, :query],
%{duration: 650},
%{query: "SELECT * FROM users"}
)

Then add a matching alert:

%{
id: :slow_query,
event: [:my_app, :db, :query],
measurement_key: :duration,
threshold: 500,
alert_message: "🐢 Slow query: %{value}",
calm_message: "✅ Query speed recovered: %{value}",
resolution_ms: 60_000,
notifiers: [:console]
}

Message Formatting

Why NatureWhistle

NatureWhistle sits between raw telemetry and full observability stacks. If you already emit metrics with tools like Phoenix, Ecto, Oban, or PromEx, it gives you a lightweight alerting path without introducing a separate alert manager or a new service to operate.

Use it when you want:

License

MIT