♻️ oo-connector: refactored health (#525)

This commit is contained in:
Eric Doughty-Papassideris
2024-09-18 19:04:04 +02:00
parent 488d5a5936
commit be10393435
6 changed files with 40 additions and 19 deletions
@@ -13,6 +13,7 @@ export const {
SERVER_PREFIX, SERVER_PREFIX,
SERVER_ORIGIN, SERVER_ORIGIN,
INSTANCE_ID, INSTANCE_ID,
OOCONNECTOR_HEALTH_SECRET,
} = process.env; } = process.env;
const secs = 1000, const secs = 1000,
@@ -1,23 +1,16 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import onlyofficeService from '@/services/onlyoffice.service'; import { getProviders } from '@/services/health-providers.service';
import apiService from '@/services/api.service'; import { OOCONNECTOR_HEALTH_SECRET } from '@/config';
import forgottenProcessorService from '@/services/forgotten-processor.service';
/** /**
* Health response for devops operational purposes. Should not be exposed. * Health response for devops operational purposes. Should not be exposed.
*/ */
export const HealthStatusController = { export const HealthStatusController = {
async get(req: Request<{}, {}, {}, {}>, res: Response): Promise<void> { async get(req: Request<{}, {}, {}, { secret: string }>, res: Response): Promise<void> {
Promise.all([onlyofficeService.getLatestLicence(), apiService.hasToken(), forgottenProcessorService.getHealthData()]).then( if (req.query.secret !== OOCONNECTOR_HEALTH_SECRET) return void res.status(404).send(`Cannot GET ${req.path}`);
([onlyOfficeLicense, twakeDriveToken, forgottenProcessorHealth]) => const entries = await Promise.all(getProviders().map(p => p.getHealthData()));
res.status(onlyOfficeLicense && twakeDriveToken ? 200 : 500).send({ let result = {};
uptimeS: Math.floor(process.uptime()), entries.forEach(entry => (result = { ...result, ...entry }));
onlyOfficeLicense, res.send(result);
twakeDriveToken,
...forgottenProcessorHealth,
}),
err => res.status(500).send(err),
);
}, },
}; };
@@ -9,22 +9,28 @@ import { CREDENTIALS_ENDPOINT, CREDENTIALS_ID, CREDENTIALS_SECRET, twakeDriveTok
import logger from '../lib/logger'; import logger from '../lib/logger';
import * as Utils from '@/utils'; import * as Utils from '@/utils';
import { PolledThingieValue } from '@/lib/polled-thingie-value'; import { PolledThingieValue } from '@/lib/polled-thingie-value';
import { IHealthProvider, registerHealthProvider } from './health-providers.service';
/** /**
* Client for the Twake Drive backend API on behalf of the plugin (or provided token in parameters). * Client for the Twake Drive backend API on behalf of the plugin (or provided token in parameters).
* Periodically updates authorization and adds to requests. * Periodically updates authorization and adds to requests.
*/ */
class ApiService implements IApiService { class ApiService implements IApiService, IHealthProvider {
private readonly tokenPoller: PolledThingieValue<Axios>; private readonly tokenPoller: PolledThingieValue<Axios>;
constructor() { constructor() {
this.tokenPoller = new PolledThingieValue('Refresh Twake Drive token', async () => await this.refreshToken(), twakeDriveTokenRefrehPeriodMS); this.tokenPoller = new PolledThingieValue('Refresh Twake Drive token', async () => await this.refreshToken(), twakeDriveTokenRefrehPeriodMS);
registerHealthProvider(this);
} }
public async hasToken() { public async hasToken() {
return (await this.tokenPoller.latestValueWithTry()) !== undefined; return (await this.tokenPoller.latestValueWithTry()) !== undefined;
} }
async getHealthData() {
return { TwakeDriveApi: { tokenAgeS: this.tokenPoller.latest()?.ageS ?? -1 } };
}
private requireAxios() { private requireAxios() {
return this.tokenPoller.requireLatestValueWithTry('No Twake Drive app token.'); return this.tokenPoller.requireLatestValueWithTry('No Twake Drive app token.');
} }
@@ -6,12 +6,13 @@ import { createSingleProcessorLock } from '@/lib/single-processor-lock';
import apiService from './api.service'; import apiService from './api.service';
import onlyofficeService from './onlyoffice.service'; import onlyofficeService from './onlyoffice.service';
import driveService, { UnknownKeyInDriveError } from './drive.service'; import driveService, { UnknownKeyInDriveError } from './drive.service';
import { IHealthProvider, registerHealthProvider } from './health-providers.service';
/** /**
* Periodically poll the Only Office document server for forgotten * Periodically poll the Only Office document server for forgotten
* files and try to upload to Twake Drive or get rid if unknown. * files and try to upload to Twake Drive or get rid if unknown.
*/ */
class ForgottenProcessor { class ForgottenProcessor implements IHealthProvider {
private readonly forgottenFilesPoller: PolledThingieValue<number>; private readonly forgottenFilesPoller: PolledThingieValue<number>;
public readonly forgottenSynchroniser = createSingleProcessorLock<boolean>(); public readonly forgottenSynchroniser = createSingleProcessorLock<boolean>();
private lastStart = 0; private lastStart = 0;
@@ -23,13 +24,14 @@ class ForgottenProcessor {
async () => (skippedFirst ? await this.processForgottenFiles() : ((skippedFirst = true), -1)), async () => (skippedFirst ? await this.processForgottenFiles() : ((skippedFirst = true), -1)),
onlyOfficeForgottenFilesCheckPeriodMS, onlyOfficeForgottenFilesCheckPeriodMS,
); );
registerHealthProvider(this);
} }
public async getHealthData() { public async getHealthData() {
const keys = await onlyofficeService.getForgottenList(); const keys = await onlyofficeService.getForgottenList();
return { return {
forgotten: { forgotten: {
timeSinceLastStartS: ~~((new Date().getTime() - this.lastStart) / 1000), timeSinceLastStartS: this.lastStart ? ~~((new Date().getTime() - this.lastStart) / 1000) : -1,
count: keys?.length ?? 0, count: keys?.length ?? 0,
locks: this.forgottenSynchroniser.getWorstStats(), locks: this.forgottenSynchroniser.getWorstStats(),
}, },
@@ -0,0 +1,13 @@
const providers: IHealthProvider[] = [];
export interface IHealthProvider {
getHealthData(): Promise<{ [key: string]: unknown }>;
}
export function registerHealthProvider(provider: IHealthProvider) {
providers.push(provider);
}
export function getProviders() {
return providers;
}
@@ -5,6 +5,7 @@ import { PolledThingieValue } from '@/lib/polled-thingie-value';
import { PendingRequestQueue, PendingRequestCallback } from '@/lib/pending-request-matcher'; import { PendingRequestQueue, PendingRequestCallback } from '@/lib/pending-request-matcher';
import logger from '@/lib/logger'; import logger from '@/lib/logger';
import * as Utils from '@/utils'; import * as Utils from '@/utils';
import { IHealthProvider, registerHealthProvider } from './health-providers.service';
/** @see https://api.onlyoffice.com/editors/basic */ /** @see https://api.onlyoffice.com/editors/basic */
export enum ErrorCode { export enum ErrorCode {
@@ -218,7 +219,7 @@ class CallbackResponseFromCommand {
* Exposed OnlyOffice command service * Exposed OnlyOffice command service
* @see https://api.onlyoffice.com/editors/command/ * @see https://api.onlyoffice.com/editors/command/
*/ */
class OnlyOfficeService { class OnlyOfficeService implements IHealthProvider {
private readonly poller: PolledThingieValue<CommandService.License.Response>; private readonly poller: PolledThingieValue<CommandService.License.Response>;
// Technically the timeout field is from the PendingRequestQueue but avoid 2 classes // Technically the timeout field is from the PendingRequestQueue but avoid 2 classes
private readonly pendingRequests = new PendingRequestQueue<CallbackResponseFromCommand>(onlyOfficeCallbackTimeoutMS); private readonly pendingRequests = new PendingRequestQueue<CallbackResponseFromCommand>(onlyOfficeCallbackTimeoutMS);
@@ -232,6 +233,7 @@ class OnlyOfficeService {
}, },
onlyOfficeConnectivityCheckPeriodMS, onlyOfficeConnectivityCheckPeriodMS,
); );
registerHealthProvider(this);
} }
/** Get the latest Only Office licence status from polling. If the return is `undefined` /** Get the latest Only Office licence status from polling. If the return is `undefined`
* it probably means there is a connection issue contacting the OnlyOffice server * it probably means there is a connection issue contacting the OnlyOffice server
@@ -299,6 +301,10 @@ class OnlyOfficeService {
return new CommandService.License.Request().post(); return new CommandService.License.Request().post();
} }
async getHealthData() {
return { OO: { license: this.poller.latest() } };
}
/** /**
* Requests a document status and the list of the identifiers of the users who opened the document for editing. * Requests a document status and the list of the identifiers of the users who opened the document for editing.
* The response will be sent to the callback handler. * The response will be sent to the callback handler.