SnmpKit Unified API Guide
A comprehensive guide to SnmpKit's new unified API design
π― Overview
SnmpKit provides a unified API that organizes all functionality into logical, context-based modules. This design eliminates naming conflicts, improves discoverability, and provides a cleaner developer experience. In 1.0, result shapes are standardized as enriched maps across operations.
Note: Migrating from 0.x to 1.x? See the Enriched Output Migration Guide: enriched-output-migration.html
ποΈ API Architecture
Context-Based Modules
| Module | Purpose | Functions |
|---|---|---|
SnmpKit | Preferred concise helpers | get, set, walk, walk_table, get_bulk, bulk_walk, get_bulk_multi, walk_multi |
SnmpKit.SNMP | Full namespaced API | async ops, streaming, pretty formatting, engine/metrics |
SnmpKit.MIB | MIB management | resolve, compile, load, tree navigation |
SnmpKit.Sim | Device simulation | start devices, create populations, testing |
Design Benefits
β
Concise by default - Prefer SnmpKit helpers for common tasks
β
No Naming Conflicts - Context prevents function name collisions
β
Improved Discoverability - Related functions grouped logically
β
Clear Documentation - Module boundaries define responsibilities
β
Backward Compatibility - Namespaced API remains available
β
Flexible Usage - Use concise helpers or full namespaced API as needed
π‘ SNMP Operations (prefer concise helpers on SnmpKit)
While the namespaced SnmpKit.SNMP API remains available, prefer the concise helpers on SnmpKit for everyday use. They are thin delegates with the same behavior.
Basic Operations
# GET operations (enriched varbind map)
{:ok, %{name: name, oid: oid, type: type, value: value, formatted: formatted}} =
SnmpKit.get("192.168.1.1", "sysDescr.0")
# WALK operations (list of enriched maps)
{:ok, results} = SnmpKit.walk("192.168.1.1", "system")
{:ok, table} = SnmpKit.walk_table("192.168.1.1", "ifTable")
# SET operations (to simulation devices)
:ok = SnmpKit.set("127.0.0.1:1161", "sysContact.0", "admin@example.com")Bulk Operations
# Efficient bulk retrieval
{:ok, results} = SnmpKit.get_bulk("192.168.1.1", "interfaces", max_repetitions: 10)
# Bulk walk (uses GETBULK for v2c)
{:ok, results} = SnmpKit.bulk_walk("192.168.1.1", "system")
# Bang variants (raise on error)
results = SnmpKit.get_bulk!("192.168.1.1", "interfaces", max_repetitions: 10)
results = SnmpKit.bulk_walk!("192.168.1.1", "system")Multi-Target Operations
Concurrent Multi (formerly βMulti v2β)
- High-throughput multi-target execution using a shared socket and non-blocking correlation.
- Works with existing return_format options (:list, :with_targets, :map).
- No manual setup expected in typical app usage; some advanced examples may show explicit engine starts, which will be optional moving forward.
# Query multiple devices simultaneously
targets_and_oids = [
{"router1.example.com", "sysDescr.0"},
{"switch1.example.com", "sysUpTime.0"},
{"ap1.example.com", "sysLocation.0"}
]
{:ok, results} = SnmpKit.get_multi(targets_and_oids)
# Bulk operations across multiple targets
{:ok, results} = SnmpKit.walk_multi([
{"host1", "interfaces"},
{"host2", "system"}
])
# GETBULK across multiple targets
{:ok, results} = SnmpKit.get_bulk_multi([
{"switch1", "ifTable"},
{"switch2", "ifTable"}
], max_repetitions: 20)Streaming Operations
# Memory-efficient streaming for large datasets (bulk semantics enforced)
stream = SnmpKit.SNMP.bulk_walk_stream("192.168.1.1", "interfaces")
results = stream |> Stream.take(1000) |> Enum.to_list()
# Table streaming with bulk semantics
table_stream = SnmpKit.SNMP.table_bulk_stream("192.168.1.1", "ifTable")Async Operations
# Non-blocking operations
task = SnmpKit.SNMP.get_async("192.168.1.1", "sysDescr.0")
{:ok, result} = Task.await(task, 5000)
# Bulk async operations
task = SnmpKit.SNMP.get_bulk_async("192.168.1.1", "interfaces")Pretty Formatting
# Human-readable output that still preserves type and raw value
{:ok, %{formatted: uptime, value: ticks, type: :timeticks}} =
SnmpKit.SNMP.get_pretty("192.168.1.1", "sysUpTime.0")
{:ok, formatted_walk} = SnmpKit.SNMP.walk_pretty("192.168.1.1", "system")
# Returns: [%{oid: "...", type: :octet_string, value: "...", formatted: "..."}, ...]
{:ok, formatted_bulk} = SnmpKit.SNMP.bulk_walk_pretty("192.168.1.1", "interfaces")Advanced Features
# Engine management for performance
{:ok, _engine} = SnmpKit.SNMP.start_engine()
{:ok, stats} = SnmpKit.SNMP.get_engine_stats()
# Circuit breaker for reliability
{:ok, result} = SnmpKit.SNMP.with_circuit_breaker("unreliable.host", fn ->
SnmpKit.get("unreliable.host", "sysDescr.0")
end)
# Performance analysis
{:ok, analysis} = SnmpKit.SNMP.analyze_table(table_data)
{:ok, benchmark} = SnmpKit.SNMP.benchmark_device("192.168.1.1", "system")
# Metrics recording
SnmpKit.SNMP.record_metric(:counter, :requests_total, 1, %{host: "router1"})π MIB Operations (SnmpKit.MIB)
OID Resolution
# Name to OID resolution
{:ok, oid} = SnmpKit.MIB.resolve("sysDescr.0")
# Returns: [1, 3, 6, 1, 2, 1, 1, 1, 0]
{:ok, oid} = SnmpKit.MIB.resolve("ifInOctets.1")
# Returns: [1, 3, 6, 1, 2, 1, 2, 2, 1, 10, 1]
# Group resolution
{:ok, oid} = SnmpKit.MIB.resolve("system")
# Returns: [1, 3, 6, 1, 2, 1, 1]
# Reverse lookup
{:ok, name} = SnmpKit.MIB.reverse_lookup([1, 3, 6, 1, 2, 1, 1, 1, 0])
# Returns: "sysDescr.0"MIB Tree Navigation
# Get children of an OID node
{:ok, children} = SnmpKit.MIB.children([1, 3, 6, 1, 2, 1, 1])
# Returns: [[1, 3, 6, 1, 2, 1, 1, 1], [1, 3, 6, 1, 2, 1, 1, 2], ...]
# Get parent of an OID
{:ok, parent} = SnmpKit.MIB.parent([1, 3, 6, 1, 2, 1, 1, 1, 0])
# Returns: [1, 3, 6, 1, 2, 1, 1, 1]
# Walk MIB tree
{:ok, tree} = SnmpKit.MIB.walk_tree([1, 3, 6, 1, 2, 1, 1])MIB Compilation and Loading
# High-level compilation (recommended)
{:ok, compiled} = SnmpKit.MIB.compile("MY-ENTERPRISE-MIB.mib")
{:ok, _} = SnmpKit.MIB.load(compiled)
# Compile entire directory
{:ok, results} = SnmpKit.MIB.compile_dir("mibs/")
# Low-level compilation (advanced)
{:ok, mib} = SnmpKit.MIB.compile_raw("MY-MIB.mib")
{:ok, _} = SnmpKit.MIB.load_compiled("compiled_mib.bin")
# Compile multiple files
{:ok, compiled_mibs} = SnmpKit.MIB.compile_all(["mib1.mib", "mib2.mib"])MIB Parsing and Analysis
# Parse MIB file
{:ok, parsed} = SnmpKit.MIB.parse_mib_file("CUSTOM-MIB.mib")
# Parse MIB content string
mib_content = File.read!("MY-MIB.mib")
{:ok, parsed} = SnmpKit.MIB.parse_mib_content(mib_content)
# Enhanced resolution with custom MIBs
{:ok, oid} = SnmpKit.MIB.resolve_enhanced("customObject.0")
# Integrate compilation and parsing
{:ok, integrated} = SnmpKit.MIB.load_and_integrate_mib("ENTERPRISE-MIB.mib")Standard MIBs
# Load built-in standard MIBs
:ok = SnmpKit.MIB.load_standard_mibs()
# Start MIB registry (for advanced usage)
{:ok, _pid} = SnmpKit.MIB.start_link()π§ͺ Device Simulation (SnmpKit.Sim)
Single Device Simulation
# Load a device profile
{:ok, profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(
:cable_modem,
{:walk_file, "priv/walks/cable_modem.walk"}
)
# Start simulated device
{:ok, device} = SnmpKit.Sim.start_device(profile, [
port: 1161,
community: "public"
])
# Device will respond to SNMP queries on localhost:1161
{:ok, description} = SnmpKit.SNMP.get("127.0.0.1:1161", "sysDescr.0")Population Simulation
# Create multiple devices for testing
device_configs = [
%{type: :cable_modem, port: 30001, community: "public"},
%{type: :switch, port: 30002, community: "public"},
%{type: :router, port: 30003, community: "private"}
]
{:ok, devices} = SnmpKit.Sim.start_device_population(device_configs)
# Query the simulated devices
{:ok, cm_desc} = SnmpKit.SNMP.get("127.0.0.1:30001", "sysDescr.0")
{:ok, switch_desc} = SnmpKit.SNMP.get("127.0.0.1:30002", "sysDescr.0")Testing Integration
defmodule MyNetworkTest do
use ExUnit.Case
setup do
# Start simulated devices for each test
{:ok, cable_modem_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(:cable_modem)
{:ok, router_profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(:router)
{:ok, cm} = SnmpKit.Sim.start_device(cable_modem_profile, port: 1161)
{:ok, router} = SnmpKit.Sim.start_device(router_profile, port: 1162)
%{
cable_modem: "127.0.0.1:1161",
router: "127.0.0.1:1162"
}
end
test "can monitor cable modem", %{cable_modem: cm_target} do
{:ok, signal_noise} = SnmpKit.SNMP.get(cm_target, "docsIfSigQSignalNoise.1")
assert is_integer(signal_noise)
end
test "can get interface statistics", %{router: router_target} do
{:ok, interfaces} = SnmpKit.SNMP.get_table(router_target, "ifTable")
assert length(interfaces) > 0
end
endπ― Direct Access (SnmpKit)
For convenience and backward compatibility, common operations are available directly:
# These are equivalent to their SnmpKit.SNMP.* counterparts
{:ok, value} = SnmpKit.get("192.168.1.1", "sysDescr.0")
{:ok, results} = SnmpKit.walk("192.168.1.1", "system")
:ok = SnmpKit.set("127.0.0.1:1161", "sysContact.0", "admin@example.com")
# MIB resolution
{:ok, oid} = SnmpKit.resolve("sysDescr.0")π Migration Guide
Enriched Output (1.0+)
- All SNMP operations now return enriched maps per varbind:
%{name?, oid, type, value, formatted?} - Defaults:
include_names: true,include_formatted: true - Disable per call:
include_names: false,include_formatted: false - Disable globally: use SnmpKit.SnmpMgr.Config.set_default_include_names(false) or set_default_include_formatted(false)
Examples:
# Before (0.x):
{:ok, value} = SnmpKit.get(target, oid)
{:ok, {next_oid, value}} = SnmpKit.get_next(target, oid)
{:ok, [{oid, type, value}]} = SnmpKit.walk(target, root)
# After (1.x):
{:ok, %{value: value}} = SnmpKit.get(target, oid)
{:ok, %{oid: next_oid, value: value}} = SnmpKit.get_next(target, oid)
{:ok, [%{oid: oid, type: type, value: value}]} = SnmpKit.walk(target, root)
# Pretty functions return the same map shape with formatted included
{:ok, %{formatted: f, value: v, type: t}} = SnmpKit.get_pretty(target, oid)From Direct Module Usage
# Before (still works)
{:ok, value} = SnmpKit.SnmpMgr.get("host", "oid")
{:ok, oid} = SnmpKit.SnmpMgr.MIB.resolve("name")
# After (recommended)
{:ok, value} = SnmpKit.SNMP.get("host", "oid")
{:ok, oid} = SnmpKit.MIB.resolve("name")
# Or use direct access
{:ok, value} = SnmpKit.get("host", "oid")
{:ok, oid} = SnmpKit.resolve("name")Gradual Migration Strategy
- Phase 1: Start using unified API for new code
- Phase 2: Gradually update existing code module by module
- Phase 3: Adopt consistent style across codebase
Import Strategy
# Option 1: Import specific modules
alias SnmpKit.{SNMP, MIB, Sim}
{:ok, value} = SNMP.get("host", "oid")
{:ok, oid} = MIB.resolve("name")
# Option 2: Use fully qualified names
{:ok, value} = SnmpKit.SNMP.get("host", "oid")
{:ok, oid} = SnmpKit.MIB.resolve("name")
# Option 3: Direct access for simple operations
{:ok, value} = SnmpKit.get("host", "oid")
{:ok, oid} = SnmpKit.resolve("name")π Function Reference Quick Guide
Most Common Operations
| Task | Function | Example |
|---|---|---|
| Get single value | SnmpKit.SNMP.get/3 | get("host", "sysDescr.0") |
| Walk OID tree | SnmpKit.SNMP.walk/3 | walk("host", "system") |
| Get table | SnmpKit.SNMP.get_table/3 | get_table("host", "ifTable") |
| Resolve OID name | SnmpKit.MIB.resolve/1 | resolve("sysDescr.0") |
| Start simulated device | SnmpKit.Sim.start_device/2 | start_device(profile, port: 1161) |
Performance Operations
| Task | Function | Example |
|---|---|---|
| Bulk retrieval | SnmpKit.SNMP.get_bulk/3 | get_bulk("host", "interfaces") |
| Multi-target query | SnmpKit.SNMP.get_multi/2 | get_multi([{"h1", "oid1"}, {"h2", "oid2"}]) |
| Streaming walk | SnmpKit.SNMP.walk_stream/3 | walk_stream("host", "large_table") |
| Adaptive walk | SnmpKit.SNMP.adaptive_walk/3 | adaptive_walk("host", "interfaces") |
Advanced Operations
| Task | Function | Example |
|---|---|---|
| Circuit breaker | SnmpKit.SNMP.with_circuit_breaker/3 | with_circuit_breaker("host", fn -> ... end) |
| Engine stats | SnmpKit.SNMP.get_engine_stats/1 | get_engine_stats() |
| MIB compilation | SnmpKit.MIB.compile/2 | compile("MY-MIB.mib") |
| Tree navigation | SnmpKit.MIB.children/1 | children([1,3,6,1,2,1,1]) |
π Best Practices
1. Choose the Right API Level
# For simple scripts and convenience
{:ok, value} = SnmpKit.get("host", "oid")
# For applications with multiple SNMP operations
alias SnmpKit.SNMP
{:ok, value} = SNMP.get("host", "oid")
{:ok, table} = SNMP.get_table("host", "ifTable")
# For complex applications
defmodule MyMonitor do
alias SnmpKit.{SNMP, MIB, Sim}
def monitor_device(host) do
with {:ok, description} <- SNMP.get(host, "sysDescr.0"),
{:ok, interfaces} <- SNMP.get_table(host, "ifTable") do
%{description: description, interfaces: interfaces}
end
end
end2. Use Appropriate Operations for Scale
# For single values
{:ok, value} = SnmpKit.SNMP.get("host", "oid")
# For multiple values from same host
{:ok, results} = SnmpKit.SNMP.walk("host", "system")
# For multiple hosts
{:ok, results} = SnmpKit.SNMP.get_multi([{"h1", "oid"}, {"h2", "oid"}])
# For large datasets
stream = SnmpKit.SNMP.walk_stream("host", "large_table")3. Handle Errors Gracefully
case SnmpKit.SNMP.get("host", "oid", timeout: 1000) do
{:ok, value} ->
process_value(value)
{:error, :timeout} ->
log_timeout_error("host")
{:error, reason} ->
log_snmp_error("host", reason)
end4. Use Simulated Devices for Testing
# In test setup
setup do
{:ok, profile} = SnmpKit.SnmpSim.ProfileLoader.load_profile(:generic_device)
{:ok, _device} = SnmpKit.Sim.start_device(profile, port: 1161)
%{target: "127.0.0.1:1161"}
end
test "my network function", %{target: target} do
# Test against simulated device
result = my_network_function(target)
assert result.status == :ok
endπ Conclusion
The unified API makes SnmpKit more approachable for new users while maintaining all the power and flexibility for advanced use cases. Choose the approach that best fits your needs:
- Direct access (
SnmpKit.*) for simple operations - Namespaced modules (
SnmpKit.SNMP.*) for organized applications - Mixed approach based on context and preference
All approaches provide the same functionality with 100% backward compatibility.
Happy SNMP monitoring! π