mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-31 04:23:40 +00:00
statically type server metrics and use websocket (#2124)
Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
committed by
Aiden McClelland
parent
873f2b2814
commit
8313dfaeb9
@@ -1,26 +0,0 @@
|
|||||||
import { NgModule } from '@angular/core'
|
|
||||||
import { CommonModule } from '@angular/common'
|
|
||||||
import { Routes, RouterModule } from '@angular/router'
|
|
||||||
import { IonicModule } from '@ionic/angular'
|
|
||||||
import { AppMetricsPage } from './app-metrics.page'
|
|
||||||
import { SharedPipesModule } from '@start9labs/shared'
|
|
||||||
import { SkeletonListComponentModule } from 'src/app/components/skeleton-list/skeleton-list.component.module'
|
|
||||||
|
|
||||||
const routes: Routes = [
|
|
||||||
{
|
|
||||||
path: '',
|
|
||||||
component: AppMetricsPage,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
IonicModule,
|
|
||||||
RouterModule.forChild(routes),
|
|
||||||
SharedPipesModule,
|
|
||||||
SkeletonListComponentModule,
|
|
||||||
],
|
|
||||||
declarations: [AppMetricsPage],
|
|
||||||
})
|
|
||||||
export class AppMetricsPageModule {}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
<ion-header>
|
|
||||||
<ion-toolbar>
|
|
||||||
<ion-buttons slot="start">
|
|
||||||
<ion-back-button [defaultHref]="'/services/' + pkgId"></ion-back-button>
|
|
||||||
</ion-buttons>
|
|
||||||
<ion-title>Monitor</ion-title>
|
|
||||||
<ion-title slot="end"
|
|
||||||
><ion-spinner name="dots" class="fader"></ion-spinner
|
|
||||||
></ion-title>
|
|
||||||
</ion-toolbar>
|
|
||||||
</ion-header>
|
|
||||||
|
|
||||||
<ion-content class="ion-padding with-widgets">
|
|
||||||
<skeleton-list *ngIf="loading"></skeleton-list>
|
|
||||||
<ion-item-group *ngIf="!loading">
|
|
||||||
<ion-item *ngFor="let metric of metrics | keyvalue : asIsOrder">
|
|
||||||
<ion-label>{{ metric.key }}</ion-label>
|
|
||||||
<ion-note *ngIf="metric.value" slot="end" class="metric-note">
|
|
||||||
<ion-text style="color: white"
|
|
||||||
>{{ metric.value.value }} {{ metric.value.unit }}</ion-text
|
|
||||||
>
|
|
||||||
</ion-note>
|
|
||||||
</ion-item>
|
|
||||||
</ion-item-group>
|
|
||||||
</ion-content>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
.metric-note {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
import { Component } from '@angular/core'
|
|
||||||
import { ActivatedRoute } from '@angular/router'
|
|
||||||
import { Metric } from 'src/app/services/api/api.types'
|
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
|
||||||
import { pauseFor, ErrorToastService, getPkgId } from '@start9labs/shared'
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-metrics',
|
|
||||||
templateUrl: './app-metrics.page.html',
|
|
||||||
styleUrls: ['./app-metrics.page.scss'],
|
|
||||||
})
|
|
||||||
export class AppMetricsPage {
|
|
||||||
loading = true
|
|
||||||
readonly pkgId = getPkgId(this.route)
|
|
||||||
going = false
|
|
||||||
metrics?: Metric
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
private readonly route: ActivatedRoute,
|
|
||||||
private readonly errToast: ErrorToastService,
|
|
||||||
private readonly embassyApi: ApiService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.startDaemon()
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy() {
|
|
||||||
this.stopDaemon()
|
|
||||||
}
|
|
||||||
|
|
||||||
async startDaemon(): Promise<void> {
|
|
||||||
this.going = true
|
|
||||||
while (this.going) {
|
|
||||||
const startTime = Date.now()
|
|
||||||
await this.getMetrics()
|
|
||||||
await pauseFor(Math.max(4000 - (Date.now() - startTime), 0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stopDaemon() {
|
|
||||||
this.going = false
|
|
||||||
}
|
|
||||||
|
|
||||||
async getMetrics(): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.metrics = await this.embassyApi.getPkgMetrics({ id: this.pkgId })
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errToast.present(e)
|
|
||||||
this.stopDaemon()
|
|
||||||
} finally {
|
|
||||||
this.loading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
asIsOrder(a: any, b: any) {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -36,13 +36,6 @@ const routes: Routes = [
|
|||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
|
import('./app-logs/app-logs.module').then(m => m.AppLogsPageModule),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: ':pkgId/metrics',
|
|
||||||
loadChildren: () =>
|
|
||||||
import('./app-metrics/app-metrics.module').then(
|
|
||||||
m => m.AppMetricsPageModule,
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: ':pkgId/properties',
|
path: ':pkgId/properties',
|
||||||
loadChildren: () =>
|
loadChildren: () =>
|
||||||
|
|||||||
@@ -4,58 +4,121 @@
|
|||||||
<ion-back-button defaultHref="system"></ion-back-button>
|
<ion-back-button defaultHref="system"></ion-back-button>
|
||||||
</ion-buttons>
|
</ion-buttons>
|
||||||
<ion-title>Monitor</ion-title>
|
<ion-title>Monitor</ion-title>
|
||||||
<ion-title slot="end"
|
|
||||||
><ion-spinner name="dots" class="fader"></ion-spinner
|
|
||||||
></ion-title>
|
|
||||||
</ion-toolbar>
|
</ion-toolbar>
|
||||||
</ion-header>
|
</ion-header>
|
||||||
|
|
||||||
<ion-content class="ion-padding with-widgets">
|
<ion-content class="ion-padding with-widgets">
|
||||||
<skeleton-list *ngIf="loading" [groups]="2"></skeleton-list>
|
<ion-item-group *ngIf="serverData$ | async as serverData; else loading">
|
||||||
|
<p *ngIf="websocketFail" class="loading-dots ion-text-center">
|
||||||
|
<ion-text color="warning">Websocket Failed. Reconnecting</ion-text>
|
||||||
|
</p>
|
||||||
|
|
||||||
<div id="metricSection">
|
<ng-container *ngIf="serverData[0] as timeInfo">
|
||||||
<ng-container *ngIf="!loading">
|
<ion-item-divider>System Time</ion-item-divider>
|
||||||
<ion-item-group>
|
<ion-item>
|
||||||
<ion-item-divider>Time</ion-item-divider>
|
<ion-label>Current Time (UTC)</ion-label>
|
||||||
<ion-item>
|
<h6 slot="end">
|
||||||
<ion-label>System Time</ion-label>
|
{{ timeInfo.systemCurrentTime | date : 'MMMM d, y, h:mm:ss a' : 'UTC'
|
||||||
<ion-note slot="end" class="metric-note">
|
}}
|
||||||
<ion-text style="color: white"
|
</h6>
|
||||||
>{{ systemTime$ | async | date:'MMMM d, y, h:mm a z':'UTC'
|
</ion-item>
|
||||||
}}</ion-text
|
<ion-item>
|
||||||
>
|
<ion-label>Start Time (UTC)</ion-label>
|
||||||
</ion-note>
|
<h6 slot="end">
|
||||||
</ion-item>
|
{{ timeInfo.systemStartTime | date : 'MMMM d, y, h:mm:ss a' : 'UTC' }}
|
||||||
<ion-item>
|
</h6>
|
||||||
<ion-label>System Uptime</ion-label>
|
</ion-item>
|
||||||
<ion-note
|
<ion-item>
|
||||||
*ngIf="systemUptime$ | async as uptime"
|
<ion-label>Uptime</ion-label>
|
||||||
slot="end"
|
<h6 *ngIf="timeInfo.systemUptime as uptime" slot="end">
|
||||||
class="metric-note"
|
<b>{{ uptime.days }}</b> Days, <b>{{ uptime.hours }}</b> Hours,
|
||||||
>
|
<b>{{ uptime.minutes }}</b> Minutes,
|
||||||
<ion-text style="color: white">
|
<b>{{ uptime.seconds }}</b> Seconds
|
||||||
<b>{{ uptime.days }}</b> Days, <b>{{ uptime.hours }}</b> Hours,
|
</h6>
|
||||||
<b>{{ uptime.minutes }}</b> Minutes
|
</ion-item>
|
||||||
</ion-text>
|
|
||||||
</ion-note>
|
|
||||||
</ion-item>
|
|
||||||
</ion-item-group>
|
|
||||||
|
|
||||||
<ion-item-group
|
|
||||||
*ngFor="let metricGroup of metrics | keyvalue : asIsOrder"
|
|
||||||
>
|
|
||||||
<ion-item-divider>{{ metricGroup.key }}</ion-item-divider>
|
|
||||||
<ion-item
|
|
||||||
*ngFor="let metric of metricGroup.value | keyvalue : asIsOrder"
|
|
||||||
>
|
|
||||||
<ion-label>{{ metric.key }}</ion-label>
|
|
||||||
<ion-note *ngIf="metric.value" slot="end" class="metric-note">
|
|
||||||
<ion-text style="color: white"
|
|
||||||
>{{ metric.value.value }} {{ metric.value.unit }}</ion-text
|
|
||||||
>
|
|
||||||
</ion-note>
|
|
||||||
</ion-item>
|
|
||||||
</ion-item-group>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
|
||||||
|
<ng-container *ngIf="serverData[1] as metrics">
|
||||||
|
<!-- General -->
|
||||||
|
<ion-item-divider>General</ion-item-divider>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Temperature</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.general.temperature }} °C</h6>
|
||||||
|
</ion-item>
|
||||||
|
<!-- Memory -->
|
||||||
|
<ion-item-divider>Memory</ion-item-divider>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Percentage Used</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.memory['percentage-used'] }} %</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Total</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.memory.total }} MiB</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Available</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.memory.available }} MiB</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Used</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.memory.used }} MiB</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Swap Total</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.memory['swap-total'] }} MiB</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Swap Free</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.memory['swap-free'] }} MiB</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Swap Used</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.memory['swap-used'] }} MiB</h6>
|
||||||
|
</ion-item>
|
||||||
|
<!-- CPU -->
|
||||||
|
<ion-item-divider>CPU</ion-item-divider>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>User Space</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.cpu['user-space'] }} %</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Kernel Space</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.cpu['kernel-space'] }} %</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>I/O Wait</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.cpu['io-wait'] }} %</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Idle</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.cpu.idle }} %</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Usage</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.cpu.usage }} %</h6>
|
||||||
|
</ion-item>
|
||||||
|
<!-- Disk -->
|
||||||
|
<ion-item-divider>Disk</ion-item-divider>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Size</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.disk.size }} GB</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Used</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.disk.used }} GB</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Percentage Used</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.disk['percentage-used'] }} %</h6>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>Available</ion-label>
|
||||||
|
<h6 slot="end">{{ metrics.disk.available }} GB</h6>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
</ion-item-group>
|
||||||
|
|
||||||
|
<ng-template #loading>
|
||||||
|
<skeleton-list [groups]="3"></skeleton-list>
|
||||||
|
</ng-template>
|
||||||
</ion-content>
|
</ion-content>
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
.metric-note {
|
ion-note {
|
||||||
font-size: 16px;
|
font-size: medium;
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,17 @@
|
|||||||
import { Component } from '@angular/core'
|
import { Component } from '@angular/core'
|
||||||
import { Metrics } from 'src/app/services/api/api.types'
|
import { Metrics } from 'src/app/services/api/api.types'
|
||||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||||
import { TimeService } from 'src/app/services/time-service'
|
import { TimeInfo, TimeService } from 'src/app/services/time-service'
|
||||||
import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
import {
|
||||||
|
catchError,
|
||||||
|
combineLatest,
|
||||||
|
filter,
|
||||||
|
from,
|
||||||
|
Observable,
|
||||||
|
startWith,
|
||||||
|
switchMap,
|
||||||
|
} from 'rxjs'
|
||||||
|
import { ConnectionService } from 'src/app/services/connection.service'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'server-metrics',
|
selector: 'server-metrics',
|
||||||
@@ -10,63 +19,43 @@ import { pauseFor, ErrorToastService } from '@start9labs/shared'
|
|||||||
styleUrls: ['./server-metrics.page.scss'],
|
styleUrls: ['./server-metrics.page.scss'],
|
||||||
})
|
})
|
||||||
export class ServerMetricsPage {
|
export class ServerMetricsPage {
|
||||||
loading = true
|
websocketFail = false
|
||||||
going = false
|
|
||||||
metrics: Metrics = {}
|
|
||||||
|
|
||||||
readonly systemTime$ = this.timeService.systemTime$
|
readonly serverData$ = this.getServerData$()
|
||||||
readonly systemUptime$ = this.timeService.systemUptime$
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly errToast: ErrorToastService,
|
private readonly api: ApiService,
|
||||||
private readonly embassyApi: ApiService,
|
readonly timeService: TimeService,
|
||||||
private readonly timeService: TimeService,
|
private readonly connectionService: ConnectionService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
private getServerData$(): Observable<[TimeInfo, Metrics]> {
|
||||||
await this.getMetrics()
|
return combineLatest([
|
||||||
let headersCount = 0
|
this.timeService.getTimeInfo$(),
|
||||||
let rowsCount = 0
|
this.getMetrics$(),
|
||||||
Object.values(this.metrics).forEach(groupVal => {
|
]).pipe(
|
||||||
headersCount++
|
catchError(() => {
|
||||||
Object.keys(groupVal).forEach(_ => {
|
this.websocketFail = true
|
||||||
rowsCount++
|
return this.connectionService.connected$.pipe(
|
||||||
})
|
filter(Boolean),
|
||||||
})
|
switchMap(() => this.getServerData$()),
|
||||||
const height = headersCount * 54 + rowsCount * 50 + 24 // extra 24 for room at the bottom
|
)
|
||||||
const elem = document.getElementById('metricSection')
|
}),
|
||||||
if (elem) elem.style.height = `${height}px`
|
)
|
||||||
this.startDaemon()
|
|
||||||
this.loading = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy() {
|
private getMetrics$(): Observable<Metrics> {
|
||||||
this.stopDaemon()
|
return from(this.api.getServerMetrics({})).pipe(
|
||||||
}
|
switchMap(({ metrics, guid }) =>
|
||||||
|
this.api
|
||||||
private async startDaemon(): Promise<void> {
|
.openMetricsWebsocket$({
|
||||||
this.going = true
|
url: `/rpc/${guid}`,
|
||||||
while (this.going) {
|
openObserver: {
|
||||||
const startTime = Date.now()
|
next: () => (this.websocketFail = false),
|
||||||
await this.getMetrics()
|
},
|
||||||
await pauseFor(4000 - Math.max(Date.now() - startTime, 0))
|
})
|
||||||
}
|
.pipe(startWith(metrics)),
|
||||||
}
|
),
|
||||||
|
)
|
||||||
private stopDaemon() {
|
|
||||||
this.going = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private async getMetrics(): Promise<void> {
|
|
||||||
try {
|
|
||||||
this.metrics = await this.embassyApi.getServerMetrics({})
|
|
||||||
} catch (e: any) {
|
|
||||||
this.errToast.present(e)
|
|
||||||
this.stopDaemon()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
asIsOrder(a: any, b: any) {
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import {
|
|||||||
PackageState,
|
PackageState,
|
||||||
ServerStatusInfo,
|
ServerStatusInfo,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { Metric, RR, NotificationLevel, ServerNotifications } from './api.types'
|
import {
|
||||||
|
RR,
|
||||||
|
NotificationLevel,
|
||||||
|
ServerNotifications,
|
||||||
|
Metrics,
|
||||||
|
} from './api.types'
|
||||||
|
|
||||||
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
|
import { BTC_ICON, LND_ICON, PROXY_ICON } from './api-icons'
|
||||||
import {
|
import {
|
||||||
DependencyMetadata,
|
DependencyMetadata,
|
||||||
@@ -365,112 +371,36 @@ export module Mock {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export function getServerMetrics() {
|
export function getMetrics(): Metrics {
|
||||||
return {
|
return {
|
||||||
Group1: {
|
general: {
|
||||||
Metric1: {
|
temperature: (Math.random() * 100).toFixed(1),
|
||||||
value: Math.random(),
|
|
||||||
unit: 'mi/b',
|
|
||||||
},
|
|
||||||
Metric2: {
|
|
||||||
value: Math.random(),
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
Metric3: {
|
|
||||||
value: 10.1,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Group2: {
|
memory: {
|
||||||
Hmmmm1: {
|
'percentage-used': '20',
|
||||||
value: 22.2,
|
total: (Math.random() * 100).toFixed(2),
|
||||||
unit: 'mi/b',
|
available: '18000',
|
||||||
},
|
used: '4000',
|
||||||
Hmmmm2: {
|
'swap-total': '1000',
|
||||||
value: 50,
|
'swap-free': Math.random().toFixed(2),
|
||||||
unit: '%',
|
'swap-used': '0',
|
||||||
},
|
|
||||||
Hmmmm3: {
|
|
||||||
value: 10.1,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Group3: {
|
cpu: {
|
||||||
Hmmmm1: {
|
'user-space': '100',
|
||||||
value: Math.random(),
|
'kernel-space': '50',
|
||||||
unit: 'mi/b',
|
'io-wait': String(Math.random() * 50),
|
||||||
},
|
idle: '80',
|
||||||
Hmmmm2: {
|
usage: '30',
|
||||||
value: 50,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
Hmmmm3: {
|
|
||||||
value: 10.1,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
Group4: {
|
disk: {
|
||||||
Hmmmm1: {
|
size: '1000',
|
||||||
value: Math.random(),
|
used: '900',
|
||||||
unit: 'mi/b',
|
available: '100',
|
||||||
},
|
'percentage-used': '90',
|
||||||
Hmmmm2: {
|
|
||||||
value: 50,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
Hmmmm3: {
|
|
||||||
value: 10.1,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Group5: {
|
|
||||||
Hmmmm1: {
|
|
||||||
value: Math.random(),
|
|
||||||
unit: 'mi/b',
|
|
||||||
},
|
|
||||||
Hmmmm2: {
|
|
||||||
value: 50,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
Hmmmm3: {
|
|
||||||
value: 10.1,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
Hmmmm4: {
|
|
||||||
value: Math.random(),
|
|
||||||
unit: 'mi/b',
|
|
||||||
},
|
|
||||||
Hmmmm5: {
|
|
||||||
value: 50,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
Hmmmm6: {
|
|
||||||
value: 10.1,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAppMetrics() {
|
|
||||||
const metr: Metric = {
|
|
||||||
Metric1: {
|
|
||||||
value: Math.random(),
|
|
||||||
unit: 'mi/b',
|
|
||||||
},
|
|
||||||
Metric2: {
|
|
||||||
value: Math.random(),
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
Metric3: {
|
|
||||||
value: 10.1,
|
|
||||||
unit: '%',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return metr
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ServerLogs: Log[] = [
|
export const ServerLogs: Log[] = [
|
||||||
{
|
{
|
||||||
timestamp: '2022-07-28T03:52:54.808769Z',
|
timestamp: '2022-07-28T03:52:54.808769Z',
|
||||||
|
|||||||
@@ -47,7 +47,10 @@ export module RR {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type GetServerMetricsReq = {} // server.metrics
|
export type GetServerMetricsReq = {} // server.metrics
|
||||||
export type GetServerMetricsRes = Metrics
|
export type GetServerMetricsRes = {
|
||||||
|
guid: string
|
||||||
|
metrics: Metrics
|
||||||
|
}
|
||||||
|
|
||||||
export type UpdateServerReq = { 'marketplace-url': string } // server.update
|
export type UpdateServerReq = { 'marketplace-url': string } // server.update
|
||||||
export type UpdateServerRes = 'updating' | 'no-updates'
|
export type UpdateServerRes = 'updating' | 'no-updates'
|
||||||
@@ -233,9 +236,6 @@ export module RR {
|
|||||||
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
|
export type FollowPackageLogsReq = FollowServerLogsReq & { id: string } // package.logs.follow
|
||||||
export type FollowPackageLogsRes = FollowServerLogsRes
|
export type FollowPackageLogsRes = FollowServerLogsRes
|
||||||
|
|
||||||
export type GetPackageMetricsReq = { id: string } // package.metrics
|
|
||||||
export type GetPackageMetricsRes = Metric
|
|
||||||
|
|
||||||
export type InstallPackageReq = {
|
export type InstallPackageReq = {
|
||||||
id: string
|
id: string
|
||||||
'version-spec'?: string
|
'version-spec'?: string
|
||||||
@@ -350,18 +350,30 @@ export interface ActionResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface Metrics {
|
export interface Metrics {
|
||||||
[key: string]: {
|
general: {
|
||||||
[key: string]: {
|
temperature: string
|
||||||
value: string | number | null
|
|
||||||
unit?: string
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
memory: {
|
||||||
|
'percentage-used': string
|
||||||
export interface Metric {
|
total: string
|
||||||
[key: string]: {
|
available: string
|
||||||
value: string | number | null
|
used: string
|
||||||
unit?: string
|
'swap-total': string
|
||||||
|
'swap-free': string
|
||||||
|
'swap-used': string
|
||||||
|
}
|
||||||
|
cpu: {
|
||||||
|
'user-space': string
|
||||||
|
'kernel-space': string
|
||||||
|
'io-wait': string
|
||||||
|
idle: string
|
||||||
|
usage: string
|
||||||
|
}
|
||||||
|
disk: {
|
||||||
|
size: string
|
||||||
|
used: string
|
||||||
|
available: string
|
||||||
|
'percentage-used': string
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BehaviorSubject, Observable } from 'rxjs'
|
import { BehaviorSubject, Observable } from 'rxjs'
|
||||||
import { Update } from 'patch-db-client'
|
import { Update } from 'patch-db-client'
|
||||||
import { RR, Encrypted, BackupTargetType } from './api.types'
|
import { RR, Encrypted, BackupTargetType, Metrics } from './api.types'
|
||||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||||
import { Log } from '@start9labs/shared'
|
import { Log } from '@start9labs/shared'
|
||||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
@@ -63,6 +63,10 @@ export abstract class ApiService {
|
|||||||
config: WebSocketSubjectConfig<Log>,
|
config: WebSocketSubjectConfig<Log>,
|
||||||
): Observable<Log>
|
): Observable<Log>
|
||||||
|
|
||||||
|
abstract openMetricsWebsocket$(
|
||||||
|
config: WebSocketSubjectConfig<Metrics>,
|
||||||
|
): Observable<Metrics>
|
||||||
|
|
||||||
abstract getSystemTime(
|
abstract getSystemTime(
|
||||||
params: RR.GetSystemTimeReq,
|
params: RR.GetSystemTimeReq,
|
||||||
): Promise<RR.GetSystemTimeRes>
|
): Promise<RR.GetSystemTimeRes>
|
||||||
@@ -93,10 +97,6 @@ export abstract class ApiService {
|
|||||||
params: RR.GetServerMetricsReq,
|
params: RR.GetServerMetricsReq,
|
||||||
): Promise<RR.GetServerMetricsRes>
|
): Promise<RR.GetServerMetricsRes>
|
||||||
|
|
||||||
abstract getPkgMetrics(
|
|
||||||
params: RR.GetPackageMetricsReq,
|
|
||||||
): Promise<RR.GetPackageMetricsRes>
|
|
||||||
|
|
||||||
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
|
abstract updateServer(url?: string): Promise<RR.UpdateServerRes>
|
||||||
|
|
||||||
abstract restartServer(
|
abstract restartServer(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
RPCOptions,
|
RPCOptions,
|
||||||
} from '@start9labs/shared'
|
} from '@start9labs/shared'
|
||||||
import { ApiService } from './embassy-api.service'
|
import { ApiService } from './embassy-api.service'
|
||||||
import { BackupTargetType, RR } from './api.types'
|
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||||
import { ConfigService } from '../config.service'
|
import { ConfigService } from '../config.service'
|
||||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||||
@@ -126,6 +126,12 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.openWebsocket(config)
|
return this.openWebsocket(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openMetricsWebsocket$(
|
||||||
|
config: WebSocketSubjectConfig<Metrics>,
|
||||||
|
): Observable<Metrics> {
|
||||||
|
return this.openWebsocket(config)
|
||||||
|
}
|
||||||
|
|
||||||
async getSystemTime(
|
async getSystemTime(
|
||||||
params: RR.GetSystemTimeReq,
|
params: RR.GetSystemTimeReq,
|
||||||
): Promise<RR.GetSystemTimeRes> {
|
): Promise<RR.GetSystemTimeRes> {
|
||||||
@@ -400,12 +406,6 @@ export class LiveApiService extends ApiService {
|
|||||||
return this.rpcRequest({ method: 'package.logs.follow', params })
|
return this.rpcRequest({ method: 'package.logs.follow', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPkgMetrics(
|
|
||||||
params: RR.GetPackageMetricsReq,
|
|
||||||
): Promise<RR.GetPackageMetricsRes> {
|
|
||||||
return this.rpcRequest({ method: 'package.metrics', params })
|
|
||||||
}
|
|
||||||
|
|
||||||
async installPackage(
|
async installPackage(
|
||||||
params: RR.InstallPackageReq,
|
params: RR.InstallPackageReq,
|
||||||
): Promise<RR.InstallPackageRes> {
|
): Promise<RR.InstallPackageRes> {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
PackageMainStatus,
|
PackageMainStatus,
|
||||||
PackageState,
|
PackageState,
|
||||||
} from 'src/app/services/patch-db/data-model'
|
} from 'src/app/services/patch-db/data-model'
|
||||||
import { BackupTargetType, CifsBackupTarget, RR } from './api.types'
|
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||||
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
|
||||||
import { Mock } from './api.fixures'
|
import { Mock } from './api.fixures'
|
||||||
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
import markdown from 'raw-loader!../../../../../shared/assets/markdown/md-sample.md'
|
||||||
@@ -181,6 +181,19 @@ export class MockApiService extends ApiService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openMetricsWebsocket$(
|
||||||
|
config: WebSocketSubjectConfig<Metrics>,
|
||||||
|
): Observable<Metrics> {
|
||||||
|
return interval(2000).pipe(
|
||||||
|
map((_, index) => {
|
||||||
|
// mock fire open observer
|
||||||
|
if (index === 0) config.openObserver?.next(new Event(''))
|
||||||
|
if (index === 4) throw new Error('HAHAHA')
|
||||||
|
return Mock.getMetrics()
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async getSystemTime(
|
async getSystemTime(
|
||||||
params: RR.GetSystemTimeReq,
|
params: RR.GetSystemTimeReq,
|
||||||
): Promise<RR.GetSystemTimeRes> {
|
): Promise<RR.GetSystemTimeRes> {
|
||||||
@@ -268,14 +281,10 @@ export class MockApiService extends ApiService {
|
|||||||
params: RR.GetServerMetricsReq,
|
params: RR.GetServerMetricsReq,
|
||||||
): Promise<RR.GetServerMetricsRes> {
|
): Promise<RR.GetServerMetricsRes> {
|
||||||
await pauseFor(2000)
|
await pauseFor(2000)
|
||||||
return Mock.getServerMetrics()
|
return {
|
||||||
}
|
guid: 'iqudh37um-i38u3-34-a51b-jkhd783ein',
|
||||||
|
metrics: Mock.getMetrics(),
|
||||||
async getPkgMetrics(
|
}
|
||||||
params: RR.GetServerMetricsReq,
|
|
||||||
): Promise<RR.GetPackageMetricsRes> {
|
|
||||||
await pauseFor(2000)
|
|
||||||
return Mock.getAppMetrics()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
async updateServer(url?: string): Promise<RR.UpdateServerRes> {
|
||||||
|
|||||||
@@ -1,63 +1,78 @@
|
|||||||
import { Injectable } from '@angular/core'
|
import { Injectable } from '@angular/core'
|
||||||
import {
|
import { map, startWith, switchMap } from 'rxjs/operators'
|
||||||
map,
|
|
||||||
shareReplay,
|
|
||||||
startWith,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators'
|
|
||||||
import { PatchDB } from 'patch-db-client'
|
import { PatchDB } from 'patch-db-client'
|
||||||
import { DataModel } from './patch-db/data-model'
|
import { DataModel } from './patch-db/data-model'
|
||||||
import { ApiService } from './api/embassy-api.service'
|
import { ApiService } from './api/embassy-api.service'
|
||||||
import { combineLatest, from, timer } from 'rxjs'
|
import { combineLatest, from, Observable, timer } from 'rxjs'
|
||||||
|
|
||||||
|
export interface TimeInfo {
|
||||||
|
systemStartTime: number
|
||||||
|
systemCurrentTime: number
|
||||||
|
systemUptime: {
|
||||||
|
days: number
|
||||||
|
hours: number
|
||||||
|
minutes: number
|
||||||
|
seconds: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class TimeService {
|
export class TimeService {
|
||||||
private readonly startTimeMs$ = this.patch
|
private readonly systemStartTime$ = this.patch
|
||||||
.watch$('server-info', 'system-start-time')
|
.watch$('server-info', 'system-start-time')
|
||||||
.pipe(
|
.pipe(map(startTime => new Date(startTime).valueOf()))
|
||||||
take(1),
|
|
||||||
map(startTime => new Date(startTime).valueOf()),
|
|
||||||
shareReplay(),
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly systemTime$ = from(this.apiService.getSystemTime({})).pipe(
|
|
||||||
switchMap(utcStr => {
|
|
||||||
const dateObj = new Date(utcStr)
|
|
||||||
const msRemaining = (60 - dateObj.getSeconds()) * 1000
|
|
||||||
dateObj.setSeconds(0)
|
|
||||||
const current = dateObj.valueOf()
|
|
||||||
return timer(msRemaining, 60000).pipe(
|
|
||||||
map(index => {
|
|
||||||
const incremented = index + 1
|
|
||||||
const msToAdd = 60000 * incremented
|
|
||||||
return current + msToAdd
|
|
||||||
}),
|
|
||||||
startWith(current),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
readonly systemUptime$ = combineLatest([
|
|
||||||
this.startTimeMs$,
|
|
||||||
this.systemTime$,
|
|
||||||
]).pipe(
|
|
||||||
map(([startTime, currentTime]) => {
|
|
||||||
const ms = currentTime - startTime
|
|
||||||
const days = Math.floor(ms / (24 * 60 * 60 * 1000))
|
|
||||||
const daysms = ms % (24 * 60 * 60 * 1000)
|
|
||||||
const hours = Math.floor(daysms / (60 * 60 * 1000))
|
|
||||||
const hoursms = ms % (60 * 60 * 1000)
|
|
||||||
const minutes = Math.floor(hoursms / (60 * 1000))
|
|
||||||
return { days, hours, minutes }
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly patch: PatchDB<DataModel>,
|
private readonly patch: PatchDB<DataModel>,
|
||||||
private readonly apiService: ApiService,
|
private readonly apiService: ApiService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
getTimeInfo$(): Observable<TimeInfo> {
|
||||||
|
return combineLatest([
|
||||||
|
this.systemStartTime$.pipe(),
|
||||||
|
this.getSystemCurrentTime$(),
|
||||||
|
]).pipe(
|
||||||
|
map(([systemStartTime, systemCurrentTime]) => ({
|
||||||
|
systemStartTime,
|
||||||
|
systemCurrentTime,
|
||||||
|
systemUptime: this.getSystemUptime(systemStartTime, systemCurrentTime),
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSystemCurrentTime$() {
|
||||||
|
return from(this.apiService.getSystemTime({})).pipe(
|
||||||
|
switchMap(utcStr => {
|
||||||
|
const dateObj = new Date(utcStr)
|
||||||
|
const current = dateObj.valueOf()
|
||||||
|
return timer(0, 1000).pipe(
|
||||||
|
map(index => {
|
||||||
|
const incremented = index + 1
|
||||||
|
const msToAdd = 1000 * incremented
|
||||||
|
return current + msToAdd
|
||||||
|
}),
|
||||||
|
startWith(current),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSystemUptime(systemStartTime: number, systemCurrentTime: number) {
|
||||||
|
const ms = systemCurrentTime - systemStartTime
|
||||||
|
|
||||||
|
const days = Math.floor(ms / (24 * 60 * 60 * 1000))
|
||||||
|
const daysms = ms % (24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
|
const hours = Math.floor(daysms / (60 * 60 * 1000))
|
||||||
|
const hoursms = ms % (60 * 60 * 1000)
|
||||||
|
|
||||||
|
const minutes = Math.floor(hoursms / (60 * 1000))
|
||||||
|
const minutesms = ms % (60 * 1000)
|
||||||
|
|
||||||
|
const seconds = Math.floor(minutesms / 1000)
|
||||||
|
|
||||||
|
return { days, hours, minutes, seconds }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user