add a hook for sending push notifications via ntfy.sh #543
No reviewers
Labels
No Label
bug
data loss
design finalized
good first issue
new feature area
question / support
security
waiting for response
No Milestone
No Assignees
2 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: borgmatic-collective/borgmatic#543
Loading…
Reference in New Issue
No description provided.
Delete Branch "g-a-c/borgmatic:feat/ntfy"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Out of the current monitoring options (Healthchecks.io, Cronhub, Cronitor, and PagerDuty) I didn't see any free options which would provide real-time push notifications. Healthchecks.io allows 5 SMS or WhatsApp messages per month but this is kind of limiting.
So I decided since I had a use case, and felt like a little bit of hacking on something, I'd add a hook for Ntfy that I would personally find useful, and hopefully others might too.
It can be configured as simply as providing a
topic
, which will be enough to publish a message on failure, or there are more (documented) options to allow more configuration.506907e6b6
to0ff5c6192c
OMG this is great. And documentation, too! Thanks so much for taking the time to add this functionality. The only thing I see potentially missing is tests for
ping_monitor()
. Do you want to take a stab at that, or would you prefer I do them? You can grep fortest_ping_monitor
if you want inspiration.I'm happy to take a look and finish the job, somehow I totally missed seeing the
tests/unit/hooks
directory and assumed that only the core Borgmatic functionality was tested and not external services. I'll do that this evening and push another commite218aec8ce
to4a91f6a741
4a91f6a741
toa7a99799fc
@ -0,0 +6,4 @@
logger = logging.getLogger(__name__)
MONITOR_STATE_TO_NTFY = {
This is not really necessary, but had to be done to keep
flake8
happy because otherwiseborgmatic.hooks.monitor
was imported due to being required by the tests, even though it was not referenced within this hook directly.That seems unnecessary to me, given that it's never used...? Your tests can always import directly from
borgmatic.hooks.monitor
rather than relying onmodule.monitor
. That's just a little convention in borgmatic tests to make sure you're using the exact same import that the code under test is importing. But no need to bend over backwards to do it!a7a99799fc
tod0bb740171
Too much multi-tasking with my day job and a couple of force-pushed later, and this should be reviewable...the tests don't exhaustively cover every possible scenario but they do cover:
topic
set only)topic
andserver
set)topic
andfail
set)topic
andstates
set)topic
set, withdryrun
set on the test)So I realised I was missing one last test to get 100% code coverage, which is the "connection error" test. I stole the one from one of the other tests as this seemed a relatively simple one, but as it is in the most recent commit, it fails, and it seems to be not necessarily related to my hook but related to logging:
So this raises a couple of questions I had:
what is the purpose of theit is transparently used by Requests, andlogging.getLogger('urllib3').setLevel()
call? I don't see anywhere that a logger calledurllib3
is either created or used (unless this happens transparently through Requests?)urllib3
as the underlying library - answered my own question there...except
block is going to output through the__name__
logger anyway?test_cronitor.py
(for example) pass this same test when thelogger
object is instantiated identically in bothcronitor.py
andntfy.py
?Yeah, IIRC, requests' use of urllib3 had some pretty verbose logging by default—and it uses its own urllib3 logger rather than the global logger—so this
setLevel()
call is just to suppress (most of) its logging.So, about that. This one is pretty bad, but I was able to track it down. I ended up having to hack Python logging's
addHandler()
and raising whenhdlr
was None so I could see where a None handler was getting set. Turned out, it was in the tests for Healthchecks! Apparently those tests were (accidentally, implicitly) adding a None handler and then not cleaning it up, thereby impacting downstream tests. Your Ntfy tests were lucky enough to be after the Healthchecks tests, while the Cronitor tests came before both. I think the fix is to make the Healthchecks tests not change any global state—a pretty clear no-no in any testing.I'll take on this task in borgmatic master, and let you know when that's fixed!
Okay, this is fixed in master! Feel free to pull that changeset in. Sorry you had to discover this the hard way.
Lookin' really good. As far as I'm concerned, this is ready to go once tests are passing!
@ -0,0 +30,4 @@
run_states = hook_config.get('states', ['fail'])
if state.name.lower() in run_states:
Might not need to
.lower()
this, sincestates
in configuration is anenum
? Meaning there's no way to make those values uppercase..Similar below.
It is an
enum
, but my understanding (which appears to be backed up by my testing) is that when it's retrieved, because theenum
is capitalised on the left hand side the.name
is uppercase, and it feels clunky making the user fill out this state list in the config in uppercase when nothing else is?Right you are! My bad.. I didn't remember that the
state
passed intoping_monitor()
didn't come from the configuration schema enum.@ -0,0 +36,4 @@
state_config = hook_config.get(
state.name.lower(),
{
'title': 'A Borgmatic event happened',
You could interpolate the state name into the title/message here. (Do not feel strongly.)
@ -0,0 +46,4 @@
base_url = hook_config.get('server', 'https://ntfy.sh')
topic = hook_config.get('topic')
logger.info('{}: Pinging ntfy topic {}{}'.format(config_filename, topic, dry_run_label))
You could use a Python f-string here and elsewhere. (Do not feel strongly.)
Yeah, I actually wasn't aware of the f-string until I was poking around in other modules to try and work out that test issue, but I do find that cleaner to read so I think I'll refactor to use those.
@ -273,0 +308,4 @@
fail:
title: A Borgmatic backup failed
message: You should probably fix it
tags: borgmatic,-1,skull
Love it. (The skull.)
@ -0,0 +45,4 @@
def test_ping_monitor_minimal_config_does_not_hit_hosted_ntfy_on_start():
hook_config = {
'topic': topic,
}
Could all be on one line. (Do not feel strongly.)
Similar elsewhere.
I think one of the auto-formatters may have done this, as I don't remember doing it for these simple single-element
dict
s.I'll clean it up and see if it comes back when
black
orflake8
get run.@ -0,0 +134,4 @@
module.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
These tests look great! I appreciate you taking the time to add them. IMO not every possible permutation needs testing as long as most of the code paths are hit. (Which they are here.)
💩 happens! Sounds like it was an experience for both of us.
That does make a ton of sense, because when I ran just my
ntfy
test to get it to go a little quicker, it passed in isolation. I guess if I'd done some more messing around I might have worked out that when I ran all of thehook
tests together the problem would have come back and isolated it that way - but I don't think I'd have come any closer to tracking down the root cause myselfe0be85cbcb
toa8b8d507b6
I've squashed everything down to a single commit instead of my chopping and hacking around, 100% code coverage,
black
,isort
andflake8
are all happy so I think this is ready for reviewing again...Awesome! Thanks again for doing this—and slogging through the tests. This will be part of the next release.
Released in borgmatic 1.6.3!