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