add log viewer features and set log level functionality in admin panel
add "Get Logs for a new Release", which will create preformatted markdown text you can paste directly to the issue
This commit is contained in:
@@ -244,4 +244,8 @@ def get_latest_release(tag_name :str):
|
|||||||
return False, ''
|
return False, ''
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
app.logger.error(f"Error fetching latest version: {str(e)}")
|
app.logger.error(f"Error fetching latest version: {str(e)}")
|
||||||
return False,''
|
return False,''
|
||||||
|
|
||||||
|
def set_log_level(level):
|
||||||
|
app.logger.setLevel(level)
|
||||||
|
app.logger.info(f"Log level set to {level}")
|
||||||
@@ -108,7 +108,65 @@ def link_issues():
|
|||||||
|
|
||||||
return render_template('admin/link_issues.html' , tracks = tracks )
|
return render_template('admin/link_issues.html' , tracks = tracks )
|
||||||
|
|
||||||
|
@app.route('/admin/logs')
|
||||||
|
@functions.jellyfin_admin_required
|
||||||
|
def view_logs():
|
||||||
|
# parse the query parameter
|
||||||
|
log_name = request.args.get('name')
|
||||||
|
logs = []
|
||||||
|
if log_name == 'logs' or not log_name and os.path.exists('/var/log/jellyplist.log'):
|
||||||
|
with open('/var/log/jellyplist.log', 'r',encoding='utf-8') as f:
|
||||||
|
logs = f.readlines()
|
||||||
|
if log_name == 'worker' and os.path.exists('/var/log/jellyplist_worker.log'):
|
||||||
|
with open('/var/log/jellyplist_worker.log', 'r', encoding='utf-8') as f:
|
||||||
|
logs = f.readlines()
|
||||||
|
if log_name == 'beat' and os.path.exists('/var/log/jellyplist_beat.log'):
|
||||||
|
with open('/var/log/jellyplist_beat.log', 'r',encoding='utf-8') as f:
|
||||||
|
logs = f.readlines()
|
||||||
|
return render_template('admin/logview.html', logs=str.join('',logs).replace('<',"_").replace('>',"_"),name=log_name)
|
||||||
|
|
||||||
|
@app.route('/admin/setloglevel', methods=['POST'])
|
||||||
|
@functions.jellyfin_admin_required
|
||||||
|
def set_log_level():
|
||||||
|
loglevel = request.form.get('logLevel')
|
||||||
|
if loglevel:
|
||||||
|
if loglevel in ['DEBUG','INFO','WARNING','ERROR','CRITICAL']:
|
||||||
|
functions.set_log_level(loglevel)
|
||||||
|
flash(f'Log level set to {loglevel}', category='success')
|
||||||
|
return redirect(url_for('view_logs'))
|
||||||
|
|
||||||
|
@app.route('/admin/logs/getLogsForIssue')
|
||||||
|
@functions.jellyfin_admin_required
|
||||||
|
def get_logs_for_issue():
|
||||||
|
# get the last 200 lines of all log files
|
||||||
|
last_lines = -300
|
||||||
|
logs = []
|
||||||
|
logs += f'## Logs and Details for Issue ##\n'
|
||||||
|
logs += f'Version: *{__version__}{read_dev_build_file()}*\n'
|
||||||
|
if os.path.exists('/var/log/jellyplist.log'):
|
||||||
|
with open('/var/log/jellyplist.log', 'r',encoding='utf-8') as f:
|
||||||
|
logs += f'### jellyfin.log\n'
|
||||||
|
logs += f'```log\n'
|
||||||
|
logs += f.readlines()[last_lines:]
|
||||||
|
logs += f'```\n'
|
||||||
|
|
||||||
|
if os.path.exists('/var/log/jellyplist_worker.log'):
|
||||||
|
with open('/var/log/jellyplist_worker.log', 'r', encoding='utf-8') as f:
|
||||||
|
logs += f'### jellyfin_worker.log\n'
|
||||||
|
logs += f'```log\n'
|
||||||
|
logs += f.readlines()[last_lines:]
|
||||||
|
logs += f'```\n'
|
||||||
|
|
||||||
|
if os.path.exists('/var/log/jellyplist_beat.log'):
|
||||||
|
with open('/var/log/jellyplist_beat.log', 'r',encoding='utf-8') as f:
|
||||||
|
logs += f'### jellyplist_beat.log\n'
|
||||||
|
logs += f'```log\n'
|
||||||
|
logs += f.readlines()[last_lines:]
|
||||||
|
logs += f'```\n'
|
||||||
|
# in the logs array, anonymize IP addresses
|
||||||
|
logs = [re.sub(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})', 'xxx.xxx.xxx.xxx', log) for log in logs]
|
||||||
|
|
||||||
|
return jsonify({'logs': logs})
|
||||||
|
|
||||||
@app.route('/run_task/<task_name>', methods=['POST'])
|
@app.route('/run_task/<task_name>', methods=['POST'])
|
||||||
@functions.jellyfin_admin_required
|
@functions.jellyfin_admin_required
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/admin/lidarr">Lidarr</a>
|
<a class="nav-link" href="/admin/lidarr">Lidarr</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/admin/logs?name=logs">Logs</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
162
templates/admin/logview.html
Normal file
162
templates/admin/logview.html
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
{% extends "admin.html" %}
|
||||||
|
|
||||||
|
{% block admin_content %}
|
||||||
|
{% if not logs %}
|
||||||
|
{% set logs = "Logfile empty or not found" %}
|
||||||
|
{% endif %}
|
||||||
|
{% set log_level = config['LOG_LEVEL'] %}
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.21.2/min/vs/loader.js"></script>
|
||||||
|
|
||||||
|
<div class="container-fluid mt-5">
|
||||||
|
<h1>Log Viewer</h1>
|
||||||
|
<div class="mb-3 row">
|
||||||
|
|
||||||
|
<form action="/admin/setloglevel" method="post" class="d-inline">
|
||||||
|
<label for="logLevel" class="form-label">Log Level</label>
|
||||||
|
<select class="form-select" id="logLevel" name="logLevel" required aria-describedby="loglevelHelp">
|
||||||
|
<option value="DEBUG" {% if log_level=="DEBUG" %}selected{% endif %}>DEBUG</option>
|
||||||
|
<option value="INFO" {% if log_level=="INFO" %}selected{% endif %}>INFO</option>
|
||||||
|
<option value="WARNING" {% if log_level=="WARNING" %}selected{% endif %}>WARNING</option>
|
||||||
|
<option value="ERROR" {% if log_level=="ERROR" %}selected{% endif %}>ERROR</option>
|
||||||
|
<option value="CRITICAL" {% if log_level=="CRITICAL" %}selected{% endif %}>CRITICAL</option>
|
||||||
|
</select>
|
||||||
|
<div id="loglevelHelp" class="form-text">Set the log level on demand.</div>
|
||||||
|
<button type="submit" class="btn btn-primary mt-2">Set Log Level</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="mb-5 mt-3 row">
|
||||||
|
<button type="button" class="btn btn-warning" onclick="openCreateIssueModal()">Get Logs for a new Issue</button>
|
||||||
|
|
||||||
|
<!-- Modal HTML -->
|
||||||
|
<div class="modal fade" id="createIssueModal" tabindex="-1" aria-labelledby="createIssueModalLabel"
|
||||||
|
aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-sm">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="createIssueModalLabel">Create Issue</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<span class="m-2">Hit the copy button or copy this text manually and paste it to your GitHub
|
||||||
|
Issue.</span>
|
||||||
|
<div id="issue-text" style="height: 400px;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
<button type="button" id="copyText" class="btn btn-primary">Copy</button>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
async function setClipboard(text) {
|
||||||
|
const type = "text/plain";
|
||||||
|
const blob = new Blob([text], { type });
|
||||||
|
const data = [new ClipboardItem({ [type]: blob })];
|
||||||
|
await navigator.clipboard.write(data);
|
||||||
|
}
|
||||||
|
document.getElementById('copyText').addEventListener('click', function () {
|
||||||
|
const issueEditor = monaco.editor.getModels()[1];
|
||||||
|
const issueText = issueEditor.getValue();
|
||||||
|
if(!window.isSecureContext){
|
||||||
|
alert('Clipboard API is not available in insecure context. Please use a secure context (HTTPS) or just copy the text manually.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setClipboard(issueText);
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5 mt-3 row">
|
||||||
|
<label for="logType" class="form-label">Select Logs</label>
|
||||||
|
<select class="form-select" id="logType" name="logType" required
|
||||||
|
onchange="location.href='/admin/logs?name=' + this.value;">
|
||||||
|
<option value="logs" {% if name=="logs" %}selected{% endif %}>Logs</option>
|
||||||
|
<option value="worker" {% if name=="worker" %}selected{% endif %}>Worker Logs</option>
|
||||||
|
<option value="beat" {% if name=="beat" %}selected{% endif %}>Beat Logs</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 row" id="editor" style="height: 700px;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
require.config({ paths: { 'vs': 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.52.0/min/vs' } });
|
||||||
|
require(['vs/editor/editor.main'], function () {
|
||||||
|
monaco.languages.register({ id: "jellyplistLog" });
|
||||||
|
|
||||||
|
// Register a tokens provider for the language
|
||||||
|
monaco.languages.setMonarchTokensProvider("jellyplistLog", {
|
||||||
|
tokenizer: {
|
||||||
|
root: [
|
||||||
|
[/ERROR -.*/, "custom-error"],
|
||||||
|
[/WARNING -/, "custom-notice"],
|
||||||
|
[/INFO -/, "custom-info"],
|
||||||
|
[/DEBUG -.*/, "custom-debug"],
|
||||||
|
[/^\[\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2},\d{3}\]/, "custom-date"],
|
||||||
|
[/\s.*[a-zA-Z0-9_]+\.[a-z]{2,4}(?=:)/, "custom-filename"],
|
||||||
|
[/\d+(?= -)/, "custom-lineno"]
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
monaco.editor.defineTheme("jellyplistLogTheme", {
|
||||||
|
base: "vs-dark",
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{ token: "custom-info", foreground: "808080" },
|
||||||
|
{ token: "custom-error", foreground: "ff0000", fontStyle: "bold" },
|
||||||
|
{ token: "custom-notice", foreground: "FFA500" },
|
||||||
|
{ token: "custom-debug", foreground: "851ea5" },
|
||||||
|
{ token: "custom-date", foreground: "90cc6f" },
|
||||||
|
{ token: "custom-filename", foreground: "d9d04f", fontStyle: "italic" },
|
||||||
|
{ token: "custom-lineno", foreground: "d9d04f", fontStyle: "light" },
|
||||||
|
],
|
||||||
|
colors: {
|
||||||
|
"editor.foreground": "#ffffff",
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
let editor = monaco.editor.create(document.getElementById('editor'), {
|
||||||
|
value: `{{ logs }}`,
|
||||||
|
language: 'jellyplistLog',
|
||||||
|
readOnly: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
theme: 'jellyplistLogTheme',
|
||||||
|
automaticLayout: true
|
||||||
|
|
||||||
|
});
|
||||||
|
editor.revealLine(editor.getModel().getLineCount())
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
function openCreateIssueModal() {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('createIssueModal'));
|
||||||
|
|
||||||
|
fetch('/admin/logs/getLogsForIssue')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
|
||||||
|
const issueText = data.logs;
|
||||||
|
const issueTextInput = document.getElementById('issue-text');
|
||||||
|
// before creating the new editor, remove the old one
|
||||||
|
while (issueTextInput.firstChild) {
|
||||||
|
issueTextInput.removeChild(issueTextInput.firstChild);
|
||||||
|
}
|
||||||
|
const issueEditor = monaco.editor.create(issueTextInput, {
|
||||||
|
value: issueText.join(''),
|
||||||
|
language: 'markdown',
|
||||||
|
minimap: { enabled: false },
|
||||||
|
automaticLayout: true
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching issue logs:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
<script src="https://unpkg.com/htmx.org"></script>
|
<script src="https://unpkg.com/htmx.org"></script>
|
||||||
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"
|
||||||
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -150,8 +151,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="alerts"></div>
|
<div id="alerts"></div>
|
||||||
<!-- Bootstrap JS -->
|
<!-- Bootstrap JS -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("showToastMessages", function () {
|
document.addEventListener("showToastMessages", function () {
|
||||||
console.log("showToastMessages")
|
console.log("showToastMessages")
|
||||||
|
|||||||
Reference in New Issue
Block a user