├── .gitignore ├── Dockerfile ├── PROMETHEUS-TRAPPER-MIB.txt ├── README.md ├── config └── config.go ├── main.go ├── sample-alert.json ├── snmptrapper ├── sample_oids.go ├── send_trap.go └── snmptrapper.go ├── trapdebug ├── Dockerfile ├── net-snmp │ ├── Dockerfile │ └── PROMETHEUS-TRAPPER-MIB.txt └── trapdebug.go ├── types ├── alert.go └── trap_oids.go └── webhook ├── webhook.go ├── webhook_handler.go └── webhook_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | prometheus_webhook_snmptrapper 2 | prometheus_webhook_snmptrapper.linux-amd64 3 | trapdebug/trapdebug 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | MAINTAINER Prawn 3 | USER root 4 | 5 | RUN apk update 6 | RUN apk add curl 7 | 8 | ENV SNMP_COMMUNITY="public" 9 | ENV SNMP_RETRIES=1 10 | ENV SNMP_TRAP_ADDRESS="localhost:162" 11 | ENV WEBHOOK_ADDRESS="0.0.0.0:9099" 12 | 13 | EXPOSE 9099 14 | 15 | COPY prometheus_webhook_snmptrapper.linux-amd64 /usr/local/bin/prometheus_webhook_snmptrapper 16 | COPY sample-alert.json / 17 | 18 | CMD exec /usr/local/bin/prometheus_webhook_snmptrapper -snmpcommunity=$SNMP_COMMUNITY -snmpretries=$SNMP_RETRIES -snmptrapaddress=$SNMP_TRAP_ADDRESS -webhookaddress=$WEBHOOK_ADDRESS 19 | 20 | # docker build -t "prawn/prometheus-webhook-snmptrapper" . 21 | -------------------------------------------------------------------------------- /PROMETHEUS-TRAPPER-MIB.txt: -------------------------------------------------------------------------------- 1 | PROMETHEUS-TRAPPER-MIB DEFINITIONS ::= BEGIN 2 | 3 | -- 4 | -- Notifications for the Prometheus SNMP Trapper 5 | -- https://github.com/chrusty/prometheus_webhook_snmptrapper 6 | -- 7 | 8 | IMPORTS 9 | MODULE-IDENTITY, OBJECT-TYPE, TimeTicks, experimental, NOTIFICATION-TYPE 10 | FROM SNMPv2-SMI 11 | DisplayString, TimeStamp 12 | FROM SNMPv2-TC 13 | ; 14 | 15 | prometheusTrapper MODULE-IDENTITY 16 | LAST-UPDATED "201610310000Z" 17 | ORGANIZATION "Chrusty" 18 | CONTACT-INFO "https://github.com/chrusty/prometheus_webhook_snmptrapper" 19 | DESCRIPTION "Example MIB objects for agent module example implementations" 20 | REVISION "201610310000Z" 21 | DESCRIPTION "Created MIB" 22 | ::= { experimental 1977 } 23 | 24 | -- 25 | -- Top level structure 26 | -- 27 | prometheusTrapperNotifications OBJECT IDENTIFIER ::= { prometheusTrapper 1 } 28 | prometheusTrapperNotificationsPrefix OBJECT IDENTIFIER ::= { prometheusTrapperNotifications 0 } 29 | prometheusTrapperNotificationObjects OBJECT IDENTIFIER ::= { prometheusTrapperNotifications 1 } 30 | 31 | -- 32 | -- Notifications 33 | -- 34 | 35 | prometheusTrapperNotificationInstance OBJECT-TYPE 36 | SYNTAX DisplayString 37 | MAX-ACCESS accessible-for-notify 38 | STATUS current 39 | DESCRIPTION 40 | "Unique identifier for the instance (hostname / instance-id / ip-address etc)." 41 | ::= { prometheusTrapperNotificationObjects 1 } 42 | 43 | prometheusTrapperNotificationService OBJECT-TYPE 44 | SYNTAX DisplayString 45 | MAX-ACCESS accessible-for-notify 46 | STATUS current 47 | DESCRIPTION 48 | "A name for the service(s) affected." 49 | ::= { prometheusTrapperNotificationObjects 2 } 50 | 51 | prometheusTrapperNotificationLocation OBJECT-TYPE 52 | SYNTAX DisplayString 53 | MAX-ACCESS accessible-for-notify 54 | STATUS current 55 | DESCRIPTION 56 | "The location of the system(s) generating the notification." 57 | ::= { prometheusTrapperNotificationObjects 3 } 58 | 59 | prometheusTrapperNotificationSeverity OBJECT-TYPE 60 | SYNTAX DisplayString 61 | MAX-ACCESS accessible-for-notify 62 | STATUS current 63 | DESCRIPTION 64 | "The severity of the notification." 65 | ::= { prometheusTrapperNotificationObjects 4 } 66 | 67 | prometheusTrapperNotificationDescription OBJECT-TYPE 68 | SYNTAX DisplayString 69 | MAX-ACCESS accessible-for-notify 70 | STATUS current 71 | DESCRIPTION 72 | "Description field." 73 | ::= { prometheusTrapperNotificationObjects 5 } 74 | 75 | prometheusTrapperNotificationJob OBJECT-TYPE 76 | SYNTAX DisplayString 77 | MAX-ACCESS accessible-for-notify 78 | STATUS current 79 | DESCRIPTION 80 | "The name of the Prometheus JOB which scrapes this data." 81 | ::= { prometheusTrapperNotificationObjects 6 } 82 | 83 | prometheusTrapperNotificationTimestamp OBJECT-TYPE 84 | SYNTAX DisplayString 85 | MAX-ACCESS accessible-for-notify 86 | STATUS current 87 | DESCRIPTION 88 | "The time the notification-event occurred." 89 | ::= { prometheusTrapperNotificationObjects 7 } 90 | 91 | prometheusTrapperFiringNotification NOTIFICATION-TYPE 92 | OBJECTS { 93 | prometheusTrapperNotificationInstance, 94 | prometheusTrapperNotificationService, 95 | prometheusTrapperNotificationLocation, 96 | prometheusTrapperNotificationSeverity, 97 | prometheusTrapperNotificationDescription, 98 | prometheusTrapperNotificationTimestamp 99 | } 100 | STATUS current 101 | DESCRIPTION 102 | "A notification describing the OCCURRENCE of a Prometheus Alert event." 103 | ::= { prometheusTrapperNotificationsPrefix 1 } 104 | 105 | prometheusTrapperRecoveryNotification NOTIFICATION-TYPE 106 | OBJECTS { 107 | prometheusTrapperNotificationInstance, 108 | prometheusTrapperNotificationService, 109 | prometheusTrapperNotificationLocation, 110 | prometheusTrapperNotificationSeverity, 111 | prometheusTrapperNotificationDescription, 112 | prometheusTrapperNotificationTimestamp 113 | } 114 | STATUS current 115 | DESCRIPTION 116 | "A notification describing the RECOVERY of a Prometheus Alert event." 117 | ::= { prometheusTrapperNotificationsPrefix 2 } 118 | 119 | END -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Prometheus WebHook to SNMP-trap forwarder 2 | ========================================= 3 | 4 | This is a quick (and dirty) way to get Prometheus to send SNMP traps, by mapping AlertManager "Annotations" and "Labels" to generic SNMP OIDs. 5 | 6 | Integration with Prometheus 7 | --------------------------- 8 | 1. Prometheus gathers metrics 9 | 2. Prometheus appraises metrics against rules 10 | 3. If rules are triggered then alerts are raised through the AlertManager 11 | 4. The AlertManager triggers notifications to the webhook_snmptrapper 12 | 5. The webhook_snmptrapper forwards alerts as SNMP traps to the configured trap-address 13 | 14 | SNMP integration 15 | ---------------- 16 | The provided MIB (`PROMETHEUS-TRAPPER-MIB.txt`) defines two notifications: 17 | - ***prometheusTrapperFiringNotification***: Notification for an alert that has occured 18 | - ***prometheusTrapperRecoveryNotification***: Notification for an alert that has recovered 19 | 20 | The MIB can be loaded into whatever SNMP Trap-server you're using. See [Dockerfile](trapdebug/net-snmp/Dockerfile) for a working demo using net-snmp on Alpine Linux. 21 | 22 | ### SNMP variables 23 | Both of these traps contain the following variables: 24 | - ***prometheusTrapperNotificationInstance***: The instance or hostname 25 | - ***prometheusTrapperNotificationService***: A name for the service affected 26 | - ***prometheusTrapperNotificationLocation***: The physical location where the alert was generated 27 | - ***prometheusTrapperNotificationSeverity***: The severity of the alert 28 | - ***prometheusTrapperNotificationDescription***: Text description of the alert 29 | - ***prometheusTrapperNotificationTimestamp***: When the alert was first generated 30 | 31 | AlertManager configuration 32 | -------------------------- 33 | AlertManager needs to be configured to fire webhooks as notifications, with a pre-defined assortment of labels and annotations (these map to the SNMP MIB provided). Each alert should have the following parameters: 34 | 35 | ### Annotations: 36 | - ***description***: A string describing the alert (_prometheusTrapperNotificationDescription_) 37 | 38 | ### Labels: 39 | - ***instance***: A string containing a unique host-identifier / hostname / instance-id / IP-address etc (_prometheusTrapperNotificationInstance_) 40 | - ***severity***: A string describing the severity of the alert (_prometheusTrapperNotificationSeverity_) 41 | - ***location***: A string describing the location of the instance(s) / system(s) generating the alert (_prometheusTrapperNotificationLocation_) 42 | - ***service***: A string describing the service affected (_prometheusTrapperNotificationService_) 43 | 44 | Command-line flags 45 | ------------------ 46 | - **-snmpcommunity**: The SNMP community string (_default_ = `public`) 47 | - **-snmpretries**: The number of times to retry sending traps (_default_ = `1`) 48 | - **-snmptrapaddress**: The address to send traps to (_default_ = `127.0.0.1:162`) 49 | - **-webhookaddress**: The address to listen for incoming webhooks on (_default_ = `0.0.0.0:9099`) 50 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | type Config struct { 4 | SNMPTrapAddress string 5 | SNMPCommunity string 6 | SNMPRetries uint 7 | WebhookAddress string 8 | } 9 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | flag "flag" 5 | sync "sync" 6 | 7 | config "github.com/chrusty/prometheus_webhook_snmptrapper/config" 8 | snmptrapper "github.com/chrusty/prometheus_webhook_snmptrapper/snmptrapper" 9 | types "github.com/chrusty/prometheus_webhook_snmptrapper/types" 10 | webhook "github.com/chrusty/prometheus_webhook_snmptrapper/webhook" 11 | 12 | logrus "github.com/Sirupsen/logrus" 13 | ) 14 | 15 | var ( 16 | conf config.Config 17 | log = logrus.WithFields(logrus.Fields{"logger": "main"}) 18 | waitGroup = &sync.WaitGroup{} 19 | ) 20 | 21 | func init() { 22 | // Process the command-line parameters: 23 | flag.StringVar(&conf.SNMPTrapAddress, "snmptrapaddress", "127.0.0.1:162", "Address to send SNMP traps to") 24 | flag.StringVar(&conf.SNMPCommunity, "snmpcommunity", "public", "SNMP community string") 25 | flag.UintVar(&conf.SNMPRetries, "snmpretries", 1, "Number of times to retry sending SNMP traps") 26 | flag.StringVar(&conf.WebhookAddress, "webhookaddress", "0.0.0.0:9099", "Address and port to listen for webhooks on") 27 | flag.Parse() 28 | 29 | // Set the log-level: 30 | logrus.SetLevel(logrus.DebugLevel) 31 | } 32 | 33 | func main() { 34 | 35 | // Make sure we wait for everything to complete before bailing out: 36 | defer waitGroup.Wait() 37 | 38 | // Prepare a channel of events (to feed the digester): 39 | log.Info("Preparing the alerts channel") 40 | alertsChannel := make(chan types.Alert) 41 | 42 | // Prepare to have background GoRoutines running: 43 | waitGroup.Add(1) 44 | 45 | // Start webhook server: 46 | go webhook.Run(conf, alertsChannel, waitGroup) 47 | 48 | // Start the SNMP trapper: 49 | go snmptrapper.Run(conf, alertsChannel, waitGroup) 50 | 51 | } 52 | -------------------------------------------------------------------------------- /sample-alert.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2", 3 | "status": "firing", 4 | "alerts": [ 5 | { 6 | "labels": { 7 | "location": "dc1", 8 | "service": "MySQL", 9 | "instance": "mysqldb-01", 10 | "severity": "critical" 11 | }, 12 | "annotations": { 13 | "description": "MySQL service is unreachable" 14 | }, 15 | "startsAt": "2016-10-27T14:27:00Z", 16 | "endsAt": "2016-10-27T14:27:00Z" 17 | }, 18 | { 19 | "labels": { 20 | "location": "dc1", 21 | "service": "filesystem (/var)", 22 | "instance": "mysqldb-01", 23 | "severity": "warning" 24 | }, 25 | "annotations": { 26 | "description": "Filesystem (/var) has reached 80%" 27 | }, 28 | "startsAt": "2016-11-02T16:00:00Z", 29 | "endsAt": "2016-10-27T14:27:00Z" 30 | } 31 | ] 32 | } -------------------------------------------------------------------------------- /snmptrapper/sample_oids.go: -------------------------------------------------------------------------------- 1 | package snmptrapper 2 | 3 | // SNMPv2 "generic" OIDs (including "RMON"): http://www.oidview.com/mibs/0/md-0-1.html 4 | 5 | const ( 6 | oidSNMPv2SysDescr = "1.3.6.1.2.1.1.1" // Variable: summary 7 | oidSNMPv2SysName = "1.3.6.1.2.1.1.5" // Variable: instance 8 | oidSNMPv2SysLocation = "1.3.6.1.2.1.1.6" // Variable: location 9 | oidSNMPv2SysServices = "1.3.6.1.2.1.1.7" // Variable: service 10 | oidSNMPv2SysORLastChange = "1.3.6.1.2.1.1.8" // 11 | oidRMONAlarmStatus = "1.3.6.1.2.1.16.3.1.1.12" // 12 | oidRMONHostAddress = "1.3.6.1.2.1.16.4.2.1.1" // Variable: instance 13 | oidRMONEventDescription = "1.3.6.1.2.1.16.9.1.1.2" // Variable: summary 14 | oidRMONEventType = "1.3.6.1.2.1.16.9.1.1.3" // Variable: severity 15 | oidSNMPv2LinkDown = "1.3.6.1.2.1.11.2" // Trap: firing 16 | oidSNMPv2LinkUp = "1.3.6.1.2.1.11.3" // Trap: recovery 17 | oidIFLinkDown = "1.3.6.1.6.3.1.1.5.3" // Trap: firing 18 | oidIFLinkUp = "1.3.6.1.6.3.1.1.5.4" // Trap: recovery 19 | oidRMONRisingAlarm = "1.3.6.1.2.1.16.0.1" // Notification: firing 20 | oidRMONFallingAlarm = "1.3.6.1.2.1.16.0.2" // Notification: recovery 21 | oidPrometheusTrapperFiringNotification = "1.3.6.1.3.1977.1.0.1" // Notification: firing 22 | oidPrometheusTrapperRecoveryNotification = "1.3.6.1.3.1977.1.0.2" // Notification: recovery 23 | oidPrometheusTrapperNotificationInstance = "1.3.6.1.3.1977.1.1.1" // Variable: instance 24 | oidPrometheusTrapperNotificationService = "1.3.6.1.3.1977.1.1.2" // Variable: service 25 | oidPrometheusTrapperNotificationLocation = "1.3.6.1.3.1977.1.1.3" // Variable: location 26 | oidPrometheusTrapperNotificationSeverity = "1.3.6.1.3.1977.1.1.4" // Variable: severity 27 | oidPrometheusTrapperNotificationDescription = "1.3.6.1.3.1977.1.1.5" // Variable: description 28 | oidPrometheusTrapperNotificationJob = "1.3.6.1.3.1977.1.1.6" // Variable: job 29 | oidPrometheusTrapperNotificationTimestamp = "1.3.6.1.3.1977.1.1.7" // Variable: timestamp 30 | ) 31 | -------------------------------------------------------------------------------- /snmptrapper/send_trap.go: -------------------------------------------------------------------------------- 1 | package snmptrapper 2 | 3 | import ( 4 | "time" 5 | 6 | types "github.com/chrusty/prometheus_webhook_snmptrapper/types" 7 | 8 | logrus "github.com/Sirupsen/logrus" 9 | snmpgo "github.com/k-sone/snmpgo" 10 | ) 11 | 12 | func sendTrap(alert types.Alert) { 13 | 14 | // Prepare an SNMP handler: 15 | snmp, err := snmpgo.NewSNMP(snmpgo.SNMPArguments{ 16 | Version: snmpgo.V2c, 17 | Address: myConfig.SNMPTrapAddress, 18 | Retries: myConfig.SNMPRetries, 19 | Community: myConfig.SNMPCommunity, 20 | }) 21 | if err != nil { 22 | log.WithFields(logrus.Fields{"error": err}).Error("Failed to create snmpgo.SNMP object") 23 | return 24 | } else { 25 | log.WithFields(logrus.Fields{"address": myConfig.SNMPTrapAddress, "retries": myConfig.SNMPRetries, "community": myConfig.SNMPCommunity}).Debug("Created snmpgo.SNMP object") 26 | } 27 | 28 | // Build VarBind list: 29 | var varBinds snmpgo.VarBinds 30 | 31 | // The "enterprise OID" for the trap (rising/firing or falling/recovery): 32 | if alert.Status == "firing" { 33 | varBinds = append(varBinds, snmpgo.NewVarBind(snmpgo.OidSnmpTrap, trapOIDs.FiringTrap)) 34 | varBinds = append(varBinds, snmpgo.NewVarBind(trapOIDs.TimeStamp, snmpgo.NewOctetString([]byte(alert.StartsAt.Format(time.RFC3339))))) 35 | } else { 36 | varBinds = append(varBinds, snmpgo.NewVarBind(snmpgo.OidSnmpTrap, trapOIDs.RecoveryTrap)) 37 | varBinds = append(varBinds, snmpgo.NewVarBind(trapOIDs.TimeStamp, snmpgo.NewOctetString([]byte(alert.EndsAt.Format(time.RFC3339))))) 38 | } 39 | 40 | // Insert the AlertManager variables: 41 | varBinds = append(varBinds, snmpgo.NewVarBind(trapOIDs.Description, snmpgo.NewOctetString([]byte(alert.Annotations["description"])))) 42 | varBinds = append(varBinds, snmpgo.NewVarBind(trapOIDs.Instance, snmpgo.NewOctetString([]byte(alert.Labels["instance"])))) 43 | varBinds = append(varBinds, snmpgo.NewVarBind(trapOIDs.Severity, snmpgo.NewOctetString([]byte(alert.Labels["severity"])))) 44 | varBinds = append(varBinds, snmpgo.NewVarBind(trapOIDs.Location, snmpgo.NewOctetString([]byte(alert.Labels["location"])))) 45 | varBinds = append(varBinds, snmpgo.NewVarBind(trapOIDs.Service, snmpgo.NewOctetString([]byte(alert.Labels["service"])))) 46 | varBinds = append(varBinds, snmpgo.NewVarBind(trapOIDs.JobName, snmpgo.NewOctetString([]byte(alert.Labels["job"])))) 47 | 48 | // Create an SNMP "connection": 49 | if err = snmp.Open(); err != nil { 50 | log.WithFields(logrus.Fields{"error": err}).Error("Failed to open SNMP connection") 51 | return 52 | } 53 | defer snmp.Close() 54 | 55 | // Send the trap: 56 | if err = snmp.V2Trap(varBinds); err != nil { 57 | log.WithFields(logrus.Fields{"error": err}).Error("Failed to send SNMP trap") 58 | return 59 | } else { 60 | log.WithFields(logrus.Fields{"status": alert.Status}).Info("It's a trap!") 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /snmptrapper/snmptrapper.go: -------------------------------------------------------------------------------- 1 | package snmptrapper 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "sync" 7 | 8 | config "github.com/chrusty/prometheus_webhook_snmptrapper/config" 9 | types "github.com/chrusty/prometheus_webhook_snmptrapper/types" 10 | 11 | logrus "github.com/Sirupsen/logrus" 12 | snmpgo "github.com/k-sone/snmpgo" 13 | ) 14 | 15 | var ( 16 | log = logrus.WithFields(logrus.Fields{"logger": "SNMP-trapper"}) 17 | myConfig config.Config 18 | trapOIDs types.TrapOIDs 19 | ) 20 | 21 | func init() { 22 | // Set the log-level: 23 | logrus.SetLevel(logrus.DebugLevel) 24 | 25 | // Configure which OIDs to use for the SNMP Traps: 26 | trapOIDs.FiringTrap, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.0.1") 27 | trapOIDs.RecoveryTrap, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.0.2") 28 | trapOIDs.Instance, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.1.1") 29 | trapOIDs.Service, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.1.2") 30 | trapOIDs.Location, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.1.3") 31 | trapOIDs.Severity, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.1.4") 32 | trapOIDs.Description, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.1.5") 33 | trapOIDs.JobName, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.1.6") 34 | trapOIDs.TimeStamp, _ = snmpgo.NewOid("1.3.6.1.3.1977.1.1.7") 35 | } 36 | 37 | func Run(myConfigFromMain config.Config, alertsChannel chan types.Alert, waitGroup *sync.WaitGroup) { 38 | 39 | log.WithFields(logrus.Fields{"address": myConfigFromMain.SNMPTrapAddress}).Info("Starting the SNMP trapper") 40 | 41 | // Populate the config: 42 | myConfig = myConfigFromMain 43 | 44 | // Set up a channel to handle shutdown: 45 | signals := make(chan os.Signal, 1) 46 | signal.Notify(signals, os.Kill, os.Interrupt) 47 | 48 | // Handle incoming alerts: 49 | go func() { 50 | for { 51 | select { 52 | 53 | case alert := <-alertsChannel: 54 | 55 | // Send a trap based on this alert: 56 | log.WithFields(logrus.Fields{"status": alert.Status}).Debug("Received an alert") 57 | sendTrap(alert) 58 | } 59 | } 60 | }() 61 | 62 | // Wait for shutdown: 63 | for { 64 | select { 65 | case <-signals: 66 | log.Warn("Shutting down the SNMP trapper") 67 | 68 | // Tell main() that we're done: 69 | waitGroup.Done() 70 | return 71 | } 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /trapdebug/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | MAINTAINER Prawn 3 | USER root 4 | 5 | ENV LISTEN_PORT=162 6 | 7 | EXPOSE 162/udp 8 | 9 | COPY trapdebug /usr/local/bin/trapdebug 10 | 11 | CMD exec /usr/local/bin/trapdebug -listenport=$LISTEN_PORT 12 | 13 | # docker build -t "prawn/snmp-trapdebug" . 14 | -------------------------------------------------------------------------------- /trapdebug/net-snmp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | RUN apk update 4 | RUN apk add net-snmp 5 | 6 | COPY PROMETHEUS-TRAPPER-MIB.txt /usr/share/snmp/mibs/ 7 | 8 | CMD ["/usr/sbin/snmptrapd", "-f", "-Lo", "-m", "PROMETHEUS-TRAPPER-MIB", "-M", "/usr/share/snmp/mibs"] 9 | 10 | # docker build -t "prawn/snmptrapd" . 11 | # docker run --net=prometheus_default --ip=172.15.0.24 --name=snmptrapd -d prawn/snmptrapd 12 | -------------------------------------------------------------------------------- /trapdebug/net-snmp/PROMETHEUS-TRAPPER-MIB.txt: -------------------------------------------------------------------------------- 1 | PROMETHEUS-TRAPPER-MIB DEFINITIONS ::= BEGIN 2 | 3 | -- 4 | -- Notifications for the Prometheus SNMP Trapper 5 | -- https://github.com/chrusty/prometheus_webhook_snmptrapper 6 | -- 7 | 8 | IMPORTS 9 | MODULE-IDENTITY, OBJECT-TYPE, TimeTicks, experimental, NOTIFICATION-TYPE FROM SNMPv2-SMI 10 | SnmpAdminString FROM SNMP-FRAMEWORK-MIB 11 | ; 12 | 13 | prometheusTrapper MODULE-IDENTITY 14 | LAST-UPDATED "201610310000Z" 15 | ORGANIZATION "Chrusty" 16 | CONTACT-INFO "https://github.com/chrusty/prometheus_webhook_snmptrapper" 17 | DESCRIPTION "Example MIB objects for agent module example implementations" 18 | REVISION "201610310000Z" 19 | DESCRIPTION "Created MIB" 20 | ::= { experimental 1977 } 21 | 22 | -- 23 | -- Top level structure 24 | -- 25 | prometheusTrapperNotifications OBJECT IDENTIFIER ::= { prometheusTrapper 1 } 26 | prometheusTrapperNotificationsPrefix OBJECT IDENTIFIER ::= { prometheusTrapperNotifications 0 } 27 | prometheusTrapperNotificationObjects OBJECT IDENTIFIER ::= { prometheusTrapperNotifications 1 } 28 | 29 | -- 30 | -- Notifications 31 | -- 32 | 33 | prometheusTrapperNotificationInstance OBJECT-TYPE 34 | SYNTAX SnmpAdminString 35 | MAX-ACCESS accessible-for-notify 36 | STATUS current 37 | DESCRIPTION 38 | "Unique identifier for the instance (hostname / instance-id / ip-address etc)." 39 | ::= { prometheusTrapperNotificationObjects 1 } 40 | 41 | prometheusTrapperNotificationService OBJECT-TYPE 42 | SYNTAX SnmpAdminString 43 | MAX-ACCESS accessible-for-notify 44 | STATUS current 45 | DESCRIPTION 46 | "A name for the service(s) affected." 47 | ::= { prometheusTrapperNotificationObjects 2 } 48 | 49 | prometheusTrapperNotificationLocation OBJECT-TYPE 50 | SYNTAX SnmpAdminString 51 | MAX-ACCESS accessible-for-notify 52 | STATUS current 53 | DESCRIPTION 54 | "The location of the system(s) generating the notification." 55 | ::= { prometheusTrapperNotificationObjects 3 } 56 | 57 | prometheusTrapperNotificationSeverity OBJECT-TYPE 58 | SYNTAX SnmpAdminString 59 | MAX-ACCESS accessible-for-notify 60 | STATUS current 61 | DESCRIPTION 62 | "The severity of the notification." 63 | ::= { prometheusTrapperNotificationObjects 4 } 64 | 65 | prometheusTrapperNotificationDescription OBJECT-TYPE 66 | SYNTAX SnmpAdminString 67 | MAX-ACCESS accessible-for-notify 68 | STATUS current 69 | DESCRIPTION 70 | "Description field." 71 | ::= { prometheusTrapperNotificationObjects 5 } 72 | 73 | prometheusTrapperNotificationJob OBJECT-TYPE 74 | SYNTAX SnmpAdminString 75 | MAX-ACCESS accessible-for-notify 76 | STATUS current 77 | DESCRIPTION 78 | "The name of the Prometheus JOB which scrapes this data." 79 | ::= { prometheusTrapperNotificationObjects 6 } 80 | 81 | prometheusTrapperNotificationTimestamp OBJECT-TYPE 82 | SYNTAX TimeTicks 83 | MAX-ACCESS accessible-for-notify 84 | STATUS current 85 | DESCRIPTION 86 | "The time the notification-event occurred." 87 | ::= { prometheusTrapperNotificationObjects 7 } 88 | 89 | prometheusTrapperFiringNotification NOTIFICATION-TYPE 90 | OBJECTS { 91 | prometheusTrapperNotificationInstance, 92 | prometheusTrapperNotificationService, 93 | prometheusTrapperNotificationLocation, 94 | prometheusTrapperNotificationSeverity, 95 | prometheusTrapperNotificationDescription, 96 | prometheusTrapperNotificationTimestamp 97 | } 98 | STATUS current 99 | DESCRIPTION 100 | "A notification describing the OCCURRENCE of a Prometheus Alert event." 101 | ::= { prometheusTrapperNotificationsPrefix 1 } 102 | 103 | prometheusTrapperRecoveryNotification NOTIFICATION-TYPE 104 | OBJECTS { 105 | prometheusTrapperNotificationInstance, 106 | prometheusTrapperNotificationService, 107 | prometheusTrapperNotificationLocation, 108 | prometheusTrapperNotificationSeverity, 109 | prometheusTrapperNotificationDescription, 110 | prometheusTrapperNotificationTimestamp 111 | } 112 | STATUS current 113 | DESCRIPTION 114 | "A notification describing the RECOVERY of a Prometheus Alert event." 115 | ::= { prometheusTrapperNotificationsPrefix 2 } 116 | 117 | END -------------------------------------------------------------------------------- /trapdebug/trapdebug.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "net" 6 | 7 | logrus "github.com/Sirupsen/logrus" 8 | gosnmptrap "github.com/ebookbug/gosnmptrap" 9 | ) 10 | 11 | var ( 12 | listenPort = flag.Int("listenport", 162, "Port to listen for traps on") 13 | ) 14 | 15 | func init() { 16 | 17 | // Process the command-line parameters: 18 | flag.Parse() 19 | 20 | // Set the log-level: 21 | logrus.SetLevel(logrus.DebugLevel) 22 | } 23 | 24 | func main() { 25 | 26 | logrus.WithFields(logrus.Fields{"port": *listenPort}).Info("Starting SNMP TrapDebugger") 27 | 28 | // Open a UDP socket: 29 | socket, err := net.ListenUDP("udp4", &net.UDPAddr{ 30 | IP: net.IPv4(0, 0, 0, 0), 31 | Port: *listenPort, 32 | }) 33 | if err != nil { 34 | logrus.WithFields(logrus.Fields{"error": err}).Fatal("Error opening socket") 35 | } 36 | defer socket.Close() 37 | 38 | // Loop forever: 39 | for { 40 | // Make a buffer to read into: 41 | buf := make([]byte, 2048) 42 | 43 | // Read from the socket: 44 | read, from, _ := socket.ReadFromUDP(buf) 45 | 46 | // Report that we have data: 47 | logrus.WithFields(logrus.Fields{"client": from.IP}).Debug("Data received") 48 | 49 | // Handle the data: 50 | go HandleUdp(buf[:read]) 51 | } 52 | } 53 | 54 | // Handle SNMP data: 55 | func HandleUdp(data []byte) { 56 | 57 | // Attempt to parse the SNMP data: 58 | trap, err := gosnmptrap.ParseUdp(data) 59 | if err != nil { 60 | logrus.WithFields(logrus.Fields{"error": err}).Error("Unable to parse SNMP data") 61 | return 62 | } 63 | 64 | // Dump the metadata: 65 | logrus.WithFields(logrus.Fields{"version": trap.Version, "community": trap.Community, "enterprise_id": trap.EnterpriseId, "address": trap.Address}).Info("SNMP trap received") 66 | logrus.WithFields(logrus.Fields{"general": trap.GeneralTrap, "special": trap.SpeicalTrap}).Info("SNMP trap values") 67 | 68 | // Dump the values: 69 | for trapOID, trapValue := range trap.Values { 70 | logrus.WithFields(logrus.Fields{"OID": trapOID, "value": trapValue}).Info("Trap variable") 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /types/alert.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Alert struct { 8 | Address string 9 | Status string 10 | Annotations map[string]string 11 | Labels map[string]string 12 | StartsAt time.Time 13 | EndsAt time.Time 14 | GeneratorURL string 15 | } 16 | -------------------------------------------------------------------------------- /types/trap_oids.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | snmpgo "github.com/k-sone/snmpgo" 5 | ) 6 | 7 | type TrapOIDs struct { 8 | FiringTrap *snmpgo.Oid 9 | RecoveryTrap *snmpgo.Oid 10 | Instance *snmpgo.Oid 11 | Service *snmpgo.Oid 12 | Location *snmpgo.Oid 13 | Severity *snmpgo.Oid 14 | Description *snmpgo.Oid 15 | JobName *snmpgo.Oid 16 | TimeStamp *snmpgo.Oid 17 | } 18 | -------------------------------------------------------------------------------- /webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "sync" 7 | 8 | "net/http" 9 | 10 | config "github.com/chrusty/prometheus_webhook_snmptrapper/config" 11 | types "github.com/chrusty/prometheus_webhook_snmptrapper/types" 12 | 13 | logrus "github.com/Sirupsen/logrus" 14 | ) 15 | 16 | var ( 17 | log = logrus.WithFields(logrus.Fields{"logger": "Webhook-server"}) 18 | myConfig config.Config 19 | ) 20 | 21 | func init() { 22 | // Set the log-level: 23 | logrus.SetLevel(logrus.DebugLevel) 24 | } 25 | 26 | func Run(myConfigFromMain config.Config, alertsChannel chan types.Alert, waitGroup *sync.WaitGroup) { 27 | 28 | log.WithFields(logrus.Fields{"address": myConfigFromMain.WebhookAddress}).Info("Starting the Webhook server") 29 | 30 | // Populate the config: 31 | myConfig = myConfigFromMain 32 | 33 | // Set up a channel to handle shutdown: 34 | signals := make(chan os.Signal, 1) 35 | signal.Notify(signals, os.Kill, os.Interrupt) 36 | 37 | // Listen for webhooks: 38 | http.ListenAndServe(myConfig.WebhookAddress, &WebhookHandler{AlertsChannel: alertsChannel}) 39 | 40 | // Wait for shutdown: 41 | for { 42 | select { 43 | case <-signals: 44 | log.Info("Shutting down the Webhook server") 45 | 46 | // Tell main() that we're done: 47 | waitGroup.Done() 48 | return 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /webhook/webhook_handler.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "net/http" 7 | 8 | types "github.com/chrusty/prometheus_webhook_snmptrapper/types" 9 | 10 | logrus "github.com/Sirupsen/logrus" 11 | template "github.com/prometheus/alertmanager/template" 12 | ) 13 | 14 | // A webhook handler with a "ServeHTTP" method: 15 | type WebhookHandler struct { 16 | AlertsChannel chan types.Alert 17 | } 18 | 19 | // Handle webhook requests: 20 | func (webhookHandler *WebhookHandler) ServeHTTP(responseWriter http.ResponseWriter, request *http.Request) { 21 | 22 | // Read the request body: 23 | payload, err := ioutil.ReadAll(request.Body) 24 | defer request.Body.Close() 25 | if err != nil { 26 | log.WithFields(logrus.Fields{"error": err}).Error("Failed to read the request body") 27 | http.Error(responseWriter, "Failed to read the request body", http.StatusBadRequest) 28 | return 29 | } 30 | 31 | // Validate the payload: 32 | err, alerts := validatePayload(payload) 33 | if err != nil { 34 | http.Error(responseWriter, "Failed to unmarshal the request-body into an alert", http.StatusBadRequest) 35 | return 36 | } 37 | 38 | // Send the alerts to the snmp-trapper: 39 | for alertIndex, alert := range alerts { 40 | log.WithFields(logrus.Fields{"index": alertIndex, "status": alert.Status, "labels": alert.Labels}).Debug("Forwarding an alert to the SNMP trapper") 41 | 42 | // Enrich the request with the remote-address: 43 | alert.Address = request.RemoteAddr 44 | 45 | // Put the alert onto the alerts-channel: 46 | webhookHandler.AlertsChannel <- alert 47 | } 48 | 49 | } 50 | 51 | // Validate a webhook payload and return a list of Alerts: 52 | func validatePayload(payload []byte) (error, []types.Alert) { 53 | 54 | // Make our response: 55 | alerts := make([]types.Alert, 0) 56 | 57 | // Make a new Prometheus data-structure to unmarshal the request body into: 58 | prometheusData := &template.Data{} 59 | 60 | // Unmarshal the request body into the alert: 61 | err := json.Unmarshal(payload, prometheusData) 62 | if err != nil { 63 | log.WithFields(logrus.Fields{"error": err, "payload": payload}).Error("Failed to unmarshal the request body into an alert") 64 | return err, alerts 65 | } else { 66 | log.WithFields(logrus.Fields{"payload": string(payload)}).Debug("Received a valid webhook alert") 67 | } 68 | 69 | // Iterate over the list of alerts: 70 | for _, alertDetails := range prometheusData.Alerts { 71 | 72 | // Make a new SNMP alert: 73 | alerts = append(alerts, types.Alert{ 74 | Status: prometheusData.Status, 75 | Labels: alertDetails.Labels, 76 | Annotations: alertDetails.Annotations, 77 | StartsAt: alertDetails.StartsAt, 78 | EndsAt: alertDetails.EndsAt, 79 | }) 80 | 81 | } 82 | 83 | return nil, alerts 84 | } 85 | -------------------------------------------------------------------------------- /webhook/webhook_test.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | const ( 10 | invalidPayload = `cruft goes here` 11 | validPayload = ` 12 | { 13 | "version": "2", 14 | "status": "firing", 15 | "alerts": [ 16 | { 17 | "labels": { 18 | "datacentre": "dc1" 19 | }, 20 | "annotations": { 21 | "summary": "this is a test cruft alert" 22 | }, 23 | "startsAt": "2016-10-27T14:27:00Z", 24 | "endsAt": "2016-10-27T14:27:00Z" 25 | }, 26 | { 27 | "labels": { 28 | "datacentre": "dc1" 29 | }, 30 | "annotations": { 31 | "summary": "this is a test garbage alert" 32 | }, 33 | "startsAt": "2016-10-27T14:27:00Z", 34 | "endsAt": "2016-10-27T14:27:00Z" 35 | } 36 | ] 37 | } 38 | ` 39 | ) 40 | 41 | func TestWebhook(t *testing.T) { 42 | 43 | testWebhookValidPayload(t) 44 | testWebhookInvalidPayload(t) 45 | 46 | } 47 | 48 | func testWebhookValidPayload(t *testing.T) { 49 | 50 | // Validate the payload: 51 | err, alerts := validatePayload([]byte(validPayload)) 52 | 53 | // Assess the results: 54 | assert.NoError(t, err, "Unable to validate a valid payload") 55 | assert.EqualValues(t, 2, len(alerts), "Got the wrong number of validated alerts") 56 | } 57 | 58 | func testWebhookInvalidPayload(t *testing.T) { 59 | 60 | // Validate the payload: 61 | err, alerts := validatePayload([]byte(invalidPayload)) 62 | 63 | // Assess the results: 64 | assert.Error(t, err, "Validated an invalid JSON payload") 65 | assert.EqualValues(t, 0, len(alerts), "Got the wrong number of validated alerts") 66 | } 67 | --------------------------------------------------------------------------------