diff --git a/tdrive/connectors/onlyoffice-connector/src/config/index.ts b/tdrive/connectors/onlyoffice-connector/src/config/index.ts index 5fdc7997..9dc31436 100644 --- a/tdrive/connectors/onlyoffice-connector/src/config/index.ts +++ b/tdrive/connectors/onlyoffice-connector/src/config/index.ts @@ -13,6 +13,7 @@ export const { SERVER_PREFIX, SERVER_ORIGIN, INSTANCE_ID, + OOCONNECTOR_HEALTH_SECRET, } = process.env; const secs = 1000, diff --git a/tdrive/connectors/onlyoffice-connector/src/controllers/health-status.controller.ts b/tdrive/connectors/onlyoffice-connector/src/controllers/health-status.controller.ts index 97fa2826..61ca91be 100644 --- a/tdrive/connectors/onlyoffice-connector/src/controllers/health-status.controller.ts +++ b/tdrive/connectors/onlyoffice-connector/src/controllers/health-status.controller.ts @@ -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 { - 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 { + 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); }, }; diff --git a/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts index 4d26860e..92a75c3d 100644 --- a/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts +++ b/tdrive/connectors/onlyoffice-connector/src/services/api.service.ts @@ -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; 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.'); } diff --git a/tdrive/connectors/onlyoffice-connector/src/services/forgotten-processor.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/forgotten-processor.service.ts index c295af3a..de47e632 100644 --- a/tdrive/connectors/onlyoffice-connector/src/services/forgotten-processor.service.ts +++ b/tdrive/connectors/onlyoffice-connector/src/services/forgotten-processor.service.ts @@ -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; public readonly forgottenSynchroniser = createSingleProcessorLock(); 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(), }, diff --git a/tdrive/connectors/onlyoffice-connector/src/services/health-providers.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/health-providers.service.ts new file mode 100644 index 00000000..da85ff62 --- /dev/null +++ b/tdrive/connectors/onlyoffice-connector/src/services/health-providers.service.ts @@ -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; +} diff --git a/tdrive/connectors/onlyoffice-connector/src/services/onlyoffice.service.ts b/tdrive/connectors/onlyoffice-connector/src/services/onlyoffice.service.ts index cca8c733..985729fb 100644 --- a/tdrive/connectors/onlyoffice-connector/src/services/onlyoffice.service.ts +++ b/tdrive/connectors/onlyoffice-connector/src/services/onlyoffice.service.ts @@ -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; // Technically the timeout field is from the PendingRequestQueue but avoid 2 classes private readonly pendingRequests = new PendingRequestQueue(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.