Jelajahi Sumber

Service Checks detail page - finished scroll paging; fixed infinite reload problem by inserting dummy 'missing log' entries

Christian Kahlau 2 tahun lalu
induk
melakukan
113e7a70b7

+ 1 - 0
common/lib/http-check-data.module.ts

@@ -1,4 +1,5 @@
 export enum HttpCheckStatus {
+  Invalid = -1,
   OK = 0,
   Unknown = 1,
   RequestFailed = 2,

+ 1 - 1
ng/src/app/components/service-checks-widget/service-checks-widget.component.html

@@ -1,5 +1,5 @@
 <div *ngIf="serviceChecks; else loading" class="d-flex flex-column">
-  <div *ngFor="let check of serviceChecks" class="position-relative mt-1 mb-1" [routerLink]="'/svc/' + check.serverId + '/' + check.id">
+  <div *ngFor="let check of serviceChecks" class="position-relative mt-1 mb-1 pointer" [routerLink]="'/svc/' + check.serverId + '/' + check.id">
     <app-status-timeline-widget [title]="check.title" [data]="check.data"></app-status-timeline-widget>
   </div>
 </div>

+ 3 - 1
ng/src/app/lib/conversions.lib.ts

@@ -50,8 +50,10 @@ export function convertToStatusTimelineData(start: Date, end: Date, rawData?: Se
 }
 
 export function mapStatusClass(data: ServiceCheckData) {
-  const maxStatus = data.data.reduce((res, d) => (res = Math.max(res, d.status)), 0);
+  const maxStatus = data.data.reduce((res, d) => (res = Math.max(res, d.status)), Number.MIN_SAFE_INTEGER);
   switch (maxStatus) {
+    case HttpCheckStatus.Invalid:
+      return 'bg-progress';
     case HttpCheckStatus.OK:
       return 'bg-peak';
     default:

+ 74 - 10
ng/src/app/pages/service-check-detail-page/service-check-detail-page.component.ts

@@ -1,9 +1,9 @@
 // Third party
-import { Component, ViewChild } from '@angular/core';
+import { Component } from '@angular/core';
 import { ActivatedRoute } from '@angular/router';
 
 // Common
-import { ServiceCheckData } from '../../../../../common/lib/http-check-data.module';
+import { HttpCheckStatus, ServiceCheckData } from '../../../../../common/lib/http-check-data.module';
 
 // App
 import { convertToStatusTimelineData } from 'src/app/lib/conversions.lib';
@@ -53,7 +53,7 @@ export class ServiceCheckDetailPageComponent {
 
       const pageEnd = end;
       const pageStart = new Date(end.getTime() - 1000 * 60 * 60 * 4);
-      this.log.entries = await this.serviceApi.queryServiceLog(serverID, serviceID, pageStart, pageEnd);
+      this.log.entries = await this.queryAndFillServiceLog(serverID, serviceID, pageStart, pageEnd, this.serviceCheck.interval);
 
       this.log.end = end;
       this.log.start = start;
@@ -83,21 +83,16 @@ export class ServiceCheckDetailPageComponent {
 
   private async reloadOnScroll() {
     if (this.log.loading) return;
-    if (!this.log.start || !this.log.end || !this.params || !this.log.visible) return;
+    if (!this.log.start || !this.log.end || !this.params || !this.serviceCheck || !this.log.visible) return;
     try {
       this.log.loading = true;
 
       const end = new Date(this.log.visible.first.getTime() - 1000);
       let start = new Date(end.getTime() - 1000 * 60 * 60 * 2);
-
       if (start.getTime() < this.log.start.getTime()) start = this.log.start;
 
-      const entries = await this.serviceApi.queryServiceLog(this.params.serverID, this.params.serviceID, start, end);
-
-      // TODO: Problem: This runs into infinite reloading if entries.length is 0 here (because nothing got logged in the last hour)
-
+      const entries = await this.queryAndFillServiceLog(this.params.serverID, this.params.serviceID, start, end, this.serviceCheck.interval);
       this.log.entries = [...entries, ...(this.log.entries ?? [])];
-
       this.log.loading = false;
 
       setTimeout(this.logScroll.bind(this), 1000);
@@ -121,4 +116,73 @@ export class ServiceCheckDetailPageComponent {
 
     return { first: new Date(first), last: new Date(last) };
   }
+
+  private async queryAndFillServiceLog(serverID: number, serviceID: number, start: Date, end: Date, interval: number) {
+    const expectedNum = Math.floor((end.getTime() - start.getTime()) / (interval * 1000));
+    if (expectedNum <= 0) return [];
+
+    const toleranceMs = 2500;
+    const entries = await this.serviceApi.queryServiceLog(serverID, serviceID, start, end);
+    if (entries.length < expectedNum) {
+      if (entries.length) {
+        // Insert dummy log entries before first existing entry?
+        if (entries[0].time.getTime() - start.getTime() > interval * 1000 + toleranceMs) {
+          const desiredNum = Math.floor((entries[0].time.getTime() - start.getTime()) / (interval * 1000));
+          entries.unshift(
+            ...Array(desiredNum)
+              .fill(0)
+              .map((v, i) => new Date(entries[0].time.getTime() - i * interval * 1000))
+              .reverse()
+              .map(this.dummyEntry)
+          );
+        }
+
+        // Insert dummy log entries after last existing entry?
+        if (end.getTime() - entries[entries.length - 1].time.getTime() > interval * 1000 + toleranceMs) {
+          const desiredNum = Math.floor((end.getTime() - entries[entries.length - 1].time.getTime()) / (interval * 1000));
+          entries.push(
+            ...Array(desiredNum)
+              .fill(0)
+              .map((v, i) => new Date(entries[entries.length - 1].time.getTime() + i * interval * 1000))
+              .map(this.dummyEntry)
+          );
+        }
+
+        // Insert dummy log entries in between existing entries?
+        entries
+          .slice()
+          .reverse()
+          .forEach((entry, invIdx, arr) => {
+            if (invIdx === 0) return; // skip last/newest entry
+
+            const newerEntry = arr[invIdx - 1];
+            const i = arr.length - 1 - invIdx;
+            if (newerEntry.time.getTime() - entry.time.getTime() > interval * 1000 + toleranceMs) {
+              const desiredNum = Math.floor((newerEntry.time.getTime() - entry.time.getTime()) / (interval * 1000));
+              entries.splice(
+                i,
+                0,
+                ...Array(desiredNum)
+                  .fill(0)
+                  .map((v, idx) => new Date(entry.time.getTime() + idx * interval * 1000))
+                  .map(this.dummyEntry)
+              );
+            }
+          });
+      } else {
+        // Response all empty -> fill with #expectedNum dummy entries
+        entries.push(
+          ...Array(expectedNum)
+            .fill(0)
+            .map((v, i) => new Date(start.getTime() + i * interval * 1000))
+            .map(this.dummyEntry)
+        );
+      }
+    }
+    return entries;
+  }
+
+  private dummyEntry(time: Date) {
+    return { time, data: [{ status: HttpCheckStatus.Invalid, message: 'missing log entry' }] };
+  }
 }

+ 1 - 1
ng/src/app/pipes/status-color.pipe.ts

@@ -9,6 +9,6 @@ import { mapStatusClass } from 'src/app/lib/conversions.lib';
 })
 export class StatusColorPipe implements PipeTransform {
   transform(value?: ServiceCheckData, ...args: unknown[]) {
-    return !!value ? mapStatusClass(value) : 'bg-max';
+    return !value && value !== 0 ? 'bg-max' : mapStatusClass(value);
   }
 }

+ 1 - 1
server/src/ctrl/http-check-controller.class.ts

@@ -160,7 +160,7 @@ export class HttpCheckController {
       }
       if (log) Logger.error('[ERROR] HTTP Service Check failed:', err);
     }
-    if (!success && conf.notify) {
+    if (!success && conf.notify && !process.env.DEV_MODE) {
       try {
         const lastErrors = await this.db.getLastErrors(conf.id, conf.notifyThreshold + 1);
         if (lastErrors.length > conf.notifyThreshold) {