[Feature] Add Uptime Kuma hook #885

Merged
witten merged 24 commits from pswilde/borgmatic:main into main 2024-06-26 22:50:12 +00:00
Contributor

Hi,
I've created a basic hook for working with Uptime Kuma.
Uptime Kuma is a self-hosted uptime monitoring service that, traditionally, is used for performing minutely checks on services via pings, curls, tcp sockets, and so on for servers. However, it can also be used in a similar way to healthchecks.io, whereby it can await a get request from a host instead of making a request to it.
It just expects a simple get request to an API endpoint with a few query parameters - nothing too in depth, no authentication, etc.
I've been using it for backup monitoring in this way with some scripts and it works very well.
I've recently started using borg and borgmatic and though the ability to have hooks in borgmatic is fantastic, and as similar hooks are already available I thought it a good idea to get Uptime Kuma working as well.

Apologies for not writing any tests, as far as I'm aware there are no public Uptime Kuma hosts so I need to work on this. If you would prefer tests to be in place then please stand by and I will create them, otherwise I have tested this working with my Uptime Kuma server and it works correctly.

Thanks for all your hard work creating borgmatic!

Hi, I've created a basic hook for working with Uptime Kuma. Uptime Kuma is a self-hosted uptime monitoring service that, traditionally, is used for performing minutely checks on services via pings, curls, tcp sockets, and so on for servers. However, it can also be used in a similar way to healthchecks.io, whereby it can await a get request from a host instead of making a request to it. It just expects a simple get request to an API endpoint with a few query parameters - nothing too in depth, no authentication, etc. I've been using it for backup monitoring in this way with some scripts and it works very well. I've recently started using borg and borgmatic and though the ability to have hooks in borgmatic is fantastic, and as similar hooks are already available I thought it a good idea to get Uptime Kuma working as well. Apologies for not writing any tests, as far as I'm aware there are no public Uptime Kuma hosts so I need to work on this. If you would prefer tests to be in place then please stand by and I will create them, otherwise I have tested this working with my Uptime Kuma server and it works correctly. Thanks for all your hard work creating borgmatic!
pswilde added 4 commits 2024-06-21 20:18:30 +00:00
pswilde added 1 commit 2024-06-21 20:21:08 +00:00
pswilde changed title from Add Uptime Kuma hook to [Feature] Add Uptime Kuma hook 2024-06-21 20:21:42 +00:00
Owner

Wow, thank you so much for implementing this hook! I will say that Uptime Kuma integration for borgmatic was vaguely on my radar, so this is a great addition IMO.

I haven't had a chance to look at the code yet, but in terms of tests, I would like to see coverage. But the good news is that it doesn't need to (shouldn't) actually hit any Uptime Kuma server. The tests just need to assert that the basic logic is solid. For instance, take a look at the existing tests/unit/hooks/test_healthchecks.py or the tests for any of the other monitoring hooks. They mock out any network calls (and dependencies on units other than the one under test) and only test the code on a per-function basis.

If you run into any difficulties when adding tests, I'd be happy to answer questions.. While the mocking library in use is pretty powerful, it's not always intuitive.

Thank you!

Wow, thank you so much for implementing this hook! I will say that Uptime Kuma integration for borgmatic was vaguely on my radar, so this is a great addition IMO. I haven't had a chance to look at the code yet, but in terms of tests, I would like to see coverage. But the good news is that it doesn't need to (_shouldn't_) actually hit any Uptime Kuma server. The tests just need to assert that the basic logic is solid. For instance, take a look at the existing `tests/unit/hooks/test_healthchecks.py` or the tests for any of the other monitoring hooks. They mock out any network calls (and dependencies on units other than the one under test) and only test the code on a per-function basis. If you run into any difficulties when adding tests, I'd be happy to answer questions.. While the mocking library in use is pretty powerful, it's not always intuitive. Thank you!
pswilde added 1 commit 2024-06-22 09:19:40 +00:00
Author
Contributor

Thanks!
I'll copy that test_healthchecks.py tests and try to get them working for Uptime Kuma.
I probably won't have loads of time for the next few days to do it, but I'll do my best then get back in touch.
Thanks,

Thanks! I'll copy that `test_healthchecks.py` tests and try to get them working for Uptime Kuma. I probably won't have loads of time for the next few days to do it, but I'll do my best then get back in touch. Thanks,
pswilde changed title from [Feature] Add Uptime Kuma hook to WIP: [Feature] Add Uptime Kuma hook 2024-06-22 09:33:18 +00:00
pswilde added 1 commit 2024-06-22 09:46:54 +00:00
Author
Contributor

Thanks!
I'll copy that test_healthchecks.py tests and try to get them working for Uptime Kuma.
I probably won't have loads of time for the next few days to do it, but I'll do my best then get back in touch.
Thanks,

Actually, scrap that, turns out that wasn't so hard to implement at all! (I hope!)

So some basic start, finish, fail tests are in place now, I've run tox which is also coming back clear after some further adjustments.

Thanks,

> Thanks! > I'll copy that `test_healthchecks.py` tests and try to get them working for Uptime Kuma. > I probably won't have loads of time for the next few days to do it, but I'll do my best then get back in touch. > Thanks, Actually, scrap that, turns out that wasn't so hard to implement at all! (I hope!) So some basic start, finish, fail tests are in place now, I've run tox which is also coming back clear after some further adjustments. Thanks,
pswilde changed title from WIP: [Feature] Add Uptime Kuma hook to [Feature] Add Uptime Kuma hook 2024-06-22 09:48:33 +00:00
witten requested changes 2024-06-23 19:49:47 +00:00
Dismissed
witten left a comment
Owner

This looks great, including the tests! Most of my comments are on super minor stuff. One thing I see missing though is an entry for Uptime Kuma in the docs at https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/ ... I'd be happy to add this myself, but I thought I'd mention it in case you wanted to take a stab at it as part of this PR. Either way is fine with me!

This looks great, including the tests! Most of my comments are on super minor stuff. One thing I see missing though is an entry for Uptime Kuma in the docs at https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/ ... I'd be happy to add this myself, but I thought I'd mention it in case you wanted to take a stab at it as part of this PR. Either way is fine with me!
@ -62,6 +62,7 @@ borgmatic is powered by [Borg Backup](https://www.borgbackup.org/).
<a href="https://www.mongodb.com/"><img src="docs/static/mongodb.png" alt="MongoDB" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://sqlite.org/"><img src="docs/static/sqlite.png" alt="SQLite" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://healthchecks.io/"><img src="docs/static/healthchecks.png" alt="Healthchecks" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
<a href="https://uptime.kuma.pet/"><img src="docs/static/uptimekuma.png" alt="Uptime Kuma" height="60px" style="margin-bottom:20px; margin-right:20px;"></a>
Owner

Thank you for even adding the logo to the docs!

Thank you for even adding the logo to the docs!
pswilde marked this conversation as resolved
@ -1679,0 +1681,4 @@
required: ['server', 'push_code']
additionalProperties: false
properties:
server:
Owner

What do you think of calling this ping_url or server_url for consistency with the other monitoring hooks? (I realize it can be a UUID as well, similar to the Healthchecks hook.)

What do you think of calling this `ping_url` or `server_url` for consistency with the other monitoring hooks? (I realize it can be a UUID as well, similar to the Healthchecks hook.)
pswilde marked this conversation as resolved
@ -1679,0 +1685,4 @@
type: string
description: |
Uptime Kuma base URL or UUID to notify when a backup
begins, ends, or errors
Owner

Minor nit: Convention is periods a the end of sentences / fragments in schema descriptions. (Here and elsewhere.)

Minor nit: Convention is periods a the end of sentences / fragments in schema descriptions. (Here and elsewhere.)
Owner

This says this can be a UUID, but I don't see any code below to handle that case. So is this accurate or is this maybe just a copy-paste error?

This says this can be a UUID, but I don't see any code below to handle that case. So is this accurate or is this maybe just a copy-paste error?
pswilde marked this conversation as resolved
@ -1679,0 +1691,4 @@
type: string
description: |
Uptime Kuma "Push Code" from the push URL you have been
given. For example, the push code for
Owner

I'm a little unclear on "the push URL you have been given." Is this referring to the server value above.. or is it a separate URL? (I've never used Uptime Kuma.) Would it be clearer to just say "the server URL" or similar?

I'm a little unclear on "the push URL you have been given." Is this referring to the `server` value above.. or is it a separate URL? (I've never used Uptime Kuma.) Would it be clearer to just say "the server URL" or similar?
pswilde marked this conversation as resolved
@ -30,6 +31,7 @@ HOOK_NAME_TO_MODULE = {
'postgresql_databases': postgresql,
'sqlite_databases': sqlite,
'loki': loki,
'uptimekuma': uptimekuma,
Owner

For this dict and also for MONITOR_HOOK_NAMES, it might be nice to alpha order the contents. Do not feel strongly.

For this dict and also for `MONITOR_HOOK_NAMES`, it might be nice to alpha order the contents. Do not feel strongly.
pswilde marked this conversation as resolved
@ -0,0 +22,4 @@
run_states = hook_config.get('states', ['start', 'finish', 'fail'])
if state.name.lower() in run_states:
Owner

You can lose a level of indention on most of this function if you "early out" here. Example:

if state.name.lower() not in run_states:
    return

Do not feel strongly.

You can lose a level of indention on most of this function if you "early out" here. Example: ```python if state.name.lower() not in run_states: return ``` Do not feel strongly.
pswilde marked this conversation as resolved
@ -0,0 +23,4 @@
run_states = hook_config.get('states', ['start', 'finish', 'fail'])
if state.name.lower() in run_states:
Owner

Nit: Remove newline.

Nit: Remove newline.
pswilde marked this conversation as resolved
@ -0,0 +28,4 @@
status = 'up'
if state.name.lower() == 'fail':
status = 'down'
Owner

This could be shortened to:

status = 'down' if state.name.lower() == 'fail' else 'up'

Do not feel strongly.

This could be shortened to: ```python status = 'down' if state.name.lower() == 'fail' else 'up' ``` Do not feel strongly.
pswilde marked this conversation as resolved
@ -0,0 +34,4 @@
push_code = hook_config.get('push_code')
logger.info(f'{config_filename}: Pinging Uptime Kuma push_code {push_code}{dry_run_label}')
logger.debug(f'{config_filename}: Using Uptime Kuma ping URL {base_url}/{push_code}')
Owner

Seems like this log entry is unnecessary given that the next one is the same except with the addition of query parameters. (And they're both at the same log level.)

Seems like this log entry is unnecessary given that the next one is the same except with the addition of query parameters. (And they're both at the same log level.)
pswilde marked this conversation as resolved
@ -0,0 +36,4 @@
logger.info(f'{config_filename}: Pinging Uptime Kuma push_code {push_code}{dry_run_label}')
logger.debug(f'{config_filename}: Using Uptime Kuma ping URL {base_url}/{push_code}')
logger.debug(
f'{config_filename}: Full Uptime Kuma state URL {base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping='
Owner

You could save the full URL into a variable for use both here and below in the requests.get() call. That way, you don't run the risk of a change being made in one place and not the other.

You could save the full URL into a variable for use both here and below in the `requests.get()` call. That way, you don't run the risk of a change being made in one place and not the other.
pswilde marked this conversation as resolved
@ -0,0 +39,4 @@
f'{config_filename}: Full Uptime Kuma state URL {base_url}/{push_code}?status={status}&msg={state.name.lower()}&ping='
)
if not dry_run:
Owner

You could also early out here.

You could also early out here.
pswilde marked this conversation as resolved
@ -0,0 +5,4 @@
default_base_url = 'https://example.uptime.kuma'
custom_base_url = 'https://uptime.example.com'
push_code = 'abcd1234'
Owner

Code convention: All of these variables should be all-capitals since they're actually global constants. πŸ˜„

Code convention: All of these variables should be all-capitals since they're actually global constants. πŸ˜„
pswilde marked this conversation as resolved
@ -0,0 +146,4 @@
)
def test_ping_monitor_does_not_hit_custom_uptimekuma_on_start_dry_run():
Owner

Personally, I don't think you have to test every conceivable combination of inputs (custom / not custom, each state, dry run / not dry run) as long as you test the major combinations to exercise the code as it's written or where things could reasonably go wrong in the future. I do not feel strongly though, so if you want this level of extensive coverage, that's fine with me!

Personally, I don't think you have to test _every_ conceivable combination of inputs (custom / not custom, each state, dry run / not dry run) as long as you test the major combinations to exercise the code as it's written or where things could reasonably go wrong in the future. I do not feel strongly though, so if you want this level of extensive coverage, that's fine with me!
pswilde marked this conversation as resolved
@ -0,0 +223,4 @@
borgmatic.hooks.monitor.State.FAIL,
monitoring_log_level=1,
dry_run=False,
)
Owner

The one test I see missing though is a test that a given state that's not in run_states results in the function not pinging anything.

The one test I see missing though is a test that a given state that's not in `run_states` results in the function not pinging anything.
pswilde marked this conversation as resolved
pswilde added 8 commits 2024-06-24 09:47:42 +00:00
Author
Contributor

Hi,
Thanks for all your pointers there, really helpful.
I've refactored a bit a changed a couple of things.
The main change I've made is to drop entirely the "server" and "push_code" variables and replace them with a "push_url" variable. This is because Uptime Kuma basically just gives you a URL named "Push" to make calls to with a query string, as below:
image

Having slept on it, I felt it's easier for someone to understand just to paste in that URL (without the query string) instead of splitting it up. This resolves a few of your points i.e. the UUID (copy-paste error), server, push_code, etc. confusion, and I think makes it much easier to set up in the first place.
I've also removed the "&ping=" query parameter from the code as it is not required and is actually there for a reverse uptime speed test solution (i.e. we can ping out at 10ms) so not really necessary for backups.

I've removed many of the tests calling to the "default" URL, as ultimately these would never be used anyway. We should only be interested in the custom URL. I've added a test for an invalid run state as you have suggested.

Otherwise I've just changed things as you've advised - many of them are because I haven't written any python in a few years so I'm a bit rusty! πŸ˜†

I'm happy to take a look at the docs as well, and will get to those shortly.

Thanks,

Hi, Thanks for all your pointers there, really helpful. I've refactored a bit a changed a couple of things. The main change I've made is to drop entirely the "server" and "push_code" variables and replace them with a "push_url" variable. This is because Uptime Kuma basically just gives you a URL named "Push" to make calls to with a query string, as below: ![image](/attachments/18dc78c1-13ea-4f1e-ac15-1413a156bedf) Having slept on it, I felt it's easier for someone to understand just to paste in that URL (without the query string) instead of splitting it up. This resolves a few of your points i.e. the UUID (copy-paste error), server, push_code, etc. confusion, and I think makes it much easier to set up in the first place. I've also removed the "&ping=" query parameter from the code as it is not required and is actually there for a reverse uptime speed test solution (i.e. we can ping out at 10ms) so not really necessary for backups. I've removed many of the tests calling to the "default" URL, as ultimately these would never be used anyway. We should only be interested in the custom URL. I've added a test for an invalid run state as you have suggested. Otherwise I've just changed things as you've advised - many of them are because I haven't written any python in a few years so I'm a bit rusty! πŸ˜† I'm happy to take a look at the docs as well, and will get to those shortly. Thanks,
pswilde added 1 commit 2024-06-24 10:07:23 +00:00
Author
Contributor

Just another quick note, I've changed the ping_monitor name to push_monitor as it then matches the variable names a bit better, do you think this is OK?
Basically all that's happening is a GET request to the Uptime Kuma URL, so "ping" doesn't really suit it (I copied a lot from the ntfy and healthchecks hooks). I think "push" is better as that's what Uptime Kuma calls it, but let me know if you feel otherwise.
Thanks,

Just another quick note, I've changed the `ping_monitor` name to `push_monitor` as it then matches the variable names a bit better, do you think this is OK? Basically all that's happening is a GET request to the Uptime Kuma URL, so "ping" doesn't really suit it (I copied a lot from the ntfy and healthchecks hooks). I think "push" is better as that's what Uptime Kuma calls it, but let me know if you feel otherwise. Thanks,
pswilde added 1 commit 2024-06-24 10:46:45 +00:00
pswilde added 1 commit 2024-06-24 10:51:48 +00:00
pswilde added 3 commits 2024-06-24 10:57:48 +00:00
pswilde added 1 commit 2024-06-24 11:00:27 +00:00
Owner

Thanks for making all of these changes! I'll have a look when I get a chance.

Having slept on it, I felt it's easier for someone to understand just to paste in that URL (without the query string) instead of splitting it up.

Makes sense to me. Good call!

Just another quick note, I've changed the ping_monitor name to push_monitor as it then matches the variable names a bit better, do you think this is OK?

It's totally fine everywhere except at the top level functions of the hook module (def ping_monitor(): ..., etc.). That's because those functions are called by name from the borgmatic monitoring dispatch code, and if those names don't exist as expected, the hook can't be called. However inside those functions and in the configuration schema you can call things whatever makes the most sense for Uptime Kuma.

Basically all that's happening is a GET request to the Uptime Kuma URL, so "ping" doesn't really suit it (I copied a lot from the ntfy and healthchecks hooks). I think "push" is better as that's what Uptime Kuma calls it, but let me know if you feel otherwise.

FWIW, many of the existing monitoring hooks just do a GET as well.

Thanks for making all of these changes! I'll have a look when I get a chance. > Having slept on it, I felt it's easier for someone to understand just to paste in that URL (without the query string) instead of splitting it up. Makes sense to me. Good call! > Just another quick note, I've changed the ping_monitor name to push_monitor as it then matches the variable names a bit better, do you think this is OK? It's totally fine everywhere _except_ at the top level functions of the hook module (`def ping_monitor(): ...`, etc.). That's because those functions are called _by name_ from the borgmatic monitoring dispatch code, and if those names don't exist as expected, the hook can't be called. However _inside_ those functions and in the configuration schema you can call things whatever makes the most sense for Uptime Kuma. > Basically all that's happening is a GET request to the Uptime Kuma URL, so "ping" doesn't really suit it (I copied a lot from the ntfy and healthchecks hooks). I think "push" is better as that's what Uptime Kuma calls it, but let me know if you feel otherwise. FWIW, many of the existing monitoring hooks just do a GET as well.
pswilde added 1 commit 2024-06-26 19:57:45 +00:00
pswilde added 1 commit 2024-06-26 19:58:59 +00:00
Author
Contributor

It's totally fine everywhere except at the top level functions of the hook module

Thanks for the note on this, I've changed the top level function back to ping_monitor πŸ‘

> It's totally fine everywhere except at the top level functions of the hook module Thanks for the note on this, I've changed the top level function back to ping_monitor πŸ‘
witten approved these changes 2024-06-26 22:47:51 +00:00
witten left a comment
Owner

Thanks for making these changes.. This all looks good! There are a few minor formatting/whitespace things, but rather than doing another PR go-round, I'll just make those changes after merging.

Thanks for making these changes.. This all looks good! There are a few minor formatting/whitespace things, but rather than doing another PR go-round, I'll just make those changes after merging.
witten merged commit 4a0c167c1c into main 2024-06-26 22:50:12 +00:00
Owner

FYI I'm also renaming the configuration option from uptimekuma to uptime_kuma!

FYI I'm also renaming the configuration option from `uptimekuma` to `uptime_kuma`!
Owner

Also, I really appreciate you taking the time to write up docs too.

Also, I really appreciate you taking the time to write up docs too.
Author
Contributor

All good, thanks again for making a great piece of software!

All good, thanks again for making a great piece of software!
Author
Contributor

Sorry, just a quick note to say the renaming of the config option has stopped calls being made - looks like it needs to be updated in both monitor.py and dispatch.py as well.
Sent a new PR for this, not that it's a big one to fix 🀣

Sorry, just a quick note to say the renaming of the config option has stopped calls being made - looks like it needs to be updated in both monitor.py and dispatch.py as well. Sent a new PR for this, not that it's a big one to fix 🀣
Owner

Released in borgmatic 1.8.13. Thanks again!

Released in borgmatic 1.8.13. Thanks again!
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: borgmatic-collective/borgmatic#885
No description provided.