diff --git a/App/Backend/cmd/kubernetes/handlers/MetricsSocket.go b/App/Backend/cmd/kubernetes/handlers/MetricsSocket.go new file mode 100644 index 0000000..56b6bf7 --- /dev/null +++ b/App/Backend/cmd/kubernetes/handlers/MetricsSocket.go @@ -0,0 +1,48 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/gorilla/websocket" + "log" + "net/http" + "time" +) + +var upgrader = websocket.Upgrader{ + CheckOrigin: func(r *http.Request) bool { + origin := r.Header.Get("Origin") + return origin == GetFrontendIP() + }, +} + +func HandleMetricsSocket(ctx *gin.Context) { + conn, err := upgrader.Upgrade(ctx.Writer, ctx.Request, nil) + if err != nil { + log.Println("Error upgrading metrics socket:", err) + return + } + defer func(conn *websocket.Conn) { + err := conn.Close() + if err != nil { + log.Println("Error closing metrics socket:", err) + } + }(conn) + + for { + stats, err := c.GetTotalUsage() + if err != nil { + log.Println("Error getting metrics stats:", err) + return + } + + err = conn.WriteJSON(stats) + if err != nil { + log.Println("Error writing metrics stats:", err) + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { + log.Println("WebSocket connection closed by client.") + } + break + } + time.Sleep(500 * time.Millisecond) + } +} diff --git a/App/Backend/cmd/main.go b/App/Backend/cmd/main.go index daabf13..e0cb9de 100644 --- a/App/Backend/cmd/main.go +++ b/App/Backend/cmd/main.go @@ -12,9 +12,11 @@ import ( "time" ) +var frontendUrl = handlers.GetFrontendIP() + func main() { - frontendUrl := handlers.GetFrontendIP() - trustedProxies := []string{"10.0.0.0/8"} + frontendUrl = handlers.GetFrontendIP() + trustedProxies := []string{"10.0.0.0/8"} if frontendUrl == "http://localhost:4200" { gin.SetMode(gin.DebugMode) @@ -56,6 +58,7 @@ func main() { secured.POST("/createresources", handlers.CreateResourcesHandler) secured.GET("/namespaces", handlers.GetNamespacesHandler) secured.GET("/nodenames", handlers.GetNodeNamesHandler) + secured.GET("/statsocket", handlers.HandleMetricsSocket) err = r.Run(":8082") if err != nil { diff --git a/App/Backend/go.mod b/App/Backend/go.mod index cdd90f9..759191b 100644 --- a/App/Backend/go.mod +++ b/App/Backend/go.mod @@ -38,6 +38,7 @@ require ( github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect diff --git a/App/Backend/go.sum b/App/Backend/go.sum index bd2d8e2..3f6e2df 100644 --- a/App/Backend/go.sum +++ b/App/Backend/go.sum @@ -63,6 +63,8 @@ github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgY github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/App/Frontend/src/app/dashboard/dashboard.component.html b/App/Frontend/src/app/dashboard/dashboard.component.html index c346168..d9700e5 100644 --- a/App/Frontend/src/app/dashboard/dashboard.component.html +++ b/App/Frontend/src/app/dashboard/dashboard.component.html @@ -44,33 +44,31 @@

{{ 'DASHBOARD.RAM' | translate }}

{{ 'DASHBOARD.DISK' | translate }}

-
+

- - {{ usage.DiskUsage | byteFormat }} / {{ usage.DiskCapacity | byteFormat }} + + {{ diskUsage | byteFormat }} / {{ diskCapacity | byteFormat }}

- - -
- - - - -
-
K10s
-
-
- -
-
-
- - - -
-
- + + + + +
+
K10s
+
+
+ +
+
+
+ + + +
+
+ \ No newline at end of file diff --git a/App/Frontend/src/app/dashboard/dashboard.component.ts b/App/Frontend/src/app/dashboard/dashboard.component.ts index ed87ac9..32d3fa6 100644 --- a/App/Frontend/src/app/dashboard/dashboard.component.ts +++ b/App/Frontend/src/app/dashboard/dashboard.component.ts @@ -3,25 +3,25 @@ /* @author K10s Open Source Team */ /**********************************/ -import {AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; -import { NavComponent } from '../nav/nav.component'; -import { FooterComponent } from "../footer/footer.component"; -import { TranslatePipe } from "@ngx-translate/core"; -import { StatsService } from "../services/stats.service"; -import { ByteFormatPipe } from "../byte-format.pipe"; -import { Color, NgxChartsModule, ScaleType } from "@swimlane/ngx-charts"; -import { LoadingComponent } from "../loading/loading.component"; -import { SpiderWebComponent } from "../spider-web/spider-web.component"; +import {AfterViewInit, Component, ElementRef, OnDestroy, OnInit, ViewChild} from '@angular/core'; +import {NavComponent} from '../nav/nav.component'; +import {FooterComponent} from "../footer/footer.component"; +import {TranslatePipe} from "@ngx-translate/core"; +import {ByteFormatPipe} from "../byte-format.pipe"; +import {Color, NgxChartsModule, ScaleType} from "@swimlane/ngx-charts"; +import {SpiderWebComponent} from "../spider-web/spider-web.component"; +import {StatWebSocketService} from "../services/statWebsocket.service"; +import {Metrics} from "../domain/Metrics"; @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html', styleUrls: ['./dashboard.component.css'], - imports: [NavComponent, FooterComponent, TranslatePipe, ByteFormatPipe, NgxChartsModule, SpiderWebComponent, LoadingComponent], + imports: [NavComponent, FooterComponent, TranslatePipe, ByteFormatPipe, NgxChartsModule, SpiderWebComponent], standalone: true }) -export class DashboardComponent implements AfterViewInit, OnInit { +export class DashboardComponent implements AfterViewInit, OnInit, OnDestroy { // Fullscreen button @ViewChild('dashboardMain') dashboardMain!: ElementRef; @ViewChild('dashboardTitle') dashboardTitle!: ElementRef; @@ -58,12 +58,14 @@ export class DashboardComponent implements AfterViewInit, OnInit { } // get stats - usage: any = null; + usage: Metrics | undefined = undefined; memoryChartData: any[] = []; cpuChartData: any[] = []; diskUsagePercentage: number = 0.00; + diskUsage: number = 0.00; + diskCapacity: number = 0.00; - colorScheme:Color = { + colorScheme: Color = { name: 'customScheme', selectable: true, group: ScaleType.Ordinal, @@ -72,48 +74,48 @@ export class DashboardComponent implements AfterViewInit, OnInit { colorSchemeCpu: Color = {...this.colorScheme} diskColor = ""; - constructor(private usageService: StatsService) {} - - ngOnInit(): void { - this.loadUsage(); + constructor(private usageService: StatWebSocketService) { } - loading: boolean = false; + ngOnInit(): void { + this.usageService.connect() - loadUsage(): void { - this.loading = true; - this.usageService.getStats().subscribe({ + this.usageService.getMetrics().subscribe({ next: (data) => { - this.usage = data; - this.updateChartData(); + this.updateChartData(data); this.loading = false; }, error: (error) => { console.error(error); - this.loading = false; + this.loading = true; } - }); + }) } + + loading: boolean = false; + valueFormatting(usage: number): string { - return usage+`%`; + return usage + `%`; } - updateChartData(): void { + updateChartData(metrics: Metrics): void { this.memoryChartData = [ - { name: 'Used', value: parseFloat(this.usage.MemUsage.toFixed(2)) }, + {name: 'Used', value: parseFloat(metrics.MemUsage.toFixed(2))}, ]; this.cpuChartData = [ - { name: 'Used', value: parseFloat((this.usage?.CpuUsage || 0).toFixed(2)) }, + {name: 'Used', value: parseFloat(metrics.CpuUsage.toFixed(2))}, ]; - this.diskUsagePercentage = (this.usage.DiskUsage / this.usage.DiskCapacity) * 100; + this.diskUsage = metrics.DiskUsage; + this.diskCapacity = metrics.DiskCapacity; + this.diskUsagePercentage = (metrics.DiskUsage / metrics.DiskCapacity) * 100; this.colorScheme = { ...this.colorScheme, - domain: [this.getUsageColor(this.usage.MemUsage), '#E0E0E0'] + domain: [this.getUsageColor(metrics.MemUsage), '#E0E0E0'] }; this.colorSchemeCpu = { ...this.colorScheme, - domain: [this.getUsageColor(this.usage.CpuUsage), '#E0E0E0'] + domain: [this.getUsageColor(metrics.CpuUsage), '#E0E0E0'] }; this.diskColor = this.getUsageColor(this.diskUsagePercentage); } @@ -128,4 +130,9 @@ export class DashboardComponent implements AfterViewInit, OnInit { if (usage < 85) return orange; return red; } + + ngOnDestroy() { + this.usageService.disconnect(); + } + } \ No newline at end of file diff --git a/App/Frontend/src/app/services/statWebsocket.service.ts b/App/Frontend/src/app/services/statWebsocket.service.ts new file mode 100644 index 0000000..a278e9d --- /dev/null +++ b/App/Frontend/src/app/services/statWebsocket.service.ts @@ -0,0 +1,54 @@ +import {Injectable, OnDestroy} from "@angular/core"; +import {Subject} from "rxjs"; +import {Metrics} from "../domain/Metrics"; +import {environment} from "../../environments/environment"; + +@Injectable({ + providedIn: 'root' +}) +export class StatWebSocketService implements OnDestroy { + private url = `${environment.BASE_URL}/secured/statsocket`; + private socket!: WebSocket; + private messagesSubject: Subject = new Subject() + + constructor() { + } + + connect(): void { + this.socket = new WebSocket(this.url); + + this.socket.onopen = () => { + } + + this.socket.onmessage = (event: MessageEvent) => { + try { + const data: Metrics = JSON.parse(event.data); + this.messagesSubject.next(data) + } catch (error) { + console.error("[WebSocketService] Error parsing data:", error) + } + } + + this.socket.onerror = () => { + console.error("[WebSocketService] WebSocket Error: " + this.url); + } + + this.socket.onclose = () => { + } + } + + disconnect(): void { + if (this.socket && this.socket.readyState === WebSocket.OPEN) { + this.socket.close(); + } + } + + getMetrics() { + return this.messagesSubject.asObservable(); + } + + ngOnDestroy() { + this.disconnect(); + } + +} \ No newline at end of file diff --git a/App/Frontend/src/app/services/stats.service.ts b/App/Frontend/src/app/services/stats.service.ts index a81ae93..d0c31b9 100644 --- a/App/Frontend/src/app/services/stats.service.ts +++ b/App/Frontend/src/app/services/stats.service.ts @@ -3,11 +3,11 @@ /* @author K10s Open Source Team */ /**********************************/ -import { Injectable } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { Observable } from 'rxjs'; -import { Metrics } from "../domain/Metrics"; -import { environment } from "../../environments/environment"; +import {Injectable} from '@angular/core'; +import {HttpClient} from '@angular/common/http'; +import {Observable} from 'rxjs'; +import {Metrics} from "../domain/Metrics"; +import {environment} from "../../environments/environment"; @Injectable({ providedIn: 'root' @@ -22,4 +22,6 @@ export class StatsService { getStats(): Observable { return this.http.get(this.apiUrl, {withCredentials: true}); } + + } \ No newline at end of file