Skip to content

Commit

Permalink
Provide supervisor log context for pending backups
Browse files Browse the repository at this point in the history
  • Loading branch information
sabeechen committed Nov 18, 2023
1 parent 8c7c7a6 commit 3a4a221
Show file tree
Hide file tree
Showing 8 changed files with 132 additions and 3 deletions.
15 changes: 15 additions & 0 deletions hassio-google-drive-backup/backup/ha/hasource.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def __init__(self, backupType, protected, options: CreateOptions, request_info,
self._time = time
self._pending_subverted = False
self._start_time = time.now()
self._logs = None

def considerForPurge(self) -> bool:
return False
Expand Down Expand Up @@ -111,6 +112,15 @@ def isStale(self):
Setting.FAILED_BACKUP_TIMEOUT_SECONDS))
staleTime = self.getFailureTime() + delta
return self._time.now() >= staleTime

def attach_logs(self, logs):
self._logs = logs

def error_logs(self):
if self._logs:
return self._logs
else:
return None

def madeByTheAddon(self):
return True
Expand Down Expand Up @@ -512,6 +522,11 @@ async def _requestAsync(self, pending: PendingBackup, start=[]) -> None:
logger.error("Backup failed:")
logger.printException(e)
pending.failed(e, self.time.now())
try:
pending.attach_logs(await self.harequests.getSuperLogs())
except Exception as e:
logger.error("Failed to get sueprvisor logs after failed backup request")
logger.printException(e)
finally:
await self.stopper.startAddons()
self.trigger()
Expand Down
7 changes: 6 additions & 1 deletion hassio-google-drive-backup/backup/static/js/scripts.js
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,12 @@ function populateBackupDiv(backup_div, backups, icon) {

if (backup.isPending) {
$("#loading" + backup.slug).show();
$("#backup_card" + backup.slug).css("cursor", "auto");
if (backup.super_logs) {
$("#backup_card" + backup.slug).css("cursor", "pointer");
$("#has-logs", template).show();
} else {
$("#backup_card" + backup.slug).css("cursor", "auto");
}
} else {
$("#loading" + backup.slug).hide();
$("#backup_card" + backup.slug).css("cursor", "pointer");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,30 @@
</div>`;
}
function colorizeLog(log) {
if (log.includes("WARN")) return `<span class="super-log-warn">${log}</span>`;
if (log.includes("INFO")) return `<span class="super-log-info">${log}</span>`;
if (log.includes("ERROR")) return `<span class="super-log-error">${log}</span>`;
if (log.includes("DEBUG")) return `<span class="super-log-debug">${log}</span>`;
return `<span class="super-log-info">${log}</span>`;
}
function showDetails(target) {
var backup = $(target).data('backup');
if (backup.isPending) {
if (backup.super_logs) {
let modal = $('#pending_backup_modal');
modal.data('slug', backup.slug);
modal.data('backup', backup);
const logs = backup.super_logs.split('\n');
const container = document.getElementById('super_logs_pending_request');
logs.forEach(log => {
const colorizedLog = colorizeLog(log);
container.innerHTML += `${colorizedLog}\n`;
});
M.Modal.getInstance(document.getElementById('pending_backup_modal')).open();
container.scrollTop = container.scrollHeight;
}
return;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<div id="pending_backup_modal" class="modal">
<style>
.tall {
display: flex; /* Use Flexbox */
flex-direction: column; /* Stack children vertically */
height: 100%; /* Set a specific height for the parent container */
box-sizing: border-box;
}
.tall div {
flex-grow: 1; /* Grow to take the remaining space */
resize: none; /* Optional: prevent manual resizing */
}
.super-log-container {
background-color: black;
color: white;
width: 95%;
height: 400px;
margin: 20px auto;
overflow-y: scroll;
font-family: monospace;
white-space: pre-line; /* preserves your newline characters */
padding: 10px;
border-radius: 4px;
}
/* Log level color coding */
.super-log-warn {
color: yellow;
}
.super-log-error {
color: red;
}
.super-log-debug {
color: lightgreen;
}
.super-log-info {
color: white;
}
</style>
<div class="modal-content">
<h4>Pending Backup Info</h4>
<p>This addon thinks addon a backup is in progress, but that just an assumption. In reality Home Assistant doesn't tell this addon the reason the
request to create a backup was rejected, a backup already being in progress is just the most common reason. A lot of less common reasons can cause it to be rejected like

<ul class="browser-default">
<li>You're using a network share to store backups, and its unavailable</li>
<li>Something is wrong with your hard drive and Home Assistant can't read/write to it</li>
<li>Home Assistant has frozen itself because it thinks something is wrong</li>
</ul>
The only place these less common errors show up is in the Supervisor's logs. The Supervisor is a part of Home Assistant that manages it in the background. Its logs are
kind of hard to get to, so for conveinence a snapshot of the logs right after the backup was most recently requested is shown below. If you're seeing a backup thats stuck
in the "pending" state, these logs might provide a clue why. If you're able to identify and resolve that problem, restart this addon to have it try creating a backup again.</p>

<h5>Supervisor Logs</h5>
<div class="tall">
<div class="super-log-container" id="super_logs_pending_request"></div>
</div>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close btn-flat">Close</a>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
{% include 'layouts/partials/modals/about.jinja2' %}
{% include 'layouts/partials/modals/about.jinja2' %}
{% include 'layouts/partials/modals/crypto_donate.jinja2' %}
{% include 'layouts/partials/modals/pending_backup_info.jinja2' %}

{% if coordEnabled %}
<script type="text/javascript">
Expand All @@ -37,6 +38,7 @@
$('.collapsible').collapsible();
$('.sidenav').sidenav();
$('.fixed-action-btn').floatingActionButton();
$('#pending_backup_modal').modal();
const dropdownTriggers = document.querySelectorAll('.dropdown-trigger');
const instances = M.Dropdown.init(dropdownTriggers, { 'constrainWidth': false });
Expand Down
1 change: 1 addition & 0 deletions hassio-google-drive-backup/backup/static/working.jinja2
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
</div>
<div id="type"></div>
<div id="createdAt"></div>
<div class="default-hidden" id="has-logs" style="margin-top: 5px">Click for more info</div>
<div id="note" style="font-style: italic;" class="truncate tooltipped" data-position="bottom" data-tooltip=""></div>
</div>
</div>
Expand Down
5 changes: 4 additions & 1 deletion hassio-google-drive-backup/backup/ui/uiserver.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ def getBackupDetails(self, backup: Backup):
'ignored': source.ignore(),
})

return {
data = {
'name': backup.name(),
'slug': backup.slug(),
'size': backup.sizeString(),
Expand All @@ -223,6 +223,9 @@ def getBackupDetails(self, backup: Backup):
'timestamp': backup.date().timestamp(),
'note': backup.note()
}
if isinstance(ha, PendingBackup):
data["super_logs"] = ha.error_logs()
return data

def formatAddons(self, backup_data):
addons = []
Expand Down
19 changes: 18 additions & 1 deletion hassio-google-drive-backup/dev/simulated_supervisor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from asyncio.tasks import sleep
from datetime import timedelta
import random
import string
import io

from backup.config import Config, Version
Expand Down Expand Up @@ -123,6 +124,7 @@ def routes(self):
get('/backups/new/full', self._newbackup),
get('/backups/{slug}/download', self._backupDownload),
get('/backups/{slug}/info', self._backupDetail),
get('/debug/backups/lock', self._lock_backups),

# TODO: remove once the api path is fully deprecated
get('/snapshots', self._getSnapshots),
Expand Down Expand Up @@ -245,7 +247,18 @@ async def _allAddons(self, request: Request):

async def _supervisorLogs(self, request: Request):
await self._verifyHeader(request)
return Response(body="Supervisor Log line 1\nSupervisor Log Line 2")
return Response(body=self.generate_random_text(20, 10, 20))

def generate_random_text(self, line_count, min_words=5, max_words=10):
lines = []
log_levels = ["WARN", "WARNING", "INFO", "ERROR", "DEBUG"]
for _ in range(line_count):
level = random.choice(log_levels)
word_count = random.randint(min_words, max_words)
words = [random.choice(string.ascii_lowercase) for _ in range(word_count)]
line = level + " " + ' '.join(''.join(random.choices(string.ascii_lowercase + string.digits, k=random.randint(3, 10))) for _ in words)
lines.append(line)
return '\n'.join(lines)

async def _coreLogs(self, request: Request):
await self._verifyHeader(request)
Expand Down Expand Up @@ -301,6 +314,10 @@ async def _newbackup(self, request: Request):
input_json = await request.json()
task = asyncio.shield(asyncio.create_task(self._internalNewBackup(request, input_json)))
return self._formatDataResponse({"slug": await task})

async def _lock_backups(self, request: Request):
await self._backup_lock.acquire()
return self._formatDataResponse({"message": "locked"})

async def _uploadbackup(self, request: Request):
await self._verifyHeader(request)
Expand Down

0 comments on commit 3a4a221

Please sign in to comment.