From 7fc7ec61b948fb91f603902007e98744626d0369 Mon Sep 17 00:00:00 2001 From: phoenix <vova24848@gmail.com> Date: Wed, 23 Oct 2024 13:30:27 +0300 Subject: [PATCH 1/2] Add chart for health check --- CHANGELOG.md | 3 + client/package-lock.json | 18 ++- client/package.json | 3 +- client/src/main.ts | 2 + client/src/plugins/chartjs.ts | 20 +++ .../src/views/HealthCheck/HealthCheckView.vue | 20 +-- .../views/HealthCheck/HealthDay.service.ts | 83 ++++++++++++ .../views/HealthCheck/HealthTargetView.vue | 126 +++++++++++------- .../src/views/Project/Status/StatusRect.vue | 3 +- client/src/views/Utils.ts | 3 + .../api/job/health_request_job.py | 20 +-- 11 files changed, 227 insertions(+), 74 deletions(-) create mode 100644 client/src/plugins/chartjs.ts create mode 100644 client/src/views/HealthCheck/HealthDay.service.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index afd1025..c2f7313 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [Add widget for moderate users permissions](https://github.com/PhoenixNazarov/prompt-admin/pull/25) - [Create HealthCheck status system](https://github.com/PhoenixNazarov/prompt-admin/pull/27) - [Upgrade status time react, add description](https://github.com/PhoenixNazarov/prompt-admin/pull/27) +- Update design: Add borders, add resizable +- Add Charts for healthCheck ### Changed @@ -47,3 +49,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix start loading a list table component without a filter - Bugfix load popup tables, back home, ident for test_case - [Fix /auth/me return user password, now return password: null](https://github.com/PhoenixNazarov/prompt-admin/pull/25) +- Fix health check, add timezone \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 2e04d45..5abaadd 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -18,7 +18,8 @@ "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", "axios": "==1.7.4", - "chart.js": "^4.4.4", + "chart.js": "^4.4.5", + "chartjs-adapter-moment": "^1.0.1", "dedent": "^1.5.3", "json-editor-vue": "^0.15.1", "moment": "^2.30.1", @@ -1310,9 +1311,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.4.tgz", - "integrity": "sha512-emICKGBABnxhMjUjlYRR12PmOXhJ2eJjEHL2/dZlWjxRAZT1D8xplLFq5M0tMQK8ja+wBS/tuVEJB5C6r7VxJA==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.5.tgz", + "integrity": "sha512-CVVjg1RYTJV9OCC8WeJPMx8gsV8K6WIyIEQUE3ui4AR9Hfgls9URri6Ja3hyMVBbTF8Q2KFa19PE815gWcWhng==", "dependencies": { "@kurkle/color": "^0.3.0" }, @@ -1320,6 +1321,15 @@ "pnpm": ">=8" } }, + "node_modules/chartjs-adapter-moment": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chartjs-adapter-moment/-/chartjs-adapter-moment-1.0.1.tgz", + "integrity": "sha512-Uz+nTX/GxocuqXpGylxK19YG4R3OSVf8326D+HwSTsNw1LgzyIGRo+Qujwro1wy6X+soNSnfj5t2vZ+r6EaDmA==", + "peerDependencies": { + "chart.js": ">=3.0.0", + "moment": "^2.10.2" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", diff --git a/client/package.json b/client/package.json index d04d524..2e11b18 100644 --- a/client/package.json +++ b/client/package.json @@ -20,7 +20,8 @@ "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", "axios": "==1.7.4", - "chart.js": "^4.4.4", + "chart.js": "^4.4.5", + "chartjs-adapter-moment": "^1.0.1", "dedent": "^1.5.3", "json-editor-vue": "^0.15.1", "moment": "^2.30.1", diff --git a/client/src/main.ts b/client/src/main.ts index b6b3150..70ad5b1 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -7,6 +7,7 @@ import {installFortAwesome} from "./plugins/fortawesome.ts"; import {installVuetify} from "./plugins/vuetify.ts"; import {installRouter} from "./plugins/router.ts"; import {installPrimevue} from "./plugins/primevue.ts"; +import {installChartJs} from "./plugins/chartjs.ts"; const app = createApp(App) @@ -18,6 +19,7 @@ installFortAwesome(app) installVuetify(app) installRouter(app) installPrimevue(app) +installChartJs(app) // Mount diff --git a/client/src/plugins/chartjs.ts b/client/src/plugins/chartjs.ts new file mode 100644 index 0000000..af12b39 --- /dev/null +++ b/client/src/plugins/chartjs.ts @@ -0,0 +1,20 @@ +import { + BarElement, + CategoryScale, + Chart as ChartJS, + Legend, + LinearScale, + LineElement, + LogarithmicScale, + PointElement, + TimeScale, + Title, + Tooltip +} from 'chart.js' +import type {App} from "vue"; +import 'chartjs-adapter-moment'; + +export function installChartJs(app: App) { + ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, LogarithmicScale, LineElement, PointElement) +} + diff --git a/client/src/views/HealthCheck/HealthCheckView.vue b/client/src/views/HealthCheck/HealthCheckView.vue index 09e9792..bfdae88 100644 --- a/client/src/views/HealthCheck/HealthCheckView.vue +++ b/client/src/views/HealthCheck/HealthCheckView.vue @@ -19,16 +19,18 @@ export default defineComponent({ </script> <template> - <VContainer> - <VCard> - <VCardTitle>Health Monitor</VCardTitle> - <VCardText> - <VSkeletonLoader type="card" v-if="healthCheckStore.loadings.targets"/> + <div class="outer-y" style="height: calc(100vh - 3rem)"> + <VContainer> + <VCard> + <VCardTitle>Health Monitor</VCardTitle> + <VCardText> + <VSkeletonLoader type="card" v-if="healthCheckStore.loadings.targets"/> - <HealthTargetView v-else :health-target="target" v-for="target in healthCheckStore.targets"/> - </VCardText> - </VCard> - </VContainer> + <HealthTargetView v-else :health-target="target" v-for="target in healthCheckStore.targets"/> + </VCardText> + </VCard> + </VContainer> + </div> </template> <style scoped> diff --git a/client/src/views/HealthCheck/HealthDay.service.ts b/client/src/views/HealthCheck/HealthDay.service.ts new file mode 100644 index 0000000..d852a48 --- /dev/null +++ b/client/src/views/HealthCheck/HealthDay.service.ts @@ -0,0 +1,83 @@ +import {HealthDay, HealthTarget, useHealthCheckStore} from "../../stores/healthcheck.store.ts"; +import {equalDate} from "../Utils.ts"; + +export class HealthDayService { + private healthDay: HealthDay | undefined + + constructor(healthDay: HealthDay | undefined) { + this.healthDay = healthDay + } + + static fromDay(healthTarget: HealthTarget, d: number) { + const healthCheckStore = useHealthCheckStore() + const date = this.showDate(d) + const healthDay = healthCheckStore.getDays(healthTarget)?.find(el => { + const day = new Date(el.date) + return equalDate(date, day) + }) + return new HealthDayService(healthDay) + } + + static showDate(d: number) { + const date = new Date(); + date.setDate(date.getDate() - d + 1); + return date + } + + getStatus() { + const fallTimes = this.getFallTimes() + if (fallTimes == undefined) return 'Lost' + if (fallTimes > 0) { + return 'Downtime' + } + return `Operational` + } + + getDescription() { + const fallTimes = this.getFallTimes() + const lostTimes = this.getLostTimes() + let res = '' + if (fallTimes != undefined && fallTimes > 0) { + res += `Down for ${fallTimes} minutes` + } + if (lostTimes != undefined && lostTimes > 0) { + res += `<br>Lost for ${lostTimes} minutes` + } + return res; + } + + getPercentage() { + const fallTimes = this.getFallTimes() + const lostTimes = this.getLostTimes() + if (!fallTimes) return undefined + return (fallTimes / (60 * 24)) * 100 + (lostTimes / (60 * 24)) * 30 + } + + getFallTimes() { + const lostTimes = this.getLostTimes() + if (!this.healthDay || lostTimes == undefined) return + return this.healthDay.fall_times + } + + getLostTimes() { + const maxTimes = this.getMaxTimes() + const responseTime = this.healthDay?.count_response_time + if (responseTime == undefined) return 0 + const lostTimes = maxTimes - responseTime + return lostTimes > 30 ? lostTimes : 0 + } + + getMaxTimes() { + let maxTimes = 60 * 24 + if (this.isToday()) { + const now = new Date() + return now.getHours() * 24 + now.getMinutes() + } + return maxTimes + } + + isToday() { + if (!this.healthDay) return + return equalDate(new Date(), new Date(this.healthDay.date)) + } +} \ No newline at end of file diff --git a/client/src/views/HealthCheck/HealthTargetView.vue b/client/src/views/HealthCheck/HealthTargetView.vue index 4a79ad5..a5a5da0 100644 --- a/client/src/views/HealthCheck/HealthTargetView.vue +++ b/client/src/views/HealthCheck/HealthTargetView.vue @@ -1,12 +1,15 @@ <script lang="ts"> import {defineComponent, PropType} from 'vue' -import {HealthDay, HealthTarget, useHealthCheckStore} from "../../stores/healthcheck.store.ts"; +import {HealthTarget, useHealthCheckStore} from "../../stores/healthcheck.store.ts"; import {FontAwesomeIcon} from "@fortawesome/vue-fontawesome"; import StatusRect from "../Project/Status/StatusRect.vue"; +import {HealthDayService} from "./HealthDay.service.ts"; +import {Line} from 'vue-chartjs' +import moment from "moment"; export default defineComponent({ name: "HealthTargetView", - components: {StatusRect, FontAwesomeIcon}, + components: {StatusRect, FontAwesomeIcon, Line}, props: { healthTarget: { type: Object as PropType<HealthTarget>, @@ -19,6 +22,44 @@ export default defineComponent({ healthCheckStore } }, + computed: { + options() { + return { + plugins: { + tooltip: { + mode: 'index', + intersect: false + }, + legend: { + display: false // это отключает отображение легенды на графике + }, + }, + elements: { + point: { + radius: 0.1 + } + }, + scales: { + y: { + ticks: { + // Include a dollar sign in the ticks + callback: function(value, index, ticks) { + return value + ' s'; + } + }, + }, + x: { + type: 'time', + grid: { + display: false + } + } + }, + responsive: true, + maintainAspectRatio: false, + } + } + }, methods: { getTargetStatus() { const lastUnit = this.healthCheckStore.getLastUnit(this.healthTarget) @@ -27,52 +68,33 @@ export default defineComponent({ if (timeSpent > 180) return; - return lastUnit.status; - }, - getStatus(d: number) { - const day = this.getDay(d) - if (day == undefined) return 'Undefined' - const fallTimes = this.getFallTimes(day) - if (fallTimes > 0) { - return 'Downtime' - } - return `Operational` + return lastUnit.status }, - getDescription(d: number) { - const day = this.getDay(d) - if (day == undefined) return undefined - const fallTimes = this.getFallTimes(day) - if (fallTimes > 0) { - return `Down for ${fallTimes} minutes` + getChartData() { + const units = this.healthCheckStore.getUnits(this.healthTarget)?.reverse() + if (units == undefined) return + return { + labels: units?.map(el => moment(el.request_datetime)), + datasets: [{ + label: 'Response Time', + data: units?.map(el => el.response_time), + borderWidth: 1, + borderColor: 'rgb(0,83,255)', + }] } }, - getPercentage(d: number) { - const day = this.getDay(d) - if (day == undefined) return undefined - const fallTimes = this.getFallTimes(day) - return (fallTimes / (60 * 24)) * 200 - }, - getFallTimes(day: HealthDay) { - let maxTimes = 60 * 24 - if (day.id == this.getDay(1)?.id) { - const now = new Date() - maxTimes = now.getHours() * 24 + now.getMinutes() + buildDayStatuses() { + const result = [] + for (let i = 1; i < 90; i++) { + const healthDayService = HealthDayService.fromDay(this.healthTarget, i); + result.push({ + percentage: healthDayService.getPercentage(), + date: HealthDayService.showDate(i), + status: healthDayService.getStatus(), + description: healthDayService.getDescription(), + }) } - - const lostTimes = maxTimes - day.count_response_time - return day.fall_times + lostTimes > 5 ? lostTimes : 0 - }, - showDate(d: number) { - const date = new Date(); - date.setDate(date.getDate() - d + 1); - return date - }, - getDay(d: number) { - const date = this.showDate(d) - return this.healthCheckStore.getDays(this.healthTarget)?.find(el => { - const day = new Date(el.date) - return day.getDate() == date.getDate() && day.getMonth() == date.getMonth() && day.getFullYear() == date.getFullYear() - }) + return result } } }) @@ -98,13 +120,19 @@ export default defineComponent({ <VCardText> <div class="chart"> <StatusRect - :percentage="getPercentage(d)" - :date="showDate(d)" - :status="getStatus(d)" - :description="getDescription(d)" - v-for="d in 90" + :percentage="d.percentage" + :date="d.date" + :status="d.status" + :description="d.description" + v-for="d in buildDayStatuses()" /> </div> + <p class="ma-1"> + Response Times: + </p> + <div style="height: 10rem" v-if="getChartData()"> + <Line :data="getChartData()" :options="options"/> + </div> </VCardText> </VCard> </template> diff --git a/client/src/views/Project/Status/StatusRect.vue b/client/src/views/Project/Status/StatusRect.vue index 896d9e6..01dd9ed 100644 --- a/client/src/views/Project/Status/StatusRect.vue +++ b/client/src/views/Project/Status/StatusRect.vue @@ -50,8 +50,7 @@ export default defineComponent({ <div v-if="status"> {{ status }} </div> - <div v-if="description"> - {{ description }} + <div v-if="description" v-html="description"> </div> <div> {{ dateFormat(date) }} diff --git a/client/src/views/Utils.ts b/client/src/views/Utils.ts index bd68cac..8ced0c3 100644 --- a/client/src/views/Utils.ts +++ b/client/src/views/Utils.ts @@ -55,3 +55,6 @@ export function chunk<T>(arr: T[], size: number): T[][] { ); } +export function equalDate(a: Date, b: Date) { + return a.getDate() == b.getDate() && a.getMonth() == b.getMonth() && a.getFullYear() == b.getFullYear() +} \ No newline at end of file diff --git a/server/promptadmin_server/api/job/health_request_job.py b/server/promptadmin_server/api/job/health_request_job.py index 4276ab2..a78e9d5 100644 --- a/server/promptadmin_server/api/job/health_request_job.py +++ b/server/promptadmin_server/api/job/health_request_job.py @@ -45,15 +45,17 @@ async def polling(self): async def status_target(self, target: HealthTarget): t_start = time.time() - - try: - async with httpx.AsyncClient() as client: - r = await client.get(target.url) - status = r.is_success - if status is None: - print(r.status_code) - except Exception as e: - status = False + status = False + for i in range(3): + t_start = time.time() + try: + async with httpx.AsyncClient() as client: + r = await client.get(target.url) + status = r.is_success + break + except Exception as e: + status = False + continue health_unit = HealthUnit( request_datetime=datetime.datetime.now(pytz.utc), From a051758db2eda329fb355595d39ec2fd68921603 Mon Sep 17 00:00:00 2001 From: phoenix <vova24848@gmail.com> Date: Wed, 23 Oct 2024 13:33:54 +0300 Subject: [PATCH 2/2] bugfix display zero permission value --- client/src/views/HealthCheck/HealthDay.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/views/HealthCheck/HealthDay.service.ts b/client/src/views/HealthCheck/HealthDay.service.ts index d852a48..f34fb83 100644 --- a/client/src/views/HealthCheck/HealthDay.service.ts +++ b/client/src/views/HealthCheck/HealthDay.service.ts @@ -47,15 +47,15 @@ export class HealthDayService { } getPercentage() { + if (this.healthDay == undefined) return const fallTimes = this.getFallTimes() const lostTimes = this.getLostTimes() if (!fallTimes) return undefined - return (fallTimes / (60 * 24)) * 100 + (lostTimes / (60 * 24)) * 30 + return (fallTimes / this.healthDay.count_response_time) * 100 + (lostTimes / (60 * 24)) * 30 } getFallTimes() { - const lostTimes = this.getLostTimes() - if (!this.healthDay || lostTimes == undefined) return + if (!this.healthDay) return return this.healthDay.fall_times }