157 lines
6.4 KiB
Python
157 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
# -*- encoding: utf-8 -*-
|
|
#
|
|
# This script is licensed under GNU GPL version 2.0 or above
|
|
# (c) 2022 Antonio J. Delgado
|
|
# Export to node exporter the summary of a restic backup
|
|
|
|
import sys
|
|
import os
|
|
import logging
|
|
import json
|
|
import click
|
|
import click_config_file
|
|
from logging.handlers import SysLogHandler
|
|
|
|
class restic_exporter:
|
|
|
|
def __init__(self, debug_level, log_file, json_file, job_name, extra_labels, metric_name, metric_description):
|
|
''' Initial function called when object is created '''
|
|
self.config = dict()
|
|
self.config['debug_level'] = debug_level
|
|
if log_file is None:
|
|
log_file = os.path.join(os.environ.get('HOME', os.environ.get('USERPROFILE', os.getcwd())), 'log', 'restic_exporter.log')
|
|
self.config['log_file'] = log_file
|
|
self._init_log()
|
|
|
|
self.metric_name = metric_name.replace(' ', '_')
|
|
self.job_name = job_name.replace(' ', '_')
|
|
self.metric_description = metric_description
|
|
self.labels= {"job_name": job_name}
|
|
|
|
if extra_labels:
|
|
self.labels = {
|
|
**self.labels,
|
|
**self._read_extra_labels(extra_labels)
|
|
}
|
|
|
|
self._read_summary_from_json(json_file)
|
|
|
|
self._show_metrics()
|
|
|
|
def _show_metrics(self):
|
|
labels = self._convert_labels(self.labels)
|
|
counters = [
|
|
'files_new',
|
|
'files_changed',
|
|
'files_unmodified',
|
|
'dirs_new',
|
|
'dirs_changed',
|
|
'dirs_unmodified',
|
|
'data_blobs',
|
|
'tree_blobs',
|
|
'data_added',
|
|
'total_files_processed',
|
|
'total_bytes_processed',
|
|
'total_duration'
|
|
]
|
|
print(f"# HELP {self.metric_name}_{self.job_name} {self.metric_description}.")
|
|
print(f"# TYPE {self.metric_name}_{self.job_name} counter")
|
|
for counter in counters:
|
|
if counter in self.summary:
|
|
print(f"{self.metric_name}_{counter}{labels} {float(self.summary[counter])}") # {self.summary['timestamp']}")
|
|
|
|
def _read_summary_from_json(self, json_file):
|
|
try:
|
|
with open(json_file, 'r') as file_pointer:
|
|
content = file_pointer.readlines()
|
|
except Exception as error:
|
|
self._log.error(f"# Error reading file '{json_file}'. Check permissions. {error}")
|
|
|
|
self.summary = {}
|
|
for line in content:
|
|
try:
|
|
line_data = json.loads(line)
|
|
if 'message_type' in line_data:
|
|
if line_data['message_type'] == 'summary':
|
|
self.summary = line_data
|
|
if 'snapshot_id' in line_data:
|
|
self.labels["snapshot_id"] = line_data['snapshot_id']
|
|
except json.decoder.JSONDecodeError as error:
|
|
fixed_line = line.replace('\n', '')
|
|
self._log.error(f"# Error decoding line '{fixed_line}'. {error}")
|
|
file_stats = os.stat(json_file)
|
|
self.summary['timestamp'] = round(file_stats.st_mtime * 1000)
|
|
self._log.debug(f"# Summary: {json.dumps(self.summary, indent=2)}")
|
|
self._log.debug(f"# Labels: {self.labels}")
|
|
|
|
def _read_extra_labels(self, extra_labels):
|
|
labels_ls = {}
|
|
for pair in extra_labels.split(','):
|
|
if '=' in pair:
|
|
k, v=pair.split('=', 1)
|
|
labels_ls[k] = v
|
|
self._log.debug(f"# Added extra label '{k}'='{v}'")
|
|
return labels_ls
|
|
|
|
def _convert_labels(self, labels):
|
|
labels_ls = []
|
|
for key in labels.keys():
|
|
labels_ls.append(f"{key}=\"{labels[key]}\"")
|
|
text_labels = ','.join(labels_ls)
|
|
labels_string = "{" + text_labels + "}"
|
|
self._log.debug(f"# Labels: {labels_string}")
|
|
return labels_string
|
|
|
|
|
|
def _init_log(self):
|
|
''' Initialize log object '''
|
|
self._log = logging.getLogger("restic_exporter")
|
|
self._log.setLevel(logging.DEBUG)
|
|
|
|
sysloghandler = SysLogHandler()
|
|
sysloghandler.setLevel(logging.DEBUG)
|
|
self._log.addHandler(sysloghandler)
|
|
|
|
streamhandler = logging.StreamHandler(sys.stdout)
|
|
streamhandler.setLevel(logging.getLevelName(self.config.get("debug_level", 'INFO')))
|
|
self._log.addHandler(streamhandler)
|
|
|
|
if 'log_file' in self.config:
|
|
log_file = self.config['log_file']
|
|
else:
|
|
home_folder = os.environ.get('HOME', os.environ.get('USERPROFILE', ''))
|
|
log_folder = os.path.join(home_folder, "log")
|
|
log_file = os.path.join(log_folder, "restic_exporter.log")
|
|
|
|
if not os.path.exists(os.path.dirname(log_file)):
|
|
os.mkdir(os.path.dirname(log_file))
|
|
|
|
filehandler = logging.handlers.RotatingFileHandler(log_file, maxBytes=102400000)
|
|
# create formatter
|
|
formatter = logging.Formatter('%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
|
|
filehandler.setFormatter(formatter)
|
|
filehandler.setLevel(logging.DEBUG)
|
|
self._log.addHandler(filehandler)
|
|
return True
|
|
|
|
@click.command()
|
|
@click.option("--debug-level", "-d", default="INFO",
|
|
type=click.Choice(
|
|
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"],
|
|
case_sensitive=False,
|
|
), help='Set the debug level for the standard output.')
|
|
@click.option('--log-file', '-l', help="File to store all debug messages.")
|
|
#@click.option("--dummy","-n" is_flag=True, help="Don't do anything, just show what would be done.") # Don't forget to add dummy to parameters of main function
|
|
@click.option("--json-file", "-j", required=True, help='JSON file containing the output of restic')
|
|
@click.option('--job-name', '-n', required=True, help='Restic job name to attach to the exported metrics')
|
|
@click.option('--extra-labels', '-a', required=False, default=None, help='Pairs key=value separated by commas with extra labels to add to the summary')
|
|
@click.option('--metric-name', '-m', default='restic_summary', help='Metric name. Spaces will be replaced with underscore (_).')
|
|
@click.option('--metric-description', '-d', default='Restic summary metrics', help='Metric description.')
|
|
@click_config_file.configuration_option()
|
|
def __main__(debug_level, log_file, json_file, job_name, extra_labels, metric_name, metric_description):
|
|
return restic_exporter(debug_level, log_file, json_file, job_name, extra_labels, metric_name, metric_description)
|
|
|
|
if __name__ == "__main__":
|
|
__main__()
|
|
|