From 7adb04e3707f5928e30e2186bd3c93e173a4650e Mon Sep 17 00:00:00 2001 From: mabasian <54101509+mabasian@users.noreply.github.com> Date: Mon, 3 Jun 2024 03:13:32 -0700 Subject: [PATCH] FIX: export all logs in the log page (#1899) * FIX: export all logs in the log page * REDO: export all logs for best practice * fix: eslint error * ADD: loafing for export all logs * fix: the loading UI * UPDATE: export all logs function to be faster and more clean * ADD: 100000 Logs Limit --------- Co-authored-by: NeoPlays <80448387+NeoPlays@users.noreply.github.com> --- launcher/public/output.css | 20 +++++-------- launcher/src/backend/Monitoring.js | 25 ++++++++-------- launcher/src/backend/NodeConnection.js | 30 ++++++++++++++++--- launcher/src/background.js | 21 +++++++++---- .../components/UI/node-page/NodeScreen.vue | 18 +++++++++++ .../node-page/components/logs/LogFooter.vue | 19 +++++++++--- .../UI/node-page/sections/LogsSection.vue | 8 +++-- .../UI/the-control/ControlScreen.vue | 16 ++++++++++ launcher/src/store/ControlService.js | 4 +++ launcher/src/store/theNode.js | 3 +- 10 files changed, 123 insertions(+), 41 deletions(-) diff --git a/launcher/public/output.css b/launcher/public/output.css index 49761ee96..48d626671 100755 --- a/launcher/public/output.css +++ b/launcher/public/output.css @@ -1519,6 +1519,10 @@ video { margin-top: 10rem; } +.mt-5{ + margin-top: 1.25rem; +} + .mt-6{ margin-top: 1.5rem; } @@ -1527,10 +1531,6 @@ video { margin-top: 2rem; } -.mt-5{ - margin-top: 1.25rem; -} - .box-border{ -webkit-box-sizing: border-box; box-sizing: border-box; @@ -2098,6 +2098,10 @@ video { width: 1.5rem; } +.w-60{ + width: 15rem; +} + .w-64{ width: 16rem; } @@ -2252,14 +2256,6 @@ video { width: 100vw; } -.w-40{ - width: 10rem; -} - -.w-60{ - width: 15rem; -} - .min-w-\[100px\]{ min-width: 100px; } diff --git a/launcher/src/backend/Monitoring.js b/launcher/src/backend/Monitoring.js index a120565e1..e7469d2b2 100755 --- a/launcher/src/backend/Monitoring.js +++ b/launcher/src/backend/Monitoring.js @@ -436,11 +436,11 @@ export class Monitoring { var query = rpc_method.trim().indexOf("{") < 0 ? JSON.stringify({ - jsonrpc: "2.0", - method: rpc_method.trim(), - params: rpc_params, - id: 1, - }) + jsonrpc: "2.0", + method: rpc_method.trim(), + params: rpc_params, + id: 1, + }) : rpc_method; // Define default response @@ -1575,9 +1575,10 @@ export class Monitoring { try { //Nethermind returns the peers per client type (e.g. Geth, Erigon, Nethermind ...), therefore we need to sum them up if (clt.service == "NethermindService") { - details[clientType]["numPeer"] = parseInt(xx - .filter((s) => s.metric.__name__ == services[clientType][clt.service][index]) - .reduce((total, obj) => total + parseInt(obj.value.pop()), 0) + details[clientType]["numPeer"] = parseInt( + xx + .filter((s) => s.metric.__name__ == services[clientType][clt.service][index]) + .reduce((total, obj) => total + parseInt(obj.value.pop()), 0) ); } else { details[clientType]["numPeer"] = parseInt( @@ -2622,8 +2623,8 @@ export class Monitoring { const addr_type = Array.isArray(addr) ? "arr" : typeof addr === "string" && ["public", "local"].includes(addr) - ? "str" - : "invalid"; + ? "str" + : "invalid"; addr = addr_type == "str" ? addr.toLowerCase().trim() : addr; if (addr_type == "invalid") { return { @@ -2711,7 +2712,7 @@ export class Monitoring { for (let i = 0; i < serviceInfos.length; i++) { const hashDependencies = serviceInfos[i].config.dependencies.consensusClients.length || - serviceInfos[i].config.dependencies.executionClients.length + serviceInfos[i].config.dependencies.executionClients.length ? "yes" : "no"; easyInfos.push({ @@ -3225,7 +3226,7 @@ rm -rf diskoutput if (result.data === undefined) { throw result; } - const curlTag = await this.nodeConnection.ensureCurlImage() + const curlTag = await this.nodeConnection.ensureCurlImage(); const exitMsg = result.data; const exitCommand = `docker run --rm --network=stereum curlimages/curl:${curlTag} curl 'http://stereum-${serviceId}:${beaconAPIPort}/eth/v1/beacon/pool/voluntary_exits' -H 'accept: */*' -H 'Content-Type: application/json' -d '${JSON.stringify( exitMsg diff --git a/launcher/src/backend/NodeConnection.js b/launcher/src/backend/NodeConnection.js index 15d47e134..15d12a8ae 100755 --- a/launcher/src/backend/NodeConnection.js +++ b/launcher/src/backend/NodeConnection.js @@ -160,7 +160,8 @@ export class NodeConnection { "sudo -u root apt update &&\ sudo -u root apt install -y software-properties-common &&\ sudo -u root add-apt-repository --yes --update ppa:ansible/ansible &&\ - sudo -u root apt install -y pip ansible tar gzip wget git", false + sudo -u root apt install -y pip ansible tar gzip wget git", + false ); } catch (err) { log.error(err); @@ -2359,7 +2360,7 @@ export class NodeConnection { throw new Error(SSHService.extractExecError(result)); } - return "latest" + return "latest"; } catch (error) { // if pulling the latest image fails, try fetching the latest installed image try { @@ -2369,8 +2370,13 @@ export class NodeConnection { throw new Error(SSHService.extractExecError(fetchedImages)); } - const images = fetchedImages.stdout.split(/\n/).slice(0, -1).map((json) => { return JSON.parse(json) }); - log.info(`installed images: ${images}`) + const images = fetchedImages.stdout + .split(/\n/) + .slice(0, -1) + .map((json) => { + return JSON.parse(json); + }); + log.info(`installed images: ${images}`); if (images.length === 0) return "latest"; // get the latest installed image @@ -2388,4 +2394,20 @@ export class NodeConnection { } } } + + async getAllServiceLogs(args) { + const containerName = `stereum-${args}`; + try { + const logResult = await this.sshService.exec(`docker logs ${containerName} --tail=100000 2>&1`); + + if (logResult.rc || !logResult.stdout || logResult.stderr) { + throw new Error(logResult.stderr || "Error fetching logs"); + } + + return { containerId: containerName, logs: logResult.stdout.trim().split("\n") }; + } catch (err) { + log.error(`Failed to get logs for container ${containerName}: `, err); + return { containerId: "ERROR", logs: err.message }; + } + } } diff --git a/launcher/src/background.js b/launcher/src/background.js index be9ed1d46..e13e72ac7 100755 --- a/launcher/src/background.js +++ b/launcher/src/background.js @@ -24,7 +24,7 @@ const oneClickInstall = new OneClickInstall(); const serviceManager = new ServiceManager(nodeConnection); const validatorAccountManager = new ValidatorAccountManager(nodeConnection, serviceManager); const authenticationService = new AuthenticationService(nodeConnection); -const sshService = new SSHService; +const sshService = new SSHService(); const { globalShortcut } = require("electron"); const log = require("electron-log"); const stereumUpdater = new StereumUpdater(log, createWindow, isDevelopment); @@ -220,6 +220,10 @@ ipcMain.handle("getServiceLogs", async (event, args) => { return await monitoring.getServiceLogs(args); }); +ipcMain.handle("getAllServiceLogs", async (event, args) => { + return await nodeConnection.getAllServiceLogs(args); +}); + ipcMain.handle("getServiceConfig", async (event, args) => { return await nodeConnection.readServiceConfiguration(args); }); @@ -497,15 +501,20 @@ ipcMain.handle("getCurrentEpochSlot", async (event, args) => { ipcMain.handle("beginAuthSetup", async (event, args) => { const current_window = event.sender; - return await authenticationService.beginAuthSetup(args.timeBased, args.increaseTimeLimit, args.enableRateLimit, current_window) + return await authenticationService.beginAuthSetup( + args.timeBased, + args.increaseTimeLimit, + args.enableRateLimit, + current_window + ); }); ipcMain.handle("finishAuthSetup", async () => { - return await authenticationService.finishAuthSetup() + return await authenticationService.finishAuthSetup(); }); ipcMain.handle("authenticatorVerification", async (event, args) => { - return await authenticationService.authenticatorVerification(args) + return await authenticationService.authenticatorVerification(args); }); ipcMain.handle("removeAuthenticator", async (event, args) => { @@ -517,7 +526,7 @@ ipcMain.handle("checkForAuthenticator", async (event, args) => { }); ipcMain.handle("cancelVerification", async (event, args) => { - return await sshService.cancelVerification(args) + return await sshService.cancelVerification(args); }); ipcMain.handle("changePassword", async (event, args) => { @@ -676,7 +685,7 @@ ipcMain.handle("stopShell", async () => { ipcMain.handle("create2FAQRCode", async (event, args) => { return await authenticationService.create2FAQRCode(args.type, args.name, args.ip, args.secret); -}) +}); // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([{ scheme: "app", privileges: { secure: true, standard: true } }]); diff --git a/launcher/src/components/UI/node-page/NodeScreen.vue b/launcher/src/components/UI/node-page/NodeScreen.vue index 61e824a0d..a4c9cd797 100755 --- a/launcher/src/components/UI/node-page/NodeScreen.vue +++ b/launcher/src/components/UI/node-page/NodeScreen.vue @@ -34,6 +34,7 @@ :client="nodeStore.clientToLogs" @close-log="closeLogPage" @export-log="exportLogs" + @export-all-log="updateAndExportAllLogs" /> @@ -112,6 +113,7 @@ onMounted(() => { updateConnectionStats(); updateServiceLogs(); + polling = setInterval(updateServiceLogs, 10000); // refresh logs pollingVitals = setInterval(updateServerVitals, 1000); // refresh server vitals pollingNodeStats = setInterval(updateNodeStats, 1000); // refresh server vitals @@ -157,6 +159,21 @@ const updateServiceLogs = async () => { nodeStore.serviceLogs = data; } }; + +const updateAndExportAllLogs = async (client) => { + nodeStore.isLogLoading = true; + + nodeStore.allLogsForExp = await ControlService.getAllServiceLogs(client.config?.serviceID); + + const fileName = `${client.name}_all_logs.txt`; + const data = [...nodeStore.allLogsForExp.logs].reverse(); + const lineByLine = data.map((line, index) => `#${data.length - index}: ${line}`).join("\n\n"); + const blob = new Blob([lineByLine], { type: "text/plain;charset=utf-8" }); + saveAs(blob, fileName); + + nodeStore.isLogLoading = false; +}; + const updateServerVitals = async () => { try { if (serviceStore.installedServices && serviceStore.installedServices.length > 0 && headerStore.refresh) { @@ -169,6 +186,7 @@ const updateServerVitals = async () => { console.log("couldn't check server vitals"); } }; + const openExpertModal = (item) => { nodeStore.isLineHidden = true; expertModeClient.value = item; diff --git a/launcher/src/components/UI/node-page/components/logs/LogFooter.vue b/launcher/src/components/UI/node-page/components/logs/LogFooter.vue index ff708c809..746441344 100644 --- a/launcher/src/components/UI/node-page/components/logs/LogFooter.vue +++ b/launcher/src/components/UI/node-page/components/logs/LogFooter.vue @@ -18,7 +18,8 @@ import { useNodeStore } from "@/store/theNode"; -import { ref } from "vue"; +import { ref, computed } from "vue"; const props = defineProps({ client: { @@ -82,17 +83,27 @@ const props = defineProps({ }, }); -const emit = defineEmits(["export-log"]); +const emit = defineEmits(["export-log", "export-all-log"]); const nodeStore = useNodeStore(); const isAllHovered = ref(false); const is150Hovered = ref(false); +const loadingIconsClass = computed(() => { + return nodeStore.isLogLoading + ? "/img/icon/loading-icons/loading-circle.png" + : "/img/icon/service-log-icons/all-log-export-button.png"; +}); + +const isLoadingSpinning = computed(() => { + return nodeStore.isLogLoading ? "animate-spin" : ""; +}); + const exportAllLogs = () => { nodeStore.exportLogs = false; nodeStore.exportAllLogs = true; - emit("export-log", props.client); + emit("export-all-log", props.client); }; const exportLogs = () => { diff --git a/launcher/src/components/UI/node-page/sections/LogsSection.vue b/launcher/src/components/UI/node-page/sections/LogsSection.vue index 2ac9219ba..a2cbd640a 100644 --- a/launcher/src/components/UI/node-page/sections/LogsSection.vue +++ b/launcher/src/components/UI/node-page/sections/LogsSection.vue @@ -4,7 +4,7 @@ > - + @@ -24,7 +24,7 @@ const { client } = defineProps({ }, }); -const emit = defineEmits(["close-log", "export-log"]); +const emit = defineEmits(["close-log", "export-log", "export-all-log"]); const closeLog = () => { emit("close-log"); @@ -33,4 +33,8 @@ const closeLog = () => { const exportLog = (item) => { emit("export-log", item); }; + +const exportAllLog = (item) => { + emit("export-all-log", item); +}; diff --git a/launcher/src/components/UI/the-control/ControlScreen.vue b/launcher/src/components/UI/the-control/ControlScreen.vue index 7b84a3420..4bb55177d 100644 --- a/launcher/src/components/UI/the-control/ControlScreen.vue +++ b/launcher/src/components/UI/the-control/ControlScreen.vue @@ -133,6 +133,7 @@ :client="nodeStore.clientToLogs" @close-log="closeLogPage" @export-log="exportLogs" + @export-all-log="updateAndExportAllLogs" /> @@ -226,6 +227,21 @@ const closeLogPage = () => { isLogsPageActive.value = false; controlStore.serviceLogs = null; }; + +const updateAndExportAllLogs = async (client) => { + nodeStore.isLogLoading = true; + + nodeStore.allLogsForExp = await ControlService.getAllServiceLogs(client.config?.serviceID); + + const fileName = `${client.name}_all_logs.txt`; + const data = [...nodeStore.allLogsForExp.logs].reverse(); + const lineByLine = data.map((line, index) => `#${data.length - index}: ${line}`).join("\n\n"); + const blob = new Blob([lineByLine], { type: "text/plain;charset=utf-8" }); + saveAs(blob, fileName); + + nodeStore.isLogLoading = false; +}; + const exportLogs = async (client) => { const currentService = nodeStore.serviceLogs.find( (service) => service.config?.serviceID === client.config?.serviceID diff --git a/launcher/src/store/ControlService.js b/launcher/src/store/ControlService.js index 58db95d17..2fdc88f71 100755 --- a/launcher/src/store/ControlService.js +++ b/launcher/src/store/ControlService.js @@ -187,6 +187,10 @@ class ControlService extends EventEmitter { return await this.promiseIpc.send("getServiceLogs", args); } + async getAllServiceLogs(args) { + return await this.promiseIpc.send("getAllServiceLogs", args); + } + async getServiceConfig(args) { return await this.promiseIpc.send("getServiceConfig", args); } diff --git a/launcher/src/store/theNode.js b/launcher/src/store/theNode.js index f2ab9fef4..37c043a68 100755 --- a/launcher/src/store/theNode.js +++ b/launcher/src/store/theNode.js @@ -13,7 +13,8 @@ export const useNodeStore = defineStore("theNode", { runNodePowerModal: false, clientToLogs: null, logs: [], - exportAllLogs: false, + allLogsForExp: false, + isLogLoading: false, exportLogs: false, searchLogs: "", serviceLogs: [],