2022-07-11 02:44:05 -04:00
from maubot import Plugin , MessageEvent
from maubot . handlers import web , command
from aiohttp . web import Request , Response , json_response
import json
2022-08-24 13:41:01 -04:00
import datetime
2022-12-14 11:56:26 -05:00
from mautrix . errors . request import MForbidden
2022-07-11 02:44:05 -04:00
2022-08-24 13:41:01 -04:00
helpstring = f """ # Alertbot
2022-07-11 02:44:05 -04:00
2022-08-24 13:41:01 -04:00
To control the alertbot you can use the following commands :
* ` ! help ` : To show this help
* ` ! ping ` : To check if the bot is alive
* ` ! raw ` : To toggle raw mode ( where webhook data is not parsed but simply forwarded as copyable text )
* ` ! roomid ` : To let the bot show you the current matrix room id
* ` ! url ` : To let the bot show you the webhook url
More information is on [ Github ] ( https : / / github . com / moan0s / alertbot )
"""
2022-09-09 19:02:35 -04:00
def get_alert_type ( data ) :
"""
Currently supported are [ " grafana-alert " , " grafana-resolved " , " prometheus-alert " , " not-found " ]
: return : alert type
"""
# Uptime-kuma has heartbeat
try :
if data [ " heartbeat " ] [ " status " ] == 0 :
return " uptime-kuma-alert "
elif data [ " heartbeat " ] [ " status " ] == 1 :
return " uptime-kuma-resolved "
except KeyError :
pass
# Grafana
try :
data [ " alerts " ] [ 0 ] [ " labels " ] [ " grafana_folder " ]
if data [ ' status ' ] == " firing " :
return " grafana-alert "
else :
return " grafana-resolved "
except KeyError :
pass
# Prometheus
try :
if data [ " alerts " ] [ 0 ] [ " labels " ] [ " job " ] :
if data [ ' status ' ] == " firing " :
return " prometheus-alert "
else :
2022-12-18 16:31:19 -05:00
return " prometheus-resolved "
2022-09-09 19:02:35 -04:00
except KeyError :
pass
return " not-found "
2022-09-09 19:49:12 -04:00
def get_alert_messages ( alert_data : dict , raw_mode = False ) - > list :
2022-07-11 02:44:05 -04:00
"""
Returns a list of messages in markdown format
2022-09-09 19:49:12 -04:00
: param alert_data : The data send to the bot as dict
2022-08-24 13:41:01 -04:00
: param raw_mode : Toggles a mode where the data is not parsed but simply returned as code block in a message
2022-07-11 02:44:05 -04:00
: return : List of alert messages in markdown format
"""
2022-09-09 19:49:12 -04:00
alert_type = get_alert_type ( alert_data )
2022-07-11 02:44:05 -04:00
2023-04-24 15:28:42 -04:00
if raw_mode :
2022-09-09 19:49:12 -04:00
return [ " **Data received** \n ``` \n " + str ( alert_data ) . strip ( " \n " ) . strip ( ) + " \n ``` " ]
2023-04-24 15:28:42 -04:00
elif alert_type == " not-found " :
return [ " **Data received** \n " + dict_to_markdown ( alert_data ) ]
2022-08-24 13:41:01 -04:00
else :
2022-09-18 04:38:23 -04:00
try :
if alert_type == " grafana-alert " :
messages = grafana_alert_to_markdown ( alert_data )
elif alert_type == " grafana-resolved " :
messages = grafana_alert_to_markdown ( alert_data )
elif alert_type == " prometheus-alert " :
messages = prometheus_alert_to_markdown ( alert_data )
2022-12-18 16:31:19 -05:00
elif alert_type == " prometheus-resolved " :
messages = prometheus_alert_to_markdown ( alert_data )
2022-09-18 04:38:23 -04:00
elif alert_type == " uptime-kuma-alert " :
messages = uptime_kuma_alert_to_markdown ( alert_data )
elif alert_type == " uptime-kuma-resolved " :
messages = uptime_kuma_resolved_to_markdown ( alert_data )
except KeyError as e :
messages = [ " **Data received** \n ``` \n " + str ( alert_data ) . strip (
" \n " ) . strip ( ) + f " \n ``` \n The data was detected as { alert_type } but was not in an expected format. If you want to help the development of this bot, file a bug report [here](https://github.com/moan0s/alertbot/issues) \n { e . with_traceback ( ) } " ]
2022-09-09 19:49:12 -04:00
return messages
def uptime_kuma_alert_to_markdown ( alert_data : dict ) :
2022-09-18 04:38:06 -04:00
tags_readable = " , " . join ( [ tag [ " name " ] for tag in alert_data [ " monitor " ] [ " tags " ] ] )
message = (
f """ **Firing 🔥**: Monitor down: { alert_data [ " monitor " ] [ " url " ] }
* * * Error : * * { alert_data [ " heartbeat " ] [ " msg " ] }
* * * Started at : * * { alert_data [ " heartbeat " ] [ " time " ] }
* * * Tags : * * { tags_readable }
* * * Source : * * " Uptime Kuma "
"""
)
return [ message ]
2022-09-09 19:49:12 -04:00
2023-04-24 15:28:42 -04:00
def dict_to_markdown ( alert_data : dict ) :
md = " "
for key_or_dict in alert_data :
try :
alert_data [ key_or_dict ]
except TypeError :
md + = " " + dict_to_markdown ( key_or_dict )
continue
if not ( isinstance ( alert_data [ key_or_dict ] , str ) or isinstance ( alert_data [ key_or_dict ] , int ) ) :
md + = " " + dict_to_markdown ( alert_data [ key_or_dict ] )
else :
md + = f " * { key_or_dict } : { alert_data [ key_or_dict ] } \n "
return md
2022-09-09 19:49:12 -04:00
def uptime_kuma_resolved_to_markdown ( alert_data : dict ) :
2022-12-14 11:15:30 -05:00
tags_readable = " , " . join ( [ tag [ " name " ] for tag in alert_data [ " monitor " ] [ " tags " ] ] )
message = (
f """ **Resolved 💚**: { alert_data [ " monitor " ] [ " url " ] }
* * * Status : * * { alert_data [ " heartbeat " ] [ " msg " ] }
* * * Started at : * * { alert_data [ " heartbeat " ] [ " time " ] }
2022-12-14 12:42:12 -05:00
* Duration until resolved { alert_data [ " heartbeat " ] [ " duration " ] } s
2022-12-14 11:15:30 -05:00
* * * Tags : * * { tags_readable }
* * * Source : * * " Uptime Kuma "
"""
)
return [ message ]
2022-08-24 13:41:01 -04:00
2022-09-09 19:49:12 -04:00
def grafana_alert_to_markdown ( alert_data : dict ) - > list :
2022-07-11 02:44:05 -04:00
"""
2022-08-24 13:41:01 -04:00
Converts a grafana alert json to markdown
2022-07-11 02:44:05 -04:00
2022-09-09 19:49:12 -04:00
: param alert_data :
: return : Alerts as formatted markdown string list
2022-07-11 02:44:05 -04:00
"""
2022-09-09 19:49:12 -04:00
messages = [ ]
for alert in alert_data [ " alerts " ] :
datetime_format = " % Y- % m- %d T % H: % M: % S % z "
if alert [ ' status ' ] == " firing " :
message = (
f """ **Firing 🔥**: { alert [ ' labels ' ] [ ' alertname ' ] }
2022-08-24 13:41:01 -04:00
* * * Instance : * * { alert [ " valueString " ] }
* * * Silence : * * { alert [ " silenceURL " ] }
* * * Started at : * * { alert [ ' startsAt ' ] }
* * * Fingerprint : * * { alert [ ' fingerprint ' ] }
2022-09-09 19:49:12 -04:00
"""
)
if alert [ ' status ' ] == " resolved " :
end_at = datetime . datetime . strptime ( alert [ ' endsAt ' ] , datetime_format )
start_at = datetime . datetime . strptime ( alert [ ' startsAt ' ] , datetime_format )
message = (
f """ **Resolved 🥳**: { alert [ ' labels ' ] [ ' alertname ' ] }
2022-08-24 13:41:01 -04:00
* * * Duration until resolved : * * { end_at - start_at }
* * * Fingerprint : * * { alert [ ' fingerprint ' ] }
2022-09-09 19:49:12 -04:00
"""
)
messages . append ( message )
return messages
2022-08-24 13:41:01 -04:00
2022-09-09 19:49:12 -04:00
def prometheus_alert_to_markdown ( alert_data : dict ) - > str :
2022-08-24 13:41:01 -04:00
"""
Converts a prometheus alert json to markdown
2022-09-09 19:49:12 -04:00
: param alert_data :
2022-08-24 13:41:01 -04:00
: return : Alert as fomatted markdown
"""
2022-09-09 19:49:12 -04:00
messages = [ ]
2023-03-28 13:09:08 -04:00
known_labels = [ ' alertname ' , ' instance ' , ' job ' ]
2022-09-09 19:49:12 -04:00
for alert in alert_data [ " alerts " ] :
2023-03-28 13:09:08 -04:00
title = alert [ ' annotations ' ] [ ' description ' ] if hasattr ( alert [ ' annotations ' ] , ' description ' ) else alert [ ' annotations ' ] [ ' summary ' ]
message = f """ ** { alert [ ' status ' ] } ** { ' 💚 ' if alert [ ' status ' ] == ' resolved ' else ' 🔥 ' } : { title } """
for label_name in known_labels :
try :
message + = " \n * ** {0} **: {1} " . format ( label_name . capitalize ( ) , alert [ " labels " ] [ label_name ] )
except :
pass
2022-09-09 19:49:12 -04:00
messages . append ( message )
return messages
2022-07-11 02:44:05 -04:00
class AlertBot ( Plugin ) :
2022-08-24 13:41:01 -04:00
raw_mode = False
2022-07-11 05:58:36 -04:00
async def send_alert ( self , req , room ) :
2022-07-11 02:44:05 -04:00
text = await req . text ( )
self . log . info ( text )
content = json . loads ( f " { text } " )
2022-08-24 13:41:01 -04:00
for message in get_alert_messages ( content , self . raw_mode ) :
2022-07-11 05:58:36 -04:00
self . log . debug ( f " Sending alert to { room } " )
await self . client . send_markdown ( room , message )
@web.post ( " /webhook/ {room_id} " )
async def webhook_room ( self , req : Request ) - > Response :
room_id = req . match_info [ " room_id " ] . strip ( )
2022-12-14 11:56:26 -05:00
try :
await self . send_alert ( req , room = room_id )
except MForbidden :
self . log . error ( f " Could not send to { room_id } : Forbidden. Most likely the bot is not invited in the room. " )
return json_response ( ' { " status " : " forbidden " , " error " : " forbidden " } ' , status = 403 )
2022-07-11 05:58:36 -04:00
return json_response ( { " status " : " ok " } )
2022-07-11 02:44:05 -04:00
@command.new ( )
async def ping ( self , evt : MessageEvent ) - > None :
""" Answers pong to check if the bot is running """
await evt . reply ( " pong " )
@command.new ( )
async def roomid ( self , evt : MessageEvent ) - > None :
""" Answers with the current room id """
await evt . reply ( f " ` { evt . room_id } ` " )
@command.new ( )
async def url ( self , evt : MessageEvent ) - > None :
""" Answers with the url of the webhook """
2023-03-15 02:04:30 -04:00
await evt . reply ( f " ` { self . webapp_url } /webhook/ { evt . room_id } ` " )
2022-08-24 13:41:01 -04:00
@command.new ( )
async def raw ( self , evt : MessageEvent ) - > None :
self . raw_mode = not self . raw_mode
""" Switches the bot to raw mode or disables raw mode (mode where data is not formatted but simply forwarded) """
await evt . reply ( f " Mode is now: ` { ' raw ' if self . raw_mode else ' normal ' } mode` " )
@command.new ( )
async def help ( self , evt : MessageEvent ) - > None :
await self . client . send_markdown ( evt . room_id , helpstring )