From 935788cd6bfbd3a72ecf7cf916836bba3e09e5d6 Mon Sep 17 00:00:00 2001 From: Alexander Onnikov Date: Fri, 26 Apr 2024 17:46:44 +0700 Subject: [PATCH] UBERF-6747 Expose collaborator statistics (#5483) Signed-off-by: Alexander Onnikov --- .../src/components/ServerManager.svelte | 25 ++++++++++++- pods/collaborator/src/__start.ts | 6 +++- server/collaborator/package.json | 1 + server/collaborator/src/__start.ts | 6 +++- server/collaborator/src/extensions/storage.ts | 12 ++----- server/collaborator/src/metrics.ts | 36 ------------------- server/collaborator/src/server.ts | 28 ++++++++++++++- server/collaborator/src/starter.ts | 12 +++---- 8 files changed, 71 insertions(+), 55 deletions(-) delete mode 100644 server/collaborator/src/metrics.ts diff --git a/plugins/workbench-resources/src/components/ServerManager.svelte b/plugins/workbench-resources/src/components/ServerManager.svelte index d173da7d69..4c93d44385 100644 --- a/plugins/workbench-resources/src/components/ServerManager.svelte +++ b/plugins/workbench-resources/src/components/ServerManager.svelte @@ -49,15 +49,26 @@ console.error(err) }) } + async function fetchCollabStats (): Promise { + const collaborator = getMetadata(presentation.metadata.CollaboratorApiUrl) + await fetch(collaborator + `/api/v1/statistics?token=${token}`, {}) + .then(async (json) => { + dataCollab = await json.json() + }) + .catch((err) => { + console.error(err) + }) + } let data: any let dataFront: any + let dataCollab: any let admin = false onDestroy( ticker.subscribe(() => { void fetchStats() - void fetchUIStats() + void fetchCollabStats() }) ) const tabs: TabItem[] = [ @@ -73,6 +84,10 @@ id: 'statistics-front', labelIntl: getEmbeddedLabel('Front') }, + { + id: 'statistics-collab', + labelIntl: getEmbeddedLabel('Collaborator') + }, { id: 'users', labelIntl: getEmbeddedLabel('Users') @@ -121,6 +136,8 @@ $: metricsDataFront = dataFront?.metrics as Metrics | undefined + $: metricsDataCollab = dataCollab?.metrics as Metrics | undefined + $: totalStats = Array.from(Object.entries(activeSessions).values()).reduce( (cur, it) => { const totalFind = it[1].sessions.reduce((it, itm) => itm.current.find + it, 0) @@ -330,6 +347,12 @@ {/if} + {:else if selectedTab === 'statistics-collab'} +
+ {#if metricsDataCollab !== undefined} + + {/if} +
{/if} {:else} diff --git a/pods/collaborator/src/__start.ts b/pods/collaborator/src/__start.ts index a2a1c1557d..cb7e70c2e3 100644 --- a/pods/collaborator/src/__start.ts +++ b/pods/collaborator/src/__start.ts @@ -13,5 +13,9 @@ // limitations under the License. // +import { MeasureMetricsContext, newMetrics } from '@hcengineering/core' import { startCollaborator } from '@hcengineering/collaborator' -void startCollaborator() + +const ctx = new MeasureMetricsContext('collaborator', {}, {}, newMetrics()) + +void startCollaborator(ctx) diff --git a/server/collaborator/package.json b/server/collaborator/package.json index b397455be8..b2aa9f2910 100644 --- a/server/collaborator/package.json +++ b/server/collaborator/package.json @@ -49,6 +49,7 @@ "@types/jest": "^29.5.5" }, "dependencies": { + "@hcengineering/analytics": "^0.6.0", "@hcengineering/core": "^0.6.28", "@hcengineering/account": "^0.6.0", "@hcengineering/platform": "^0.6.9", diff --git a/server/collaborator/src/__start.ts b/server/collaborator/src/__start.ts index 5b91ae1d98..b195ef01e1 100644 --- a/server/collaborator/src/__start.ts +++ b/server/collaborator/src/__start.ts @@ -14,5 +14,9 @@ // limitations under the License. // +import { MeasureMetricsContext, newMetrics } from '@hcengineering/core' import { startCollaborator } from './starter' -void startCollaborator() + +const ctx = new MeasureMetricsContext('collaborator', {}, {}, newMetrics()) + +void startCollaborator(ctx) diff --git a/server/collaborator/src/extensions/storage.ts b/server/collaborator/src/extensions/storage.ts index 8c55169f11..1945799539 100644 --- a/server/collaborator/src/extensions/storage.ts +++ b/server/collaborator/src/extensions/storage.ts @@ -50,9 +50,7 @@ export class StorageExtension implements Extension { async onLoadDocument ({ context, documentName }: withContext): Promise { this.configuration.ctx.info('load document', { documentName }) - return await this.configuration.ctx.with('load-document', {}, async () => { - return await this.loadDocument(documentName as DocumentId, context) - }) + return await this.loadDocument(documentName as DocumentId, context) } async onStoreDocument ({ context, documentName, document }: withContext): Promise { @@ -67,9 +65,7 @@ export class StorageExtension implements Extension { } this.collaborators.delete(documentName) - await ctx.with('store-document', {}, async () => { - await this.storeDocument(documentName as DocumentId, document, context) - }) + await this.storeDocument(documentName as DocumentId, document, context) } async onConnect ({ context, documentName, instance }: withContext): Promise { @@ -92,9 +88,7 @@ export class StorageExtension implements Extension { } this.collaborators.delete(documentName) - await ctx.with('store-document', {}, async () => { - await this.storeDocument(documentName as DocumentId, document, context) - }) + await this.storeDocument(documentName as DocumentId, document, context) } async afterUnloadDocument ({ documentName }: afterUnloadDocumentPayload): Promise { diff --git a/server/collaborator/src/metrics.ts b/server/collaborator/src/metrics.ts deleted file mode 100644 index 61d6e4a7ec..0000000000 --- a/server/collaborator/src/metrics.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core' -import { writeFile } from 'fs/promises' - -const metricsFile = process.env.METRICS_FILE -const metricsConsole = (process.env.METRICS_CONSOLE ?? 'false') === 'true' - -const METRICS_UPDATE_INTERVAL = !metricsConsole ? 1000 : 30000 - -const metrics = newMetrics() -export const metricsContext = new MeasureMetricsContext('System', {}, metrics) - -if (metricsFile !== undefined || metricsConsole) { - console.info('storing measurements into local file', metricsFile) - let oldMetricsValue = '' - - const intTimer = setInterval(() => { - const val = metricsToString(metrics, 'System', 140) - if (val !== oldMetricsValue) { - oldMetricsValue = val - if (metricsFile !== undefined) { - writeFile(metricsFile, val).catch((err) => { - console.error(err) - }) - } - if (metricsConsole) { - console.info('METRICS:\n', val) - } - } - }, METRICS_UPDATE_INTERVAL) - - const closeTimer = (): void => { - clearInterval(intTimer) - } - process.on('SIGINT', closeTimer) - process.on('SIGTERM', closeTimer) -} diff --git a/server/collaborator/src/server.ts b/server/collaborator/src/server.ts index f2b73e42bb..af785bf7d5 100644 --- a/server/collaborator/src/server.ts +++ b/server/collaborator/src/server.ts @@ -13,7 +13,8 @@ // limitations under the License. // -import { MeasureContext, generateId } from '@hcengineering/core' +import { Analytics } from '@hcengineering/analytics' +import { MeasureContext, generateId, metricsAggregate } from '@hcengineering/core' import { MinioService } from '@hcengineering/minio' import { Token, decodeToken } from '@hcengineering/server-token' import { ServerKit } from '@hcengineering/text' @@ -146,6 +147,31 @@ export async function start ( } } + app.get('/api/v1/statistics', (req, res) => { + try { + const token = req.query.token as string + const payload = decodeToken(token) + const admin = payload.extra?.admin === 'true' + res.status(200) + res.setHeader('Content-Type', 'application/json') + res.setHeader('Cache-Control', 'public, no-store, no-cache, must-revalidate, max-age=0') + + const json = JSON.stringify({ + metrics: metricsAggregate((ctx as any).metrics), + statistics: { + activeSessions: {} + }, + admin + }) + res.end(json) + } catch (err: any) { + ctx.error('statistics error', { err }) + Analytics.handleError(err) + res.writeHead(404, {}) + res.end() + } + }) + // eslint-disable-next-line @typescript-eslint/no-misused-promises app.post('/rpc', async (req, res) => { const authHeader = req.headers.authorization diff --git a/server/collaborator/src/starter.ts b/server/collaborator/src/starter.ts index f8310ec874..3a13455d4e 100644 --- a/server/collaborator/src/starter.ts +++ b/server/collaborator/src/starter.ts @@ -14,16 +14,16 @@ // limitations under the License. // +import { MeasureContext } from '@hcengineering/core' import { MinioService } from '@hcengineering/minio' import { setMetadata } from '@hcengineering/platform' import serverToken from '@hcengineering/server-token' import { MongoClient } from 'mongodb' import config from './config' -import { metricsContext } from './metrics' import { start } from './server' -export async function startCollaborator (): Promise { +export async function startCollaborator (ctx: MeasureContext, onClose?: () => void): Promise { setMetadata(serverToken.metadata.Secret, config.Secret) let minioPort = 9000 @@ -44,21 +44,21 @@ export async function startCollaborator (): Promise { const mongoClient = await MongoClient.connect(config.MongoUrl) - const shutdown = await start(metricsContext, config, minioClient, mongoClient) + const shutdown = await start(ctx, config, minioClient, mongoClient) const close = (): void => { void shutdown().then(() => { void mongoClient.close() }) - metricsContext.info('closed') + onClose?.() } process.on('uncaughtException', (e) => { - metricsContext.error('UncaughtException', { error: e }) + ctx.error('UncaughtException', { error: e }) }) process.on('unhandledRejection', (reason, promise) => { - metricsContext.error('Unhandled Rejection at:', { promise, reason }) + ctx.error('Unhandled Rejection at:', { promise, reason }) }) process.on('SIGINT', close)