♻️ 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_ORIGIN,
INSTANCE_ID,
OOCONNECTOR_HEALTH_SECRET,
} = process.env;
const secs = 1000,
@@ -1,23 +1,16 @@
import { Request, Response } from 'express';
import onlyofficeService from '@/services/onlyoffice.service';
import apiService from '@/services/api.service';
import forgottenProcessorService from '@/services/forgotten-processor.service';
import { getProviders } from '@/services/health-providers.service';
import { OOCONNECTOR_HEALTH_SECRET } from '@/config';
/**
* Health response for devops operational purposes. Should not be exposed.
*/
export const HealthStatusController = {
async get(req: Request<{}, {}, {}, {}>, res: Response): Promise<void> {
Promise.all([onlyofficeService.getLatestLicence(), apiService.hasToken(), forgottenProcessorService.getHealthData()]).then(
([onlyOfficeLicense, twakeDriveToken, forgottenProcessorHealth]) =>
res.status(onlyOfficeLicense && twakeDriveToken ? 200 : 500).send({
uptimeS: Math.floor(process.uptime()),
onlyOfficeLicense,
twakeDriveToken,
...forgottenProcessorHealth,
}),
err => res.status(500).send(err),
);
async get(req: Request<{}, {}, {}, { secret: string }>, res: Response): Promise<void> {
if (req.query.secret !== OOCONNECTOR_HEALTH_SECRET) return void res.status(404).send(`Cannot GET ${req.path}`);
const entries = await Promise.all(getProviders().map(p => p.getHealthData()));
let result = {};
entries.forEach(entry => (result = { ...result, ...entry }));
res.send(result);
},
};
@@ -9,22 +9,28 @@ import { CREDENTIALS_ENDPOINT, CREDENTIALS_ID, CREDENTIALS_SECRET, twakeDriveTok
import logger from '../lib/logger';
import * as Utils from '@/utils';
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).
* Periodically updates authorization and adds to requests.
*/
class ApiService implements IApiService {
class ApiService implements IApiService, IHealthProvider {
private readonly tokenPoller: PolledThingieValue<Axios>;
constructor() {
this.tokenPoller = new PolledThingieValue('Refresh Twake Drive token', async () => await this.refreshToken(), twakeDriveTokenRefrehPeriodMS);
registerHealthProvider(this);
}
public async hasToken() {
return (await this.tokenPoller.latestValueWithTry()) !== undefined;
}
async getHealthData() {
return { TwakeDriveApi: { tokenAgeS: this.tokenPoller.latest()?.ageS ?? -1 } };
}
private requireAxios() {
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 onlyofficeService from './onlyoffice.service';
import driveService, { UnknownKeyInDriveError } from './drive.service';
import { IHealthProvider, registerHealthProvider } from './health-providers.service';
/**
* Periodically poll the Only Office document server for forgotten
* files and try to upload to Twake Drive or get rid if unknown.
*/
class ForgottenProcessor {
class ForgottenProcessor implements IHealthProvider {
private readonly forgottenFilesPoller: PolledThingieValue<number>;
public readonly forgottenSynchroniser = createSingleProcessorLock<boolean>();
private lastStart = 0;
@@ -23,13 +24,14 @@ class ForgottenProcessor {
async () => (skippedFirst ? await this.processForgottenFiles() : ((skippedFirst = true), -1)),
onlyOfficeForgottenFilesCheckPeriodMS,
);
registerHealthProvider(this);
}
public async getHealthData() {
const keys = await onlyofficeService.getForgottenList();
return {
forgotten: {
timeSinceLastStartS: ~~((new Date().getTime() - this.lastStart) / 1000),
timeSinceLastStartS: this.lastStart ? ~~((new Date().getTime() - this.lastStart) / 1000) : -1,
count: keys?.length ?? 0,
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 logger from '@/lib/logger';
import * as Utils from '@/utils';
import { IHealthProvider, registerHealthProvider } from './health-providers.service';
/** @see https://api.onlyoffice.com/editors/basic */
export enum ErrorCode {
@@ -218,7 +219,7 @@ class CallbackResponseFromCommand {
* Exposed OnlyOffice command service
* @see https://api.onlyoffice.com/editors/command/
*/
class OnlyOfficeService {
class OnlyOfficeService implements IHealthProvider {
private readonly poller: PolledThingieValue<CommandService.License.Response>;
// Technically the timeout field is from the PendingRequestQueue but avoid 2 classes
private readonly pendingRequests = new PendingRequestQueue<CallbackResponseFromCommand>(onlyOfficeCallbackTimeoutMS);
@@ -232,6 +233,7 @@ class OnlyOfficeService {
},
onlyOfficeConnectivityCheckPeriodMS,
);
registerHealthProvider(this);
}
/** 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
@@ -299,6 +301,10 @@ class OnlyOfficeService {
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.
* The response will be sent to the callback handler.