From d4b511cbb7f9cba8cd19f2b100c66c65de34a34c Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Mon, 25 Nov 2024 12:45:35 +0200 Subject: [PATCH] feat: component --- lnbits/core/crud/audit.py | 70 +++++++- lnbits/core/migrations.py | 1 + lnbits/core/models/audit.py | 5 +- lnbits/core/templates/audit/index.html | 31 ++++ lnbits/core/views/audit_api.py | 20 ++- lnbits/middleware.py | 8 +- lnbits/static/js/audit.js | 226 ++++++++++++++++++++++++- 7 files changed, 353 insertions(+), 8 deletions(-) diff --git a/lnbits/core/crud/audit.py b/lnbits/core/crud/audit.py index 1f5972977..537952ac2 100644 --- a/lnbits/core/crud/audit.py +++ b/lnbits/core/crud/audit.py @@ -26,7 +26,7 @@ async def get_audit_entries( ) -async def get_audit_stats( +async def get_request_method_stats( filters: Optional[Filters[AuditFilters]] = None, conn: Optional[Connection] = None, ) -> list[AuditCountStat]: @@ -39,10 +39,76 @@ async def get_audit_stats( FROM audit {clause} GROUP BY request_method - ORDER BY total DESC + ORDER BY request_method """, values=filters.values(), model=AuditCountStat, ) return request_methods + + +async def get_component_stats( + filters: Optional[Filters[AuditFilters]] = None, + conn: Optional[Connection] = None, +) -> list[AuditCountStat]: + if not filters: + filters = Filters() + clause = filters.where() + components = await (conn or db).fetchall( + query=f""" + SELECT component as field, count(component) as total + FROM audit + {clause} + GROUP BY component + ORDER BY component + """, + values=filters.values(), + model=AuditCountStat, + ) + + return components + + +async def get_response_codes_stats( + filters: Optional[Filters[AuditFilters]] = None, + conn: Optional[Connection] = None, +) -> list[AuditCountStat]: + if not filters: + filters = Filters() + clause = filters.where() + request_methods = await (conn or db).fetchall( + query=f""" + SELECT response_code as field, count(response_code) as total + FROM audit + {clause} + GROUP BY response_code + ORDER BY response_code + """, + values=filters.values(), + model=AuditCountStat, + ) + + return request_methods + + +async def get_long_duration_stats( + filters: Optional[Filters[AuditFilters]] = None, + conn: Optional[Connection] = None, +) -> list[AuditCountStat]: + if not filters: + filters = Filters() + clause = filters.where() + long_duration_paths = await (conn or db).fetchall( + query=f""" + SELECT path as field, max(duration) as total FROM audit + {clause} + GROUP BY path + ORDER BY total DESC + LIMIT 5 + """, + values=filters.values(), + model=AuditCountStat, + ) + + return long_duration_paths diff --git a/lnbits/core/migrations.py b/lnbits/core/migrations.py index 89aefafca..95249c47a 100644 --- a/lnbits/core/migrations.py +++ b/lnbits/core/migrations.py @@ -671,6 +671,7 @@ async def m029_create_audit_table(db): f""" CREATE TABLE IF NOT EXISTS audit ( id {db.serial_primary_key}, + component TEXT, ip_address TEXT, user_id TEXT, path TEXT, diff --git a/lnbits/core/models/audit.py b/lnbits/core/models/audit.py index d6059268d..8616750da 100644 --- a/lnbits/core/models/audit.py +++ b/lnbits/core/models/audit.py @@ -11,6 +11,7 @@ from lnbits.settings import settings class AuditEntry(BaseModel): id: Optional[int] = None + component: Optional[str] = None ip_address: Optional[str] = None user_id: Optional[str] = None path: Optional[str] = None @@ -50,9 +51,11 @@ class AuditFilters(FilterModel): class AuditCountStat(BaseModel): field: str - total: int + total: float class AuditStats(BaseModel): request_method: list[AuditCountStat] = [] response_code: list[AuditCountStat] = [] + component: list[AuditCountStat] = [] + long_duration: list[AuditCountStat] = [] diff --git a/lnbits/core/templates/audit/index.html b/lnbits/core/templates/audit/index.html index 9c68a65f9..661fa5086 100644 --- a/lnbits/core/templates/audit/index.html +++ b/lnbits/core/templates/audit/index.html @@ -1,6 +1,37 @@ {% extends "base.html" %} {% from "macros.jinja" import window_vars with context %} {% block page %} +
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+ +
+ +
+
+
+
+
diff --git a/lnbits/core/views/audit_api.py b/lnbits/core/views/audit_api.py index 16d96d006..4574e7797 100644 --- a/lnbits/core/views/audit_api.py +++ b/lnbits/core/views/audit_api.py @@ -1,6 +1,12 @@ from fastapi import APIRouter, Depends -from lnbits.core.crud.audit import get_audit_entries, get_audit_stats +from lnbits.core.crud.audit import ( + get_audit_entries, + get_component_stats, + get_long_duration_stats, + get_request_method_stats, + get_response_codes_stats, +) from lnbits.core.models import AuditEntry, AuditFilters from lnbits.core.models.audit import AuditStats from lnbits.db import Filters, Page @@ -33,5 +39,13 @@ async def api_get_audit( async def api_get_audit_stats( filters: Filters = Depends(parse_filters(AuditFilters)), ) -> AuditStats: - request_mothod_stats = await get_audit_stats(filters) - return AuditStats(request_method=request_mothod_stats) + request_mothod_stats = await get_request_method_stats(filters) + response_code_stats = await get_response_codes_stats(filters) + components_stats = await get_component_stats(filters) + long_duration_stats = await get_long_duration_stats(filters) + return AuditStats( + request_method=request_mothod_stats, + response_code=response_code_stats, + component=components_stats, + long_duration=long_duration_stats + ) diff --git a/lnbits/middleware.py b/lnbits/middleware.py index 39008160a..6a5f0e54e 100644 --- a/lnbits/middleware.py +++ b/lnbits/middleware.py @@ -168,10 +168,16 @@ class AuditMiddleware(BaseHTTPMiddleware): user_id = request.scope.get("user_id", None) if settings.is_super_user(user_id): user_id = "super_user" + path: Optional[str] = getattr(request.scope.get("route", {}), "path", None) + component = "core" + if path and not path.startswith("/api"): + component = path.split("/")[1] + data = AuditEntry( + component=component, ip_address=ip_address, user_id=user_id, - path=getattr(request.scope.get("route", {}), "path", None), + path=path, request_type=request.scope.get("type", None), request_method=http_method, request_details=request_details, diff --git a/lnbits/static/js/audit.js b/lnbits/static/js/audit.js index 2541b3a3b..cceb17138 100644 --- a/lnbits/static/js/audit.js +++ b/lnbits/static/js/audit.js @@ -85,6 +85,9 @@ window.app = Vue.createApp({ async created() { await this.fetchAudit() }, + mounted() { + this.initCharts() + }, methods: { async fetchAudit(props) { @@ -94,13 +97,66 @@ window.app = Vue.createApp({ 'GET', `/audit/api/v1?${params}` ) - this.auditTable.loading = false + this.auditTable.pagination.rowsNumber = data.total this.auditEntries = data.data + await this.fetchAuditStats(props) + } catch (error) { + console.warn(error) + LNbits.utils.notifyApiError(error) + } finally { + this.auditTable.loading = false + } + }, + async fetchAuditStats(props) { + try { + const params = LNbits.utils.prepareFilterQuery(this.auditTable, props) + const {data} = await LNbits.api.request( + 'GET', + `/audit/api/v1/stats?${params}` + ) + console.log('### data', data) + + this.requestMethodChart.data.labels = data.request_method.map( + rm => rm.field + ) + this.requestMethodChart.data.datasets[0].data = data.request_method.map( + rm => rm.total + ) + + this.requestMethodChart.update() + + this.responseCodeChart.data.labels = data.response_code.map( + rm => rm.field + ) + this.responseCodeChart.data.datasets[0].data = data.response_code.map( + rm => rm.total + ) + + this.responseCodeChart.update() + + this.componentUseChart.data.labels = data.component.map( + rm => rm.field + ) + this.componentUseChart.data.datasets[0].data = data.component.map( + rm => rm.total + ) + + this.componentUseChart.update() + + this.longDurationChart.data.labels = data.long_duration.map( + rm => rm.field + ) + this.longDurationChart.data.datasets[0].data = data.long_duration.map( + rm => rm.total + ) + + this.longDurationChart.update() } catch (error) { console.warn(error) LNbits.utils.notifyApiError(error) } + }, async searchAuditBy(fieldName) { const fieldValue = this.searchData[fieldName] @@ -132,6 +188,174 @@ window.app = Vue.createApp({ return value } return `${value.substring(0, 5)}...${value.substring(valueLength - 5, valueLength)}` + }, + initCharts() { + // Chart.defaults.color = 'secondary' + this.responseCodeChart = new Chart( + this.$refs.responseCodeChart.getContext('2d'), + { + type: 'doughnut', + + options: { + responsive: true, + + + plugins: { + legend: { + position: 'bottom' + }, + title: { + display: true, + text: 'HTTP Response Codes' + } + } + }, + data: { + datasets: [ + { + label: '', + data: [20, 10], + backgroundColor: [ + 'rgb(100, 99, 200)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)', + 'rgb(255, 5, 86)', + 'rgb(25, 205, 86)', + 'rgb(255, 205, 250)' + ] + } + ], + labels: [] + } + } + ) + this.requestMethodChart = new Chart( + this.$refs.requestMethodChart.getContext('2d'), + { + type: 'bar', + + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + + title: { + display: true, + text: 'HTTP Methods' + } + } + }, + data: { + datasets: [ + { + label: 'HTTP Methods', + data: [], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)', + 'rgb(255, 5, 86)', + 'rgb(25, 205, 86)', + 'rgb(255, 205, 250)' + ], + hoverOffset: 4 + } + ] + } + } + ) + this.componentUseChart = new Chart( + this.$refs.componentUseChart.getContext('2d'), + { + type: 'pie', + options: { + responsive: true, + plugins: { + legend: { + position: 'xxx' + }, + title: { + display: true, + text: 'Components' + } + }, + onClick: (event, elements, chart) => { + if (elements[0]) { + const i = elements[0].index; + console.log("#### click",chart.data.labels[i] + ': ' + chart.data.datasets[0]); + } + } + }, + data: { + datasets: [ + { + label: 'Components', + data: [], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)', + 'rgb(255, 5, 86)', + 'rgb(25, 205, 86)', + 'rgb(255, 205, 250)', + 'rgb(100, 205, 250)', + 'rgb(120, 205, 250)', + 'rgb(140, 205, 250)', + 'rgb(160, 205, 250)' + ], + hoverOffset: 4 + } + ] + } + } + ) + this.longDurationChart = new Chart( + this.$refs.longDurationChart.getContext('2d'), + { + type: 'bar', + options: { + responsive: true, + indexAxis: 'y', + maintainAspectRatio: false, + plugins: { + legend: { + position: 'xxx' + }, + title: { + display: true, + text: 'Long Duration' + } + }, + onClick: (event, elements, chart) => { + if (elements[0]) { + const i = elements[0].index; + console.log("#### click",chart.data.labels[i] + ': ' + chart.data.datasets[0]); + } + } + }, + data: { + datasets: [ + { + label: 'Components', + data: [], + backgroundColor: [ + 'rgb(255, 99, 132)', + 'rgb(54, 162, 235)', + 'rgb(255, 205, 86)', + 'rgb(255, 5, 86)', + 'rgb(25, 205, 86)', + 'rgb(255, 205, 250)', + 'rgb(100, 205, 250)', + 'rgb(120, 205, 250)', + 'rgb(140, 205, 250)', + 'rgb(160, 205, 250)' + ], + hoverOffset: 4 + } + ] + } + } + ) } } })