feat: component

This commit is contained in:
Vlad Stan 2024-11-25 12:45:35 +02:00
parent 2bb0d40244
commit d4b511cbb7
7 changed files with 353 additions and 8 deletions

View File

@ -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

View File

@ -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,

View File

@ -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] = []

View File

@ -1,6 +1,37 @@
{% extends "base.html" %} {% from "macros.jinja" import window_vars with context
%} {% block page %}
<div class="row q-col-gutter-md justify-center q-mb-xl">
<div class="col-lg-3 col-md-6 col-sm-12">
<q-card>
<div style="width: 250px; height:250px" class="q-pa-sm">
<canvas ref="responseCodeChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12">
<q-card>
<div style="width: 250px; height:250px" class="q-pa-sm">
<canvas ref="requestMethodChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12">
<q-card>
<div style="width: 250px" class="q-pa-sm">
<canvas ref="componentUseChart"></canvas>
</div>
</q-card>
</div>
<div class="col-lg-3 col-md-6 col-sm-12">
<q-card>
<div style="width: 250px; height:250px" class="q-pa-sm">
<canvas ref="longDurationChart"></canvas>
</div>
</q-card>
</div>
</div>
<div class="row q-col-gutter-md justify-center">
<div class="col">
<q-card class="q-pa-md">

View File

@ -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
)

View File

@ -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,

View File

@ -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
}
]
}
}
)
}
}
})