Last active
January 22, 2026 13:42
-
-
Save CodeAlDente/1e841d820284564f5b8e0f2482c612a7 to your computer and use it in GitHub Desktop.
Adds a download button to the listmonk admin logs page to export the currently visible logs as a text file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name listmonk – Logs Downloader | |
| // @author CodeAlDente (https://github.com/CodeAlDente) | |
| // @namespace https://gist.github.com/CodeAlDente/1e841d820284564f5b8e0f2482c612a7 | |
| // @version 1.0.0 | |
| // @description Adds a native “Download logs” button to the listmonk admin logs page. When clicked, the visible logs are exported as a text file. | |
| // @match https://*/admin/settings/logs | |
| // @run-at document-idle | |
| // @grant none | |
| // @license MIT | |
| // @homepageURL https://gist.github.com/CodeAlDente/1e841d820284564f5b8e0f2482c612a7 | |
| // @supportURL https://gist.github.com/CodeAlDente/1e841d820284564f5b8e0f2482c612a7 | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| // Extra guard in case the URL matching ever changes. | |
| // The script should never run outside the listmonk logs page. | |
| if (!location.pathname.startsWith('/admin/settings/logs')) { | |
| return; | |
| } | |
| // Formats a Date object into a filesystem-friendly timestamp | |
| // used for the exported log filename. | |
| function formatDate(date) { | |
| const pad = n => String(n).padStart(2, '0'); | |
| return ( | |
| date.getFullYear() + '-' + | |
| pad(date.getMonth() + 1) + '-' + | |
| pad(date.getDate()) + '_' + | |
| pad(date.getHours()) + '-' + | |
| pad(date.getMinutes()) + '-' + | |
| pad(date.getSeconds()) | |
| ); | |
| } | |
| // Reads the currently visible log lines from the DOM and | |
| // turns them into a plain text representation. | |
| function collectLogs() { | |
| const lines = document.querySelectorAll('.log-view .lines .line'); | |
| if (!lines.length) { | |
| alert('No log lines found.'); | |
| return ''; | |
| } | |
| return Array.from(lines).map(line => { | |
| const ts = line.querySelector('.timestamp')?.innerText.replace(/\s+/g, ' ') ?? ''; | |
| const file = line.querySelector('.file')?.innerText ?? ''; | |
| const msg = line.querySelector('.log-message')?.innerText ?? ''; | |
| return `${ts}${file}${msg}`.trim(); | |
| }).join('\n'); | |
| } | |
| // Creates a local text file and triggers a download in the browser. | |
| // Everything happens client-side; nothing is sent anywhere. | |
| function downloadLogs() { | |
| const text = collectLogs(); | |
| if (!text) return; | |
| const now = new Date(); | |
| const hostname = location.hostname.replace(/[^a-zA-Z0-9.-]/g, '_'); | |
| const filename = `listmonk-logs_${hostname}_${formatDate(now)}.txt`; | |
| const blob = new Blob([text], { type: 'text/plain;charset=utf-8' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| a.remove(); | |
| } | |
| // Injects the "Download logs" button into the listmonk UI. | |
| // Uses existing listmonk/Bulma button styles to blend in. | |
| function injectButton() { | |
| if (document.getElementById('listmonk-log-download-btn')) return; | |
| const logsSection = document.querySelector('section.logs'); | |
| if (!logsSection) return; | |
| const title = logsSection.querySelector('h1.title'); | |
| if (!title) return; | |
| const btn = document.createElement('button'); | |
| btn.id = 'listmonk-log-download-btn'; | |
| btn.type = 'button'; | |
| btn.className = 'button is-primary'; | |
| btn.style.marginBottom = '1rem'; | |
| const span = document.createElement('span'); | |
| span.textContent = 'Download logs'; | |
| btn.appendChild(span); | |
| btn.addEventListener('click', downloadLogs); | |
| // Place the button directly below the page title, | |
| // matching the layout of other admin actions. | |
| title.insertAdjacentElement('afterend', btn); | |
| } | |
| // listmonk loads parts of the page dynamically. | |
| // This observer waits until the logs and title exist | |
| // before injecting the button. | |
| const observer = new MutationObserver(() => { | |
| if ( | |
| document.querySelector('.log-view .lines .line') && | |
| document.querySelector('section.logs h1.title') | |
| ) { | |
| injectButton(); | |
| } | |
| }); | |
| observer.observe(document.body, { | |
| childList: true, | |
| subtree: true | |
| }); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
How to install
/admin/settings/logs) — the Download logs button will appear automatically.Optional (recommended)
For extra safety, you can restrict the script to your own listmonk domain by editing the
@matchline in the script, or by adjusting the include/match settings in your userscript manager after installation.