mirror of
https://github.com/hcengineering/platform.git
synced 2025-01-05 10:29:51 +03:00
UBERF-6747 Expose collaborator statistics (#5483)
Signed-off-by: Alexander Onnikov <Alexander.Onnikov@xored.com>
This commit is contained in:
parent
ae1b3eb062
commit
935788cd6b
@ -49,15 +49,26 @@
|
|||||||
console.error(err)
|
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 data: any
|
||||||
let dataFront: any
|
let dataFront: any
|
||||||
|
let dataCollab: any
|
||||||
let admin = false
|
let admin = false
|
||||||
onDestroy(
|
onDestroy(
|
||||||
ticker.subscribe(() => {
|
ticker.subscribe(() => {
|
||||||
void fetchStats()
|
void fetchStats()
|
||||||
|
|
||||||
void fetchUIStats()
|
void fetchUIStats()
|
||||||
|
void fetchCollabStats()
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
const tabs: TabItem[] = [
|
const tabs: TabItem[] = [
|
||||||
@ -73,6 +84,10 @@
|
|||||||
id: 'statistics-front',
|
id: 'statistics-front',
|
||||||
labelIntl: getEmbeddedLabel('Front')
|
labelIntl: getEmbeddedLabel('Front')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'statistics-collab',
|
||||||
|
labelIntl: getEmbeddedLabel('Collaborator')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'users',
|
id: 'users',
|
||||||
labelIntl: getEmbeddedLabel('Users')
|
labelIntl: getEmbeddedLabel('Users')
|
||||||
@ -121,6 +136,8 @@
|
|||||||
|
|
||||||
$: metricsDataFront = dataFront?.metrics as Metrics | undefined
|
$: metricsDataFront = dataFront?.metrics as Metrics | undefined
|
||||||
|
|
||||||
|
$: metricsDataCollab = dataCollab?.metrics as Metrics | undefined
|
||||||
|
|
||||||
$: totalStats = Array.from(Object.entries(activeSessions).values()).reduce(
|
$: totalStats = Array.from(Object.entries(activeSessions).values()).reduce(
|
||||||
(cur, it) => {
|
(cur, it) => {
|
||||||
const totalFind = it[1].sessions.reduce((it, itm) => itm.current.find + it, 0)
|
const totalFind = it[1].sessions.reduce((it, itm) => itm.current.find + it, 0)
|
||||||
@ -330,6 +347,12 @@
|
|||||||
<MetricsInfo metrics={metricsDataFront} />
|
<MetricsInfo metrics={metricsDataFront} />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</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}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<Loading />
|
<Loading />
|
||||||
|
@ -13,5 +13,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
|
||||||
import { startCollaborator } from '@hcengineering/collaborator'
|
import { startCollaborator } from '@hcengineering/collaborator'
|
||||||
void startCollaborator()
|
|
||||||
|
const ctx = new MeasureMetricsContext('collaborator', {}, {}, newMetrics())
|
||||||
|
|
||||||
|
void startCollaborator(ctx)
|
||||||
|
@ -49,6 +49,7 @@
|
|||||||
"@types/jest": "^29.5.5"
|
"@types/jest": "^29.5.5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@hcengineering/analytics": "^0.6.0",
|
||||||
"@hcengineering/core": "^0.6.28",
|
"@hcengineering/core": "^0.6.28",
|
||||||
"@hcengineering/account": "^0.6.0",
|
"@hcengineering/account": "^0.6.0",
|
||||||
"@hcengineering/platform": "^0.6.9",
|
"@hcengineering/platform": "^0.6.9",
|
||||||
|
@ -14,5 +14,9 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
|
||||||
import { startCollaborator } from './starter'
|
import { startCollaborator } from './starter'
|
||||||
void startCollaborator()
|
|
||||||
|
const ctx = new MeasureMetricsContext('collaborator', {}, {}, newMetrics())
|
||||||
|
|
||||||
|
void startCollaborator(ctx)
|
||||||
|
@ -50,9 +50,7 @@ export class StorageExtension implements Extension {
|
|||||||
|
|
||||||
async onLoadDocument ({ context, documentName }: withContext<onLoadDocumentPayload>): Promise<any> {
|
async onLoadDocument ({ context, documentName }: withContext<onLoadDocumentPayload>): Promise<any> {
|
||||||
this.configuration.ctx.info('load document', { documentName })
|
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> {
|
async onStoreDocument ({ context, documentName, document }: withContext<onStoreDocumentPayload>): Promise<void> {
|
||||||
@ -67,9 +65,7 @@ export class StorageExtension implements Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.collaborators.delete(documentName)
|
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> {
|
async onConnect ({ context, documentName, instance }: withContext<onConnectPayload>): Promise<any> {
|
||||||
@ -92,9 +88,7 @@ export class StorageExtension implements Extension {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.collaborators.delete(documentName)
|
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> {
|
async afterUnloadDocument ({ documentName }: afterUnloadDocumentPayload): Promise<any> {
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -13,7 +13,8 @@
|
|||||||
// limitations under the License.
|
// 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 { MinioService } from '@hcengineering/minio'
|
||||||
import { Token, decodeToken } from '@hcengineering/server-token'
|
import { Token, decodeToken } from '@hcengineering/server-token'
|
||||||
import { ServerKit } from '@hcengineering/text'
|
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
|
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||||
app.post('/rpc', async (req, res) => {
|
app.post('/rpc', async (req, res) => {
|
||||||
const authHeader = req.headers.authorization
|
const authHeader = req.headers.authorization
|
||||||
|
@ -14,16 +14,16 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import { MeasureContext } from '@hcengineering/core'
|
||||||
import { MinioService } from '@hcengineering/minio'
|
import { MinioService } from '@hcengineering/minio'
|
||||||
import { setMetadata } from '@hcengineering/platform'
|
import { setMetadata } from '@hcengineering/platform'
|
||||||
import serverToken from '@hcengineering/server-token'
|
import serverToken from '@hcengineering/server-token'
|
||||||
import { MongoClient } from 'mongodb'
|
import { MongoClient } from 'mongodb'
|
||||||
|
|
||||||
import config from './config'
|
import config from './config'
|
||||||
import { metricsContext } from './metrics'
|
|
||||||
import { start } from './server'
|
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)
|
setMetadata(serverToken.metadata.Secret, config.Secret)
|
||||||
|
|
||||||
let minioPort = 9000
|
let minioPort = 9000
|
||||||
@ -44,21 +44,21 @@ export async function startCollaborator (): Promise<void> {
|
|||||||
|
|
||||||
const mongoClient = await MongoClient.connect(config.MongoUrl)
|
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 => {
|
const close = (): void => {
|
||||||
void shutdown().then(() => {
|
void shutdown().then(() => {
|
||||||
void mongoClient.close()
|
void mongoClient.close()
|
||||||
})
|
})
|
||||||
metricsContext.info('closed')
|
onClose?.()
|
||||||
}
|
}
|
||||||
|
|
||||||
process.on('uncaughtException', (e) => {
|
process.on('uncaughtException', (e) => {
|
||||||
metricsContext.error('UncaughtException', { error: e })
|
ctx.error('UncaughtException', { error: e })
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason, promise) => {
|
process.on('unhandledRejection', (reason, promise) => {
|
||||||
metricsContext.error('Unhandled Rejection at:', { promise, reason })
|
ctx.error('Unhandled Rejection at:', { promise, reason })
|
||||||
})
|
})
|
||||||
|
|
||||||
process.on('SIGINT', close)
|
process.on('SIGINT', close)
|
||||||
|
Loading…
Reference in New Issue
Block a user