Packages

A comprehensive SNMP toolkit for Elixir featuring a unified API, pure Elixir implementation, and powerful device simulation. Perfect for network monitoring, testing, and development with support for SNMP operations, MIB management, and realistic device simulation.

Current section

Files

Jump to
snmpkit lib snmpkit snmp_lib dashboard.ex
Raw

lib/snmpkit/snmp_lib/dashboard.ex

defmodule SnmpKit.SnmpLib.Dashboard do
@moduledoc """
Real-time monitoring dashboard and metrics aggregation for SNMP operations.
This module provides a comprehensive monitoring and visualization system for
production SNMP deployments. Based on patterns proven in large-scale monitoring
systems managing thousands of network devices.
## Features
- **Real-Time Metrics**: Live updates of performance and health metrics
- **Historical Analytics**: Trend analysis and capacity planning data
- **Alert Management**: Configurable thresholds and notification routing
- **Performance Insights**: Detailed breakdown of operation performance
- **Device Health**: Per-device status monitoring and diagnostics
- **Resource Utilization**: Pool, memory, and system resource tracking
## Metrics Categories
### Performance Metrics
- Request/response times (min, max, average, percentiles)
- Throughput (operations per second)
- Error rates and failure classifications
- Connection pool utilization
### Health Metrics
- Device availability and reachability
- Circuit breaker states
- Retry counts and backoff status
- Resource exhaustion indicators
### System Metrics
- Memory usage and garbage collection
- Process counts and supervision tree health
- Network socket utilization
- Queue depths and processing delays
## Dashboard Views
### Overview Dashboard
Global health and performance summary with key indicators.
### Device Dashboard
Per-device detailed metrics and troubleshooting information.
### Pool Dashboard
Connection pool health, utilization, and performance metrics.
### Alerts Dashboard
Active alerts, acknowledgments, and escalation status.
## Usage Patterns
# Start the dashboard server
{:ok, _pid} = SnmpKit.SnmpLib.Dashboard.start_link(port: 4000)
# Record custom metrics
SnmpKit.SnmpLib.Dashboard.record_metric(:custom_operation, %{
duration: 150,
device: "192.168.1.1",
status: :success
})
# Create custom alert
SnmpKit.SnmpLib.Dashboard.create_alert(:high_error_rate, %{
device: "192.168.1.100",
error_rate: 0.15,
threshold: 0.10
})
# Export metrics for external systems
prometheus_data = SnmpKit.SnmpLib.Dashboard.export_prometheus()
## Integration with External Systems
- **Prometheus**: Native metrics export in Prometheus format
- **Grafana**: Pre-built dashboards and alerting rules
- **PagerDuty**: Alert escalation and incident management
- **Slack/Teams**: Notification integration for team alerting
"""
use GenServer
require Logger
@metrics_table :snmp_lib_metrics
@alerts_table :snmp_lib_alerts
@timeseries_table :snmp_lib_timeseries
@default_port 4000
# 5 seconds
@default_update_interval 5_000
@default_retention_days 7
@type metric_name :: atom()
@type metric_value :: number()
@type metric_tags :: map()
@type alert_level :: :info | :warning | :critical
@type dashboard_opts :: [
port: pos_integer(),
update_interval: pos_integer(),
retention_days: pos_integer(),
prometheus_enabled: boolean(),
grafana_integration: boolean()
]
defstruct [
:port,
:update_interval,
:retention_days,
:web_server_pid,
:prometheus_enabled,
:grafana_integration,
metrics_buffer: [],
alerts_buffer: [],
last_cleanup: nil
]
## Public API
@doc """
Starts the dashboard server with monitoring and web interface.
## Options
- `port`: Web dashboard port (default: 4000)
- `update_interval`: Metrics update frequency in milliseconds (default: 5000)
- `retention_days`: How long to keep historical data (default: 7)
- `prometheus_enabled`: Enable Prometheus metrics endpoint (default: false)
- `grafana_integration`: Enable Grafana dashboard integration (default: false)
## Examples
# Start with defaults
{:ok, _pid} = SnmpKit.SnmpLib.Dashboard.start_link()
# Start with custom configuration
{:ok, _pid} = SnmpKit.SnmpLib.Dashboard.start_link(
port: 8080,
prometheus_enabled: true,
retention_days: 14
)
"""
@spec start_link(dashboard_opts()) :: {:ok, pid()} | {:error, any()}
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Records a metric data point for monitoring and visualization.
## Parameters
- `metric_name`: Unique identifier for the metric type
- `value`: Numeric value for the metric
- `tags`: Optional metadata for filtering and grouping
## Examples
# Record response time metric
SnmpKit.SnmpLib.Dashboard.record_metric(:snmp_response_time, 125, %{
device: "192.168.1.1",
operation: "get",
community: "public"
})
# Record error count
SnmpKit.SnmpLib.Dashboard.record_metric(:snmp_errors, 1, %{
device: "192.168.1.1",
error_type: "timeout"
})
# Record pool utilization
SnmpKit.SnmpLib.Dashboard.record_metric(:pool_utilization, 0.75, %{
pool_name: "main_pool"
})
"""
@spec record_metric(metric_name(), metric_value(), metric_tags()) :: :ok
def record_metric(metric_name, value, tags \\ %{}) when is_number(value) do
timestamp = System.system_time(:millisecond)
metric = %{
name: metric_name,
value: value,
tags: tags,
timestamp: timestamp
}
GenServer.cast(__MODULE__, {:record_metric, metric})
end
@doc """
Creates an alert for monitoring and notification systems.
## Parameters
- `alert_name`: Unique identifier for the alert type
- `level`: Alert severity level (:info, :warning, :critical)
- `details`: Alert metadata and context information
## Examples
# Create device unreachable alert
SnmpKit.SnmpLib.Dashboard.create_alert(:device_unreachable, :critical, %{
device: "192.168.1.1",
last_seen: DateTime.utc_now(),
consecutive_failures: 5
})
# Create performance degradation warning
SnmpKit.SnmpLib.Dashboard.create_alert(:slow_response, :warning, %{
device: "192.168.1.1",
avg_response_time: 5000,
threshold: 2000
})
"""
@spec create_alert(atom(), alert_level(), map()) :: :ok
def create_alert(alert_name, level, details \\ %{}) do
alert = %{
name: alert_name,
level: level,
details: details,
timestamp: System.system_time(:millisecond),
acknowledged: false
}
GenServer.cast(__MODULE__, {:create_alert, alert})
end
@doc """
Acknowledges an alert to stop notifications.
## Examples
:ok = SnmpKit.SnmpLib.Dashboard.acknowledge_alert(:device_unreachable, "192.168.1.1")
"""
@spec acknowledge_alert(atom(), any()) :: :ok
def acknowledge_alert(alert_name, identifier) do
GenServer.cast(__MODULE__, {:acknowledge_alert, alert_name, identifier})
end
@doc """
Gets current performance metrics summary.
## Returns
A map containing aggregated metrics:
- `total_operations`: Total SNMP operations performed
- `success_rate`: Percentage of successful operations
- `avg_response_time`: Average response time in milliseconds
- `active_devices`: Number of devices being monitored
- `pool_utilization`: Connection pool usage percentage
- `error_rates`: Breakdown of error types and frequencies
## Examples
metrics = SnmpKit.SnmpLib.Dashboard.get_metrics_summary()
IO.puts "Success rate: " <> Float.to_string(metrics.success_rate * 100) <> "%"
"""
@spec get_metrics_summary() :: map()
def get_metrics_summary do
GenServer.call(__MODULE__, :get_metrics_summary)
end
@doc """
Gets detailed metrics for a specific device.
## Examples
device_metrics = SnmpKit.SnmpLib.Dashboard.get_device_metrics("192.168.1.1")
IO.inspect device_metrics.response_times
"""
@spec get_device_metrics(binary()) :: map()
def get_device_metrics(device_id) do
GenServer.call(__MODULE__, {:get_device_metrics, device_id})
end
@doc """
Gets all active alerts with optional filtering.
## Examples
all_alerts = SnmpKit.SnmpLib.Dashboard.get_active_alerts()
critical_alerts = SnmpKit.SnmpLib.Dashboard.get_active_alerts(level: :critical)
"""
@spec get_active_alerts(keyword()) :: [map()]
def get_active_alerts(filters \\ []) do
GenServer.call(__MODULE__, {:get_active_alerts, filters})
end
@doc """
Exports metrics in Prometheus format for external monitoring.
## Examples
prometheus_data = SnmpKit.SnmpLib.Dashboard.export_prometheus()
File.write!("/tmp/snmp_metrics.prom", prometheus_data)
"""
@spec export_prometheus() :: binary()
def export_prometheus do
GenServer.call(__MODULE__, :export_prometheus)
end
@doc """
Gets historical time series data for a metric.
## Parameters
- `metric_name`: Name of the metric to retrieve
- `duration`: Time window in milliseconds (default: 1 hour)
- `tags`: Optional tag filters
## Examples
# Get last hour of response times
timeseries = SnmpKit.SnmpLib.Dashboard.get_timeseries(:snmp_response_time)
# Get last 24 hours for specific device
device_data = SnmpKit.SnmpLib.Dashboard.get_timeseries(
:snmp_response_time,
24 * 60 * 60 * 1000,
%{device: "192.168.1.1"}
)
"""
@spec get_timeseries(metric_name(), pos_integer(), map()) :: [map()]
def get_timeseries(metric_name, duration \\ 3_600_000, tags \\ %{}) do
GenServer.call(__MODULE__, {:get_timeseries, metric_name, duration, tags})
end
## GenServer Implementation
@impl GenServer
def init(opts) do
# Create ETS tables for metrics storage
:ets.new(@metrics_table, [:named_table, :bag, :public])
:ets.new(@alerts_table, [:named_table, :bag, :public])
:ets.new(@timeseries_table, [:named_table, :ordered_set, :public])
state = %__MODULE__{
port: Keyword.get(opts, :port, @default_port),
update_interval: Keyword.get(opts, :update_interval, @default_update_interval),
retention_days: Keyword.get(opts, :retention_days, @default_retention_days),
prometheus_enabled: Keyword.get(opts, :prometheus_enabled, false),
grafana_integration: Keyword.get(opts, :grafana_integration, false),
last_cleanup: System.system_time(:millisecond)
}
# Start web server if enabled
web_server_pid = start_web_server(state)
state = %{state | web_server_pid: web_server_pid}
# Schedule periodic tasks
schedule_update()
schedule_cleanup()
Logger.info("SnmpKit.SnmpLib.Dashboard started on port #{state.port}")
{:ok, state}
end
@impl GenServer
def handle_cast({:record_metric, metric}, state) do
# Store in main metrics table
:ets.insert(@metrics_table, {metric.name, metric})
# Store in timeseries table for historical analysis
timeseries_key = {metric.name, metric.timestamp}
:ets.insert(@timeseries_table, {timeseries_key, metric})
# Add to buffer for processing
new_buffer = [metric | state.metrics_buffer]
new_state = %{state | metrics_buffer: new_buffer}
{:noreply, new_state}
end
@impl GenServer
def handle_cast({:create_alert, alert}, state) do
# Store alert
:ets.insert(@alerts_table, {alert.name, alert})
# Add to buffer for notification processing
new_buffer = [alert | state.alerts_buffer]
new_state = %{state | alerts_buffer: new_buffer}
# Log alert based on level
case alert.level do
:critical -> Logger.error("CRITICAL ALERT: #{alert.name} - #{inspect(alert.details)}")
:warning -> Logger.warning("WARNING ALERT: #{alert.name} - #{inspect(alert.details)}")
:info -> Logger.info("INFO ALERT: #{alert.name} - #{inspect(alert.details)}")
end
{:noreply, new_state}
end
@impl GenServer
def handle_cast({:acknowledge_alert, alert_name, identifier}, state) do
# Find and update matching alerts
alerts = :ets.lookup(@alerts_table, alert_name)
Enum.each(alerts, fn {name, alert} ->
if match_alert_identifier(alert, identifier) do
updated_alert = %{alert | acknowledged: true}
:ets.delete_object(@alerts_table, {name, alert})
:ets.insert(@alerts_table, {name, updated_alert})
end
end)
{:noreply, state}
end
@impl GenServer
def handle_call(:get_metrics_summary, _from, state) do
summary = calculate_metrics_summary()
{:reply, summary, state}
end
@impl GenServer
def handle_call({:get_device_metrics, device_id}, _from, state) do
metrics = calculate_device_metrics(device_id)
{:reply, metrics, state}
end
@impl GenServer
def handle_call({:get_active_alerts, filters}, _from, state) do
alerts = get_filtered_alerts(filters)
{:reply, alerts, state}
end
@impl GenServer
def handle_call(:export_prometheus, _from, state) do
prometheus_data = generate_prometheus_export()
{:reply, prometheus_data, state}
end
@impl GenServer
def handle_call({:get_timeseries, metric_name, duration, tags}, _from, state) do
timeseries = extract_timeseries_data(metric_name, duration, tags)
{:reply, timeseries, state}
end
@impl GenServer
def handle_info(:update_metrics, state) do
# Process buffered metrics
process_metrics_buffer(state.metrics_buffer)
# Process buffered alerts
process_alerts_buffer(state.alerts_buffer)
# Clear buffers
new_state = %{state | metrics_buffer: [], alerts_buffer: []}
schedule_update()
{:noreply, new_state}
end
@impl GenServer
def handle_info(:cleanup_old_data, state) do
current_time = System.system_time(:millisecond)
cleanup_threshold = current_time - state.retention_days * 24 * 60 * 60 * 1000
# Clean up old timeseries data
cleanup_old_timeseries(cleanup_threshold)
# Clean up acknowledged alerts older than 24 hours
alert_cleanup_threshold = current_time - 24 * 60 * 60 * 1000
cleanup_old_alerts(alert_cleanup_threshold)
schedule_cleanup()
new_state = %{state | last_cleanup: current_time}
{:noreply, new_state}
end
## Private Implementation
# Web server management
defp start_web_server(state) do
# In a real implementation, this would start a web server
# For now, we'll just return a placeholder pid
spawn(fn ->
Process.sleep(1000)
Logger.info("Dashboard web interface would be available at http://localhost:#{state.port}")
end)
end
# Metrics calculation
defp calculate_metrics_summary do
current_time = System.system_time(:millisecond)
one_hour_ago = current_time - 3_600_000
# Get recent metrics
recent_metrics =
:ets.select(@timeseries_table, [
{{{:_, :"$1"}, :_}, [{:>=, :"$1", one_hour_ago}], [:"$_"]}
])
# Calculate summary statistics
total_operations = length(recent_metrics)
success_count = count_successful_operations(recent_metrics)
response_times = extract_response_times(recent_metrics)
%{
total_operations: total_operations,
success_rate: if(total_operations > 0, do: success_count / total_operations, else: 0.0),
avg_response_time:
if(length(response_times) > 0,
do: Enum.sum(response_times) / length(response_times),
else: 0.0
),
active_devices: count_active_devices(recent_metrics),
pool_utilization: get_current_pool_utilization(),
error_rates: calculate_error_rates(recent_metrics)
}
end
defp calculate_device_metrics(device_id) do
# Get metrics for specific device
device_metrics =
:ets.select(@metrics_table, [
{{:_, %{tags: %{device: device_id}}}, [], [:"$_"]}
])
%{
device_id: device_id,
total_operations: length(device_metrics),
response_times: extract_device_response_times(device_metrics),
error_count: count_device_errors(device_metrics),
last_seen: get_device_last_seen(device_metrics)
}
end
# Alert management
defp get_filtered_alerts(filters) do
level_filter = Keyword.get(filters, :level)
acknowledged_filter = Keyword.get(filters, :acknowledged, false)
:ets.tab2list(@alerts_table)
|> Enum.map(fn {_name, alert} -> alert end)
|> Enum.filter(fn alert ->
(is_nil(level_filter) or alert.level == level_filter) and
alert.acknowledged == acknowledged_filter
end)
|> Enum.sort_by(& &1.timestamp, :desc)
end
defp match_alert_identifier(alert, identifier) do
# Match alert by device ID or other identifier
case alert.details do
%{device: ^identifier} -> true
_ -> false
end
end
# Prometheus export
defp generate_prometheus_export do
metrics_summary = calculate_metrics_summary()
prometheus_lines = [
"# HELP snmp_lib_total_operations Total SNMP operations performed",
"# TYPE snmp_lib_total_operations counter",
"snmp_lib_total_operations #{metrics_summary.total_operations}",
"",
"# HELP snmp_lib_success_rate Success rate of SNMP operations",
"# TYPE snmp_lib_success_rate gauge",
"snmp_lib_success_rate #{metrics_summary.success_rate}",
"",
"# HELP snmp_lib_avg_response_time Average response time in milliseconds",
"# TYPE snmp_lib_avg_response_time gauge",
"snmp_lib_avg_response_time #{metrics_summary.avg_response_time}",
""
]
Enum.join(prometheus_lines, "\n")
end
# Timeseries data extraction
defp extract_timeseries_data(metric_name, duration, tags) do
current_time = System.system_time(:millisecond)
start_time = current_time - duration
# Get timeseries data within time window
:ets.select(@timeseries_table, [
{{{metric_name, :"$1"}, :"$2"}, [{:>=, :"$1", start_time}], [:"$2"]}
])
|> Enum.filter(fn metric ->
tags_match(metric.tags, tags)
end)
|> Enum.sort_by(& &1.timestamp)
end
defp tags_match(metric_tags, filter_tags) do
Enum.all?(filter_tags, fn {key, value} ->
Map.get(metric_tags, key) == value
end)
end
# Data processing helpers
defp count_successful_operations(metrics) do
Enum.count(metrics, fn {_key, metric} ->
case metric.tags do
%{status: :success} -> true
_ -> false
end
end)
end
defp extract_response_times(metrics) do
metrics
|> Enum.filter(fn {_key, metric} -> metric.name == :snmp_response_time end)
|> Enum.map(fn {_key, metric} -> metric.value end)
end
defp extract_device_response_times(metrics) do
metrics
|> Enum.filter(fn {_name, metric} -> metric.name == :snmp_response_time end)
|> Enum.map(fn {_name, metric} -> metric.value end)
end
defp count_device_errors(metrics) do
Enum.count(metrics, fn {_name, metric} ->
case metric.tags do
%{status: :error} -> true
_ -> false
end
end)
end
defp get_device_last_seen(metrics) do
case metrics do
[] ->
nil
_ ->
metrics
|> Enum.map(fn {_name, metric} -> metric.timestamp end)
|> Enum.max()
end
end
defp count_active_devices(metrics) do
metrics
|> Enum.map(fn {_key, metric} -> Map.get(metric.tags, :device) end)
|> Enum.filter(& &1)
|> Enum.uniq()
|> length()
end
defp get_current_pool_utilization do
# This would get actual pool utilization from SnmpKit.SnmpLib.Pool
# For now, return a placeholder
0.5
end
defp calculate_error_rates(metrics) do
error_metrics =
Enum.filter(metrics, fn {_key, metric} ->
Map.get(metric.tags, :status) == :error
end)
Enum.group_by(error_metrics, fn {_key, metric} ->
Map.get(metric.tags, :error_type, :unknown)
end)
|> Enum.map(fn {error_type, errors} -> {error_type, length(errors)} end)
|> Enum.into(%{})
end
# Cleanup functions
defp cleanup_old_timeseries(threshold) do
old_keys =
:ets.select(@timeseries_table, [
{{{:_, :"$1"}, :_}, [{:<, :"$1", threshold}], [:"$_"]}
])
Enum.each(old_keys, fn {key, _metric} ->
:ets.delete(@timeseries_table, key)
end)
Logger.debug("Cleaned up #{length(old_keys)} old timeseries points")
end
defp cleanup_old_alerts(threshold) do
old_alerts =
:ets.select(@alerts_table, [
{{:_, %{acknowledged: true, timestamp: :"$1"}}, [{:<, :"$1", threshold}], [:"$_"]}
])
Enum.each(old_alerts, fn {name, alert} ->
:ets.delete_object(@alerts_table, {name, alert})
end)
Logger.debug("Cleaned up #{length(old_alerts)} old acknowledged alerts")
end
# Buffer processing
defp process_metrics_buffer(buffer) do
# This would perform aggregation, anomaly detection, etc.
Logger.debug("Processed #{length(buffer)} metrics in buffer")
end
defp process_alerts_buffer(buffer) do
# This would send notifications, update external systems, etc.
critical_alerts = Enum.filter(buffer, &(&1.level == :critical))
if length(critical_alerts) > 0 do
Logger.warning("#{length(critical_alerts)} critical alerts need attention")
end
end
# Scheduling
defp schedule_update do
Process.send_after(self(), :update_metrics, @default_update_interval)
end
defp schedule_cleanup do
# Clean up old data every hour
Process.send_after(self(), :cleanup_old_data, 3_600_000)
end
end