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
A[Application event] --> B[:telemetry.execute/3]
B --> C[Telemetry handler]
C --> D[EventHandler]
D --> E[Alert evaluation]
E --> F[ETS state and guards]
F --> G[Notification formatting of breach alert]
G --> H[Task.Supervisor]
H --> I[Console / Slack / Teams / Webhook]
F --> J[BackgroundCleaner]
J --> K[Calm notification]
K --> 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. Metric alerts enter recovery when a healthy measurement is received and remain healthy for resolution_ms; aggregate alerts recover when their active failure windows expire; correlated event alerts recover when their matching recovery event satisfies the configured predicate.
  7. NatureWhistle.BackgroundCleaner manages metric-resolution timers, aggregate recovery sweeps, and stale guard-state cleanup.
  8. 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]
}

An event alert reacts to occurrence; it does not automatically know when the problem is resolved and therefore does not automatically send a calm message. Add correlation when another telemetry event represents recovery:

%{
id: :worker_crashed,
event: [:my_app, :worker, :crash],
condition: :event,
correlation: %{
key: fn metadata -> metadata.worker end,
recovery_event: [:my_app, :worker, :healthy],
recovery?: fn metadata -> metadata.status == :ok end
},
alert_message: "🚨 Worker crash detected",
calm_message: "✅ Worker recovered",
notifiers: [:console]
}

NatureWhistle correlates the failure and recovery using the value returned by key. A recovery for a different key does not match the failed occurrence. When the recovery predicate matches, NatureWhistle sends the calm notification and clears that correlation record.

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,
[
key: fn metadata -> Map.get(metadata, :job_id) end,
failures: 5,
within_ms: 60_000
]},
alert_message: "🚨 Repeated failures: %{value}",
calm_message: "✅ Failure rate recovered",
notifiers: [:console]
}

Aggregate options are a keyword list. The required :key function receives telemetry metadata and returns the logical identity whose failures should be counted, such as a job ID, worker, endpoint, or tenant. Failures for different keys are tracked independently.

NatureWhistle.FailureTracker determines when repeated failures become significant. Once the aggregate threshold is reached, the count flows through the same normal alert state, guard, and notification machinery used by the other alert primitives. When the aggregation window expires, the tracker reports recovery and NatureWhistle clears the breached state and sends the configured calm notification. If an alert has multiple active failure keys, it is resolved only after all active keys have recovered.

🚀 Installation & Setup

Add nature_whistle to your mix.exs dependencies:

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

NatureWhistle.Application is started automatically as the dependency's OTP application. You do not need to add it as a second child in your supervision tree. Configure the application and start your normal application as usual.

If your application deliberately uses runtime: false or starts dependencies manually, start :nature_whistle explicitly before registering alerts or emitting telemetry events.

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, [key: key_fun, failures: count, within_ms: window]}.
measurement_key No, defaults to :value Key in the telemetry measurements map that holds the numeric value. Aggregate alerts use their key function to identify the failure stream.
threshold Depends on condition Threshold/value used by metric alerts; aggregate alerts use failures inside the aggregate options.
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 notification cap. When nil or omitted, rate-limit checks and ETS bookkeeping are disabled.
sliding_window No, defaults to nil Optional recent-breach-density gate. When nil or omitted, sliding-window checks and ETS bookkeeping are disabled.
event_value No, defaults to 1 Value passed through the event-alert notification path.
message_formatter No Optional one-argument function that formats metadata for notification messages.
correlation No %{key: key_fun, recovery_event: event, recovery?: predicate} configuration for event-alert recovery.
aggregate No Derived runtime field; configure aggregation through condition: {:aggregate, options} instead.

Important note on timing and recovery

resolution_ms is the active recovery timer for metric alerts. A metric alert must receive a healthy measurement and remain healthy for that duration before NatureWhistle sends its calm notification. A later breach during recovery cancels that recovery timer.

An event alert has no numeric healthy measurement, so it does not recover from resolution_ms alone. It needs a configured correlation with a matching recovery_event and recovery? predicate. Without that, the event alert remains an occurrence record and has no automatic calm transition.

Aggregate alerts recover from their within_ms failure windows. The NatureWhistle.FailureTracker reports expired active windows to the runtime, which clears the aggregate breach when no active failure key remains and sends the calm notification.

debounce_ms is stored in the normalized alert configuration, but it is not part of the active runtime decision path yet.

Aggregation window vs notification window

These are different concepts:

rate_limit and sliding_window are opt-in. Passing nil or omitting either option disables that guard completely, including its ETS bookkeeping. They do not replace the normal breached-state transition that prevents duplicate notifications during one continuous breach.

NatureWhistle.FailureTracker tracks aggregation state independently for each alert ID and failure key. If one alert has several active failure keys, the alert remains breached until all active keys recover.

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.

Documentation

The project is configured to publish this README, the changelog, and the module API pages through ExDoc:

mix docs

The generated site is written to doc/. Publishing a new package version to Hex.pm publishes the corresponding documentation to HexDocs as part of the release process:

mix hex.publish

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 is a telemetry consumer; it does not start Oban or add Oban as a dependency. Your application supplies the real Oban package and its telemetry events, while NatureWhistle translates those events into alert definitions.

The pack consumes the real Oban job telemetry event names:

[:oban, :job, :stop]
[:oban, :job, :exception]

Oban publishes duration and queue_time measurements in native time units on both events. The pack accepts thresholds in milliseconds and converts them to native units before NatureWhistle evaluates them.

By default, the pack creates:

Alert Event Measurement or signal
:nature_whistle_oban_slow_job [:oban, :job, :stop] :duration
:nature_whistle_oban_slow_queue [:oban, :job, :stop] :queue_time
:nature_whistle_oban_job_exception [:oban, :job, :exception] Event occurrence

The slow-job and slow-queue alerts currently cover completed :stop events. They do not evaluate failed :exception events against those duration thresholds. The exception alert uses the Oban job ID, worker, and queue as its correlation key and treats a matching :stop event with state: :success as recovery:

config :nature_whistle,
packs: [
{NatureWhistle.Packs.Oban,
thresholds: [
slow_job: 5_000,
slow_queue: 1_000
],
failure_detection: [
failures: 3,
within_ms: 300_000
]}
]

failure_detection is disabled by default. When enabled, repeated exceptions for the same {job_id, worker, queue} key are aggregated before producing an alert. 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