♻️ oo-connector: refactored health (#525)
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user