prometheus-xmpp-alerting/xmpp.go

292 lines
6.8 KiB
Go

package main
import (
"crypto/tls"
"fmt"
"io"
"log"
"sort"
"strconv"
"strings"
libxmpp "github.com/mattn/go-xmpp"
"github.com/prometheus/client_golang/prometheus"
)
const (
xmppStatusChat = "chat"
xmppHelpMessage = `Help:
- help
- metrics
- quit`
)
type xmpp struct {
client *libxmpp.Client
debugMode bool
channel chan xmppMessage
status string
sendNotif []string
sendMUC []string
}
type xmppMessage struct {
to *string
message string
format Format
}
type xmppChatType int
const (
xmppChatType_Chat xmppChatType = iota
xmppChatType_GroupChat
)
func (x xmppChatType) String() string {
switch x {
case xmppChatType_GroupChat:
return "groupchat"
case xmppChatType_Chat:
return "chat"
default:
return "chat"
}
}
func xmppChatTypeFrom(s string) (xmppChatType, error) {
switch s {
case xmppChatType_Chat.String():
return xmppChatType_Chat, nil
case xmppChatType_GroupChat.String():
return xmppChatType_GroupChat, nil
default:
return xmppChatType_Chat, fmt.Errorf("unhandled chat type: %s", s)
}
}
// NewXMPP create an XMPP connection. Use Close() to end it
func NewXMPP(config *Config) SendCloser {
if config.XMPP.OverrideServer != "" {
log.Println("Connect to the XMPP account", config.XMPP.User, "using the server", config.XMPP.OverrideServer)
} else {
log.Println("Connect to the XMPP account", config.XMPP.User, "using a server from the DNS records")
}
options := libxmpp.Options{
Host: config.XMPP.OverrideServer,
User: config.XMPP.User,
Password: config.XMPP.Password,
Debug: config.Debug,
NoTLS: config.XMPP.NoTLS,
Status: xmppStatusChat,
StatusMessage: config.XMPP.Status,
}
if config.XMPP.TLSInsecure {
options.TLSConfig = &tls.Config{
InsecureSkipVerify: true,
}
}
client, err := options.NewClient()
if err != nil {
log.Fatalf("Could not connect to XMPP server: %s", err)
}
result := &xmpp{
client: client,
debugMode: config.Debug,
channel: make(chan xmppMessage),
status: config.XMPP.Status,
sendNotif: config.XMPP.SendNotif,
}
for _, muc := range config.XMPP.SendMUC {
var err error
result.sendMUC = append(result.sendMUC, muc.Room)
if muc.Password != nil {
_, err = client.JoinProtectedMUC(muc.Room, muc.Nick, *muc.Password, libxmpp.NoHistory, 0, nil)
} else {
_, err = client.JoinMUC(muc.Room, muc.Nick, libxmpp.NoHistory, 0, nil)
}
if err != nil {
log.Fatalf("Could not connect to MUC: %s", err)
}
}
prometheus.MustRegister(result)
go result.runSender()
go result.runReceiver()
if config.StartupMessage != "" {
result.Send(config.StartupMessage, config.Format)
}
return result
}
func (x *xmpp) Send(message string, format Format) error {
if message != "" {
x.channel <- xmppMessage{
message: message,
format: format,
}
}
return nil
}
func (x *xmpp) Close() error {
prometheus.Unregister(x)
close(x.channel)
x.client.SendPresence(libxmpp.Presence{
Show: "unavailable",
Status: "No monitoring",
})
return x.client.Close()
}
func (x *xmpp) sendTo(to, message string) error {
if message != "" {
x.channel <- xmppMessage{
to: &to,
message: message,
format: Format_Text,
}
}
return nil
}
func (x *xmpp) runSender() {
for payload := range x.channel {
if payload.to != nil {
x.sendToImmediate(xmppChatType_Chat, *payload.to, payload.message, payload.format)
} else {
for _, sendNotif := range x.sendNotif {
x.sendToImmediate(xmppChatType_Chat, sendNotif, payload.message, payload.format)
}
for _, room := range x.sendMUC {
x.sendToImmediate(xmppChatType_GroupChat, room, payload.message, payload.format)
}
}
}
}
func (x *xmpp) runReceiver() {
for {
stanza, err := x.client.Recv()
if err != nil {
if err == io.EOF {
x.Close()
return
}
log.Fatal(err)
}
x.debug("Stanza: %v\n", stanza)
switch v := stanza.(type) {
case libxmpp.Chat:
chatType, err := xmppChatTypeFrom(v.Type)
if err == nil && chatType == xmppChatType_Chat {
x.handleChat(&v)
}
case libxmpp.Presence:
x.handlePresence(&v)
}
}
}
func (x *xmpp) isKnown(person string) bool {
idx := sort.SearchStrings(x.sendNotif, person)
return idx < len(x.sendNotif) && x.sendNotif[idx] == person
}
func (x *xmpp) handleChat(chat *libxmpp.Chat) {
if chat.Text != "" {
remoteUser := strings.Split(chat.Remote, "/")
if len(remoteUser) == 2 && x.isKnown(remoteUser[0]) {
x.debug("CHAT type=%s, remote=%s, text=%s\n", chat.Type, chat.Remote, chat.Text)
x.handleCommand(remoteUser[0], chat.Text)
} else {
x.debug("Unknown user: %v\n", chat)
}
}
}
func (x *xmpp) handlePresence(presence *libxmpp.Presence) {
switch presence.Type {
case "unavailable":
// something puts us as unavailable
if presence.From == x.client.JID() {
x.client.SendOrg(fmt.Sprintf("<presence xml:lang='en'><show>%s</show><status>%s</status></presence>", xmppStatusChat, x.status))
}
case "subscribe":
if x.isKnown(presence.From) {
x.client.ApproveSubscription(presence.From)
x.debug("Approved subscription to %s\n", presence.From)
} else {
x.client.RevokeSubscription(presence.From)
x.debug("Revoked subscription to %s\n", presence.From)
}
case "error":
fmt.Printf("Error from %s", presence.From)
default:
x.debug("Unhandled presence: %v\n", presence)
}
}
func (x *xmpp) handleCommand(from, command string) {
promMessagesReceivedMetric.WithLabelValues(from).Inc()
switch strings.ToLower(strings.TrimSpace(command)) {
case "quit":
x.Close()
case "metrics":
if metrics, err := getMetrics(); err == nil {
for _, metric := range metrics {
x.sendTo(from, metric)
}
} else {
x.sendTo(from, fmt.Sprintf("Could not fetch the metrics: %s", err))
}
case "help":
x.sendTo(from, xmppHelpMessage)
default:
x.sendTo(from, fmt.Sprintf("Unknown command: %s\n%s", command, xmppHelpMessage))
}
}
func (x *xmpp) sendToImmediate(chatType xmppChatType, to, message string, format Format) {
promMessagesSentMetric.WithLabelValues(to, chatType.String(), format.String()).Inc()
_, err := x.sendChat(libxmpp.Chat{
Remote: to,
Type: chatType.String(),
Text: message,
}, format)
if err != nil {
log.Printf("ERROR %s\n", err)
}
}
func (x *xmpp) debug(fmt string, v ...interface{}) {
if x.debugMode {
log.Printf(fmt, v...)
}
}
func (x *xmpp) sendChat(chat libxmpp.Chat, format Format) (n int, err error) {
switch format {
case Format_Text:
return x.client.Send(chat)
case Format_HTML:
return x.client.SendHtml(chat)
default:
return 0, fmt.Errorf("unknown format: %d", format)
}
}
// prometheus Collector
func (x *xmpp) Describe(ch chan<- *prometheus.Desc) {
ch <- promInfo
}
func (x *xmpp) Collect(ch chan<- prometheus.Metric) {
ch <- prometheus.MustNewConstMetric(promInfo, prometheus.GaugeValue, 1, strconv.FormatBool(x.client.IsEncrypted()), x.client.JID())
}