From c6c999de061d8e000447164f8a256ed2dbbb370f Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 14 Nov 2024 16:38:00 +0530 Subject: [PATCH 1/9] feat: log browser --- .../src2/components/AppSidebarItemGroup.vue | 3 +- dashboard/src2/components/LinkControl.vue | 1 + dashboard/src2/components/NavigationItems.vue | 9 +- .../pages/devtools/log-browser/LogBrowser.vue | 314 ++++++++++++++++ .../pages/devtools/log-browser/LogViewer.vue | 330 +++++++++++++++++ dashboard/src2/router.js | 6 + dashboard/src2/utils/format.js | 3 +- press/api/log_browser.py | 334 ++++++++++++++++++ press/press/doctype/bench/bench.py | 3 + press/press/doctype/site/site.py | 3 + 10 files changed, 1003 insertions(+), 3 deletions(-) create mode 100644 dashboard/src2/pages/devtools/log-browser/LogBrowser.vue create mode 100644 dashboard/src2/pages/devtools/log-browser/LogViewer.vue create mode 100644 press/api/log_browser.py diff --git a/dashboard/src2/components/AppSidebarItemGroup.vue b/dashboard/src2/components/AppSidebarItemGroup.vue index 89a49a1c85..017d3f1866 100644 --- a/dashboard/src2/components/AppSidebarItemGroup.vue +++ b/dashboard/src2/components/AppSidebarItemGroup.vue @@ -21,7 +21,8 @@
diff --git a/dashboard/src2/components/LinkControl.vue b/dashboard/src2/components/LinkControl.vue index ffe69b655e..c4af8393b3 100644 --- a/dashboard/src2/components/LinkControl.vue +++ b/dashboard/src2/components/LinkControl.vue @@ -47,6 +47,7 @@ export default { query: this.query }, auto: true, + initialData: this.options.initialData || [], transform: data => { return data.map(option => ({ label: option.label || option.value, diff --git a/dashboard/src2/components/NavigationItems.vue b/dashboard/src2/components/NavigationItems.vue index 75fa33b61c..03055b9b4a 100644 --- a/dashboard/src2/components/NavigationItems.vue +++ b/dashboard/src2/components/NavigationItems.vue @@ -14,6 +14,7 @@ import WalletCards from '~icons/lucide/wallet-cards'; import Settings from '~icons/lucide/settings'; import App from '~icons/lucide/layout-grid'; import DatabaseZap from '~icons/lucide/database-zap'; +import Logs from '~icons/lucide/scroll-text'; import Globe from '~icons/lucide/globe'; import Notification from '~icons/lucide/inbox'; import Code from '~icons/lucide/code'; @@ -122,9 +123,15 @@ export default { icon: () => h(DatabaseZap), route: '/sql-playground', isActive: routeName === 'SQL Playground' + }, + { + name: 'Log Browser', + icon: () => h(Logs), + route: '/log-browser', + isActive: routeName === 'Log Browser' } ], - isActive: ['SQL Playground'].includes(routeName), + isActive: ['SQL Playground', 'Log Browser'].includes(routeName), disabled: enforce2FA }, { diff --git a/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue b/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue new file mode 100644 index 0000000000..0d3eda52d3 --- /dev/null +++ b/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue @@ -0,0 +1,314 @@ + + + diff --git a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue new file mode 100644 index 0000000000..832327164d --- /dev/null +++ b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue @@ -0,0 +1,330 @@ + + + diff --git a/dashboard/src2/router.js b/dashboard/src2/router.js index a683397900..e103691a2d 100644 --- a/dashboard/src2/router.js +++ b/dashboard/src2/router.js @@ -307,6 +307,12 @@ let router = createRouter({ component: () => import('./pages/devtools/database/DatabaseSQLPlayground.vue') }, + { + path: '/log-browser/:mode?/:docName?/:logId?', + name: 'Log Browser', + component: () => import('./pages/devtools/log-browser/LogBrowser.vue'), + props: true + }, ...generateRoutes(), { path: '/:pathMatch(.*)*', diff --git a/dashboard/src2/utils/format.js b/dashboard/src2/utils/format.js index a001f32014..88e2685973 100644 --- a/dashboard/src2/utils/format.js +++ b/dashboard/src2/utils/format.js @@ -7,7 +7,8 @@ export function bytes(bytes, decimals = 2, current = 0) { const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; - const i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k)); + let i = Math.floor(Math.log(Math.abs(bytes)) / Math.log(k)); + if (i < 0) i++; return ( parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i + current] diff --git a/press/api/log_browser.py b/press/api/log_browser.py new file mode 100644 index 0000000000..b0ac980172 --- /dev/null +++ b/press/api/log_browser.py @@ -0,0 +1,334 @@ +import datetime +import re +from enum import Enum + +import frappe + + +class LOG_TYPE(Enum): + SITE = "site" + BENCH = "bench" + + +def bench_log_formatter(log_entries: list) -> list: + """ + Formats bench logs by extracting timestamp, level, and description. + + Args: + log_entries (list): A list of log entries, where each entry is a string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + if not log_entries: + return [] # Return empty list if no log entries + + formatted_logs = [] + for entry in log_entries: + date, time, level, *description_parts = entry.split(" ") + description = " ".join(description_parts) + + formatted_time = datetime.datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M:%S,%f").strftime( + "%Y-%m-%d %H:%M:%S" + ) + + formatted_logs.append({"level": level, "time": formatted_time, "description": description}) + + return formatted_logs + + +def worker_log_formatter(log_entries: list) -> list: + """ + Formats worker logs by extracting timestamp, level, and description. + + Args: + log_entries (list): A list of log entries, where each entry is a string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + if not log_entries: + return [] # Return empty list if no log entries + + formatted_logs = [] + for entry in log_entries: + date, time, *description_parts = entry.split(" ") + description = " ".join(description_parts) + + try: + formatted_time = datetime.datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M:%S,%f").strftime( + "%Y-%m-%d %H:%M:%S" + ) + except ValueError: + formatted_time = "" + + formatted_logs.append({"time": formatted_time, "description": description}) + + return formatted_logs + + +def database_log_formatter(log_entries: list) -> list: + """ + Formats database logs by extracting timestamp, level, and description. + + Args: + log_entries (list): A list of log entries, where each entry is a string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + if not log_entries: + return [] # Return empty list if no log entries + + formatted_logs = [] + for entry in log_entries: + date, time, level, *description_parts = entry.split(" ") + description = " ".join(description_parts) + + formatted_time = datetime.datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M:%S,%f").strftime( + "%Y-%m-%d %H:%M:%S" + ) + + formatted_logs.append({"level": level, "time": formatted_time, "description": description}) + + return formatted_logs + + +def scheduler_log_formatter(log_entries: list) -> list: + """ + Formats scheduler logs by extracting timestamp, level, and description. + + Args: + log_entries (list): A list of log entries, where each entry is a string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + if not log_entries: + return [] # Return empty list if no log entries + + formatted_logs = [] + for entry in log_entries: + date, time, level, *description_parts = entry.split(" ") + description = " ".join(description_parts) + + # TODO: formatted time goes invalid + formatted_time = datetime.datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M:%S,%f").strftime( + "%Y-%m-%d %H:%M:%S" + ) + + formatted_logs.append({"level": level, "time": formatted_time, "description": description}) + + return formatted_logs + + +def redis_log_formatter(log_entries: list) -> list: + """ + Formats redis logs by extracting timestamp, level, and description. + + Args: + log_entries (list): A list of log entries, where each entry is a string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + if not log_entries: + return [] # Return empty list if no log entries + + formatted_logs = [] + for entry in log_entries: + _, day, month, year, time, *description_parts = entry.split(" ") + description = " ".join(description_parts) + + formatted_time = datetime.datetime.strptime( + f"{year}-{month}-{day} {time}", "%Y-%b-%d %H:%M:%S.%f" + ).strftime("%Y-%m-%d %H:%M:%S") + + formatted_logs.append({"time": formatted_time, "description": description}) + + return formatted_logs + + +def web_error_log_formatter(log_entries: list) -> list: + """ + Formats web error logs by extracting timestamp, level, and description. + + Args: + log_entries (list): A list of log entries, where each entry is a string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + if not log_entries: + return [] # Return empty list if no log entries + + # Regular expression pattern to match log entries specific to web.error logs + regex = r"\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{4})\] \[(\d+)\] \[(\w+)\] (.*)" + + formatted_logs = [] + for entry in log_entries: + match = re.match(regex, entry) + if not match: + formatted_logs.append({"description": entry}) # Unparsable entry + continue + + # Extract groups from the match + date, _, level, description_parts = match.groups() + description = "".join(description_parts) + + # Format date using strftime for cnsistency (no external libraries needed) + formatted_time = datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S %z").strftime( + "%Y-%m-%d %H:%M:%S" + ) + + formatted_logs.append({"level": level, "time": formatted_time, "description": description}) + + return formatted_logs + + +def monitor_json_log_formatter(log_entries: list) -> list: + """ + Formats monitor.json logs by extracting timestamp, level, and description. + + Args: + log_entries (list): A list of log entries, where each entry is a string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + import json + + if not log_entries: + return [] # Return empty list if no log entries + + formatted_logs = [] + for entry in log_entries: + # parse the json log entry + try: + log_entry = json.loads(entry) + time = log_entry.get("timestamp") + formatted_time = datetime.datetime.strptime(time, "%Y-%m-%d %H:%M:%S.%f%z").strftime( + "%Y-%m-%d %H:%M:%S" + ) + + formatted_logs.append({"time": formatted_time, "description": entry}) + except json.JSONDecodeError: + formatted_logs.append({"description": entry}) + + return formatted_logs + + +def ipython_log_formatter(log_entries: list) -> list: + """ + Formats ipython logs by extracting timestamp, level, and description. + + Args: + log_entries (list): A list of log entries, where each entry is a string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + if not log_entries: + return [] # Return empty list if no log entries + + formatted_logs = [] + for entry in log_entries: + date, time, level, *description_parts = entry.split(" ") + description = " ".join(description_parts) + + formatted_time = datetime.datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M:%S,%f").strftime( + "%Y-%m-%d %H:%M:%S" + ) + + formatted_logs.append({"level": level, "time": formatted_time, "description": description}) + + return formatted_logs + + +def fallback_log_formatter(log_entries: list) -> list: + """ + Fallback formatter for logs that don't have a specific formatter. + + Args: + log_entries (list): A list of log entries, where each entry is string. + + Returns: + list: A list of dictionaries, where each dictionary represents a formatted log entry. + """ + + formatted_logs = [] + for entry in log_entries: + formatted_logs.append({"description": entry}) + + return formatted_logs + + +FORMATTER_MAP = { + "bench": bench_log_formatter, + "worker": worker_log_formatter, + "ipython": ipython_log_formatter, + "database": database_log_formatter, + "redis-cache": redis_log_formatter, + "redis-queue": redis_log_formatter, + "scheduler": scheduler_log_formatter, + "web.error": web_error_log_formatter, + "worker.error": worker_log_formatter, + "monitor.json": monitor_json_log_formatter, +} + + +@frappe.whitelist() +def get_log(log_type: LOG_TYPE, doc_name: str, log_name: str) -> list: + log = get_raw_log(log_type, doc_name, log_name) + log_entries = [] + for k, v in log.items(): + if k == log_name: + if v == "": + return [] + if ( + log_name.startswith("database.log") + or log_name.startswith("scheduler.log") + or log_name.startswith("worker") + or log_name.startswith("ipython") + ): + # split line if nextline starts with timestamp + log_entries = re.split(r"\n(?=\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})", v) + break + + log_entries = v.strip().splitlines() + break + + return format_log(log_name, log_entries) + + +def get_raw_log(log_type: LOG_TYPE, doc_name: str, log_name: str) -> list: + if log_type == LOG_TYPE.BENCH: + # group = frappe.get_value('Bench', doc_name, 'group') + return frappe.get_doc("Bench", doc_name).get_server_log_for_log_browser(log_name) + if log_type == LOG_TYPE.SITE: + return frappe.get_doc("Site", doc_name).get_server_log_for_log_browser(log_name) + return frappe.throw("Invalid log type") + + +def format_log(log_name: str, log_entries: list) -> list: + log_key = get_log_key(log_name) + if log_key in FORMATTER_MAP: + return FORMATTER_MAP[log_key](log_entries) + return fallback_log_formatter(log_entries) + + +def get_log_key(log_name: str) -> str: + # if the log file has a number at the end, it's a rotated log + # and we don't need to consider the number for formatter mapping + if log_name[-1].isdigit(): + log_name = log_name.rsplit(".", 1)[0] + + return log_name.rsplit(".", 1)[0] diff --git a/press/press/doctype/bench/bench.py b/press/press/doctype/bench/bench.py index a8c33a97ad..4598c26338 100644 --- a/press/press/doctype/bench/bench.py +++ b/press/press/doctype/bench/bench.py @@ -518,6 +518,9 @@ def server_logs(self): def get_server_log(self, log): return Agent(self.server).get(f"benches/{self.name}/logs/{log}") + def get_server_log_for_log_browser(self, log): + return Agent(self.server).get(f"benches/{self.name}/logs_v2/{log}") + @frappe.whitelist() def move_sites(self, server: str): try: diff --git a/press/press/doctype/site/site.py b/press/press/doctype/site/site.py index d5c7011a08..ed55b829e2 100644 --- a/press/press/doctype/site/site.py +++ b/press/press/doctype/site/site.py @@ -2313,6 +2313,9 @@ def server_logs(self): def get_server_log(self, log): return Agent(self.server).get(f"benches/{self.bench}/sites/{self.name}/logs/{log}") + def get_server_log_for_log_browser(self, log): + return Agent(self.server).get(f"benches/{self.bench}/sites/{self.name}/logs_v2/{log}") + @property def has_paid(self) -> bool: """Has the site been paid for by customer.""" From 08b47c34a951a3e9afb2ca89c8529798cc4c31d4 Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 28 Nov 2024 10:42:39 +0530 Subject: [PATCH 2/9] fix: some ui/ux changes --- .../pages/devtools/log-browser/LogBrowser.vue | 67 +--------- .../pages/devtools/log-browser/LogList.vue | 117 ++++++++++++++++++ .../pages/devtools/log-browser/LogViewer.vue | 78 ++++++------ press/api/log_browser.py | 5 +- 4 files changed, 160 insertions(+), 107 deletions(-) create mode 100644 dashboard/src2/pages/devtools/log-browser/LogList.vue diff --git a/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue b/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue index 0d3eda52d3..0f43bf3d8a 100644 --- a/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue +++ b/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue @@ -106,68 +106,7 @@

-
-
- - - -
-
- - Fetching logs... -
-
- No logs found -
- -
-
-
+
import Header from '../../../components/Header.vue'; import LinkControl from '../../../components/LinkControl.vue'; +import LogList from './LogList.vue'; import LogViewer from './LogViewer.vue'; import { Breadcrumbs } from 'frappe-ui'; export default { components: { Header, + LogList, LogViewer, LinkControl, Breadcrumbs @@ -274,7 +215,7 @@ export default { // filter out rotated logs that ends with .1, .2, .3, etc // TODO: do the filtering in agent instead - logs = logs.filter(log => !log.name.match(/\.\d+$/)); + // logs = logs.filter(log => !log.name.match(/\.\d+$/)); if (this.searchLogQuery) { logs = logs.filter(log => diff --git a/dashboard/src2/pages/devtools/log-browser/LogList.vue b/dashboard/src2/pages/devtools/log-browser/LogList.vue new file mode 100644 index 0000000000..a7acc8a7db --- /dev/null +++ b/dashboard/src2/pages/devtools/log-browser/LogList.vue @@ -0,0 +1,117 @@ + + + diff --git a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue index 832327164d..d64084f978 100644 --- a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue +++ b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue @@ -1,5 +1,5 @@ @@ -284,7 +280,7 @@ const table = useVueTable({ }, initialState: { pagination: { - pageSize: 15, + pageSize: 20, pageIndex: 0 } }, diff --git a/press/api/log_browser.py b/press/api/log_browser.py index b0ac980172..e95c5b40e0 100644 --- a/press/api/log_browser.py +++ b/press/api/log_browser.py @@ -311,10 +311,9 @@ def get_log(log_type: LOG_TYPE, doc_name: str, log_name: str) -> list: def get_raw_log(log_type: LOG_TYPE, doc_name: str, log_name: str) -> list: if log_type == LOG_TYPE.BENCH: - # group = frappe.get_value('Bench', doc_name, 'group') - return frappe.get_doc("Bench", doc_name).get_server_log_for_log_browser(log_name) + return frappe.get_doc("Bench", doc_name).get_server_log(log_name) if log_type == LOG_TYPE.SITE: - return frappe.get_doc("Site", doc_name).get_server_log_for_log_browser(log_name) + return frappe.get_doc("Site", doc_name).get_server_log(log_name) return frappe.throw("Invalid log type") From 5b548c8ea2d79049bf4f025191c77f244300b99a Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 28 Nov 2024 10:46:31 +0530 Subject: [PATCH 3/9] chore: ruff lint --- press/press/doctype/bench/bench.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/press/press/doctype/bench/bench.py b/press/press/doctype/bench/bench.py index 4598c26338..29bd5b4bf2 100644 --- a/press/press/doctype/bench/bench.py +++ b/press/press/doctype/bench/bench.py @@ -1106,8 +1106,10 @@ def archive_obsolete_benches_for_server(benches: Iterable[dict]): # Bench is Broken but a reset to a working state is being attempted if ( bench.resetting_bench - or bench.last_archive_failure - and bench.last_archive_failure > frappe.utils.add_to_date(None, hours=-24) + or ( + bench.last_archive_failure + and bench.last_archive_failure > frappe.utils.add_to_date(None, hours=-24) + ) or get_archive_jobs(bench.name) # already being archived or get_ongoing_jobs(bench.name) or get_active_site_updates(bench.name) From 511f14cf0755edb9ba1173998feb3517654eb510 Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 28 Nov 2024 11:04:31 +0530 Subject: [PATCH 4/9] fix: show pagination bar at all times --- .../pages/devtools/log-browser/LogViewer.vue | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue index d64084f978..8bf67f3e59 100644 --- a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue +++ b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue @@ -151,12 +151,15 @@ + +
+

+ No log entries found +

+
-
+

@@ -183,16 +186,6 @@

- -
-

No logs found

-
@@ -296,15 +289,14 @@ const table = useVueTable({ const pageLength = computed(() => table.getState().pagination.pageSize); const currPage = computed(() => table.getState().pagination.pageIndex + 1); -const pageStart = computed(() => (currPage.value - 1) * pageLength.value + 1); +const totalRows = computed(() => props.log.length); +const pageStart = computed(() => + totalRows.value ? (currPage.value - 1) * pageLength.value + 1 : 0 +); const pageEnd = computed(() => { const end = currPage.value * pageLength.value; return end > props.log.length ? props.log.length : end; }); -const totalRows = computed(() => props.log.length); -const showPagination = computed( - () => props.log?.length && totalRows.value > pageLength.value -); function capitalizeFirstLetter(string) { if (!string) return ''; From 3404a779fa5c96f983ed04b4c887023225023a4c Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 28 Nov 2024 11:16:56 +0530 Subject: [PATCH 5/9] fix: nudge user to use log browser --- dashboard/src2/components/group/BenchLogsDialog.vue | 13 +++++++++++++ dashboard/src2/components/site/SiteLogs.vue | 11 +++++++++++ 2 files changed, 24 insertions(+) diff --git a/dashboard/src2/components/group/BenchLogsDialog.vue b/dashboard/src2/components/group/BenchLogsDialog.vue index 523e16df09..1b80848a82 100644 --- a/dashboard/src2/components/group/BenchLogsDialog.vue +++ b/dashboard/src2/components/group/BenchLogsDialog.vue @@ -41,6 +41,7 @@ import { createResource } from 'frappe-ui'; import { defineProps, ref } from 'vue'; import ObjectList from '../ObjectList.vue'; import { date } from '../../utils/format'; +import router from '../../router'; const props = defineProps({ bench: String @@ -100,6 +101,18 @@ const listOptions = ref({ return value ? date(value, 'lll') : ''; } } + ], + actions: () => [ + { + label: '✨ View in Log Browser', + onClick: () => { + show.value = false; + router.push({ + name: 'Log Browser', + params: { mode: 'bench', docName: props.bench } + }); + } + } ] }); diff --git a/dashboard/src2/components/site/SiteLogs.vue b/dashboard/src2/components/site/SiteLogs.vue index 7b4f5fea97..2c5ce6abc8 100644 --- a/dashboard/src2/components/site/SiteLogs.vue +++ b/dashboard/src2/components/site/SiteLogs.vue @@ -192,6 +192,17 @@ export default { }; } } + ], + actions: () => [ + { + label: '✨ View in Log Browser', + onClick: () => { + this.$router.push({ + name: 'Log Browser', + params: { mode: 'site', docName: this.name } + }); + } + } ] }; } From 25aaa694111067d6d4084cc15e705898f7daca5f Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 28 Nov 2024 11:43:53 +0530 Subject: [PATCH 6/9] fix: broken search --- .../pages/devtools/log-browser/LogBrowser.vue | 41 ------------------- .../pages/devtools/log-browser/LogList.vue | 18 ++++++-- 2 files changed, 15 insertions(+), 44 deletions(-) diff --git a/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue b/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue index 0f43bf3d8a..f21b080732 100644 --- a/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue +++ b/dashboard/src2/pages/devtools/log-browser/LogBrowser.vue @@ -170,27 +170,6 @@ export default { }; }, resources: { - benchLogs() { - return { - url: 'press.api.bench.logs', - params: { - name: this.bench?.split('-').slice(0, 2).join('-'), // TODO: fetch group instead of hardcoding - bench: this.bench - }, - auto: this.mode === 'bench' && this.bench, - cache: ['BenchLogs', this.bench] - }; - }, - siteLogs() { - return { - url: 'press.api.site.logs', - params: { - name: this.site - }, - auto: this.mode === 'site' && this.site, - cache: ['SiteLogs', this.site] - }; - }, log() { return { url: 'press.api.log_browser.get_log', @@ -205,26 +184,6 @@ export default { } }, computed: { - logs() { - let logs = []; - if (this.mode === 'bench') { - logs = this.$resources.benchLogs?.data || []; - } else if (this.mode === 'site') { - logs = this.$resources.siteLogs?.data || []; - } - - // filter out rotated logs that ends with .1, .2, .3, etc - // TODO: do the filtering in agent instead - // logs = logs.filter(log => !log.name.match(/\.\d+$/)); - - if (this.searchLogQuery) { - logs = logs.filter(log => - log.name.toLowerCase().includes(this.searchLogQuery.toLowerCase()) - ); - } - - return logs; - }, log() { return this.$resources.log?.data; }, diff --git a/dashboard/src2/pages/devtools/log-browser/LogList.vue b/dashboard/src2/pages/devtools/log-browser/LogList.vue index a7acc8a7db..d65b39d0eb 100644 --- a/dashboard/src2/pages/devtools/log-browser/LogList.vue +++ b/dashboard/src2/pages/devtools/log-browser/LogList.vue @@ -93,12 +93,24 @@ export default { }, computed: { logs() { + let logs = []; if (this.mode === 'bench') { - return this.$resources.benchLogs?.data || []; + logs = this.$resources.benchLogs?.data || []; } else if (this.mode === 'site') { - return this.$resources.siteLogs?.data || []; + logs = this.$resources.siteLogs?.data || []; } - return []; + + // filter out rotated logs that ends with .1, .2, .3, etc + // TODO: do the filtering in agent instead + // logs = logs.filter(log => !log.name.match(/\.\d+$/)); + + if (this.searchLogQuery) { + logs = logs.filter(log => + log.name.toLowerCase().includes(this.searchLogQuery.toLowerCase()) + ); + } + + return logs; }, bench() { if (this.mode === 'bench') { From e0a6d0243fea2d7e654859e3b4e40e40677abb33 Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 28 Nov 2024 11:53:26 +0530 Subject: [PATCH 7/9] fix: show latest entries first --- dashboard/src2/pages/devtools/log-browser/LogViewer.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue index 8bf67f3e59..ac016628e2 100644 --- a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue +++ b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue @@ -217,7 +217,7 @@ const props = defineProps({ const searchLogQuery = ref(''); const levelFilter = ref(''); -const sortOrder = ref(''); +const sortOrder = ref('desc'); const columnFilters = computed(() => { const filters = []; From 8976277904333b5527a535fb658b8bd62b2eae0c Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 28 Nov 2024 12:44:00 +0530 Subject: [PATCH 8/9] perf: better way to parse monitor.json.log --- press/api/log_browser.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/press/api/log_browser.py b/press/api/log_browser.py index e95c5b40e0..8c88396b8c 100644 --- a/press/api/log_browser.py +++ b/press/api/log_browser.py @@ -203,23 +203,22 @@ def monitor_json_log_formatter(log_entries: list) -> list: list: A list of dictionaries, where each dictionary represents a formatted log entry. """ - import json - if not log_entries: return [] # Return empty list if no log entries formatted_logs = [] for entry in log_entries: - # parse the json log entry try: - log_entry = json.loads(entry) - time = log_entry.get("timestamp") - formatted_time = datetime.datetime.strptime(time, "%Y-%m-%d %H:%M:%S.%f%z").strftime( + timestamp_key = '"timestamp":"' + timestamp_start = entry.index(timestamp_key) + len(timestamp_key) + timestamp_end = entry.index('"', timestamp_start) + time = entry[timestamp_start:timestamp_end] + formatted_time = datetime.datetime.strptime(time, "%Y-%m-%d %H:%M:%S.%f").strftime( "%Y-%m-%d %H:%M:%S" ) formatted_logs.append({"time": formatted_time, "description": entry}) - except json.JSONDecodeError: + except ValueError: formatted_logs.append({"description": entry}) return formatted_logs From f43d9baeb479627eb29604d006d894e1b9644a78 Mon Sep 17 00:00:00 2001 From: Bread Genie Date: Thu, 28 Nov 2024 12:44:23 +0530 Subject: [PATCH 9/9] fix: fixed width and mono time --- .../pages/devtools/log-browser/LogViewer.vue | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue index ac016628e2..87104cba05 100644 --- a/dashboard/src2/pages/devtools/log-browser/LogViewer.vue +++ b/dashboard/src2/pages/devtools/log-browser/LogViewer.vue @@ -61,6 +61,10 @@ :key="header.id" :colSpan="header.colSpan" class="text-gray-800" + :class="{ + 'w-2/12': header.column.columnDef.id === 'level', + 'w-3/12': header.column.columnDef.id === 'time' + }" >
-

- -