Add log sending for the Apprise logging hook, enabled by default.
This commit is contained in:
parent
a0e5dbff96
commit
9647301b99
13 changed files with 567 additions and 179 deletions
5
NEWS
5
NEWS
|
@ -7,9 +7,12 @@
|
||||||
* #835: Add support for the NO_COLOR environment variable. See the documentation for more
|
* #835: Add support for the NO_COLOR environment variable. See the documentation for more
|
||||||
information:
|
information:
|
||||||
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#colored-output
|
https://torsion.org/borgmatic/docs/how-to/set-up-backups/#colored-output
|
||||||
|
* #839: Add log sending for the Apprise logging hook, enabled by default. See the documentation for
|
||||||
|
more information:
|
||||||
|
https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook
|
||||||
* Switch from Drone to Gitea Actions for continuous integration.
|
* Switch from Drone to Gitea Actions for continuous integration.
|
||||||
* Rename scripts/run-end-to-end-dev-tests to scripts/run-end-to-end-tests and use it in both dev
|
* Rename scripts/run-end-to-end-dev-tests to scripts/run-end-to-end-tests and use it in both dev
|
||||||
and CI for increased dev-CI parity.
|
and CI for better dev-CI parity.
|
||||||
* Clarify documentation about restoring a database: borgmatic does not create the database upon
|
* Clarify documentation about restoring a database: borgmatic does not create the database upon
|
||||||
restore.
|
restore.
|
||||||
|
|
||||||
|
|
|
@ -180,7 +180,7 @@ def run_configuration(config_filename, config, config_paths, arguments):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if monitoring_hooks_are_activated:
|
if monitoring_hooks_are_activated:
|
||||||
# send logs irrespective of error
|
# Send logs irrespective of error.
|
||||||
dispatch.call_hooks(
|
dispatch.call_hooks(
|
||||||
'ping_monitor',
|
'ping_monitor',
|
||||||
config,
|
config,
|
||||||
|
|
|
@ -1432,6 +1432,19 @@ properties:
|
||||||
label: kodi
|
label: kodi
|
||||||
- url: "line://Token@User"
|
- url: "line://Token@User"
|
||||||
label: line
|
label: line
|
||||||
|
send_logs:
|
||||||
|
type: boolean
|
||||||
|
description: |
|
||||||
|
Send borgmatic logs to Apprise services as part the
|
||||||
|
"finish", "fail", and "log" states. Defaults to true.
|
||||||
|
example: false
|
||||||
|
logs_size_limit:
|
||||||
|
type: integer
|
||||||
|
description: |
|
||||||
|
Number of bytes of borgmatic logs to send to Apprise
|
||||||
|
services. Set to 0 to send all logs and disable this
|
||||||
|
truncation. Defaults to 1500.
|
||||||
|
example: 100000
|
||||||
start:
|
start:
|
||||||
type: object
|
type: object
|
||||||
required: ['body']
|
required: ['body']
|
||||||
|
@ -1477,6 +1490,21 @@ properties:
|
||||||
description: |
|
description: |
|
||||||
Specify the message body.
|
Specify the message body.
|
||||||
example: Your backups have failed.
|
example: Your backups have failed.
|
||||||
|
log:
|
||||||
|
type: object
|
||||||
|
required: ['body']
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Specify the message title. If left unspecified, no
|
||||||
|
title is sent.
|
||||||
|
example: Ping!
|
||||||
|
body:
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Specify the message body.
|
||||||
|
example: Here is some info about your backups.
|
||||||
states:
|
states:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
|
@ -1485,13 +1513,15 @@ properties:
|
||||||
- start
|
- start
|
||||||
- finish
|
- finish
|
||||||
- fail
|
- fail
|
||||||
|
- log
|
||||||
uniqueItems: true
|
uniqueItems: true
|
||||||
description: |
|
description: |
|
||||||
List of one or more monitoring states to ping for: "start",
|
List of one or more monitoring states to ping for:
|
||||||
"finish", and/or "fail". Defaults to pinging for failure
|
"start", "finish", "fail", and/or "log". Defaults to
|
||||||
only. For each selected state, corresponding configuration
|
pinging for failure only. For each selected state,
|
||||||
for the message title and body should be given. If any is
|
corresponding configuration for the message title and body
|
||||||
left unspecified, a generic message is emitted instead.
|
should be given. If any is left unspecified, a generic
|
||||||
|
message is emitted instead.
|
||||||
example:
|
example:
|
||||||
- start
|
- start
|
||||||
- finish
|
- finish
|
||||||
|
|
|
@ -1,16 +1,36 @@
|
||||||
import logging
|
import logging
|
||||||
import operator
|
import operator
|
||||||
|
|
||||||
|
import borgmatic.hooks.logs
|
||||||
|
import borgmatic.hooks.monitor
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def initialize_monitor(
|
DEFAULT_LOGS_SIZE_LIMIT_BYTES = 100000
|
||||||
ping_url, config, config_filename, monitoring_log_level, dry_run
|
HANDLER_IDENTIFIER = 'apprise'
|
||||||
): # pragma: no cover
|
|
||||||
|
|
||||||
|
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
||||||
'''
|
'''
|
||||||
No initialization is necessary for this monitor.
|
Add a handler to the root logger that stores in memory the most recent logs emitted. That way,
|
||||||
|
we can send them all to an Apprise notification service upon a finish or failure state. But skip
|
||||||
|
this if the "send_logs" option is false.
|
||||||
'''
|
'''
|
||||||
pass
|
if hook_config.get('send_logs') is False:
|
||||||
|
return
|
||||||
|
|
||||||
|
logs_size_limit = max(
|
||||||
|
hook_config.get('logs_size_limit', DEFAULT_LOGS_SIZE_LIMIT_BYTES)
|
||||||
|
- len(borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
|
||||||
|
borgmatic.hooks.logs.add_handler(
|
||||||
|
borgmatic.hooks.logs.Forgetful_buffering_handler(
|
||||||
|
HANDLER_IDENTIFIER, logs_size_limit, monitoring_log_level
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
|
||||||
|
@ -59,9 +79,20 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||||
if dry_run:
|
if dry_run:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
body = state_config.get('body')
|
||||||
|
|
||||||
|
if state in (
|
||||||
|
borgmatic.hooks.monitor.State.FINISH,
|
||||||
|
borgmatic.hooks.monitor.State.FAIL,
|
||||||
|
borgmatic.hooks.monitor.State.LOG,
|
||||||
|
):
|
||||||
|
formatted_logs = borgmatic.hooks.logs.format_buffered_logs_for_payload(HANDLER_IDENTIFIER)
|
||||||
|
if formatted_logs:
|
||||||
|
body += f'\n\n{formatted_logs}'
|
||||||
|
|
||||||
result = apprise_object.notify(
|
result = apprise_object.notify(
|
||||||
title=state_config.get('title', ''),
|
title=state_config.get('title', ''),
|
||||||
body=state_config.get('body'),
|
body=body,
|
||||||
body_format=NotifyFormat.TEXT,
|
body_format=NotifyFormat.TEXT,
|
||||||
notify_type=state_to_notify_type[state.name.lower()],
|
notify_type=state_to_notify_type[state.name.lower()],
|
||||||
)
|
)
|
||||||
|
@ -70,10 +101,9 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||||
logger.warning(f'{config_filename}: Error sending some Apprise notifications')
|
logger.warning(f'{config_filename}: Error sending some Apprise notifications')
|
||||||
|
|
||||||
|
|
||||||
def destroy_monitor(
|
def destroy_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
||||||
ping_url_or_uuid, config, config_filename, monitoring_log_level, dry_run
|
|
||||||
): # pragma: no cover
|
|
||||||
'''
|
'''
|
||||||
No destruction is necessary for this monitor.
|
Remove the monitor handler that was added to the root logger. This prevents the handler from
|
||||||
|
getting reused by other instances of this monitor.
|
||||||
'''
|
'''
|
||||||
pass
|
borgmatic.hooks.logs.remove_handler(HANDLER_IDENTIFIER)
|
||||||
|
|
|
@ -2,6 +2,7 @@ import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
import borgmatic.hooks.logs
|
||||||
from borgmatic.hooks import monitor
|
from borgmatic.hooks import monitor
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -13,61 +14,8 @@ MONITOR_STATE_TO_HEALTHCHECKS = {
|
||||||
monitor.State.LOG: 'log',
|
monitor.State.LOG: 'log',
|
||||||
}
|
}
|
||||||
|
|
||||||
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
|
DEFAULT_PING_BODY_LIMIT_BYTES = 1500
|
||||||
DEFAULT_PING_BODY_LIMIT_BYTES = 100000
|
HANDLER_IDENTIFIER = 'healthchecks'
|
||||||
|
|
||||||
|
|
||||||
class Forgetful_buffering_handler(logging.Handler):
|
|
||||||
'''
|
|
||||||
A buffering log handler that stores log messages in memory, and throws away messages (oldest
|
|
||||||
first) once a particular capacity in bytes is reached. But if the given byte capacity is zero,
|
|
||||||
don't throw away any messages.
|
|
||||||
'''
|
|
||||||
|
|
||||||
def __init__(self, byte_capacity, log_level):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.byte_capacity = byte_capacity
|
|
||||||
self.byte_count = 0
|
|
||||||
self.buffer = []
|
|
||||||
self.forgot = False
|
|
||||||
self.setLevel(log_level)
|
|
||||||
|
|
||||||
def emit(self, record):
|
|
||||||
message = record.getMessage() + '\n'
|
|
||||||
self.byte_count += len(message)
|
|
||||||
self.buffer.append(message)
|
|
||||||
|
|
||||||
if not self.byte_capacity:
|
|
||||||
return
|
|
||||||
|
|
||||||
while self.byte_count > self.byte_capacity and self.buffer:
|
|
||||||
self.byte_count -= len(self.buffer[0])
|
|
||||||
self.buffer.pop(0)
|
|
||||||
self.forgot = True
|
|
||||||
|
|
||||||
|
|
||||||
def format_buffered_logs_for_payload():
|
|
||||||
'''
|
|
||||||
Get the handler previously added to the root logger, and slurp buffered logs out of it to
|
|
||||||
send to Healthchecks.
|
|
||||||
'''
|
|
||||||
try:
|
|
||||||
buffering_handler = next(
|
|
||||||
handler
|
|
||||||
for handler in logging.getLogger().handlers
|
|
||||||
if isinstance(handler, Forgetful_buffering_handler)
|
|
||||||
)
|
|
||||||
except StopIteration:
|
|
||||||
# No handler means no payload.
|
|
||||||
return ''
|
|
||||||
|
|
||||||
payload = ''.join(message for message in buffering_handler.buffer)
|
|
||||||
|
|
||||||
if buffering_handler.forgot:
|
|
||||||
return PAYLOAD_TRUNCATION_INDICATOR + payload
|
|
||||||
|
|
||||||
return payload
|
|
||||||
|
|
||||||
|
|
||||||
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
|
||||||
|
@ -81,12 +29,14 @@ def initialize_monitor(hook_config, config, config_filename, monitoring_log_leve
|
||||||
|
|
||||||
ping_body_limit = max(
|
ping_body_limit = max(
|
||||||
hook_config.get('ping_body_limit', DEFAULT_PING_BODY_LIMIT_BYTES)
|
hook_config.get('ping_body_limit', DEFAULT_PING_BODY_LIMIT_BYTES)
|
||||||
- len(PAYLOAD_TRUNCATION_INDICATOR),
|
- len(borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
|
||||||
0,
|
0,
|
||||||
)
|
)
|
||||||
|
|
||||||
logging.getLogger().addHandler(
|
borgmatic.hooks.logs.add_handler(
|
||||||
Forgetful_buffering_handler(ping_body_limit, monitoring_log_level)
|
borgmatic.hooks.logs.Forgetful_buffering_handler(
|
||||||
|
HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -117,7 +67,7 @@ def ping_monitor(hook_config, config, config_filename, state, monitoring_log_lev
|
||||||
logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')
|
logger.debug(f'{config_filename}: Using Healthchecks ping URL {ping_url}')
|
||||||
|
|
||||||
if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
|
if state in (monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG):
|
||||||
payload = format_buffered_logs_for_payload()
|
payload = borgmatic.hooks.logs.format_buffered_logs_for_payload(HANDLER_IDENTIFIER)
|
||||||
else:
|
else:
|
||||||
payload = ''
|
payload = ''
|
||||||
|
|
||||||
|
@ -138,8 +88,4 @@ def destroy_monitor(hook_config, config, config_filename, monitoring_log_level,
|
||||||
Remove the monitor handler that was added to the root logger. This prevents the handler from
|
Remove the monitor handler that was added to the root logger. This prevents the handler from
|
||||||
getting reused by other instances of this monitor.
|
getting reused by other instances of this monitor.
|
||||||
'''
|
'''
|
||||||
logger = logging.getLogger()
|
borgmatic.hooks.logs.remove_handler(HANDLER_IDENTIFIER)
|
||||||
|
|
||||||
for handler in tuple(logger.handlers):
|
|
||||||
if isinstance(handler, Forgetful_buffering_handler):
|
|
||||||
logger.removeHandler(handler)
|
|
||||||
|
|
91
borgmatic/hooks/logs.py
Normal file
91
borgmatic/hooks/logs.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
PAYLOAD_TRUNCATION_INDICATOR = '...\n'
|
||||||
|
|
||||||
|
|
||||||
|
class Forgetful_buffering_handler(logging.Handler):
|
||||||
|
'''
|
||||||
|
A buffering log handler that stores log messages in memory, and throws away messages (oldest
|
||||||
|
first) once a particular capacity in bytes is reached. But if the given byte capacity is zero,
|
||||||
|
don't throw away any messages.
|
||||||
|
|
||||||
|
The given identifier is used to distinguish the instance of this handler used for one monitoring
|
||||||
|
hook from those instances used for other monitoring hooks.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, identifier, byte_capacity, log_level):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.identifier = identifier
|
||||||
|
self.byte_capacity = byte_capacity
|
||||||
|
self.byte_count = 0
|
||||||
|
self.buffer = []
|
||||||
|
self.forgot = False
|
||||||
|
self.setLevel(log_level)
|
||||||
|
|
||||||
|
def emit(self, record):
|
||||||
|
message = record.getMessage() + '\n'
|
||||||
|
self.byte_count += len(message)
|
||||||
|
self.buffer.append(message)
|
||||||
|
|
||||||
|
if not self.byte_capacity:
|
||||||
|
return
|
||||||
|
|
||||||
|
while self.byte_count > self.byte_capacity and self.buffer:
|
||||||
|
self.byte_count -= len(self.buffer[0])
|
||||||
|
self.buffer.pop(0)
|
||||||
|
self.forgot = True
|
||||||
|
|
||||||
|
|
||||||
|
def add_handler(handler): # pragma: no cover
|
||||||
|
'''
|
||||||
|
Add the given handler to the global logger.
|
||||||
|
'''
|
||||||
|
logging.getLogger().addHandler(handler)
|
||||||
|
|
||||||
|
|
||||||
|
def get_handler(identifier):
|
||||||
|
'''
|
||||||
|
Given the identifier for an existing Forgetful_buffering_handler instance, return the handler.
|
||||||
|
|
||||||
|
Raise ValueError if the handler isn't found.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
return next(
|
||||||
|
handler
|
||||||
|
for handler in logging.getLogger().handlers
|
||||||
|
if isinstance(handler, Forgetful_buffering_handler) and handler.identifier == identifier
|
||||||
|
)
|
||||||
|
except StopIteration:
|
||||||
|
raise ValueError(f'A buffering handler for {identifier} was not found')
|
||||||
|
|
||||||
|
|
||||||
|
def format_buffered_logs_for_payload(identifier):
|
||||||
|
'''
|
||||||
|
Get the handler previously added to the root logger, and slurp buffered logs out of it to
|
||||||
|
send to Healthchecks.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
buffering_handler = get_handler(identifier)
|
||||||
|
except ValueError:
|
||||||
|
# No handler means no payload.
|
||||||
|
return ''
|
||||||
|
|
||||||
|
payload = ''.join(message for message in buffering_handler.buffer)
|
||||||
|
|
||||||
|
if buffering_handler.forgot:
|
||||||
|
return PAYLOAD_TRUNCATION_INDICATOR + payload
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
|
def remove_handler(identifier):
|
||||||
|
'''
|
||||||
|
Given the identifier for an existing Forgetful_buffering_handler instance, remove it.
|
||||||
|
'''
|
||||||
|
logger = logging.getLogger()
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.removeHandler(get_handler(identifier))
|
||||||
|
except ValueError:
|
||||||
|
pass
|
|
@ -35,6 +35,14 @@ pipx ensurepath
|
||||||
pipx install --editable .
|
pipx install --editable .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Or to work on the [Apprise
|
||||||
|
hook](https://torsion.org/borgmatic/docs/how-to/monitor-your-backups/#apprise-hook),
|
||||||
|
change that last line to:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pipx install --editable .[Apprise]
|
||||||
|
```
|
||||||
|
|
||||||
To get oriented with the borgmatic source code, have a look at the [source
|
To get oriented with the borgmatic source code, have a look at the [source
|
||||||
code reference](https://torsion.org/borgmatic/docs/reference/source-code/).
|
code reference](https://torsion.org/borgmatic/docs/reference/source-code/).
|
||||||
|
|
||||||
|
@ -141,6 +149,9 @@ the following deviations from it:
|
||||||
separate from their contents.
|
separate from their contents.
|
||||||
* Within multiline constructs, use standard four-space indentation. Don't align
|
* Within multiline constructs, use standard four-space indentation. Don't align
|
||||||
indentation with an opening delimiter.
|
indentation with an opening delimiter.
|
||||||
|
* In general, spell out words in variable names instead of shortening them.
|
||||||
|
So, think `index` instead of `idx`. There are some notable exceptions to
|
||||||
|
this though (like `config`).
|
||||||
|
|
||||||
borgmatic code uses the [Black](https://black.readthedocs.io/en/stable/) code
|
borgmatic code uses the [Black](https://black.readthedocs.io/en/stable/) code
|
||||||
formatter, the [Flake8](http://flake8.pycqa.org/en/latest/) code checker, and
|
formatter, the [Flake8](http://flake8.pycqa.org/en/latest/) code checker, and
|
||||||
|
@ -148,9 +159,12 @@ the [isort](https://github.com/timothycrosley/isort) import orderer, so
|
||||||
certain code style requirements will be enforced when running automated tests.
|
certain code style requirements will be enforced when running automated tests.
|
||||||
See the Black, Flake8, and isort documentation for more information.
|
See the Black, Flake8, and isort documentation for more information.
|
||||||
|
|
||||||
|
|
||||||
## Continuous integration
|
## Continuous integration
|
||||||
|
|
||||||
Each commit to main triggers [a continuous integration
|
Each commit to
|
||||||
|
[main](https://projects.torsion.org/borgmatic-collective/borgmatic/branches)
|
||||||
|
triggers [a continuous integration
|
||||||
build](https://projects.torsion.org/borgmatic-collective/borgmatic/actions)
|
build](https://projects.torsion.org/borgmatic-collective/borgmatic/actions)
|
||||||
which runs the test suite and updates
|
which runs the test suite and updates
|
||||||
[documentation](https://torsion.org/borgmatic/). These builds are also linked
|
[documentation](https://torsion.org/borgmatic/). These builds are also linked
|
||||||
|
|
|
@ -149,7 +149,7 @@ backup begins, ends, or errors, but only when any of the `create`, `prune`,
|
||||||
Then, if the actions complete successfully, borgmatic notifies Healthchecks of
|
Then, if the actions complete successfully, borgmatic notifies Healthchecks of
|
||||||
the success and includes borgmatic logs in the payload data sent to
|
the success and includes borgmatic logs in the payload data sent to
|
||||||
Healthchecks. This means that borgmatic logs show up in the Healthchecks UI,
|
Healthchecks. This means that borgmatic logs show up in the Healthchecks UI,
|
||||||
although be aware that Healthchecks currently has a 10-kilobyte limit for the
|
although be aware that Healthchecks currently has a 100-kilobyte limit for the
|
||||||
logs in each ping.
|
logs in each ping.
|
||||||
|
|
||||||
If an error occurs during any action or hook, borgmatic notifies Healthchecks,
|
If an error occurs during any action or hook, borgmatic notifies Healthchecks,
|
||||||
|
@ -385,7 +385,7 @@ pipx](https://torsion.org/borgmatic/docs/how-to/set-up-backups/#installation),
|
||||||
run the following to install Apprise so borgmatic can use it:
|
run the following to install Apprise so borgmatic can use it:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo pipx install --editable --force borgmatic[Apprise]
|
sudo pipx install --force borgmatic[Apprise]
|
||||||
```
|
```
|
||||||
|
|
||||||
Omit `sudo` if borgmatic is installed as a non-root user.
|
Omit `sudo` if borgmatic is installed as a non-root user.
|
||||||
|
@ -429,6 +429,37 @@ apprise:
|
||||||
- fail
|
- fail
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<span class="minilink minilink-addedin">New in version 1.8.9</span> borgmatic
|
||||||
|
logs are automatically included in the body data sent to your Apprise services
|
||||||
|
when a backup finishes or fails.
|
||||||
|
|
||||||
|
You can customize the verbosity of the logs that are sent with borgmatic's
|
||||||
|
`--monitoring-verbosity` flag. The `--list` and `--stats` flags may also be of
|
||||||
|
use. See `borgmatic create --help` for more information.
|
||||||
|
|
||||||
|
If you don't want any logs sent, you can disable this feature by setting
|
||||||
|
`send_logs` to `false`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apprise:
|
||||||
|
services:
|
||||||
|
- url: gotify://hostname/token
|
||||||
|
label: gotify
|
||||||
|
send_logs: false
|
||||||
|
```
|
||||||
|
|
||||||
|
Or to limit the size of logs sent to Apprise services:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
apprise:
|
||||||
|
services:
|
||||||
|
- url: gotify://hostname/token
|
||||||
|
label: gotify
|
||||||
|
logs_size_limit: 500
|
||||||
|
```
|
||||||
|
|
||||||
|
This may be necessary for some services that reject large requests.
|
||||||
|
|
||||||
See the [configuration
|
See the [configuration
|
||||||
reference](https://torsion.org/borgmatic/docs/reference/configuration/) for
|
reference](https://torsion.org/borgmatic/docs/reference/configuration/) for
|
||||||
details.
|
details.
|
||||||
|
|
28
tests/integration/hooks/test_apprise.py
Normal file
28
tests/integration/hooks/test_apprise.py
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.hooks import apprise as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_destroy_monitor_removes_apprise_handler():
|
||||||
|
logger = logging.getLogger()
|
||||||
|
original_handlers = list(logger.handlers)
|
||||||
|
module.borgmatic.hooks.logs.add_handler(
|
||||||
|
module.borgmatic.hooks.logs.Forgetful_buffering_handler(
|
||||||
|
identifier=module.HANDLER_IDENTIFIER, byte_capacity=100, log_level=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
module.destroy_monitor(flexmock(), flexmock(), flexmock(), flexmock(), flexmock())
|
||||||
|
|
||||||
|
assert logger.handlers == original_handlers
|
||||||
|
|
||||||
|
|
||||||
|
def test_destroy_monitor_without_apprise_handler_does_not_raise():
|
||||||
|
logger = logging.getLogger()
|
||||||
|
original_handlers = list(logger.handlers)
|
||||||
|
|
||||||
|
module.destroy_monitor(flexmock(), flexmock(), flexmock(), flexmock(), flexmock())
|
||||||
|
|
||||||
|
assert logger.handlers == original_handlers
|
|
@ -8,7 +8,11 @@ from borgmatic.hooks import healthchecks as module
|
||||||
def test_destroy_monitor_removes_healthchecks_handler():
|
def test_destroy_monitor_removes_healthchecks_handler():
|
||||||
logger = logging.getLogger()
|
logger = logging.getLogger()
|
||||||
original_handlers = list(logger.handlers)
|
original_handlers = list(logger.handlers)
|
||||||
logger.addHandler(module.Forgetful_buffering_handler(byte_capacity=100, log_level=1))
|
module.borgmatic.hooks.logs.add_handler(
|
||||||
|
module.borgmatic.hooks.logs.Forgetful_buffering_handler(
|
||||||
|
identifier=module.HANDLER_IDENTIFIER, byte_capacity=100, log_level=1
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
module.destroy_monitor(flexmock(), flexmock(), flexmock(), flexmock(), flexmock())
|
module.destroy_monitor(flexmock(), flexmock(), flexmock(), flexmock(), flexmock())
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,63 @@ def mock_apprise():
|
||||||
add=lambda servers: None, notify=lambda title, body, body_format, notify_type: None
|
add=lambda servers: None, notify=lambda title, body, body_format, notify_type: None
|
||||||
)
|
)
|
||||||
flexmock(apprise.Apprise).new_instances(apprise_mock)
|
flexmock(apprise.Apprise).new_instances(apprise_mock)
|
||||||
|
|
||||||
return apprise_mock
|
return apprise_mock
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_adheres_dry_run():
|
def test_initialize_monitor_with_send_logs_false_does_not_add_handler():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').never()
|
||||||
|
|
||||||
|
module.initialize_monitor(
|
||||||
|
hook_config={'send_logs': False},
|
||||||
|
config={},
|
||||||
|
config_filename='test.yaml',
|
||||||
|
monitoring_log_level=1,
|
||||||
|
dry_run=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_monitor_with_send_logs_true_adds_handler_with_default_log_size_limit():
|
||||||
|
truncation_indicator_length = 4
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
|
||||||
|
module.HANDLER_IDENTIFIER,
|
||||||
|
module.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
|
||||||
|
1,
|
||||||
|
).once()
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').once()
|
||||||
|
|
||||||
|
module.initialize_monitor(
|
||||||
|
hook_config={'send_logs': True},
|
||||||
|
config={},
|
||||||
|
config_filename='test.yaml',
|
||||||
|
monitoring_log_level=1,
|
||||||
|
dry_run=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_initialize_monitor_without_send_logs_adds_handler_with_default_log_size_limit():
|
||||||
|
truncation_indicator_length = 4
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
|
||||||
|
module.HANDLER_IDENTIFIER,
|
||||||
|
module.DEFAULT_LOGS_SIZE_LIMIT_BYTES - truncation_indicator_length,
|
||||||
|
1,
|
||||||
|
).once()
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('add_handler').once()
|
||||||
|
|
||||||
|
module.initialize_monitor(
|
||||||
|
hook_config={},
|
||||||
|
config={},
|
||||||
|
config_filename='test.yaml',
|
||||||
|
monitoring_log_level=1,
|
||||||
|
dry_run=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ping_monitor_respects_dry_run():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('loggy log')
|
||||||
mock_apprise().should_receive('notify').never()
|
mock_apprise().should_receive('notify').never()
|
||||||
|
|
||||||
module.ping_monitor(
|
module.ping_monitor(
|
||||||
|
@ -29,7 +82,9 @@ def test_ping_monitor_adheres_dry_run():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_does_not_hit_with_no_states():
|
def test_ping_monitor_with_no_states_does_not_notify():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
|
||||||
mock_apprise().should_receive('notify').never()
|
mock_apprise().should_receive('notify').never()
|
||||||
|
|
||||||
module.ping_monitor(
|
module.ping_monitor(
|
||||||
|
@ -42,7 +97,11 @@ def test_ping_monitor_does_not_hit_with_no_states():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_hits_fail_by_default():
|
def test_ping_monitor_notifies_fail_by_default():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('')
|
||||||
mock_apprise().should_receive('notify').with_args(
|
mock_apprise().should_receive('notify').with_args(
|
||||||
title='A borgmatic FAIL event happened',
|
title='A borgmatic FAIL event happened',
|
||||||
body='A borgmatic FAIL event happened',
|
body='A borgmatic FAIL event happened',
|
||||||
|
@ -61,7 +120,34 @@ def test_ping_monitor_hits_fail_by_default():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_hits_with_finish_default_config():
|
def test_ping_monitor_with_logs_appends_logs_to_body():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('loggy log')
|
||||||
|
mock_apprise().should_receive('notify').with_args(
|
||||||
|
title='A borgmatic FAIL event happened',
|
||||||
|
body='A borgmatic FAIL event happened\n\nloggy log',
|
||||||
|
body_format=NotifyFormat.TEXT,
|
||||||
|
notify_type=NotifyType.FAILURE,
|
||||||
|
).once()
|
||||||
|
|
||||||
|
for state in borgmatic.hooks.monitor.State:
|
||||||
|
module.ping_monitor(
|
||||||
|
{'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}]},
|
||||||
|
{},
|
||||||
|
'config.yaml',
|
||||||
|
state,
|
||||||
|
monitoring_log_level=1,
|
||||||
|
dry_run=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ping_monitor_with_finish_default_config_notifies():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('')
|
||||||
mock_apprise().should_receive('notify').with_args(
|
mock_apprise().should_receive('notify').with_args(
|
||||||
title='A borgmatic FINISH event happened',
|
title='A borgmatic FINISH event happened',
|
||||||
body='A borgmatic FINISH event happened',
|
body='A borgmatic FINISH event happened',
|
||||||
|
@ -79,7 +165,9 @@ def test_ping_monitor_hits_with_finish_default_config():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_hits_with_start_default_config():
|
def test_ping_monitor_with_start_default_config_notifies():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
|
||||||
mock_apprise().should_receive('notify').with_args(
|
mock_apprise().should_receive('notify').with_args(
|
||||||
title='A borgmatic START event happened',
|
title='A borgmatic START event happened',
|
||||||
body='A borgmatic START event happened',
|
body='A borgmatic START event happened',
|
||||||
|
@ -97,7 +185,11 @@ def test_ping_monitor_hits_with_start_default_config():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_hits_with_fail_default_config():
|
def test_ping_monitor_with_fail_default_config_notifies():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('')
|
||||||
mock_apprise().should_receive('notify').with_args(
|
mock_apprise().should_receive('notify').with_args(
|
||||||
title='A borgmatic FAIL event happened',
|
title='A borgmatic FAIL event happened',
|
||||||
body='A borgmatic FAIL event happened',
|
body='A borgmatic FAIL event happened',
|
||||||
|
@ -115,7 +207,11 @@ def test_ping_monitor_hits_with_fail_default_config():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_hits_with_log_default_config():
|
def test_ping_monitor_with_log_default_config_notifies():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('')
|
||||||
mock_apprise().should_receive('notify').with_args(
|
mock_apprise().should_receive('notify').with_args(
|
||||||
title='A borgmatic LOG event happened',
|
title='A borgmatic LOG event happened',
|
||||||
body='A borgmatic LOG event happened',
|
body='A borgmatic LOG event happened',
|
||||||
|
@ -134,6 +230,10 @@ def test_ping_monitor_hits_with_log_default_config():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_passes_through_custom_message_title():
|
def test_ping_monitor_passes_through_custom_message_title():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('')
|
||||||
mock_apprise().should_receive('notify').with_args(
|
mock_apprise().should_receive('notify').with_args(
|
||||||
title='foo',
|
title='foo',
|
||||||
body='bar',
|
body='bar',
|
||||||
|
@ -156,6 +256,10 @@ def test_ping_monitor_passes_through_custom_message_title():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_passes_through_custom_message_body():
|
def test_ping_monitor_passes_through_custom_message_body():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('')
|
||||||
mock_apprise().should_receive('notify').with_args(
|
mock_apprise().should_receive('notify').with_args(
|
||||||
title='',
|
title='',
|
||||||
body='baz',
|
body='baz',
|
||||||
|
@ -177,7 +281,37 @@ def test_ping_monitor_passes_through_custom_message_body():
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_ping_monitor_passes_through_custom_message_body_and_appends_logs():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('loggy log')
|
||||||
|
mock_apprise().should_receive('notify').with_args(
|
||||||
|
title='',
|
||||||
|
body='baz\n\nloggy log',
|
||||||
|
body_format=NotifyFormat.TEXT,
|
||||||
|
notify_type=NotifyType.FAILURE,
|
||||||
|
).once()
|
||||||
|
|
||||||
|
module.ping_monitor(
|
||||||
|
{
|
||||||
|
'services': [{'url': f'ntfys://{TOPIC}', 'label': 'ntfys'}],
|
||||||
|
'states': ['fail'],
|
||||||
|
'fail': {'body': 'baz'},
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
'config.yaml',
|
||||||
|
borgmatic.hooks.monitor.State.FAIL,
|
||||||
|
monitoring_log_level=1,
|
||||||
|
dry_run=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_pings_multiple_services():
|
def test_ping_monitor_pings_multiple_services():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('')
|
||||||
mock_apprise().should_receive('add').with_args([f'ntfys://{TOPIC}', f'ntfy://{TOPIC}']).once()
|
mock_apprise().should_receive('add').with_args([f'ntfys://{TOPIC}', f'ntfy://{TOPIC}']).once()
|
||||||
|
|
||||||
module.ping_monitor(
|
module.ping_monitor(
|
||||||
|
@ -196,6 +330,8 @@ def test_ping_monitor_pings_multiple_services():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_logs_info_for_no_services():
|
def test_ping_monitor_logs_info_for_no_services():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler').never()
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('format_buffered_logs_for_payload').never()
|
||||||
flexmock(module.logger).should_receive('info').once()
|
flexmock(module.logger).should_receive('info').once()
|
||||||
|
|
||||||
module.ping_monitor(
|
module.ping_monitor(
|
||||||
|
@ -209,6 +345,10 @@ def test_ping_monitor_logs_info_for_no_services():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_logs_warning_when_notify_fails():
|
def test_ping_monitor_logs_warning_when_notify_fails():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return('')
|
||||||
mock_apprise().should_receive('notify').and_return(False)
|
mock_apprise().should_receive('notify').and_return(False)
|
||||||
flexmock(module.logger).should_receive('warning').once()
|
flexmock(module.logger).should_receive('warning').once()
|
||||||
|
|
||||||
|
@ -221,3 +361,15 @@ def test_ping_monitor_logs_warning_when_notify_fails():
|
||||||
monitoring_log_level=1,
|
monitoring_log_level=1,
|
||||||
dry_run=False,
|
dry_run=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_destroy_monitor_does_not_raise():
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive('remove_handler')
|
||||||
|
|
||||||
|
module.destroy_monitor(
|
||||||
|
hook_config={},
|
||||||
|
config={},
|
||||||
|
config_filename='test.yaml',
|
||||||
|
monitoring_log_level=1,
|
||||||
|
dry_run=False,
|
||||||
|
)
|
||||||
|
|
|
@ -3,72 +3,6 @@ from flexmock import flexmock
|
||||||
from borgmatic.hooks import healthchecks as module
|
from borgmatic.hooks import healthchecks as module
|
||||||
|
|
||||||
|
|
||||||
def test_forgetful_buffering_handler_emit_collects_log_records():
|
|
||||||
handler = module.Forgetful_buffering_handler(byte_capacity=100, log_level=1)
|
|
||||||
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
|
||||||
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
|
||||||
|
|
||||||
assert handler.buffer == ['foo\n', 'bar\n']
|
|
||||||
assert not handler.forgot
|
|
||||||
|
|
||||||
|
|
||||||
def test_forgetful_buffering_handler_emit_collects_log_records_with_zero_byte_capacity():
|
|
||||||
handler = module.Forgetful_buffering_handler(byte_capacity=0, log_level=1)
|
|
||||||
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
|
||||||
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
|
||||||
|
|
||||||
assert handler.buffer == ['foo\n', 'bar\n']
|
|
||||||
assert not handler.forgot
|
|
||||||
|
|
||||||
|
|
||||||
def test_forgetful_buffering_handler_emit_forgets_log_records_when_capacity_reached():
|
|
||||||
handler = module.Forgetful_buffering_handler(byte_capacity=len('foo\nbar\n'), log_level=1)
|
|
||||||
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
|
||||||
assert handler.buffer == ['foo\n']
|
|
||||||
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
|
||||||
assert handler.buffer == ['foo\n', 'bar\n']
|
|
||||||
handler.emit(flexmock(getMessage=lambda: 'baz'))
|
|
||||||
assert handler.buffer == ['bar\n', 'baz\n']
|
|
||||||
handler.emit(flexmock(getMessage=lambda: 'quux'))
|
|
||||||
assert handler.buffer == ['quux\n']
|
|
||||||
assert handler.forgot
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_buffered_logs_for_payload_flattens_log_buffer():
|
|
||||||
handler = module.Forgetful_buffering_handler(byte_capacity=100, log_level=1)
|
|
||||||
handler.buffer = ['foo\n', 'bar\n']
|
|
||||||
logger = flexmock(handlers=[handler])
|
|
||||||
logger.should_receive('removeHandler')
|
|
||||||
flexmock(module.logging).should_receive('getLogger').and_return(logger)
|
|
||||||
|
|
||||||
payload = module.format_buffered_logs_for_payload()
|
|
||||||
|
|
||||||
assert payload == 'foo\nbar\n'
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_buffered_logs_for_payload_inserts_truncation_indicator_when_logs_forgotten():
|
|
||||||
handler = module.Forgetful_buffering_handler(byte_capacity=100, log_level=1)
|
|
||||||
handler.buffer = ['foo\n', 'bar\n']
|
|
||||||
handler.forgot = True
|
|
||||||
logger = flexmock(handlers=[handler])
|
|
||||||
logger.should_receive('removeHandler')
|
|
||||||
flexmock(module.logging).should_receive('getLogger').and_return(logger)
|
|
||||||
|
|
||||||
payload = module.format_buffered_logs_for_payload()
|
|
||||||
|
|
||||||
assert payload == '...\nfoo\nbar\n'
|
|
||||||
|
|
||||||
|
|
||||||
def test_format_buffered_logs_for_payload_without_handler_produces_empty_payload():
|
|
||||||
logger = flexmock(handlers=[module.logging.Handler()])
|
|
||||||
logger.should_receive('removeHandler')
|
|
||||||
flexmock(module.logging).should_receive('getLogger').and_return(logger)
|
|
||||||
|
|
||||||
payload = module.format_buffered_logs_for_payload()
|
|
||||||
|
|
||||||
assert payload == ''
|
|
||||||
|
|
||||||
|
|
||||||
def mock_logger():
|
def mock_logger():
|
||||||
logger = flexmock()
|
logger = flexmock()
|
||||||
logger.should_receive('addHandler')
|
logger.should_receive('addHandler')
|
||||||
|
@ -81,8 +15,10 @@ def test_initialize_monitor_creates_log_handler_with_ping_body_limit():
|
||||||
monitoring_log_level = 1
|
monitoring_log_level = 1
|
||||||
|
|
||||||
mock_logger()
|
mock_logger()
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler').with_args(
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
|
||||||
ping_body_limit - len(module.PAYLOAD_TRUNCATION_INDICATOR), monitoring_log_level
|
module.HANDLER_IDENTIFIER,
|
||||||
|
ping_body_limit - len(module.borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
|
||||||
|
monitoring_log_level,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.initialize_monitor(
|
module.initialize_monitor(
|
||||||
|
@ -94,8 +30,10 @@ def test_initialize_monitor_creates_log_handler_with_default_ping_body_limit():
|
||||||
monitoring_log_level = 1
|
monitoring_log_level = 1
|
||||||
|
|
||||||
mock_logger()
|
mock_logger()
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler').with_args(
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
|
||||||
module.DEFAULT_PING_BODY_LIMIT_BYTES - len(module.PAYLOAD_TRUNCATION_INDICATOR),
|
module.HANDLER_IDENTIFIER,
|
||||||
|
module.DEFAULT_PING_BODY_LIMIT_BYTES
|
||||||
|
- len(module.borgmatic.hooks.logs.PAYLOAD_TRUNCATION_INDICATOR),
|
||||||
monitoring_log_level,
|
monitoring_log_level,
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
|
@ -107,8 +45,8 @@ def test_initialize_monitor_creates_log_handler_with_zero_ping_body_limit():
|
||||||
monitoring_log_level = 1
|
monitoring_log_level = 1
|
||||||
|
|
||||||
mock_logger()
|
mock_logger()
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler').with_args(
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').with_args(
|
||||||
ping_body_limit, monitoring_log_level
|
module.HANDLER_IDENTIFIER, ping_body_limit, monitoring_log_level
|
||||||
).once()
|
).once()
|
||||||
|
|
||||||
module.initialize_monitor(
|
module.initialize_monitor(
|
||||||
|
@ -118,7 +56,7 @@ def test_initialize_monitor_creates_log_handler_with_zero_ping_body_limit():
|
||||||
|
|
||||||
def test_initialize_monitor_creates_log_handler_when_send_logs_true():
|
def test_initialize_monitor_creates_log_handler_when_send_logs_true():
|
||||||
mock_logger()
|
mock_logger()
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler').once()
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').once()
|
||||||
|
|
||||||
module.initialize_monitor(
|
module.initialize_monitor(
|
||||||
{'send_logs': True}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
|
{'send_logs': True}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
|
||||||
|
@ -127,7 +65,7 @@ def test_initialize_monitor_creates_log_handler_when_send_logs_true():
|
||||||
|
|
||||||
def test_initialize_monitor_bails_when_send_logs_false():
|
def test_initialize_monitor_bails_when_send_logs_false():
|
||||||
mock_logger()
|
mock_logger()
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler').never()
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
|
||||||
|
|
||||||
module.initialize_monitor(
|
module.initialize_monitor(
|
||||||
{'send_logs': False}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
|
{'send_logs': False}, {}, 'test.yaml', monitoring_log_level=1, dry_run=False
|
||||||
|
@ -135,7 +73,7 @@ def test_initialize_monitor_bails_when_send_logs_false():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_hits_ping_url_for_start_state():
|
def test_ping_monitor_hits_ping_url_for_start_state():
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
|
||||||
hook_config = {'ping_url': 'https://example.com'}
|
hook_config = {'ping_url': 'https://example.com'}
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
||||||
|
@ -154,7 +92,10 @@ def test_ping_monitor_hits_ping_url_for_start_state():
|
||||||
def test_ping_monitor_hits_ping_url_for_finish_state():
|
def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||||
hook_config = {'ping_url': 'https://example.com'}
|
hook_config = {'ping_url': 'https://example.com'}
|
||||||
payload = 'data'
|
payload = 'data'
|
||||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return(payload)
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
'https://example.com', data=payload.encode('utf-8'), verify=True
|
'https://example.com', data=payload.encode('utf-8'), verify=True
|
||||||
).and_return(flexmock(ok=True))
|
).and_return(flexmock(ok=True))
|
||||||
|
@ -172,7 +113,10 @@ def test_ping_monitor_hits_ping_url_for_finish_state():
|
||||||
def test_ping_monitor_hits_ping_url_for_fail_state():
|
def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||||
hook_config = {'ping_url': 'https://example.com'}
|
hook_config = {'ping_url': 'https://example.com'}
|
||||||
payload = 'data'
|
payload = 'data'
|
||||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return(payload)
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
'https://example.com/fail', data=payload.encode('utf'), verify=True
|
'https://example.com/fail', data=payload.encode('utf'), verify=True
|
||||||
).and_return(flexmock(ok=True))
|
).and_return(flexmock(ok=True))
|
||||||
|
@ -190,7 +134,10 @@ def test_ping_monitor_hits_ping_url_for_fail_state():
|
||||||
def test_ping_monitor_hits_ping_url_for_log_state():
|
def test_ping_monitor_hits_ping_url_for_log_state():
|
||||||
hook_config = {'ping_url': 'https://example.com'}
|
hook_config = {'ping_url': 'https://example.com'}
|
||||||
payload = 'data'
|
payload = 'data'
|
||||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return(payload)
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
'https://example.com/log', data=payload.encode('utf'), verify=True
|
'https://example.com/log', data=payload.encode('utf'), verify=True
|
||||||
).and_return(flexmock(ok=True))
|
).and_return(flexmock(ok=True))
|
||||||
|
@ -208,7 +155,10 @@ def test_ping_monitor_hits_ping_url_for_log_state():
|
||||||
def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
|
def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
|
||||||
hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'}
|
hook_config = {'ping_url': 'abcd-efgh-ijkl-mnop'}
|
||||||
payload = 'data'
|
payload = 'data'
|
||||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return(payload)
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
f"https://hc-ping.com/{hook_config['ping_url']}",
|
f"https://hc-ping.com/{hook_config['ping_url']}",
|
||||||
data=payload.encode('utf-8'),
|
data=payload.encode('utf-8'),
|
||||||
|
@ -228,7 +178,10 @@ def test_ping_monitor_with_ping_uuid_hits_corresponding_url():
|
||||||
def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
|
def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
|
||||||
hook_config = {'ping_url': 'https://example.com', 'verify_tls': False}
|
hook_config = {'ping_url': 'https://example.com', 'verify_tls': False}
|
||||||
payload = 'data'
|
payload = 'data'
|
||||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return(payload)
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
'https://example.com', data=payload.encode('utf-8'), verify=False
|
'https://example.com', data=payload.encode('utf-8'), verify=False
|
||||||
).and_return(flexmock(ok=True))
|
).and_return(flexmock(ok=True))
|
||||||
|
@ -246,7 +199,10 @@ def test_ping_monitor_skips_ssl_verification_when_verify_tls_false():
|
||||||
def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
|
def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
|
||||||
hook_config = {'ping_url': 'https://example.com', 'verify_tls': True}
|
hook_config = {'ping_url': 'https://example.com', 'verify_tls': True}
|
||||||
payload = 'data'
|
payload = 'data'
|
||||||
flexmock(module).should_receive('format_buffered_logs_for_payload').and_return(payload)
|
flexmock(module.borgmatic.hooks.logs).should_receive('get_handler')
|
||||||
|
flexmock(module.borgmatic.hooks.logs).should_receive(
|
||||||
|
'format_buffered_logs_for_payload'
|
||||||
|
).and_return(payload)
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
'https://example.com', data=payload.encode('utf-8'), verify=True
|
'https://example.com', data=payload.encode('utf-8'), verify=True
|
||||||
).and_return(flexmock(ok=True))
|
).and_return(flexmock(ok=True))
|
||||||
|
@ -262,7 +218,7 @@ def test_ping_monitor_executes_ssl_verification_when_verify_tls_true():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
|
||||||
hook_config = {'ping_url': 'https://example.com'}
|
hook_config = {'ping_url': 'https://example.com'}
|
||||||
flexmock(module.requests).should_receive('post').never()
|
flexmock(module.requests).should_receive('post').never()
|
||||||
|
|
||||||
|
@ -277,7 +233,7 @@ def test_ping_monitor_dry_run_does_not_hit_ping_url():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
|
def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
|
||||||
hook_config = {'ping_url': 'https://example.com', 'states': ['finish']}
|
hook_config = {'ping_url': 'https://example.com', 'states': ['finish']}
|
||||||
flexmock(module.requests).should_receive('post').never()
|
flexmock(module.requests).should_receive('post').never()
|
||||||
|
|
||||||
|
@ -292,7 +248,7 @@ def test_ping_monitor_does_not_hit_ping_url_when_states_not_matching():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_hits_ping_url_when_states_matching():
|
def test_ping_monitor_hits_ping_url_when_states_matching():
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
|
||||||
hook_config = {'ping_url': 'https://example.com', 'states': ['start', 'finish']}
|
hook_config = {'ping_url': 'https://example.com', 'states': ['start', 'finish']}
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
||||||
|
@ -309,7 +265,7 @@ def test_ping_monitor_hits_ping_url_when_states_matching():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_with_connection_error_logs_warning():
|
def test_ping_monitor_with_connection_error_logs_warning():
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
|
||||||
hook_config = {'ping_url': 'https://example.com'}
|
hook_config = {'ping_url': 'https://example.com'}
|
||||||
flexmock(module.requests).should_receive('post').with_args(
|
flexmock(module.requests).should_receive('post').with_args(
|
||||||
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
'https://example.com/start', data=''.encode('utf-8'), verify=True
|
||||||
|
@ -327,7 +283,7 @@ def test_ping_monitor_with_connection_error_logs_warning():
|
||||||
|
|
||||||
|
|
||||||
def test_ping_monitor_with_other_error_logs_warning():
|
def test_ping_monitor_with_other_error_logs_warning():
|
||||||
flexmock(module).should_receive('Forgetful_buffering_handler')
|
flexmock(module.borgmatic.hooks.logs).should_receive('Forgetful_buffering_handler').never()
|
||||||
hook_config = {'ping_url': 'https://example.com'}
|
hook_config = {'ping_url': 'https://example.com'}
|
||||||
response = flexmock(ok=False)
|
response = flexmock(ok=False)
|
||||||
response.should_receive('raise_for_status').and_raise(
|
response.should_receive('raise_for_status').and_raise(
|
||||||
|
|
103
tests/unit/hooks/test_logs.py
Normal file
103
tests/unit/hooks/test_logs.py
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import pytest
|
||||||
|
from flexmock import flexmock
|
||||||
|
|
||||||
|
from borgmatic.hooks import logs as module
|
||||||
|
|
||||||
|
|
||||||
|
def test_forgetful_buffering_handler_emit_collects_log_records():
|
||||||
|
handler = module.Forgetful_buffering_handler(identifier='test', byte_capacity=100, log_level=1)
|
||||||
|
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
||||||
|
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
||||||
|
|
||||||
|
assert handler.buffer == ['foo\n', 'bar\n']
|
||||||
|
assert not handler.forgot
|
||||||
|
|
||||||
|
|
||||||
|
def test_forgetful_buffering_handler_emit_collects_log_records_with_zero_byte_capacity():
|
||||||
|
handler = module.Forgetful_buffering_handler(identifier='test', byte_capacity=0, log_level=1)
|
||||||
|
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
||||||
|
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
||||||
|
|
||||||
|
assert handler.buffer == ['foo\n', 'bar\n']
|
||||||
|
assert not handler.forgot
|
||||||
|
|
||||||
|
|
||||||
|
def test_forgetful_buffering_handler_emit_forgets_log_records_when_capacity_reached():
|
||||||
|
handler = module.Forgetful_buffering_handler(
|
||||||
|
identifier='test', byte_capacity=len('foo\nbar\n'), log_level=1
|
||||||
|
)
|
||||||
|
handler.emit(flexmock(getMessage=lambda: 'foo'))
|
||||||
|
assert handler.buffer == ['foo\n']
|
||||||
|
handler.emit(flexmock(getMessage=lambda: 'bar'))
|
||||||
|
assert handler.buffer == ['foo\n', 'bar\n']
|
||||||
|
handler.emit(flexmock(getMessage=lambda: 'baz'))
|
||||||
|
assert handler.buffer == ['bar\n', 'baz\n']
|
||||||
|
handler.emit(flexmock(getMessage=lambda: 'quux'))
|
||||||
|
assert handler.buffer == ['quux\n']
|
||||||
|
assert handler.forgot
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_handler_matches_by_identifier():
|
||||||
|
handlers = [
|
||||||
|
flexmock(),
|
||||||
|
flexmock(),
|
||||||
|
module.Forgetful_buffering_handler(identifier='other', byte_capacity=100, log_level=1),
|
||||||
|
module.Forgetful_buffering_handler(identifier='test', byte_capacity=100, log_level=1),
|
||||||
|
flexmock(),
|
||||||
|
]
|
||||||
|
flexmock(module.logging.getLogger(), handlers=handlers)
|
||||||
|
|
||||||
|
assert module.get_handler('test') == handlers[3]
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_handler_without_match_raises():
|
||||||
|
handlers = [
|
||||||
|
flexmock(),
|
||||||
|
module.Forgetful_buffering_handler(identifier='other', byte_capacity=100, log_level=1),
|
||||||
|
]
|
||||||
|
flexmock(module.logging.getLogger(), handlers=handlers)
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
assert module.get_handler('test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_buffered_logs_for_payload_flattens_log_buffer():
|
||||||
|
handler = module.Forgetful_buffering_handler(identifier='test', byte_capacity=100, log_level=1)
|
||||||
|
handler.buffer = ['foo\n', 'bar\n']
|
||||||
|
flexmock(module).should_receive('get_handler').and_return(handler)
|
||||||
|
|
||||||
|
payload = module.format_buffered_logs_for_payload(identifier='test')
|
||||||
|
|
||||||
|
assert payload == 'foo\nbar\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_buffered_logs_for_payload_inserts_truncation_indicator_when_logs_forgotten():
|
||||||
|
handler = module.Forgetful_buffering_handler(identifier='test', byte_capacity=100, log_level=1)
|
||||||
|
handler.buffer = ['foo\n', 'bar\n']
|
||||||
|
handler.forgot = True
|
||||||
|
flexmock(module).should_receive('get_handler').and_return(handler)
|
||||||
|
|
||||||
|
payload = module.format_buffered_logs_for_payload(identifier='test')
|
||||||
|
|
||||||
|
assert payload == '...\nfoo\nbar\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_buffered_logs_for_payload_without_handler_produces_empty_payload():
|
||||||
|
flexmock(module).should_receive('get_handler').and_raise(ValueError)
|
||||||
|
|
||||||
|
payload = module.format_buffered_logs_for_payload(identifier='test')
|
||||||
|
|
||||||
|
assert payload == ''
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_handler_with_matching_handler_does_not_raise():
|
||||||
|
flexmock(module).should_receive('get_handler').and_return(flexmock())
|
||||||
|
flexmock(module.logging.getLogger()).should_receive('removeHandler')
|
||||||
|
|
||||||
|
module.remove_handler('test')
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_handler_without_matching_handler_does_not_raise():
|
||||||
|
flexmock(module).should_receive('get_handler').and_raise(ValueError)
|
||||||
|
|
||||||
|
module.remove_handler('test')
|
Loading…
Reference in a new issue