UBERF-6747 Expose collaborator statistics (#5483)

Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
Alexander Onnikov 2024-04-26 17:46:44 +07:00 committed by GitHub
parent ae1b3eb062
commit 935788cd6b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 71 additions and 55 deletions

View File

@ -49,15 +49,26 @@
console.error(err)
})
}
async function fetchCollabStats (): Promise<void> {
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 @@
<MetricsInfo metrics={metricsDataFront} />
{/if}
</div>
{:else if selectedTab === 'statistics-collab'}
<div class="flex-column p-3 h-full" style:overflow="auto">
{#if metricsDataCollab !== undefined}
<MetricsInfo metrics={metricsDataCollab} />
{/if}
</div>
{/if}
{:else}
<Loading />

View File

@ -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)

View File

@ -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",

View File

@ -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)

View File

@ -50,9 +50,7 @@ export class StorageExtension implements Extension {
async onLoadDocument ({ context, documentName }: withContext<onLoadDocumentPayload>): Promise<any> {
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<onStoreDocumentPayload>): Promise<void> {
@ -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<onConnectPayload>): Promise<any> {
@ -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<any> {

View File

@ -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)
}

View File

@ -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

View File

@ -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<void> {
export async function startCollaborator (ctx: MeasureContext, onClose?: () => void): Promise<void> {
setMetadata(serverToken.metadata.Secret, config.Secret)
let minioPort = 9000
@ -44,21 +44,21 @@ export async function startCollaborator (): Promise<void> {
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)