Compare commits

...

34 commits

Author SHA1 Message Date
229c093365 add new line 2024-04-12 11:41:05 +02:00
95dca96cab different bugs fixed 2023-10-14 14:46:19 +02:00
199a1d0562 add field-seperator for use multiple repositorys 2023-10-11 22:58:45 +02:00
caed3f9696 add docker-compose file 2023-10-11 07:18:58 +02:00
b7a705351e add dockerfile 2023-10-10 21:30:19 +02:00
adrianp
e5f2ee0e30 For system where there is no datediff, use the alternate name 2022-03-31 14:37:38 +03:00
Adrian Popa
2ea4238a84
Corrected some more legacy documentation 2022-03-31 10:57:11 +03:00
Adrian Popa
fff862e212
Improve install instructions 2022-03-31 10:50:09 +03:00
Adrian Popa
9d2eeb3ed3 Run at 08:00, giving today's backups time to finish 2021-05-31 16:40:57 +03:00
Adrian Popa
8d2d94db1f Update readme with config examples 2021-05-31 16:30:19 +03:00
Adrian Popa
03590e23b1 Add option to find and iterate through a list of repos, re-add support for textfile exporter, export data for today's backups 2021-05-31 16:13:01 +03:00
Adrian Popa
00da3fdeb6 Add option to find and iterate through a list of repos, re-add support for textfile exporter, export data for today's backups 2021-05-31 16:11:40 +03:00
Adrian Popa
deff8d3b46 Add local grafana dashboard 2021-05-31 16:10:05 +03:00
Clément Michaud
db18aaaec0 Pushgateway need to consume labels in the endpoint 2021-05-18 00:13:59 +02:00
Clément Michaud
4c47761f93 Disable borg extract which might take a lot of time in some cases 2021-05-18 00:11:11 +02:00
Clément Michaud
307091b470 Support bytes in calc bytes function. 2021-05-17 23:20:55 +02:00
Clément Michaud
7732427483 Fix permissions of file containing password 2021-05-17 23:01:10 +02:00
Clément Michaud
f86c927fc3 Push metrics to pushgateway 2021-05-17 22:51:42 +02:00
Philippe Ivaldi
9584fcb9e8
Merge pull request #2 from olegstepura/patch-1
typo 😉
2020-10-24 23:44:30 +02:00
Oleg Stepura
6f154e026e
typo 😉 2020-10-23 21:09:18 +02:00
Philippe Ivaldi
13bdb53fb5
Merge pull request #1 from Grounz/master
PR - Change Last Archives Check
2020-02-03 16:57:32 +01:00
Grounz
602613d25d
Merge pull request #1 from Grounz/Grounz-patch-listLastArchives
feat(borg_exporter): add borg last archive
2020-02-03 11:47:44 +01:00
Grounz
52126ed8a8
feat(borg_exporter): add borg last archive
with native borg command (list --last 1)
2020-02-03 11:46:23 +01:00
pivaldi
c12741435e Rename borg_hours_from_last_backup to borg_hours_from_last_archive + add metric borg_last_archive_timestamp 2018-09-11 14:36:40 +02:00
pivaldi
dc1d6f1cb4 Make service type simple instead of oneshot 2018-09-06 17:28:30 +02:00
pivaldi
a1c9e1f555 Fix mv command on the exporter 2018-09-06 15:46:09 +02:00
pivaldi
86479b3354 Fix makefile to not override an existing config 2018-09-06 15:40:33 +02:00
pivaldi
7446a08852 Update grafana dashboard URL 2018-09-06 14:53:39 +02:00
pivaldi
d6f1d3b509 Fix metrics' name 2018-09-06 11:46:15 +02:00
pivaldi
f9c809d0e4 Fix makefile 2018-09-06 10:52:42 +02:00
pivaldi
c34ac95b4c Improve doc 2018-09-06 10:31:45 +02:00
pivaldi
ea36dc32bb Best file naming. Improve code. Add makefile. Add metrics borg_extract_exit_code and bork_hours_from_last_backup. Update doc 2018-09-05 17:00:25 +02:00
Timo Derstappen
afa87e9dfb
add apache 2.0 license 2018-03-08 15:58:03 +01:00
Timo Derstappen
b8f5167a0c
version: bump to 0.1.1+git 2017-05-22 10:07:12 +02:00
13 changed files with 2136 additions and 84 deletions

23
Dockerfile Normal file
View file

@ -0,0 +1,23 @@
FROM debian:12
RUN apt-get update && \
apt-get install -y dateutils binutils borgbackup openssh-client && \
apt-get clean
COPY borg_exporter.rc borg_exporter.sh /
# Authorize SSH Host
RUN mkdir -p /root/.ssh && \
chmod 0700 /root/.ssh
# See: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/githubs-ssh-key-fingerprints
COPY known_hosts /root/.ssh/known_hosts
# Add the Keys
COPY id_rsa id_rsa.pub /root/.ssh/
# Set permissions
RUN chmod 600 /root/.ssh/id_rsa && \
chmod 600 /root/.ssh/id_rsa.pub
CMD ["/borg_exporter.sh"]

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

11
Makefile Normal file
View file

@ -0,0 +1,11 @@
.PHONY: install
install:
@cp borg_exporter.sh /usr/local/bin/ \
&& chmod +x /usr/local/bin/borg_exporter.sh \
&& cp -n borg_exporter.rc /etc/borg_exporter.rc \
&& chmod 400 /etc/borg_exporter.rc \
&& cp prometheus-borg-exporter.timer /etc/systemd/system/ \
&& cp prometheus-borg-exporter.service /etc/systemd/system/ \
&& echo -n "Edit the config file /etc/borg_exporter.rc and press [ENTER] when finished "; read _ \
&& systemctl enable prometheus-borg-exporter.timer \
&& systemctl start prometheus-borg-exporter.timer

121
README.md
View file

@ -1,40 +1,129 @@
# Borg exporter
Export borg information to prometheus.
Export borg information to prometheus. Extended to export information about a list of borg repositories (discovered via `find`), and also to export details about today's backups.
## Dependencies
* Prometheus (obviously)
* [Dateutils](http://www.fresse.org/dateutils/) `sudo apt-get install dateutils`
* Node Exporter with textfile collector
* Borg (https://github.com/borgbackup/borg)
* [Borg](https://github.com/borgbackup/borg)
* binutils (sed, grep, wc, etc)
## Install
Copy `borg_exporter` to `/usr/local/bin`.
### Manually
Copy `borg_exporter.sh` to `/usr/local/bin`.
Copy `borg.env` to `/etc/borg` and replace your repokey and repository in it.
Copy `borg_exporter.rc` to `/etc/` and configure it (see the configuration section below).
Copy the systemd unit to `/etc/systemd/system` and run
Copy the systemd unit and timer to `/etc/systemd/system`:
```
sudo cp prometheus-borg-exporter.* /etc/systemd/system
```
and run
```
systemctl enable prometheus-borg-exporter.timer
systemctl start prometheus-borg-exporter.timer
sudo systemctl enable prometheus-borg-exporter.timer
sudo systemctl start prometheus-borg-exporter.timer
```
Alternative: Use `ExecStartPost` in your borg backupt timer itself to write our the metrics.
Alternative: Use `ExecStartPost` in your borg backup timer itself to write our the metrics.
## Configure your node exporter
### Config file
The config file has a few options:
```
BORG_PASSPHRASE="mysecret"
REPOSITORY="/path/to/repository"
PUSHGATEWAY_URL=http://pushgateway.clems4ever.com
BASEREPODIR="/backup"
NODE_EXPORTER_DIR="/path/to/node/exporter/textfile/collector/dir"
```
Make sure your node exporter uses `textfile` in `--collectors.enabled` and add the following parameter: `--collector.textfile.directory=/var/lib/node_exporter/textfile_collector`
* If you leave `BORG_PASSPHRASE=""` empty, no password will be used to access your backups
* `REPOSITORY` should either point to a valid repository (if you're running this on each server you are backing-up) or should be left empty in case you set `BASEREPODIR`
* `PUSHGATEWAY_URL` should be a valid URL for push gateway. If you're not using it, leave it blank (`PUSHGATEWAY_URL=""`) and data will be exported via node_exporter textfile collector
* `BASEREPODIR` should point to a directory on disk from where you want to search for all the repos. This makes sense when you run this exporter on the backup server, so you can access all the backups in one place. It's only taken into consideration when `REPOSITORY=""`
* `NODE_EXPORTER_DIR` should point to your node_exporter textfile collector directory (where it writes .prom files). It's used only if `PUSHGATEWAY_URL=""`
## Example queries
### Caveats
* The repository names shouldn't contain spaces
* The archive names shouldn't contain spaces
* The hostnames from the machines that do the export shouldn't contain spaces
### Troubleshooting
You can manually run the script with `bash -x` to get the output of intermediary commands
## Exported metrics example
```
backup_total_size_dedup{job='node'}
backup_last_size_dedup{job='node'}
backup_chunks_total{job='node'}
# HELP borg_archives_count The total number of archives in the repo
# TYPE borg_archives_count gauge
borg_archives_count{backupserver="my_backup_server",host="server1",repo="/backup/server1/server1"} 3
borg_archives_count{backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 29
# HELP borg_archives_count_today The total number of archives created today in the repo
# TYPE borg_archives_count_today gauge
borg_archives_count_today{backupserver="my_backup_server",host="server1",repo="/backup/server1/server1"} 0
borg_archives_count_today{backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 4
# HELP borg_chunks_total The total number of chunks in the archive (today)
# TYPE borg_chunks_total gauge
borg_chunks_total{archive="_etc",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 11829
borg_chunks_total{archive="_home_user_scripts",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 11829
borg_chunks_total{archive="_usr_share_cacti",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 11829
borg_chunks_total{archive="mysqldump",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 11829
# HELP borg_chunks_unique The number of unique chunks in the archive (today)
# TYPE borg_chunks_unique gauge
borg_chunks_unique{archive="_etc",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 2076
borg_chunks_unique{archive="_home_user_scripts",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 2076
borg_chunks_unique{archive="_usr_share_cacti",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 2076
borg_chunks_unique{archive="_var_spool_cron",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 2076
borg_chunks_unique{archive="mysqldump",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 2076
# HELP borg_files_count The number of files contained in the archive (today)
# TYPE borg_files_count gauge
borg_files_count{archive="_etc",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 1030
borg_files_count{archive="_home_user_scripts",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 36
borg_files_count{archive="_usr_share_cacti",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 593
borg_files_count{archive="_var_spool_cron",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 1
borg_files_count{archive="mysqldump",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 1
# HELP borg_hours_from_last_archive How many hours have passed since the last archive was added to the repo (counted by borg_exporter.sh)
# TYPE borg_hours_from_last_archive gauge
borg_hours_from_last_archive{backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 10
# HELP borg_last_archive_timestamp The timestamp of the last archive (unixtimestamp)
# TYPE borg_last_archive_timestamp gauge
borg_last_archive_timestamp{backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 1.622421272e+09
# HELP borg_last_size The size of the archive (today)
# TYPE borg_last_size gauge
borg_last_size{archive="_etc",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 2.43479e+07
borg_last_size{archive="_home_user_scripts",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 146749
borg_last_size{archive="_usr_share_cacti",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 1.09157e+07
borg_last_size{archive="_var_spool_cron",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 1177.6
borg_last_size{archive="mysqldump",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 8.32286e+08
# HELP borg_last_size_compressed The compressed size of the archive (today)
# TYPE borg_last_size_compressed gauge
borg_last_size_compressed{archive="_etc",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 8.40958e+06
borg_last_size_compressed{archive="_home_user_scripts",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 44769.3
borg_last_size_compressed{archive="_usr_share_cacti",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 5.05414e+06
borg_last_size_compressed{archive="_var_spool_cron",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 445
borg_last_size_compressed{archive="mysqldump",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 5.55326e+07
# HELP borg_last_size_dedup The deduplicated size of the archive (today), (size on disk)
# TYPE borg_last_size_dedup gauge
borg_last_size_dedup{archive="_etc",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 608
borg_last_size_dedup{archive="_home_user_scripts",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 558
borg_last_size_dedup{archive="_usr_share_cacti",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 536
borg_last_size_dedup{archive="_var_spool_cron",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 548
borg_last_size_dedup{archive="mysqldump",backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 9.81467e+06
# HELP borg_total_size The total size of all archives in the repo
# TYPE borg_total_size gauge
borg_total_size{backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 5.35797e+09
# HELP borg_total_size_compressed The total compressed size of all archives in the repo
# TYPE borg_total_size_compressed gauge
borg_total_size_compressed{backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 4.17637e+08
# HELP borg_total_size_dedup The total deduplicated size of all archives in the repo (size on disk)
# TYPE borg_total_size_dedup gauge
borg_total_size_dedup{backupserver="my_backup_server",host="server2",repo="/backup/server2/server2"} 1.14284e+08
```
### Grafana dashboard
See [here](https://grafana.net/dashboards/1573) for a sample grafana dashboard.
See [here](https://grafana.com/dashboards/14516) for a sample grafana dashboard.
The original dashboard code is also available as `borg_backup_status_dashboard.json` in the repo.

View file

@ -1 +1 @@
0.1.0+git
0.2

View file

@ -1,2 +0,0 @@
BORG_PASSPHRASE=<your-passphrase>
REPOSITORY=<your-repository>

File diff suppressed because it is too large Load diff

View file

@ -1,62 +0,0 @@
#!/bin/bash
set -eu
source /etc/borg
TEXTFILE_COLLECTOR_DIR=/var/lib/node_exporter/textfile_collector
PROM_FILE=$TEXTFILE_COLLECTOR_DIR/backup.prom
TMP_FILE=$PROM_FILE.$$
HOSTNAME=$(hostname)
LIST=$(BORG_PASSPHRASE=$BORG_PASSPHRASE borg list $REPOSITORY |awk '{print $1}')
COUNTER=0
mkdir -p $TEXTFILE_COLLECTOR_DIR
for i in $LIST; do
COUNTER=$((COUNTER+1))
done
BORG_INFO=$(BORG_PASSPHRASE=$BORG_PASSPHRASE borg info "$REPOSITORY::$i")
echo "backup_count{host=\"${HOSTNAME}\"} $COUNTER" > $TMP_FILE
echo "backup_files{host=\"${HOSTNAME}\"} $(echo "$BORG_INFO" | grep "Number of files" | awk '{print $4}')" >> $TMP_FILE
echo "backup_chunks_unique{host=\"${HOSTNAME}\"} $(echo "$BORG_INFO" | grep "Chunk index" | awk '{print $3}')" >> $TMP_FILE
echo "backup_chunks_total{host=\"${HOSTNAME}\"} $(echo "$BORG_INFO" | grep "Chunk index" | awk '{print $4}')" >> $TMP_FILE
function calc_bytes {
NUM=$1
UNIT=$2
case "$UNIT" in
kB)
echo $NUM | awk '{ print $1 * 1024 }'
;;
MB)
echo $NUM | awk '{ print $1 * 1024 * 1024 }'
;;
GB)
echo $NUM | awk '{ print $1 * 1024 * 1024 * 1024 }'
;;
TB)
echo $NUM | awk '{ print $1 * 1024 * 1024 * 1024 * 1024 }'
;;
esac
}
# byte size
LAST_SIZE=$(calc_bytes $(echo "$BORG_INFO" |grep "This archive" |awk '{print $3}') $(echo "$BORG_INFO" |grep "This archive" |awk '{print $4}'))
LAST_SIZE_COMPRESSED=$(calc_bytes $(echo "$BORG_INFO" |grep "This archive" |awk '{print $5}') $(echo "$BORG_INFO" |grep "This archive" |awk '{print $6}'))
LAST_SIZE_DEDUP=$(calc_bytes $(echo "$BORG_INFO" |grep "This archive" |awk '{print $7}') $(echo "$BORG_INFO" |grep "This archive" |awk '{print $8}'))
TOTAL_SIZE=$(calc_bytes $(echo "$BORG_INFO" |grep "All archives" |awk '{print $3}') $(echo "$BORG_INFO" |grep "All archives" |awk '{print $4}'))
TOTAL_SIZE_COMPRESSED=$(calc_bytes $(echo "$BORG_INFO" |grep "All archives" |awk '{print $5}') $(echo "$BORG_INFO" |grep "All archives" |awk '{print $6}'))
TOTAL_SIZE_DEDUP=$(calc_bytes $(echo "$BORG_INFO" |grep "All archives" |awk '{print $7}') $(echo "$BORG_INFO" |grep "All archives" |awk '{print $8}'))
echo "backup_last_size{host=\"${HOSTNAME}\"} $LAST_SIZE" >> $TMP_FILE
echo "backup_last_size_compressed{host=\"${HOSTNAME}\"} $LAST_SIZE_COMPRESSED" >> $TMP_FILE
echo "backup_last_size_dedup{host=\"${HOSTNAME}\"} $LAST_SIZE_DEDUP" >> $TMP_FILE
echo "backup_total_size{host=\"${HOSTNAME}\"} $TOTAL_SIZE" >> $TMP_FILE
echo "backup_total_size_compressed{host=\"${HOSTNAME}\"} $TOTAL_SIZE_COMPRESSED" >> $TMP_FILE
echo "backup_total_size_dedup{host=\"${HOSTNAME}\"} $TOTAL_SIZE_DEDUP" >> $TMP_FILE
mv $TMP_FILE $PROM_FILE

5
borg_exporter.rc Normal file
View file

@ -0,0 +1,5 @@
BORG_PASSPHRASE=""
REPOSITORY=""
PUSHGATEWAY_URL=http://pushgateway.clems4ever.com
BASEREPODIR="/backup"
NODE_EXPORTER_DIR="/path/to/node/exporter/textfile/collector/dir"

213
borg_exporter.sh Executable file
View file

@ -0,0 +1,213 @@
#!/bin/bash
while true
do
source /borg_exporter.rc
#sleep 30
TMP_FILE=$(mktemp /tmp/prometheus-borg-XXXXX)
DATEDIFF=`which datediff`
if [ -z "$DATEDIFF" ]; then
#ubuntu packages have a different executable name
DATEDIFF=`which dateutils.ddiff`
fi
[ -e $TMP_FILE ] && rm -f $TMP_FILE
#prevent "Attempting to access a previously unknown unencrypted repository" prompt
export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes
export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes
HOSTNAME=$(hostname)
function calc_bytes {
NUM=$1
UNIT=$2
case "$UNIT" in
kB)
echo $NUM | awk '{ print $1 * 1024 }'
;;
MB)
echo $NUM | awk '{ print $1 * 1024 * 1024 }'
;;
GB)
echo $NUM | awk '{ print $1 * 1024 * 1024 * 1024 }'
;;
TB)
echo $NUM | awk '{ print $1 * 1024 * 1024 * 1024 * 1024 }'
;;
B)
echo $NUM | awk '{ print $1 }'
;;
esac
}
function getBorgDataForRepository {
REPOSITORY=$1 #repository we're looking into
host=$2 #the host for which the backups are made
ARCHIVES="$(BORG_PASSPHRASE=$BORG_PASSPHRASE borg list $REPOSITORY)"
COUNTER=0
BACKUPS_TODAY_COUNT=0
COUNTER=$(echo "$ARCHIVES" | wc -l)
TODAY=$(date +%Y-%m-%d)
BACKUPS_TODAY=$(echo "$ARCHIVES" | grep ", $TODAY ")
BACKUPS_TODAY_COUNT=$(echo -n "$BACKUPS_TODAY" | wc -l)
#extract data for last archive
LAST_ARCHIVE=$(BORG_PASSPHRASE=$BORG_PASSPHRASE borg list --last 1 $REPOSITORY)
#we need at least one valid backup to list anything meaningfull
if [ -n "${LAST_ARCHIVE}" ]
then
LAST_ARCHIVE_NAME=$(echo $LAST_ARCHIVE | awk '{print $1}')
LAST_ARCHIVE_DATE=$(echo $LAST_ARCHIVE | awk '{print $3" "$4}')
LAST_ARCHIVE_TIMESTAMP=$(date -d "$LAST_ARCHIVE_DATE" +"%s")
CURRENT_DATE="$(date '+%Y-%m-%d %H:%M:%S')"
NB_HOUR_FROM_LAST_BCK=$($DATEDIFF "$LAST_ARCHIVE_DATE" "$CURRENT_DATE" -f '%H')
# in case the date parsing from BORG didn't work (e.g. archive with space in it), datediff will output
# a usage message on stdout and will break prometheus formatting. We need to
# check for that here
DATEDIFF_LINES=$(echo "$NB_HOUR_FROM_LAST_BCK" | wc -l)
if [ "${DATEDIFF_LINES}" -eq 1 ]
then
echo "borg_hours_from_last_archive{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\"} $NB_HOUR_FROM_LAST_BCK" >> $TMP_FILE
BORG_INFO=$(BORG_PASSPHRASE="$BORG_PASSPHRASE" borg info "$REPOSITORY::$LAST_ARCHIVE_NAME")
echo "borg_last_archive_timestamp{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\"} $LAST_ARCHIVE_TIMESTAMP" >> $TMP_FILE
TOTAL_SIZE=$(calc_bytes $(echo "$BORG_INFO" |grep "All archives" |awk '{print $3}') $(echo "$BORG_INFO" |grep "All archives" |awk '{print $4}'))
TOTAL_SIZE_COMPRESSED=$(calc_bytes $(echo "$BORG_INFO" |grep "All archives" |awk '{print $5}') $(echo "$BORG_INFO" |grep "All archives" |awk '{print $6}'))
TOTAL_SIZE_DEDUP=$(calc_bytes $(echo "$BORG_INFO" |grep "All archives" |awk '{print $7}') $(echo "$BORG_INFO" |grep "All archives" |awk '{print $8}'))
echo "borg_total_size{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\"} $TOTAL_SIZE" >> $TMP_FILE
echo "borg_total_size_compressed{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\"} $TOTAL_SIZE_COMPRESSED" >> $TMP_FILE
echo "borg_total_size_dedup{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\"} $TOTAL_SIZE_DEDUP" >> $TMP_FILE
fi
echo "borg_archives_count{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\"} $COUNTER" >> $TMP_FILE
echo "borg_archives_count_today{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\"} $BACKUPS_TODAY_COUNT" >> $TMP_FILE
#go through the day's archives and count the files/chunks/etc.
TODAY_ARCHIVES=$(echo -n "$BACKUPS_TODAY" | awk '{print $1}' | xargs echo )
#echo $TODAY_ARCHIVES
if [ -n "${TODAY_ARCHIVES}" ]
then
for archive in $TODAY_ARCHIVES
do
echo "Looking at $REPOSITORY::$archive"
#ask for an info on it
CURRENT_INFO=$(BORG_PASSPHRASE="$BORG_PASSPHRASE" borg info "$REPOSITORY::$archive")
#cut out something that looks like a timestamp when reporting: 20210528-1315
readable_archive=$(echo $archive | sed -r "s/-[0-9]{8}-[0-9]{4,6}//")
echo "borg_files_count{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\", archive=\"$readable_archive\"} $(echo "$CURRENT_INFO" | grep "Number of files" | awk '{print $4}')" >> $TMP_FILE
echo "borg_chunks_unique{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\", archive=\"$readable_archive\"} $(echo "$CURRENT_INFO" | grep "Chunk index" | awk '{print $3}')" >> $TMP_FILE
echo "borg_chunks_total{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\", archive=\"$readable_archive\"} $(echo "$CURRENT_INFO" | grep "Chunk index" | awk '{print $4}')" >> $TMP_FILE
# byte size
LAST_SIZE=$(calc_bytes $(echo "$CURRENT_INFO" |grep "This archive" |awk '{print $3}') $(echo "$CURRENT_INFO" |grep "This archive" |awk '{print $4}'))
LAST_SIZE_COMPRESSED=$(calc_bytes $(echo "$CURRENT_INFO" |grep "This archive" |awk '{print $5}') $(echo "$CURRENT_INFO" |grep "This archive" |awk '{print $6}'))
LAST_SIZE_DEDUP=$(calc_bytes $(echo "$CURRENT_INFO" |grep "This archive" |awk '{print $7}') $(echo "$CURRENT_INFO" |grep "This archive" |awk '{print $8}'))
echo "borg_last_size{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\", archive=\"$readable_archive\"} $LAST_SIZE" >> $TMP_FILE
echo "borg_last_size_compressed{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\", archive=\"$readable_archive\"} $LAST_SIZE_COMPRESSED" >> $TMP_FILE
echo "borg_last_size_dedup{host=\"$host\", backupserver=\"$HOSTNAME\", repo=\"$REPOSITORY\", archive=\"$readable_archive\"} $LAST_SIZE_DEDUP" >> $TMP_FILE
done
else
echo "Unable to find any archives for today in $REPOSITORY."
fi
else
echo "Unable to find any archives in $REPOSITORY. Processing skipped for it"
fi
}
#print the definition of the metrics
echo "# HELP borg_hours_from_last_archive How many hours have passed since the last archive was added to the repo (counted by borg_exporter.sh)" >> $TMP_FILE
echo "# TYPE borg_hours_from_last_archive gauge" >> $TMP_FILE
echo "# HELP borg_last_archive_timestamp The timestamp of the last archive (unixtimestamp)" >> $TMP_FILE
echo "# TYPE borg_last_archive_timestamp gauge" >> $TMP_FILE
echo "# HELP borg_total_size The total size of all archives in the repo" >> $TMP_FILE
echo "# TYPE borg_total_size gauge" >> $TMP_FILE
echo "# HELP borg_total_size_compressed The total compressed size of all archives in the repo" >> $TMP_FILE
echo "# TYPE borg_total_size_compressed gauge" >> $TMP_FILE
echo "# HELP borg_total_size_dedup The total deduplicated size of all archives in the repo (size on disk)" >> $TMP_FILE
echo "# TYPE borg_total_size_dedup gauge" >> $TMP_FILE
echo "# HELP borg_archives_count The total number of archives in the repo" >> $TMP_FILE
echo "# TYPE borg_archives_count gauge" >> $TMP_FILE
echo "# HELP borg_archives_count_today The total number of archives created today in the repo" >> $TMP_FILE
echo "# TYPE borg_archives_count_today gauge" >> $TMP_FILE
echo "# HELP borg_files_count The number of files contained in the archive (today)" >> $TMP_FILE
echo "# TYPE borg_files_count gauge" >> $TMP_FILE
echo "# HELP borg_chunks_unique The number of unique chunks in the archive (today)" >> $TMP_FILE
echo "# TYPE borg_chunks_unique gauge" >> $TMP_FILE
echo "# HELP borg_chunks_total The total number of chunks in the archive (today)" >> $TMP_FILE
echo "# TYPE borg_chunks_total gauge" >> $TMP_FILE
echo "# HELP borg_last_size The size of the archive (today)" >> $TMP_FILE
echo "# TYPE borg_last_size gauge" >> $TMP_FILE
echo "# HELP borg_last_size_compressed The compressed size of the archive (today)" >> $TMP_FILE
echo "# TYPE borg_last_size_compressed gauge" >> $TMP_FILE
echo "# HELP borg_last_size_dedup The deduplicated size of the archive (today), (size on disk)" >> $TMP_FILE
echo "# TYPE borg_last_size_dedup gauge" >> $TMP_FILE
if [ -n "${REPOSITORY}" ]
then
for i in $(echo $REPOSITORY | tr ";" "\n")
do
echo "Use Repository: $i"
getBorgDataForRepository "${i}" "${HOSTNAME}"
done
# Clear Cache (https://borgbackup.readthedocs.io/en/stable/faq.html#the-borg-cache-eats-way-too-much-disk-space-what-can-i-do)
#find /root/.cache/borg -type d -name 'chunks.archive.d' -exec rm -rv {}/ \; -exec sh -c 'cd {}/.. && touch chunks.archive.d' \;
else
#discover (recursively) borg repositories starting from a path and extract info for each
#(e.g. when running on the backup server directly)
if [ -d "${BASEREPODIR}" ]
then
REPOS=`find "$BASEREPODIR" -type f -name "README" | grep -v ".cache/borg"`
# e.g. /backup/servers/server_name/README
for REPO in $REPOS
do
#cut out the /README from the name
REPO=$(echo "$REPO" | sed -r "s/\/README//")
#assume the name convention for the repo contains the hostname as the repo name
# e.g. /backup/servers/server_name
host=$(basename "$REPO")
getBorgDataForRepository $REPO $host
done
else
echo "Error: Either set REPOSITORY or BASEREPODIR in /borg_exporter.rc"
fi
fi
if [ -n "${PUSHGATEWAY_URL}" ]
then
#send data via pushgateway
cat $TMP_FILE | curl --data-binary @- ${PUSHGATEWAY_URL}/metrics/job/borg-exporter/host/$HOSTNAME/repository/$REPOSITORY
else
#send data via node_exporter
if [ -d "${NODE_EXPORTER_DIR}" ]
then
cp $TMP_FILE ${NODE_EXPORTER_DIR}/borg_exporter.prom
else
echo "Please configure either PUSHGATEWAY_URL or NODE_EXPORTER_DIR in /etc/borg_exporter.rc"
fi
fi
#cleanup
rm -f $TMP_FILE
# Wait 10 minutes
echo "sleep 100 minutes"
sleep 6000
done

13
docker-compose.yml Normal file
View file

@ -0,0 +1,13 @@
version: "3.8"
services:
borg-exporter:
build: builds/prometheus-borg-exporter/.
container_name: borg-exporter
volumes:
- ./node_exporter/textfile_collector/:/node_exporter/textfile_collector
restart: always
networks:
default:

View file

@ -3,5 +3,6 @@ Description=Prometheus Borg Exporter
After=network-online.target
[Service]
ExecStart=/usr/local/bin/borg_exporter
Type=oneshot
ExecStart=/usr/local/bin/borg_exporter.sh
EnvironmentFile=/etc/borg_exporter.rc
Type=simple

View file

@ -2,7 +2,7 @@
Description=Prometheus Borg Exporter Timer
[Timer]
OnCalendar=daily
OnCalendar=*-*-* 08:00:00
[Install]
WantedBy=timers.target