UBERF-8553: Stats as separate service (#7054)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2024-10-29 12:57:28 +07:00 committed by GitHub
parent 3a37a16fc8
commit 7c69f6f35a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 1204 additions and 683 deletions

1
.gitignore vendored
View File

@ -101,6 +101,5 @@ dev/tool/report.csv
bundle/*
bundle.js.map
tests/profiles
_*
**/bundle/model.json
.wrangler

17
.vscode/launch.json vendored
View File

@ -94,6 +94,7 @@
"REGION_INFO": "|Mongo;pg|Postgree",
"ACCOUNT_PORT": "3000",
"FRONT_URL": "http://localhost:8080",
"STATS_URL": "http://host.docker.internal:4900",
"SES_URL": "",
// "DB_NS": "account-2",
// "WS_LIVENESS_DAYS": "1",
@ -111,6 +112,22 @@
"cwd": "${workspaceRoot}/pods/account",
"protocol": "inspector"
},
{
"name": "Debug Stats",
"type": "node",
"request": "launch",
"args": ["src/__start.ts"],
"env": {
"PORT": "4900",
"SERVER_SECRET": "secret",
},
"runtimeVersion": "20",
"runtimeArgs": ["--nolazy", "-r", "ts-node/register"],
"sourceMaps": true,
"outputCapture": "std",
"cwd": "${workspaceRoot}/pods/stats",
"protocol": "inspector"
},
{
"name": "Debug Workspace",
"type": "node",

View File

@ -238,7 +238,7 @@
"summary": "Build docker with platform",
"description": "use to build all docker containers required for platform",
"safeForSimultaneousRushProcesses": true,
"shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-workspace --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot --to @hcengineering/import-tool"
"shellCommand": "rush docker:build -p 20 --to @hcengineering/pod-server --to @hcengineering/pod-front --to @hcengineering/prod --to @hcengineering/pod-account --to @hcengineering/pod-workspace --to @hcengineering/pod-collaborator --to @hcengineering/tool --to @hcengineering/pod-print --to @hcengineering/pod-sign --to @hcengineering/pod-analytics-collector --to @hcengineering/rekoni-service --to @hcengineering/pod-ai-bot --to @hcengineering/import-tool --to @hcengineering/pod-stats"
},
{
"commandKind": "global",

View File

@ -632,6 +632,9 @@ dependencies:
'@rush-temp/pod-sign':
specifier: file:./projects/pod-sign.tgz
version: file:projects/pod-sign.tgz
'@rush-temp/pod-stats':
specifier: file:./projects/pod-stats.tgz
version: file:projects/pod-stats.tgz
'@rush-temp/pod-telegram':
specifier: file:./projects/pod-telegram.tgz
version: file:projects/pod-telegram.tgz(bufferutil@4.0.8)(ts-node@10.9.2)(utf-8-validate@6.0.4)
@ -31005,7 +31008,7 @@ packages:
dev: false
file:projects/pod-collaborator.tgz:
resolution: {integrity: sha512-ayRiefa0hytLnCUUVac5/YEwko1XSxpudTToOamTBxzFfXlwGdlK6D2gQMN/dNkbTG4Wc9KLzjlwDb4VXUkLmw==, tarball: file:projects/pod-collaborator.tgz}
resolution: {integrity: sha512-AJhluNv49NI/ktWVKeHToGIi3DnJ419Qmsa8PFeR148h3X7uvmlIbPGM5bMQoVfJ1xiR8BpRIFVtu6EYzrs0iA==, tarball: file:projects/pod-collaborator.tgz}
name: '@rush-temp/pod-collaborator'
version: 0.0.0
dependencies:
@ -31458,6 +31461,46 @@ packages:
- supports-color
dev: false
file:projects/pod-stats.tgz:
resolution: {integrity: sha512-vS1oRj3hDBzCAI0SagseVBqSDxd1puF9OQJRbCcn1V0MTYC5MwW8ndQGGG1W45jMK37vgHvgFveUn7FEEhIXOw==, tarball: file:projects/pod-stats.tgz}
name: '@rush-temp/pod-stats'
version: 0.0.0
dependencies:
'@koa/cors': 5.0.0
'@types/jest': 29.5.12
'@types/koa': 2.15.0
'@types/koa-bodyparser': 4.3.12
'@types/koa-router': 7.4.8
'@types/koa__cors': 5.0.0
'@types/node': 20.11.19
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0)(eslint@8.56.0)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.56.0)(typescript@5.6.2)
cross-env: 7.0.3
esbuild: 0.20.1
eslint: 8.56.0
eslint-config-standard-with-typescript: 40.0.0(@typescript-eslint/eslint-plugin@6.21.0)(eslint-plugin-import@2.29.1)(eslint-plugin-n@15.7.0)(eslint-plugin-promise@6.1.1)(eslint@8.56.0)(typescript@5.6.2)
eslint-plugin-import: 2.29.1(eslint@8.56.0)
eslint-plugin-n: 15.7.0(eslint@8.56.0)
eslint-plugin-promise: 6.1.1(eslint@8.56.0)
jest: 29.7.0(@types/node@20.11.19)(ts-node@10.9.2)
koa: 2.15.3
koa-bodyparser: 4.4.1
koa-router: 12.0.1
prettier: 3.2.5
ts-jest: 29.1.2(esbuild@0.20.1)(jest@29.7.0)(typescript@5.6.2)
ts-node: 10.9.2(@types/node@20.11.19)(typescript@5.6.2)
typescript: 5.6.2
transitivePeerDependencies:
- '@babel/core'
- '@jest/types'
- '@swc/core'
- '@swc/wasm'
- babel-jest
- babel-plugin-macros
- node-notifier
- supports-color
dev: false
file:projects/pod-telegram-bot.tgz(bufferutil@4.0.8)(utf-8-validate@6.0.4):
resolution: {integrity: sha512-Bq0gNcjaTU0PEYhroGlOPGUD1UsNhYswf32XQ5fgz//09GK9gEBx6qBknRNYGvbN5558bUyyzKyp0KsWVfySkQ==, tarball: file:projects/pod-telegram-bot.tgz}
id: file:projects/pod-telegram-bot.tgz
@ -32395,7 +32438,7 @@ packages:
dev: false
file:projects/rekoni-service.tgz(webpack@5.90.3):
resolution: {integrity: sha512-Q0Kc1tYShHVdu2vY3Exnvm9Lvj6lKr5AdUNFpMAVP6qMB0DZPK51hWqEt0BRDSmVLfKGN8g3zWhbG3FSxFs30w==, tarball: file:projects/rekoni-service.tgz}
resolution: {integrity: sha512-z8lv7tJyejsIhl5tVjot97nBl0FX6dtUAAfLbEtfPwmiZR/uw6IpvgkfXsVJ0RJwsBMgwD0SpHBwqoxpVrgHww==, tarball: file:projects/rekoni-service.tgz}
id: file:projects/rekoni-service.tgz
name: '@rush-temp/rekoni-service'
version: 0.0.0

View File

@ -208,6 +208,7 @@ export async function configurePlatform (): Promise<void> {
setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG))
setMetadata(presentation.metadata.UploadConfig, parseUploadConfig(config.UPLOAD_CONFIG, config.UPLOAD_URL))
setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL)
setMetadata(presentation.metadata.StatsUrl, config.STATS_URL)
setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR ?? '')

View File

@ -35,6 +35,8 @@ export interface Config {
DESKTOP_UPDATES_URL?: string
DESKTOP_UPDATES_CHANNEL?: string
TELEGRAM_BOT_URL?: string
STATS_URL?: string
}
export interface Branding {

View File

@ -66,6 +66,7 @@ services:
links:
- mongodb
- minio
- stats
ports:
- 3000:3000
volumes:
@ -73,6 +74,7 @@ services:
environment:
- ACCOUNT_PORT=3000
- SERVER_SECRET=secret
- STATS_URL=http://host.docker.internal:4900
# - DB_URL=postgresql://postgres:example@postgres:5432
- DB_URL=${MONGO_URL}
# - DB_NS=account-2
@ -91,6 +93,18 @@ services:
# - INIT_SCRIPT_URL=https://raw.githubusercontent.com/hcengineering/init/main/script.yaml
# - INIT_WORKSPACE=onboarding
restart: unless-stopped
stats:
image: hardcoreeng/stats
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- 4900:4900
volumes:
- ./branding.json:/var/cfg/branding.json
environment:
- PORT=4900
- SERVER_SECRET=secret
restart: unless-stopped
workspace:
image: hardcoreeng/workspace
extra_hosts:
@ -101,12 +115,14 @@ services:
links:
- mongodb
- minio
- stats
volumes:
- ./branding.json:/var/cfg/branding.json
environment:
# - WS_OPERATION=create
- SERVER_SECRET=secret
- DB_URL=${MONGO_URL}
- STATS_URL=http://host.docker.internal:4900
# - DB_URL=postgresql://postgres:example@postgres:5432
- SES_URL=
- STORAGE_CONFIG=${STORAGE_CONFIG}
@ -126,12 +142,14 @@ services:
links:
- postgres
- minio
- stats
volumes:
- ./branding.json:/var/cfg/branding.json
environment:
# - WS_OPERATION=create
- SERVER_SECRET=secret
- DB_URL=postgresql://postgres:example@postgres:5432
- STATS_URL=http://host.docker.internal:4900
- SES_URL=
- REGION=pg
- STORAGE_CONFIG=${STORAGE_CONFIG}
@ -152,6 +170,7 @@ services:
- mongodb
- minio
- transactor
- stats
ports:
- 3078:3078
environment:
@ -159,6 +178,7 @@ services:
- SECRET=secret
- ACCOUNTS_URL=http://host.docker.internal:3000
- STORAGE_CONFIG=${STORAGE_CONFIG}
- STATS_URL=http://host.docker.internal:4900
restart: unless-stopped
front:
image: hardcoreeng/front
@ -170,6 +190,7 @@ services:
- elastic
- transactor
- collaborator
- stats
ports:
- 8087:8080
- 8088:8080
@ -177,6 +198,7 @@ services:
- SERVER_PORT=8080
- SERVER_SECRET=secret
- ACCOUNTS_URL=http://host.docker.internal:3000
- STATS_URL=http://host.docker.internal:4900
- UPLOAD_URL=/files
- ELASTIC_URL=http://host.docker.internal:9200
- GMAIL_URL=http://host.docker.internal:8088
@ -204,6 +226,7 @@ services:
- minio
- rekoni
- account
- stats
# - apm-server
ports:
- 3333:3333
@ -216,6 +239,7 @@ services:
- SERVER_PORT=3333
- SERVER_SECRET=secret
- ENABLE_COMPRESSION=true
- STATS_URL=http://host.docker.internal:4900
- ELASTIC_URL=http://host.docker.internal:9200
# - DB_URL=postgresql://postgres:example@postgres:5432
- DB_URL=${MONGO_URL}
@ -244,6 +268,7 @@ services:
- minio
- rekoni
- account
- stats
# - apm-server
ports:
- 3331:3331
@ -257,6 +282,7 @@ services:
- SERVER_SECRET=secret
- ENABLE_COMPRESSION=true
- ELASTIC_URL=http://host.docker.internal:9200
- STATS_URL=http://host.docker.internal:4900
- DB_URL=postgresql://postgres:example@postgres:5432
- MONGO_URL=${MONGO_URL}
- 'MONGO_OPTIONS={"appName": "transactor-pg", "maxPoolSize": 1}'
@ -291,6 +317,7 @@ services:
environment:
- SECRET=secret
- STORAGE_CONFIG=${STORAGE_CONFIG}
- STATS_URL=http://host.docker.internal:4900
deploy:
resources:
limits:
@ -314,6 +341,7 @@ services:
- CERTIFICATE_PATH=/var/cfg/certificate.p12
- SERVICE_ID=sign-service
- BRANDING_PATH=/var/cfg/branding.json
- STATS_URL=http://host.docker.internal:4900
deploy:
resources:
limits:
@ -333,6 +361,7 @@ services:
- SERVICE_ID=analytics-collector-service
- ACCOUNTS_URL=http://host.docker.internal:3000
- SUPPORT_WORKSPACE=support
- STATS_URL=http://host.docker.internal:4900
deploy:
resources:
limits:
@ -354,6 +383,7 @@ services:
- PASSWORD=password
- AVATAR_PATH=./avatar.png
- AVATAR_CONTENT_TYPE=.png
- STATS_URL=http://host.docker.internal:4900
deploy:
resources:
limits:
@ -372,6 +402,7 @@ services:
# - DOMAIN=domain
# - ACCOUNTS_URL=http://host.docker.internal:3000
# - SERVICE_ID=telegram-bot-service
# - STATS_URL=http://host.docker.internal:4900
# deploy:
# resources:
# limits:

View File

@ -6,5 +6,6 @@
"GMAIL_URL": "https://gmail.hc.engineering",
"CALENDAR_URL": "https://calendar.hc.engineering",
"REKONI_URL": "https://rekoni.hc.engineering",
"COLLABORATOR_URL": "wss://collaborator.hc.engineering"
"COLLABORATOR_URL": "wss://collaborator.hc.engineering",
"STATS_URL": "https://stats.hc.engineering"
}

View File

@ -19,5 +19,6 @@
"SIGN_URL": "https://sign.huly.app",
"PRINT_URL": "https://print.huly.app",
"DESKTOP_UPDATES_CHANNEL": "huly",
"TELEGRAM_BOT_URL": "https://telegram-bot.huly.app"
"TELEGRAM_BOT_URL": "https://telegram-bot.huly.app",
"STATS_URL": "https://stats.huly.app"
}

View File

@ -16,5 +16,6 @@
"AI_URL": "http://localhost:4010",
"BRANDING_URL": "/branding.json",
"VERSION": null,
"MODEL_VERSION": null
"MODEL_VERSION": null,
"STATS_URL": "http://localhost:4900"
}

View File

@ -156,6 +156,7 @@ export interface Config {
FRONT_URL?: string
PREVIEW_CONFIG?: string
UPLOAD_CONFIG?: string
STATS_URL?: string
}
export interface Branding {
@ -300,6 +301,7 @@ export async function configurePlatform() {
setMetadata(presentation.metadata.FrontUrl, config.FRONT_URL)
setMetadata(presentation.metadata.PreviewConfig, parsePreviewConfig(config.PREVIEW_CONFIG))
setMetadata(presentation.metadata.UploadConfig, parseUploadConfig(config.UPLOAD_CONFIG, config.UPLOAD_URL))
setMetadata(presentation.metadata.StatsUrl, config.STATS_URL)
setMetadata(textEditor.metadata.Collaborator, config.COLLABORATOR)

View File

@ -67,3 +67,4 @@ export * from './search'
export * from './image'
export * from './preview'
export * from './sound'
export * from './stats'

View File

@ -142,7 +142,8 @@ export default plugin(presentationId, {
UploadConfig: '' as Metadata<UploadConfig>,
PreviewConfig: '' as Metadata<PreviewConfig | undefined>,
ClientHook: '' as Metadata<ClientHook>,
SessionId: '' as Metadata<string>
SessionId: '' as Metadata<string>,
StatsUrl: '' as Metadata<string>
},
status: {
FileTooLarge: '' as StatusCode

View File

@ -0,0 +1,58 @@
import type { Metrics } from '@hcengineering/core'
// Copy from server/core/stats.ts for UI usage.
export interface MemoryStatistics {
memoryUsed: number
memoryTotal: number
memoryRSS: number
freeMem: number
totalMem: number
}
export interface CPUStatistics {
usage: number
cores: number
}
/**
* @public
*/
export interface StatisticsElement {
find: number
tx: number
}
export interface UserStatistics {
userId: string
sessionId: string
data: any
mins5: StatisticsElement
total: StatisticsElement
current: StatisticsElement
}
export interface WorkspaceStatistics {
sessions: UserStatistics[]
workspaceName: string
wsId: string
sessionsTotal: number
clientsTotal: number
service?: string
}
export interface ServiceStatistics {
serviceName: string // A service category
memory: MemoryStatistics
cpu: CPUStatistics
stats?: Metrics
workspaces?: WorkspaceStatistics[]
}
export interface OverviewStatistics {
memory: MemoryStatistics
cpu: CPUStatistics
data: Record<string, Omit<ServiceStatistics, 'stats' | 'workspaces'>>
usersTotal: number
connectionsTotal: number
admin: boolean
workspaces: WorkspaceStatistics[]
}

View File

@ -0,0 +1,38 @@
<script lang="ts">
import { getMetadata } from '@hcengineering/platform'
import presentation, { type ServiceStatistics } from '@hcengineering/presentation'
import { ticker } from '@hcengineering/ui'
import MetricsInfo from './statistics/MetricsInfo.svelte'
export let serviceName: string
export let sortOrder: 'ops' | 'avg' | 'total'
const endpoint = getMetadata(presentation.metadata.StatsUrl)
const token: string = getMetadata(presentation.metadata.Token) ?? ''
async function fetchStats (time: number): Promise<void> {
await fetch(endpoint + `/api/v1/statistics?token=${token}&name=${serviceName}`, {})
.then(async (json) => {
data = await json.json()
})
.catch((err) => {
console.error(err)
})
}
let data: ServiceStatistics | undefined
$: void fetchStats($ticker)
$: metricsData = data?.stats
</script>
<div class="flex-column p-3 h-full" style:overflow="auto">
{#if metricsData !== undefined}
<MetricsInfo metrics={metricsData} {sortOrder} />
{/if}
</div>
<style lang="scss">
.greyed {
color: rgba(black, 0.5);
}
</style>

View File

@ -1,8 +1,4 @@
<script lang="ts">
import ServerManagerCollaboratorStatistics from './ServerManagerCollaboratorStatistics.svelte'
import ServerManagerFrontStatistics from './ServerManagerFrontStatistics.svelte'
import ServerManagerServerStatistics from './ServerManagerServerStatistics.svelte'
import ServerManagerUsers from './ServerManagerUsers.svelte'
@ -10,10 +6,9 @@
import ServerManagerGeneral from './ServerManagerGeneral.svelte'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { createEventDispatcher } from 'svelte'
import presentation from '@hcengineering/presentation'
import { Header, TabItem, Switcher, Breadcrumb, IconSettings, ButtonIcon, IconClose } from '@hcengineering/ui'
import ServerManagerAccountStatistics from './ServerManagerAccountStatistics.svelte'
import { Breadcrumb, ButtonIcon, Header, IconClose, IconSettings, Switcher, TabItem } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte'
const dispatch = createEventDispatcher()
@ -22,21 +17,9 @@
id: 'general',
labelIntl: getEmbeddedLabel('General')
},
{
id: 'account',
labelIntl: getEmbeddedLabel('Account')
},
{
id: 'statistics',
labelIntl: getEmbeddedLabel('Server')
},
{
id: 'statistics-front',
labelIntl: getEmbeddedLabel('Front')
},
{
id: 'statistics-collab',
labelIntl: getEmbeddedLabel('Collaborator')
labelIntl: getEmbeddedLabel('Servers')
},
{
id: 'users',
@ -80,12 +63,6 @@
<ServerManagerUsers />
{:else if selectedTab === 'statistics'}
<ServerManagerServerStatistics />
{:else if selectedTab === 'statistics-front'}
<ServerManagerFrontStatistics />
{:else if selectedTab === 'statistics-collab'}
<ServerManagerCollaboratorStatistics />
{:else if selectedTab === 'account'}
<ServerManagerAccountStatistics />
{/if}
</div>
</div>

View File

@ -1,65 +0,0 @@
<script lang="ts">
import { Metrics } from '@hcengineering/core'
import login from '@hcengineering/login'
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { Button, IconArrowRight, ticker } from '@hcengineering/ui'
import MetricsInfo from './statistics/MetricsInfo.svelte'
const endpoint: string = getMetadata(login.metadata.AccountsUrl) ?? ''
const token: string = getMetadata(presentation.metadata.Token) ?? ''
async function fetchStats (time: number): Promise<void> {
await fetch(endpoint + `/api/v1/statistics?token=${token}`, {})
.then(async (json) => {
data = await json.json()
admin = data?.admin ?? false
})
.catch((err) => {
console.error(err)
})
}
let data: any
let admin = false
$: void fetchStats($ticker)
$: metricsData = data?.metrics as Metrics | undefined
</script>
{#if data}
<div class="flex-col p-4">
<span>
Mem: {data?.statistics?.memoryUsed} / {data?.statistics?.memoryTotal} CPU: {data?.statistics?.cpuUsage}
</span>
</div>
{/if}
{#if admin}
<div class="flex flex-col">
<div class="flex-row-center p-1">
<div class="p-3">1.</div>
<Button
icon={IconArrowRight}
label={getEmbeddedLabel('Wipe statistics')}
on:click={() => {
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=wipe-statistics`, {
method: 'PUT'
}).then(async () => {
await fetchStats(0)
})
}}
/>
</div>
</div>
{/if}
<div class="flex-column p-3 h-full" style:overflow="auto">
{#if metricsData !== undefined}
<MetricsInfo metrics={metricsData} />
{/if}
</div>
<style lang="scss">
.greyed {
color: rgba(black, 0.5);
}
</style>

View File

@ -1,37 +0,0 @@
<script lang="ts">
import { Metrics } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { ticker } from '@hcengineering/ui'
import MetricsInfo from './statistics/MetricsInfo.svelte'
const token: string = getMetadata(presentation.metadata.Token) ?? ''
async function fetchCollabStats (tick: number): Promise<void> {
const collaboratorUrl = getMetadata(presentation.metadata.CollaboratorUrl) ?? ''
const collaboratorApiUrl = collaboratorUrl.replaceAll('wss://', 'https://').replace('ws://', 'http://')
await fetch(collaboratorApiUrl + `/api/v1/statistics?token=${token}`, {})
.then(async (json) => {
dataCollab = await json.json()
})
.catch((err) => {
console.error(err)
})
}
let dataCollab: any
$: void fetchCollabStats($ticker)
$: metricsDataCollab = dataCollab?.metrics as Metrics | undefined
</script>
<div class="flex-column p-3 h-full" style:overflow="auto">
{#if metricsDataCollab !== undefined}
<MetricsInfo metrics={metricsDataCollab} />
{/if}
</div>
<style lang="scss">
.greyed {
color: rgba(black, 0.5);
}
</style>

View File

@ -1,35 +0,0 @@
<script lang="ts">
import { Metrics } from '@hcengineering/core'
import { getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { ticker } from '@hcengineering/ui'
import MetricsInfo from './statistics/MetricsInfo.svelte'
const token: string = getMetadata(presentation.metadata.Token) ?? ''
async function fetchUIStats (time: number): Promise<void> {
await fetch(`/api/v1/statistics?token=${token}`, {})
.then(async (json) => {
dataFront = await json.json()
})
.catch((err) => {
console.error(err)
})
}
let dataFront: any
$: void fetchUIStats($ticker)
$: metricsDataFront = dataFront?.metrics as Metrics | undefined
</script>
<div class="flex-column p-3 h-full" style:overflow="auto">
{#if metricsDataFront !== undefined}
<MetricsInfo metrics={metricsDataFront} />
{/if}
</div>
<style lang="scss">
.greyed {
color: rgba(black, 0.5);
}
</style>

View File

@ -241,7 +241,7 @@
{/if}
{#if metrics}
<MetricsInfo {metrics} />
<MetricsInfo {metrics} sortOrder={'avg'} />
{/if}
<style lang="scss">

View File

@ -1,21 +1,17 @@
<script lang="ts">
import { Metrics } from '@hcengineering/core'
import login from '@hcengineering/login'
import { FixedColumn } from '@hcengineering/view-resources'
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
import presentation from '@hcengineering/presentation'
import { Button, IconArrowRight, fetchMetadataLocalStorage, ticker } from '@hcengineering/ui'
import MetricsInfo from './statistics/MetricsInfo.svelte'
import presentation, { type OverviewStatistics } from '@hcengineering/presentation'
import { Button, DropdownLabels, Expandable, IconArrowRight, ticker } from '@hcengineering/ui'
import MetricsStats from './MetricsStats.svelte'
import type { MetricsData } from '@hcengineering/core'
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
const token: string = getMetadata(presentation.metadata.Token) ?? ''
let endpoint = _endpoint.replace(/^ws/g, 'http')
if (endpoint.endsWith('/')) {
endpoint = endpoint.substring(0, endpoint.length - 1)
}
const endpoint = getMetadata(presentation.metadata.StatsUrl)
async function fetchStats (time: number): Promise<void> {
await fetch(endpoint + `/api/v1/statistics?token=${token}`, {})
await fetch(endpoint + `/api/v1/overview?token=${token}`, {})
.then(async (json) => {
data = await json.json()
admin = data?.admin ?? false
@ -24,57 +20,26 @@
console.error(err)
})
}
let data: any
let data: OverviewStatistics | undefined
let admin = false
$: void fetchStats($ticker)
$: metricsData = data?.metrics as Metrics | undefined
interface StatisticsElement {
find: number
tx: number
}
$: activeSessions =
(data?.statistics?.activeSessions as Record<
string,
{
sessions: Array<{
userId: string
data?: Record<string, any>
total: StatisticsElement
mins5: StatisticsElement
current: StatisticsElement
}>
name: string
wsId: string
sessionsTotal: number
upgrading: boolean
closing: boolean
}
>) ?? {}
$: totalStats = Array.from(Object.entries(activeSessions).values()).reduce(
(cur, it) => {
const totalFind = it[1].sessions.reduce((it, itm) => itm.current.find + it, 0)
const totalTx = it[1].sessions.reduce((it, itm) => itm.current.tx + it, 0)
return {
find: cur.find + totalFind,
tx: cur.tx + totalTx
}
},
{ find: 0, tx: 0 }
)
export let sortingOrder: 'avg' | 'ops' | 'total' = 'ops'
const sortOrder = [
{ id: 'avg', label: 'Average' },
{ id: 'ops', label: 'Operations' },
{ id: 'total', label: 'Total' }
]
</script>
{#if data}
<div class="flex-col p-4">
<span>
Mem: {data.statistics.memoryUsed} / {data.statistics.memoryTotal} CPU: {data.statistics.cpuUsage}
</span>
<span>
TotalFind: {totalStats.find} / Total Tx: {totalStats.tx}
Connections: {data.connectionsTotal}
Users: {data.usersTotal}
</span>
<span> </span>
</div>
{/if}
@ -96,11 +61,38 @@
</div>
</div>
{/if}
<div class="flex-column p-3 h-full" style:overflow="auto">
{#if metricsData !== undefined}
<MetricsInfo metrics={metricsData} />
{/if}
</div>
{#if data}
<div class="p-1 flex flex-grow flex-reverse">
<DropdownLabels bind:selected={sortingOrder} items={sortOrder}></DropdownLabels>
</div>
<div class="flex-column p-3 h-full" style:overflow="auto">
{#each Object.entries(data.data).sort((a, b) => a[1].serviceName.localeCompare(b[1].serviceName)) as kv}
<Expandable bordered expandable showChevron>
<svelte:fragment slot="title">
<div class="ml-2">
{kv[1].serviceName} - {kv[0]}
</div>
</svelte:fragment>
<svelte:fragment slot="tools">
<div class="flex flex-between flex-grow">
<FixedColumn key="mem-usage">
{kv[1].memory.memoryUsed}/{kv[1].memory.memoryTotal} Mb
</FixedColumn>
<FixedColumn key="mem-rss">
{kv[1].memory.memoryRSS} Mb
</FixedColumn>
<FixedColumn key="cpu-usage">
<div class="ml-2">
{kv[1].cpu.usage}%
</div>
</FixedColumn>
</div>
</svelte:fragment>
<MetricsStats serviceName={kv[0]} sortOrder={sortingOrder} />
</Expandable>
{/each}
</div>
{/if}
<style lang="scss">
.greyed {

View File

@ -1,24 +1,19 @@
<script lang="ts">
import contact, { PersonAccount } from '@hcengineering/contact'
import { systemAccountEmail } from '@hcengineering/core'
import login from '@hcengineering/login'
import { groupByArray, systemAccountEmail } from '@hcengineering/core'
import { getEmbeddedLabel, getMetadata } from '@hcengineering/platform'
import presentation, { createQuery, isAdminUser } from '@hcengineering/presentation'
import { Button, CheckBox, fetchMetadataLocalStorage, ticker } from '@hcengineering/ui'
import presentation, { createQuery, isAdminUser, type OverviewStatistics } from '@hcengineering/presentation'
import { Button, CheckBox, ticker } from '@hcengineering/ui'
import Expandable from '@hcengineering/ui/src/components/Expandable.svelte'
import { ObjectPresenter } from '@hcengineering/view-resources'
import { workspacesStore } from '../utils'
const _endpoint: string = fetchMetadataLocalStorage(login.metadata.LoginEndpoint) ?? ''
const token: string = getMetadata(presentation.metadata.Token) ?? ''
let endpoint = _endpoint.replace(/^ws/g, 'http')
if (endpoint.endsWith('/')) {
endpoint = endpoint.substring(0, endpoint.length - 1)
}
const endpoint = getMetadata(presentation.metadata.StatsUrl)
async function fetchStats (time: number): Promise<void> {
await fetch(endpoint + `/api/v1/statistics?token=${token}`, {})
await fetch(endpoint + `/api/v1/overview?token=${token}`, {})
.then(async (json) => {
data = await json.json()
})
@ -26,34 +21,9 @@
console.error(err)
})
}
let data: any
let data: OverviewStatistics | undefined
$: void fetchStats($ticker)
interface StatisticsElement {
find: number
tx: number
}
$: activeSessions =
(data?.statistics?.activeSessions as Record<
string,
{
sessions: Array<{
userId: string
data?: Record<string, any>
total: StatisticsElement
mins5: StatisticsElement
current: StatisticsElement
}>
name: string
wsId: string
sessionsTotal: number
upgrading: boolean
closing: boolean
}
>) ?? {}
const employeeQuery = createQuery()
let employees = new Map<string, PersonAccount>()
@ -66,11 +36,13 @@
employees = emp
})
let realUsers: boolean
$: byService = groupByArray(data?.workspaces ?? [], (it) => it.service)
</script>
<div class="p-6">
<div class="flex-row-center">
Uniq users: {Object.keys(activeSessions).length} of {data?.statistics?.totalClients} connections
Uniq users: {data?.usersTotal} of {data?.connectionsTotal} connections
</div>
<div class="flex-row-center">
<CheckBox bind:checked={realUsers} />
@ -78,103 +50,100 @@
</div>
</div>
<div class="flex-column p-3 h-full" style:overflow="auto">
{#each Object.entries(activeSessions) as act}
{@const wsInstance = $workspacesStore.find((it) => it.workspaceId === act[0])}
{@const totalFind = act[1].sessions.reduce((it, itm) => itm.total.find + it, 0)}
{@const totalTx = act[1].sessions.reduce((it, itm) => itm.total.tx + it, 0)}
{#each byService.keys() as s}
Service: {s}
{#each byService.get(s) ?? [] as act}
{@const wsInstance = $workspacesStore.find((it) => it.workspaceId === act.wsId)}
{@const totalFind = act.sessions.reduce((it, itm) => itm.total.find + it, 0)}
{@const totalTx = act.sessions.reduce((it, itm) => itm.total.tx + it, 0)}
{@const currentFind = act[1].sessions.reduce((it, itm) => itm.current.find + it, 0)}
{@const currentTx = act[1].sessions.reduce((it, itm) => itm.current.tx + it, 0)}
{@const employeeGroups = Array.from(new Set(act[1].sessions.map((it) => it.userId))).filter(
(it) => systemAccountEmail !== it || !realUsers
)}
{@const realGroup = Array.from(new Set(act[1].sessions.map((it) => it.userId))).filter(
(it) => systemAccountEmail !== it
)}
{#if employeeGroups.length > 0}
<span class="flex-col">
<Expandable contentColor expanded={false} expandable={true} bordered>
<svelte:fragment slot="title">
<div class="flex flex-row-center flex-between flex-grow p-1">
<div class="fs-title" class:greyed={realGroup.length === 0}>
Workspace: {wsInstance?.workspaceName ?? act[0]}: {employeeGroups.length} current 5 mins => {currentFind}/{currentTx},
total => {totalFind}/{totalTx}
{#if act[1].upgrading}
(Upgrading)
{/if}
{#if act[1].closing}
(Closing)
{@const currentFind = act.sessions.reduce((it, itm) => itm.current.find + it, 0)}
{@const currentTx = act.sessions.reduce((it, itm) => itm.current.tx + it, 0)}
{@const employeeGroups = Array.from(new Set(act.sessions.map((it) => it.userId))).filter(
(it) => systemAccountEmail !== it || !realUsers
)}
{@const realGroup = Array.from(new Set(act.sessions.map((it) => it.userId))).filter(
(it) => systemAccountEmail !== it
)}
{#if employeeGroups.length > 0}
<span class="flex-col">
<Expandable contentColor expanded={false} expandable={true} bordered>
<svelte:fragment slot="title">
<div class="flex flex-row-center flex-between flex-grow p-1">
<div class="fs-title" class:greyed={realGroup.length === 0}>
Workspace: {wsInstance?.workspaceName ?? act.wsId}: {employeeGroups.length} current 5 mins => {currentFind}/{currentTx},
total => {totalFind}/{totalTx}
</div>
{#if isAdminUser()}
<Button
label={getEmbeddedLabel('Force close')}
size={'small'}
kind={'ghost'}
on:click={() => {
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=force-close&wsId=${act.wsId}`, {
method: 'PUT'
})
}}
/>
{/if}
</div>
{#if isAdminUser()}
<Button
label={getEmbeddedLabel('Force close')}
size={'small'}
kind={'ghost'}
on:click={() => {
void fetch(endpoint + `/api/v1/manage?token=${token}&operation=force-close&wsId=${act[1].wsId}`, {
method: 'PUT'
})
}}
/>
{/if}
</div>
</svelte:fragment>
<div class="flex-col">
{#each employeeGroups as employeeId}
{@const employee = employees.get(employeeId)}
{@const connections = act[1].sessions.filter((it) => it.userId === employeeId)}
</svelte:fragment>
<div class="flex-col">
{#each employeeGroups as employeeId}
{@const employee = employees.get(employeeId)}
{@const connections = act.sessions.filter((it) => it.userId === employeeId)}
{@const find = connections.reduce((it, itm) => itm.current.find + it, 0)}
{@const txes = connections.reduce((it, itm) => itm.current.tx + it, 0)}
<div class="p-1 flex-col ml-4">
<Expandable>
<svelte:fragment slot="title">
<div class="flex-row-center p-1">
{#if employee}
<ObjectPresenter
_class={contact.mixin.Employee}
objectId={employee.person}
props={{ shouldShowAvatar: true, disabled: true }}
/>
{:else}
{employeeId}
{/if}
: {connections.length}
<div class="ml-4">
<div class="ml-1">{find} rx/{txes} tx</div>
</div>
</div>
</svelte:fragment>
{#each connections as user, i}
<div class="flex-row-center ml-10">
#{i}
{user.userId}
<div class="p-1">
Total: {user.total.find} rx/{user.total.tx} tx
</div>
<div class="p-1">
Previous 5 mins: {user.mins5.find} rx/{user.mins5.tx} tx
</div>
<div class="p-1">
Current 5 mins: {user.current.find} tx/{user.current.tx} tx
</div>
</div>
<div class="p-1 flex-col ml-10">
{#each Object.entries(user.data ?? {}) as [k, v]}
<div class="p-1">
{k}: {JSON.stringify(v)}
{@const find = connections.reduce((it, itm) => itm.current.find + it, 0)}
{@const txes = connections.reduce((it, itm) => itm.current.tx + it, 0)}
<div class="p-1 flex-col ml-4">
<Expandable>
<svelte:fragment slot="title">
<div class="flex-row-center p-1">
{#if employee}
<ObjectPresenter
_class={contact.mixin.Employee}
objectId={employee.person}
props={{ shouldShowAvatar: true, disabled: true }}
/>
{:else}
{employeeId}
{/if}
: {connections.length}
<div class="ml-4">
<div class="ml-1">{find} rx/{txes} tx</div>
</div>
{/each}
</div>
{/each}
</Expandable>
</div>
{/each}
</div>
</Expandable>
</span>
{/if}
</div>
</svelte:fragment>
{#each connections as user, i}
<div class="flex-row-center ml-10">
#{i}
{user.userId}
<div class="p-1">
Total: {user.total.find} rx/{user.total.tx} tx
</div>
<div class="p-1">
Previous 5 mins: {user.mins5.find} rx/{user.mins5.tx} tx
</div>
<div class="p-1">
Current 5 mins: {user.current.find} tx/{user.current.tx} tx
</div>
</div>
<div class="p-1 flex-col ml-10">
{#each Object.entries(user.data ?? {}) as [k, v]}
<div class="p-1">
{k}: {JSON.stringify(v)}
</div>
{/each}
</div>
{/each}
</Expandable>
</div>
{/each}
</div>
</Expandable>
</span>
{/if}
{/each}
{/each}
</div>

View File

@ -2,13 +2,13 @@
import { Metrics, type MetricsData } from '@hcengineering/core'
import { getEmbeddedLabel } from '@hcengineering/platform'
import { Button, Expandable, showPopup } from '@hcengineering/ui'
import DropdownLabels from '@hcengineering/ui/src/components/DropdownLabels.svelte'
import { FixedColumn } from '@hcengineering/view-resources'
import Params from './Params.svelte'
export let metrics: Metrics
export let level = 0
export let name: string = 'System'
export let sortOrder: 'avg' | 'ops' | 'total'
$: haschilds =
Object.keys(metrics.measurements).length > 0 ||
@ -24,13 +24,6 @@
}
return `${Math.floor((time / ops) * 100) / 100}`
}
export let sortingOrder: 'avg' | 'ops' | 'total' = 'ops'
const sortOrder = [
{ id: 'avg', label: 'Average' },
{ id: 'ops', label: 'Operations' },
{ id: 'total', label: 'Total' }
]
const getSorted = (v: Record<string, MetricsData>, sortingOrder: 'avg' | 'ops' | 'total') => {
if (sortingOrder === 'avg') {
return Object.entries(v).sort((a, b) => b[1].value / (b[1].operations + 1) - a[1].value / (a[1].operations + 1))
@ -40,13 +33,23 @@
return Object.entries(v).sort((a, b) => b[1].value - a[1].value)
}
}
function getSortedMeasurements (
m: Record<string, Metrics>,
sortingOrder: 'avg' | 'ops' | 'total'
): [string, Metrics][] {
const ms = [...Object.entries(m)]
if (sortingOrder === 'avg') {
ms.sort((a, b) => b[1].value / (b[1].operations + 1) - a[1].value / (a[1].operations + 1))
} else if (sortingOrder === 'ops') {
ms.sort((a, b) => b[1].operations + 1 - (a[1].operations + 1))
} else {
ms.sort((a, b) => b[1].value - a[1].value)
}
return ms
}
</script>
{#if level === 0}
<div class="p-1 flex flex-grow flex-reverse">
<DropdownLabels bind:selected={sortingOrder} items={sortOrder}></DropdownLabels>
</div>
{/if}
<Expandable
expanded={level === 0}
expandable={level !== 0 && haschilds}
@ -113,14 +116,14 @@
</Expandable>
</div>
{/if}
{#each Object.entries(metrics.measurements) as [k, v], i (k)}
{#each getSortedMeasurements(metrics.measurements, sortOrder) as [k, v], i (k)}
<div style:margin-left={`${level * 0.5}rem`}>
<svelte:self metrics={v} name="{i}. {k}" level={level + 1} {sortingOrder} />
<svelte:self metrics={v} name="{i}. {k}" level={level + 1} {sortOrder} />
</div>
{/each}
{#each Object.entries(metrics.params) as [k, v], i}
{#each Object.entries(metrics.params) as [k, v]}
<div style:margin-left={`${level * 0.5}rem`}>
{#each getSorted(v, sortingOrder) as [kk, vv]}
{#each getSorted(v, sortOrder) as [kk, vv]}
{@const childExpandable =
vv.topResult !== undefined &&
vv.topResult.length > 0 &&

View File

@ -1,42 +1,30 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
import { Analytics } from '@hcengineering/analytics'
import { MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core'
import { loadBrandingMap } from '@hcengineering/server-core'
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import { serveAccount } from '@hcengineering/account-service'
import { Analytics } from '@hcengineering/analytics'
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { initStatisticsContext, loadBrandingMap } from '@hcengineering/server-core'
import { join } from 'path'
configureAnalytics(process.env.SENTRY_DSN, {})
Analytics.setTag('application', 'account')
const metricsContext = new MeasureMetricsContext(
'account',
{},
{},
newMetrics(),
new SplitLogger('account', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
let oldMetricsValue = ''
const intTimer = setInterval(() => {
const val = metricsToString(metricsContext.metrics, 'Account', 140)
if (val !== oldMetricsValue) {
oldMetricsValue = val
void writeFile('metrics.txt', val).catch((err) => {
console.error(err)
})
}
}, 30000)
const metricsContext = initStatisticsContext('account', {
factory: () =>
new MeasureMetricsContext(
'account',
{},
{},
newMetrics(),
new SplitLogger('account', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
const brandingPath = process.env.BRANDING_PATH
serveAccount(metricsContext, loadBrandingMap(brandingPath), () => {
clearInterval(intTimer)
})
serveAccount(metricsContext, loadBrandingMap(brandingPath), () => {})

View File

@ -16,48 +16,33 @@
import { Analytics } from '@hcengineering/analytics'
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
import { startBackup } from '@hcengineering/backup-service'
import { MeasureMetricsContext, metricsToString, newMetrics, type Tx } from '@hcengineering/core'
import { type PipelineFactory } from '@hcengineering/server-core'
import { MeasureMetricsContext, newMetrics, type Tx } from '@hcengineering/core'
import { initStatisticsContext, type PipelineFactory } from '@hcengineering/server-core'
import { createBackupPipeline, getConfig } from '@hcengineering/server-pipeline'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import { readFileSync } from 'node:fs'
const model = JSON.parse(readFileSync(process.env.MODEL_JSON ?? 'model.json').toString()) as Tx[]
const metricsContext = new MeasureMetricsContext(
'backup',
{},
{},
newMetrics(),
new SplitLogger('backup-service', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
const metricsContext = initStatisticsContext('backup', {
factory: () =>
new MeasureMetricsContext(
'backup',
{},
{},
newMetrics(),
new SplitLogger('backup', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
const sentryDSN = process.env.SENTRY_DSN
configureAnalytics(sentryDSN, {})
Analytics.setTag('application', 'backup-service')
let oldMetricsValue = ''
const intTimer = setInterval(() => {
const val = metricsToString(metricsContext.metrics, 'Backup', 140)
if (val !== oldMetricsValue) {
oldMetricsValue = val
void writeFile('metrics.txt', val).catch((err) => {
console.error(err)
})
}
}, 30000)
const onClose = (): void => {
clearInterval(intTimer)
metricsContext.info('Closed')
}
startBackup(
metricsContext,
(mongoUrl, storageAdapter) => {
@ -78,6 +63,3 @@ startBackup(
})
}
)
process.on('SIGINT', onClose)
process.on('SIGTERM', onClose)

View File

@ -51,6 +51,7 @@
"@hcengineering/collaborator": "^0.6.0",
"@hcengineering/platform": "^0.6.11",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/analytics-service": "^0.6.0"
"@hcengineering/analytics-service": "^0.6.0",
"@hcengineering/server-core": "^0.6.1"
}
}

View File

@ -14,38 +14,27 @@
//
import { Analytics } from '@hcengineering/analytics'
import { MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core'
import { startCollaborator } from '@hcengineering/collaborator'
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
import { writeFile } from 'fs/promises'
import { startCollaborator } from '@hcengineering/collaborator'
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { initStatisticsContext } from '@hcengineering/server-core'
import { join } from 'path'
configureAnalytics(process.env.SENTRY_DSN, {})
Analytics.setTag('application', 'collaborator')
const ctx = new MeasureMetricsContext(
'collaborator',
{},
{},
newMetrics(),
new SplitLogger('collaborator', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
let oldMetricsValue = ''
const intTimer = setInterval(() => {
const val = metricsToString(ctx.metrics, 'Collaborator', 140)
if (val !== oldMetricsValue) {
oldMetricsValue = val
void writeFile('metrics.txt', val).catch((err) => {
console.error(err)
})
}
}, 30000)
void startCollaborator(ctx, () => {
clearInterval(intTimer)
const metricsContext = initStatisticsContext('collaborator', {
factory: () =>
new MeasureMetricsContext(
'collaborator',
{},
{},
newMetrics(),
new SplitLogger('collaborator', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
void startCollaborator(metricsContext, () => {})

View File

@ -7,20 +7,24 @@ import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { startFront } from '@hcengineering/front/src/starter'
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
import { join } from 'path'
import { initStatisticsContext } from '@hcengineering/server-core'
configureAnalytics(process.env.SENTRY_DSN, {})
Analytics.setTag('application', 'front')
const metricsContext = new MeasureMetricsContext(
'front',
{},
{},
newMetrics(),
new SplitLogger('front', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
const metricsContext = initStatisticsContext('front', {
factory: () =>
new MeasureMetricsContext(
'front',
{},
{},
newMetrics(),
new SplitLogger('front', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
startFront(metricsContext, {
GITHUB_APP: process.env.GITHUB_APP ?? '',
@ -40,5 +44,6 @@ startFront(metricsContext, {
DESKTOP_UPDATES_CHANNEL: process.env.DESKTOP_UPDATES_CHANNEL,
ANALYTICS_COLLECTOR_URL: process.env.ANALYTICS_COLLECTOR_URL,
AI_URL: process.env.AI_URL,
TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL
TELEGRAM_BOT_URL: process.env.TELEGRAM_BOT_URL,
STATS_URL: process.env.STATS_URL
})

View File

@ -11,7 +11,16 @@ import notification from '@hcengineering/notification'
import { setMetadata } from '@hcengineering/platform'
import { serverConfigFromEnv } from '@hcengineering/server'
import serverAiBot from '@hcengineering/server-ai-bot'
import serverCore, { type StorageConfiguration, loadBrandingMap } from '@hcengineering/server-core'
import serverCore, {
type ConnectionSocket,
type Session,
type StorageConfiguration,
type UserStatistics,
type Workspace,
type WorkspaceStatistics,
initStatisticsContext,
loadBrandingMap
} from '@hcengineering/server-core'
import serverNotification from '@hcengineering/server-notification'
import { storageConfigFromEnv } from '@hcengineering/server-storage'
import serverTelegram from '@hcengineering/server-telegram'
@ -20,14 +29,19 @@ import { startHttpServer } from '@hcengineering/server-ws'
import { join } from 'path'
import { start } from '.'
import { profileStart, profileStop } from './inspector'
import { getMetricsContext } from './metrics'
configureAnalytics(process.env.SENTRY_DSN, {})
Analytics.setTag('application', 'transactor')
let getUsers: () => WorkspaceStatistics[] = () => {
return []
}
// Force create server metrics context with proper logging
getMetricsContext(
() =>
const metricsContext = initStatisticsContext('transactor', {
getUsers: (): WorkspaceStatistics[] => {
return getUsers()
},
factory: () =>
new MeasureMetricsContext(
'server',
{},
@ -38,7 +52,7 @@ getMetricsContext(
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
)
})
setOperationLogProfiling(process.env.OPERATION_PROFILING === 'true')
@ -59,7 +73,7 @@ setMetadata(serverCore.metadata.ElasticIndexVersion, 'v1')
setMetadata(serverTelegram.metadata.BotUrl, process.env.TELEGRAM_BOT_URL)
setMetadata(serverAiBot.metadata.SupportWorkspaceId, process.env.SUPPORT_WORKSPACE)
const shutdown = start(config.dbUrl, {
const { shutdown, sessionManager } = start(metricsContext, config.dbUrl, {
fullTextUrl: config.elasticUrl,
storageConfig,
rekoniUrl: config.rekoniUrl,
@ -77,6 +91,31 @@ const shutdown = start(config.dbUrl, {
mongoUrl: config.mongoUrl
})
const entryToUserStats = (session: Session, socket: ConnectionSocket): UserStatistics => {
return {
current: session.current,
mins5: session.mins5,
userId: session.getUser(),
sessionId: socket.id,
total: session.total,
data: socket.data
}
}
const workspaceToWorkspaceStats = (ws: Workspace): WorkspaceStatistics => {
return {
clientsTotal: new Set(Array.from(ws.sessions.values()).map((it) => it.session.getUser())).size,
sessionsTotal: ws.sessions.size,
workspaceName: ws.workspaceName,
wsId: ws.workspaceId.name,
sessions: Array.from(ws.sessions.values()).map((it) => entryToUserStats(it.session, it.socket))
}
}
getUsers = () => {
return Array.from(sessionManager.workspaces.values()).map((it) => workspaceToWorkspaceStats(it))
}
const close = (): void => {
console.trace('Exiting from server')
console.log('Shutdown request accepted')

View File

@ -1,57 +0,0 @@
import { type MeasureContext, MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core'
import { writeFile } from 'fs/promises'
const metricsFile = process.env.METRICS_FILE
// const logsRoot = process.env.LOGS_ROOT
const metricsConsole = (process.env.METRICS_CONSOLE ?? 'false') === 'true'
const METRICS_UPDATE_INTERVAL = !metricsConsole ? 1000 : 60000
/**
* @public
*/
let metricsContext: MeasureContext | undefined
/**
* @public
*/
export function getMetricsContext (factory?: () => MeasureMetricsContext): MeasureContext {
if (metricsContext !== undefined) {
return metricsContext
}
const metrics = newMetrics()
if (factory !== undefined) {
metricsContext = factory()
} else {
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:', val)
}
}
}, METRICS_UPDATE_INTERVAL)
const closeTimer = (): void => {
clearInterval(intTimer)
}
process.on('SIGINT', closeTimer)
process.on('SIGTERM', closeTimer)
}
return metricsContext
}

View File

@ -14,12 +14,23 @@
// limitations under the License.
//
import { type Branding, type BrandingMap, type Tx, type WorkspaceIdWithUrl } from '@hcengineering/core'
import {
type Branding,
type BrandingMap,
type MeasureContext,
type Tx,
type WorkspaceIdWithUrl
} from '@hcengineering/core'
import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { getMetricsContext } from './metrics'
import { ClientSession, startSessionManager } from '@hcengineering/server'
import { type Pipeline, type ServerFactory, type Session, type StorageConfiguration } from '@hcengineering/server-core'
import {
type Pipeline,
type ServerFactory,
type Session,
type SessionManager,
type StorageConfiguration
} from '@hcengineering/server-core'
import { type Token } from '@hcengineering/server-token'
import { serverAiBotId } from '@hcengineering/server-ai-bot'
@ -35,6 +46,7 @@ registerStringLoaders()
* @public
*/
export function start (
metrics: MeasureContext,
dbUrl: string,
opt: {
fullTextUrl: string
@ -58,9 +70,7 @@ export function start (
mongoUrl?: string
}
): () => Promise<void> {
const metrics = getMetricsContext()
): { shutdown: () => Promise<void>, sessionManager: SessionManager } {
registerServerPlugins()
const externalStorage = buildStorageFromConfig(opt.storageConfig)
@ -91,7 +101,7 @@ export function start (
return new ClientSession(token, pipeline, workspaceId, branding, token.extra?.mode === 'backup')
}
const onClose = startSessionManager(getMetricsContext(), {
const { shutdown: onClose, sessionManager } = startSessionManager(metrics, {
pipelineFactory,
sessionFactory,
port: opt.port,
@ -102,8 +112,11 @@ export function start (
externalStorage,
profiling: opt.profiling
})
return async () => {
await externalStorage.close()
await onClose()
return {
shutdown: async () => {
await externalStorage.close()
await onClose()
},
sessionManager
}
}

7
pods/stats/.eslintrc.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
extends: ['./node_modules/@hcengineering/platform-rig/profiles/node/eslint.config.json'],
parserOptions: {
tsconfigRootDir: __dirname,
project: './tsconfig.json'
}
}

4
pods/stats/.npmignore Normal file
View File

@ -0,0 +1,4 @@
*
!/lib/**
!CHANGELOG.md
/lib/**/__tests__/

15
pods/stats/Dockerfile Normal file
View File

@ -0,0 +1,15 @@
FROM node:20
WORKDIR /usr/src/app
RUN apt-get update
RUN apt-get install libjemalloc2
RUN apt-get clean
ENV LD_PRELOAD=libjemalloc.so.2
ENV MALLOC_CONF=dirty_decay_ms:1000,narenas:2,background_thread:true
COPY bundle/bundle.js ./
EXPOSE 4900
CMD [ "node", "bundle.js" ]

20
pods/stats/build.sh Executable file
View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
#
# Copyright © 2020, 2021 Anticrm Platform Contributors.
# Copyright © 2021 Hardcore Engineering Inc.
#
# Licensed under the Eclipse Public License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may
# obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#
# See the License for the specific language governing permissions and
# limitations under the License.
#
rushx bundle
rushx docker:build
rushx docker:push

View File

@ -0,0 +1,5 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/rig-package/rig.schema.json",
"rigPackageName": "@hcengineering/platform-rig",
"rigProfile": "node"
}

View File

@ -0,0 +1,7 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
roots: ["./src"],
coverageReporters: ["text-summary", "html"]
}

65
pods/stats/package.json Normal file
View File

@ -0,0 +1,65 @@
{
"name": "@hcengineering/pod-stats",
"version": "0.6.0",
"main": "lib/index.js",
"svelte": "src/index.ts",
"types": "types/index.d.ts",
"author": "Anticrm Platform Contributors",
"template": "@hcengineering/node-package",
"license": "EPL-2.0",
"scripts": {
"start": "ts-node src/__start.ts",
"build": "compile",
"build:watch": "compile",
"_phase:bundle": "rushx bundle",
"_phase:docker-build": "rushx docker:build",
"_phase:docker-staging": "rushx docker:staging",
"bundle": "mkdir -p bundle && esbuild src/__start.ts --external:*.node --external:snappy --keep-names --bundle --define:process.env.MODEL_VERSION=$(node ../../common/scripts/show_version.js) --platform=node --outfile=bundle/bundle.js --log-level=error --sourcemap=external",
"docker:build": "../../common/scripts/docker_build.sh hardcoreeng/stats",
"docker:tbuild": "docker build -t hardcoreeng/stats . --platform=linux/amd64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/stats",
"docker:abuild": "docker build -t hardcoreeng/stats . --platform=linux/arm64 && ../../common/scripts/docker_tag_push.sh hardcoreeng/stats",
"docker:staging": "../../common/scripts/docker_tag.sh hardcoreeng/stats staging",
"docker:push": "../../common/scripts/docker_tag.sh hardcoreeng/stats",
"format": "format src",
"test": "jest --passWithNoTests --silent --forceExit",
"_phase:build": "compile transpile src",
"_phase:test": "jest --passWithNoTests --silent --forceExit",
"_phase:format": "format src",
"_phase:validate": "compile validate"
},
"devDependencies": {
"cross-env": "~7.0.3",
"@hcengineering/platform-rig": "^0.6.0",
"@types/node": "~20.11.16",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-promise": "^6.1.1",
"eslint-plugin-n": "^15.4.0",
"eslint": "^8.54.0",
"esbuild": "^0.20.0",
"@types/koa-bodyparser": "^4.3.12",
"@types/koa-router": "^7.4.8",
"@types/koa": "^2.15.0",
"@types/koa__cors": "^5.0.0",
"@typescript-eslint/parser": "^6.11.0",
"eslint-config-standard-with-typescript": "^40.0.0",
"prettier": "^3.1.0",
"ts-node": "^10.8.0",
"typescript": "^5.3.3",
"jest": "^29.7.0",
"ts-jest": "^29.1.1",
"@types/jest": "^29.5.5"
},
"dependencies": {
"@hcengineering/platform": "^0.6.11",
"@hcengineering/core": "^0.6.32",
"koa": "^2.15.3",
"koa-router": "^12.0.1",
"koa-bodyparser": "^4.4.1",
"@koa/cors": "^5.0.0",
"@hcengineering/server-token": "^0.6.11",
"@hcengineering/server-core": "^0.6.1",
"@hcengineering/analytics": "^0.6.0",
"@hcengineering/analytics-service": "^0.6.0"
}
}

39
pods/stats/src/__start.ts Normal file
View File

@ -0,0 +1,39 @@
//
// Copyright © 2023 Hardcore Engineering Inc.
//
import { Analytics } from '@hcengineering/analytics'
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
import { MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import { serveStats } from './stats'
configureAnalytics(process.env.SENTRY_DSN, {})
Analytics.setTag('application', 'stats')
const metricsContext = new MeasureMetricsContext(
'stats',
{},
{},
newMetrics(),
new SplitLogger('stats', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
let oldMetricsValue = ''
const intTimer = setInterval(() => {
const val = metricsToString(metricsContext.metrics, 'Stats', 140)
if (val !== oldMetricsValue) {
oldMetricsValue = val
void writeFile('metrics.txt', val).catch((err) => {
console.error(err)
})
}
}, 30000)
serveStats(metricsContext, () => {
clearInterval(intTimer)
})

239
pods/stats/src/stats.ts Normal file
View File

@ -0,0 +1,239 @@
//
// Copyright © 2024 Hardcore Engineering Inc.
//
import { Analytics } from '@hcengineering/analytics'
import { metricsAggregate, type MeasureContext } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import {
getCPUInfo,
getMemoryInfo,
type CPUStatistics,
type MemoryStatistics,
type ServiceStatistics,
type WorkspaceStatistics
} from '@hcengineering/server-core'
import serverToken, { decodeToken } from '@hcengineering/server-token'
import cors from '@koa/cors'
import Koa from 'koa'
import bodyParser from 'koa-bodyparser'
import Router from 'koa-router'
const serviceTimeout = 30000
interface ServiceStatisticsEx extends ServiceStatistics {
lastUpdate: number // Last updated
}
interface OverviewStatistics {
memory: MemoryStatistics
cpu: CPUStatistics
data: Record<string, Omit<ServiceStatistics, 'stats' | 'workspaces'>>
usersTotal: number
connectionsTotal: number
admin: boolean
workspaces: WorkspaceStatistics[]
}
/**
* @public
*/
export function serveStats (ctx: MeasureContext, onClose?: () => void): void {
const servicePort = parseInt(process.env.PORT ?? '4900')
ctx.info('Starting stats service')
const serverSecret = process.env.SERVER_SECRET
if (serverSecret === undefined) {
ctx.info('Please provide server secret')
process.exit(1)
}
setMetadata(serverToken.metadata.Secret, serverSecret)
const statistics = new Map<string, ServiceStatisticsEx>()
const timeouts = new Map<string, number>()
const app = new Koa()
const router = new Router()
app.use(
cors({
credentials: true
})
)
app.use(bodyParser())
router.get('/api/v1/overview', (req, res) => {
try {
const token = req.query.token as string
const payload = decodeToken(token)
const admin = payload.extra?.admin === 'true'
if (!admin) {
req.res.setHeader('Content-Type', 'application/json')
const dta: OverviewStatistics = {
memory: getMemoryInfo(),
cpu: getCPUInfo(),
data: {},
usersTotal: 0,
connectionsTotal: 0,
admin: false,
workspaces: []
}
req.body = dta
return
}
const toClean: string[] = []
let usersTotal: number = 0
let connectionsTotal: number = 0
const allWorkspaces: WorkspaceStatistics[] = []
const json: Record<string, Omit<ServiceStatistics, 'stats' | 'workspaces'>> = {}
for (const [k, v] of statistics.entries()) {
if (Date.now() - v.lastUpdate > serviceTimeout) {
timeouts.set(v.serviceName, (timeouts.get(v.serviceName) ?? 0) + 1)
toClean.push(k)
continue
}
const { stats: _, workspaces, ...data } = v
allWorkspaces.push(...(workspaces ?? []))
if (workspaces !== undefined) {
for (const ws of workspaces) {
ws.service = k
usersTotal += ws.clientsTotal
connectionsTotal += ws.sessionsTotal
}
}
json[k] = {
...data
}
}
for (const k of toClean) {
statistics.delete(k)
}
const dta: OverviewStatistics = {
memory: getMemoryInfo(),
cpu: getCPUInfo(),
data: json,
usersTotal,
connectionsTotal,
admin: true,
workspaces: allWorkspaces
}
req.body = dta
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
req.res.writeHead(404, {})
req.res.end()
}
})
router.get('/api/v1/statistics', (req, res) => {
try {
const token = req.query.token as string
const payload = decodeToken(token)
const admin = payload.extra?.admin === 'true'
ctx.info('get stats', { admin, service: req.query.name })
if (admin) {
const json = statistics.get((req.query.name as string) ?? '')
if (json !== undefined) {
req.res.setHeader('Content-Type', 'application/json')
const result: ServiceStatistics = {
...json,
stats: json.stats !== undefined ? metricsAggregate(json.stats) : undefined
}
req.body = result
return
}
}
const json = {}
req.res.setHeader('Content-Type', 'application/json')
req.body = json
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
req.res.writeHead(404, {})
req.res.end()
}
})
router.put('/api/v1/statistics', (req, res) => {
try {
const token = req.query.token as string
const payload = decodeToken(token)
const service = payload.extra?.service === 'true'
const serviceName = (req.query.name as string) ?? ''
if (service) {
ctx.info('put stats', { service: req.query.name })
statistics.set(serviceName, {
...(req.request.body as ServiceStatistics),
lastUpdate: Date.now()
})
}
req.res.writeHead(200)
req.res.end()
} catch (err: any) {
Analytics.handleError(err)
console.error(err)
req.res.writeHead(404, {})
req.res.end()
}
})
router.put('/api/v1/manage', async (req, res) => {
try {
const token = req.query.token as string
const payload = decodeToken(token)
if (payload.extra?.admin !== 'true') {
req.res.writeHead(404, {})
req.res.end()
return
}
const operation = req.query.operation
switch (operation) {
case 'wipe-statistics': {
statistics.clear()
req.res.writeHead(200)
req.res.end()
return
}
}
req.res.writeHead(404, {})
req.res.end()
} catch (err: any) {
Analytics.handleError(err)
req.res.writeHead(404, {})
req.res.end()
}
})
app.use(router.routes()).use(router.allowedMethods())
const server = app.listen(servicePort, () => {
console.log(`server started on port ${servicePort}`)
})
const close = (): void => {
onClose?.()
server.close()
}
process.on('uncaughtException', (e) => {
ctx.error('uncaughtException', { error: e })
})
process.on('unhandledRejection', (reason, promise) => {
ctx.error('Unhandled Rejection at:', { reason, promise })
})
process.on('SIGINT', close)
process.on('SIGTERM', close)
process.on('exit', close)
}

10
pods/stats/tsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"extends": "./node_modules/@hcengineering/platform-rig/profiles/node/tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"outDir": "./lib",
"declarationDir": "./types",
"tsBuildInfoFile": ".build/build.tsbuildinfo"
}
}

View File

@ -2,13 +2,12 @@
// Copyright © 2023 Hardcore Engineering Inc.
//
import { Analytics } from '@hcengineering/analytics'
import { MeasureMetricsContext, metricsToString, newMetrics, type Tx } from '@hcengineering/core'
import { loadBrandingMap } from '@hcengineering/server-core'
import { configureAnalytics, SplitLogger } from '@hcengineering/analytics-service'
import { MeasureMetricsContext, newMetrics, type Tx } from '@hcengineering/core'
import builder, { getModelVersion, migrateOperations } from '@hcengineering/model-all'
import { writeFile } from 'fs/promises'
import { join } from 'path'
import { initStatisticsContext, loadBrandingMap } from '@hcengineering/server-core'
import { serveWorkspaceAccount } from '@hcengineering/workspace-service'
import { join } from 'path'
const enabled = (process.env.MODEL_ENABLED ?? '*').split(',').map((it) => it.trim())
const disabled = (process.env.MODEL_DISABLED ?? '').split(',').map((it) => it.trim())
@ -18,31 +17,28 @@ const txes = JSON.parse(JSON.stringify(builder(enabled, disabled).getTxes())) as
configureAnalytics(process.env.SENTRY_DSN, {})
Analytics.setTag('application', 'workspace')
const metricsContext = new MeasureMetricsContext(
'workspace',
{},
{},
newMetrics(),
new SplitLogger('workspace', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
let oldMetricsValue = ''
const intTimer = setInterval(() => {
const val = metricsToString(metricsContext.metrics, 'Workspace', 140)
if (val !== oldMetricsValue) {
oldMetricsValue = val
void writeFile('metrics.txt', val).catch((err) => {
console.error(err)
})
}
}, 30000)
// Force create server metrics context with proper logging
const metricsContext = initStatisticsContext('workspace', {
factory: () =>
new MeasureMetricsContext(
'workspace',
{},
{},
newMetrics(),
new SplitLogger('workspace', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
const brandingPath = process.env.BRANDING_PATH
serveWorkspaceAccount(metricsContext, getModelVersion(), txes, migrateOperations, loadBrandingMap(brandingPath), () => {
clearInterval(intTimer)
})
serveWorkspaceAccount(
metricsContext,
getModelVersion(),
txes,
migrateOperations,
loadBrandingMap(brandingPath),
() => {}
)

View File

@ -825,6 +825,11 @@
"projectFolder": "pods/front",
"shouldPublish": false
},
{
"packageName": "@hcengineering/pod-stats",
"projectFolder": "pods/stats",
"shouldPublish": false
},
{
"packageName": "@hcengineering/pod-server",
"projectFolder": "pods/server",

View File

@ -32,4 +32,5 @@ export * from './dbAdapterManager'
export * from './domainHelper'
export * from './nullAdapter'
export * from './service'
export * from './stats'
export * from './triggers'

155
server/core/src/stats.ts Normal file
View File

@ -0,0 +1,155 @@
import type { MeasureContext, Metrics } from '@hcengineering/core'
import { concatLink, MeasureMetricsContext, metricsToString, newMetrics, systemAccountEmail } from '@hcengineering/core'
import { generateToken } from '@hcengineering/server-token'
import { writeFile } from 'fs/promises'
import os from 'os'
export interface MemoryStatistics {
memoryUsed: number
memoryTotal: number
memoryRSS: number
freeMem: number
totalMem: number
}
export interface CPUStatistics {
usage: number
cores: number
}
/**
* @public
*/
export interface StatisticsElement {
find: number
tx: number
}
export interface UserStatistics {
userId: string
sessionId: string
data: any
mins5: StatisticsElement
total: StatisticsElement
current: StatisticsElement
}
export interface WorkspaceStatistics {
sessions: UserStatistics[]
workspaceName: string
wsId: string
sessionsTotal: number
clientsTotal: number
service?: string
}
export interface ServiceStatistics {
serviceName: string // A service category
memory: MemoryStatistics
cpu: CPUStatistics
stats?: Metrics
workspaces?: WorkspaceStatistics[]
}
export function getMemoryInfo (): MemoryStatistics {
const memU = process.memoryUsage()
return {
memoryUsed: Math.round((memU.heapUsed / 1024 / 1024) * 100) / 100,
memoryRSS: Math.round((memU.rss / 1024 / 1024) * 100) / 100,
memoryTotal: Math.round((memU.heapTotal / 1024 / 1024) * 100) / 100,
freeMem: Math.round((os.freemem() / 1024 / 1024) * 100) / 100,
totalMem: Math.round((os.totalmem() / 1024 / 1024) * 100) / 100
}
}
export function getCPUInfo (): CPUStatistics {
return {
usage: Math.round(os.loadavg()[0] * 100) / 100,
cores: os.cpus().length
}
}
const METRICS_UPDATE_INTERVAL = 5000
/**
* @public
*/
export function initStatisticsContext (
serviceName: string,
ops?: {
logFile?: string
logConsole?: boolean
factory?: () => MeasureMetricsContext
getUsers?: () => WorkspaceStatistics[]
}
): MeasureContext {
let metricsContext: MeasureMetricsContext
if (ops?.factory !== undefined) {
metricsContext = ops.factory()
} else {
metricsContext = new MeasureMetricsContext(serviceName, {}, {}, newMetrics())
}
const statsUrl = process.env.STATS_URL
const metricsFile = ops?.logFile
let errorToSend = 0
if (metricsFile !== undefined || ops?.logConsole === true || statsUrl !== undefined) {
if (metricsFile !== undefined) {
console.info('storing measurements into local file', metricsFile)
}
let oldMetricsValue = ''
const token = generateToken(systemAccountEmail, { name: '' }, { service: 'true' })
const serviceId = encodeURIComponent(os.hostname() + '-' + serviceName)
const intTimer = setInterval(() => {
if (metricsFile !== undefined || ops?.logConsole === true) {
const val = metricsToString(metricsContext.metrics, serviceName, 140)
if (val !== oldMetricsValue) {
oldMetricsValue = val
if (metricsFile !== undefined) {
writeFile(metricsFile, val).catch((err) => {
console.error(err)
})
}
if (ops?.logConsole === true) {
console.info('METRICS:', val)
}
}
}
if (statsUrl !== undefined) {
const data: ServiceStatistics = {
serviceName,
cpu: getCPUInfo(),
memory: getMemoryInfo(),
stats: metricsContext.metrics,
workspaces: ops?.getUsers?.()
}
void fetch(
concatLink(statsUrl, '/api/v1/statistics') + `/?token=${encodeURIComponent(token)}&name=${serviceId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
}
).catch((err) => {
errorToSend++
if (errorToSend % 20 === 0) {
console.error(err)
}
})
}
}, METRICS_UPDATE_INTERVAL)
const closeTimer = (): void => {
clearInterval(intTimer)
}
process.on('SIGINT', closeTimer)
process.on('SIGTERM', closeTimer)
}
return metricsContext
}

View File

@ -49,6 +49,7 @@ import type { Token } from '@hcengineering/server-token'
import { type Readable } from 'stream'
import type { DbAdapter, DomainHelper } from './adapter'
import { type StorageAdapter } from './storage'
import type { StatisticsElement } from './stats'
export interface ServerFindOptions<T extends Doc> extends FindOptions<T> {
domain?: Domain // Allow to find for Doc's in specified domain only.
@ -534,14 +535,6 @@ export interface SessionRequest {
start: number
}
/**
* @public
*/
export interface StatisticsElement {
find: number
tx: number
}
export interface ClientSessionCtx {
ctx: MeasureContext
sendResponse: (msg: any) => Promise<void>

View File

@ -1060,7 +1060,7 @@ export function startSessionManager (
stop: () => Promise<string | undefined>
}
} & Partial<Timeouts>
): () => Promise<void> {
): { shutdown: () => Promise<void>, sessionManager: SessionManager } {
const sessions = createSessionManager(
ctx,
opt.sessionFactory,
@ -1071,16 +1071,19 @@ export function startSessionManager (
},
opt.profiling
)
return opt.serverFactory(
sessions,
(rctx, service, ws, msg, workspace) => {
sessions.handleRequest(rctx, service, ws, msg, workspace)
},
ctx,
opt.pipelineFactory,
opt.port,
opt.enableCompression ?? false,
opt.accountsUrl,
opt.externalStorage
)
return {
shutdown: opt.serverFactory(
sessions,
(rctx, service, ws, msg, workspace) => {
sessions.handleRequest(rctx, service, ws, msg, workspace)
},
ctx,
opt.pipelineFactory,
opt.port,
opt.enableCompression ?? false,
opt.accountsUrl,
opt.externalStorage
),
sessionManager: sessions
}
}

View File

@ -109,7 +109,7 @@ describe('server', () => {
}
afterAll(async () => {
await cancelOp()
await cancelOp.shutdown()
})
it('should connect to server', (done) => {
@ -278,7 +278,7 @@ describe('server', () => {
console.error(err)
} finally {
console.log('calling shutdown')
await cancelOp()
await cancelOp.shutdown()
}
})
})

View File

@ -14,17 +14,17 @@
//
import { setMetadata } from '@hcengineering/platform'
import serverToken from '@hcengineering/server-token'
import serverAiBot from '@hcengineering/server-ai-bot'
import { MeasureMetricsContext } from '@hcengineering/core'
import serverClient from '@hcengineering/server-client'
import serverToken from '@hcengineering/server-token'
import config from './config'
import { closeDB, DbStorage, getDB } from './storage'
import { AIBotController } from './controller'
import { initStatisticsContext } from '@hcengineering/server-core'
import { createBotAccount } from './account'
import config from './config'
import { AIBotController } from './controller'
import { registerLoaders } from './loaders'
import { createServer, listen } from './server'
import { closeDB, DbStorage, getDB } from './storage'
export const start = async (): Promise<void> => {
setMetadata(serverToken.metadata.Secret, config.ServerSecret)
@ -33,7 +33,7 @@ export const start = async (): Promise<void> => {
setMetadata(serverClient.metadata.Endpoint, config.AccountsURL)
registerLoaders()
const ctx = new MeasureMetricsContext('ai-bot-service', {})
const ctx = initStatisticsContext('ai-bot-service', {})
ctx.info('AI Bot Service started', { firstName: config.FirstName, lastName: config.LastName })

View File

@ -26,17 +26,21 @@ import { createServer, listen } from './server'
import { Collector } from './collector'
import { registerLoaders } from './loaders'
import { closeDB, getDB } from './storage'
import { initStatisticsContext } from '@hcengineering/server-core'
const ctx = new MeasureMetricsContext(
'analytics-collector-service',
{},
{},
newMetrics(),
new SplitLogger('analytics-collector-service', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
const ctx = initStatisticsContext('analytics-collector', {
factory: () =>
new MeasureMetricsContext(
'analytics-collector-service',
{},
{},
newMetrics(),
new SplitLogger('analytics-collector-service', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
configureAnalytics(config.SentryDSN, config)
Analytics.setTag('application', 'analytics-collector-service')

View File

@ -4,41 +4,31 @@
import { Analytics } from '@hcengineering/analytics'
import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service'
import { MeasureMetricsContext, metricsToString, newMetrics } from '@hcengineering/core'
import { loadBrandingMap } from '@hcengineering/server-core'
import { writeFile } from 'fs/promises'
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { initStatisticsContext, loadBrandingMap } from '@hcengineering/server-core'
import { join } from 'path'
import config from './config'
import { start } from './server'
// Load and inc startID, to have easy logs.
const metricsContext = new MeasureMetricsContext(
'github',
{},
{},
newMetrics(),
new SplitLogger('github-service', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
const metricsContext = initStatisticsContext('github', {
factory: () =>
new MeasureMetricsContext(
'github',
{},
{},
newMetrics(),
new SplitLogger('github-service', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
configureAnalytics(config.SentryDSN, config)
Analytics.setTag('application', 'github-service')
let oldMetricsValue = ''
const intTimer = setInterval(() => {
const val = metricsToString(metricsContext.metrics, 'Github', 140)
if (val !== oldMetricsValue) {
oldMetricsValue = val
void writeFile('metrics.txt', val).catch((err) => {
console.error(err)
})
}
}, 30000)
let doOnClose: () => Promise<void> = async () => {}
void start(metricsContext, loadBrandingMap(config.BrandingPath)).then((r) => {
@ -46,7 +36,6 @@ void start(metricsContext, loadBrandingMap(config.BrandingPath)).then((r) => {
})
const onClose = (): void => {
clearInterval(intTimer)
metricsContext.info('Closed')
void doOnClose().then((r) => {
process.exit(0)

View File

@ -14,10 +14,9 @@
// limitations under the License.
//
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import serverClient from '@hcengineering/server-client'
import { type StorageConfiguration } from '@hcengineering/server-core'
import { initStatisticsContext, type StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken, { decodeToken } from '@hcengineering/server-token'
import { type IncomingHttpHeaders } from 'http'
@ -37,7 +36,7 @@ const extractToken = (header: IncomingHttpHeaders): any => {
}
export const main = async (): Promise<void> => {
const ctx = new MeasureMetricsContext('gmail', {}, {}, newMetrics())
const ctx = initStatisticsContext('gmail', {})
setMetadata(serverClient.metadata.Endpoint, config.AccountsURL)
setMetadata(serverClient.metadata.UserAgent, config.ServiceID)

View File

@ -13,10 +13,10 @@
// limitations under the License.
//
import { MeasureMetricsContext, WorkspaceId, newMetrics, toWorkspaceString } from '@hcengineering/core'
import { toWorkspaceString, WorkspaceId } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import serverClient from '@hcengineering/server-client'
import { StorageConfig, StorageConfiguration } from '@hcengineering/server-core'
import { initStatisticsContext, StorageConfig, StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken, { decodeToken } from '@hcengineering/server-token'
import cors from 'cors'
@ -49,7 +49,9 @@ export const main = async (): Promise<void> => {
setMetadata(serverToken.metadata.Secret, config.Secret)
const storageConfigs: StorageConfiguration = storageConfigFromEnv()
const ctx = new MeasureMetricsContext('love', {}, {}, newMetrics())
const ctx = initStatisticsContext('love', {})
const storageConfig = storageConfigs.storages.findLast((p) => p.name === config.StorageProviderName)
const storageAdapter = buildStorageFromConfig(storageConfigs)
const app = express()

View File

@ -14,8 +14,8 @@
// limitations under the License.
//
import { MeasureMetricsContext, generateId } from '@hcengineering/core'
import { StorageConfiguration } from '@hcengineering/server-core'
import { generateId } from '@hcengineering/core'
import { StorageConfiguration, initStatisticsContext } from '@hcengineering/server-core'
import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { Token, decodeToken } from '@hcengineering/server-token'
import cors from 'cors'
@ -112,7 +112,7 @@ const wrapRequest = (fn: AsyncRequestHandler) => (req: Request, res: Response, n
export function createServer (storageConfig: StorageConfiguration): { app: Express, close: () => void } {
const storageAdapter = buildStorageFromConfig(storageConfig)
const measureCtx = new MeasureMetricsContext('print', {})
const measureCtx = initStatisticsContext('print', {})
const app = express()
app.use(cors())

View File

@ -85,7 +85,8 @@
"mime-types": "~2.1.34",
"pdfjs-dist": "2.12.313",
"sharp": "~0.32.0",
"morgan": "^1.10.0"
"morgan": "^1.10.0",
"@hcengineering/server-core": "^0.6.1"
},
"description": "Document recognition service"
}

View File

@ -13,7 +13,7 @@
// limitations under the License.
//
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { initStatisticsContext } from '@hcengineering/server-core'
import bodyParser from 'body-parser'
import cors from 'cors'
import express from 'express'
@ -42,7 +42,7 @@ const extractToken = (header: IncomingHttpHeaders): any => {
export const startServer = async (): Promise<void> => {
const app = express()
const ctx = new MeasureMetricsContext('rekini', {}, {}, newMetrics())
const ctx = initStatisticsContext('rekoni', {})
class MyStream {
write (text: string): void {

View File

@ -14,8 +14,8 @@
// limitations under the License.
//
import { MeasureMetricsContext, generateId } from '@hcengineering/core'
import { StorageConfiguration } from '@hcengineering/server-core'
import { generateId } from '@hcengineering/core'
import { initStatisticsContext, StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig } from '@hcengineering/server-storage'
import { Token } from '@hcengineering/server-token'
import cors from 'cors'
@ -60,7 +60,7 @@ const wrapRequest =
export function createServer (storageConfig: StorageConfiguration, brandings: BrandingMap): Express {
const storageAdapter = buildStorageFromConfig(storageConfig)
const measureCtx = new MeasureMetricsContext('sign', {})
const measureCtx = initStatisticsContext('sign', {})
const app = express()
app.use(cors())

View File

@ -13,37 +13,40 @@
// limitations under the License.
//
import { Analytics } from '@hcengineering/analytics'
import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service'
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { setMetadata, translate } from '@hcengineering/platform'
import serverToken from '@hcengineering/server-token'
import serverClient from '@hcengineering/server-client'
import { SplitLogger, configureAnalytics } from '@hcengineering/analytics-service'
import { Analytics } from '@hcengineering/analytics'
import { join } from 'path'
import type { StorageConfiguration } from '@hcengineering/server-core'
import { initStatisticsContext, type StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken from '@hcengineering/server-token'
import telegram from '@hcengineering/telegram'
import { join } from 'path'
import { Telegraf } from 'telegraf'
import config from './config'
import { Limiter } from './limiter'
import { registerLoaders } from './loaders'
import { createServer, listen } from './server'
import { setUpBot } from './telegraf/bot'
import { PlatformWorker } from './worker'
import { registerLoaders } from './loaders'
import { TgContext } from './telegraf/types'
import { Command } from './telegraf/commands'
import { Limiter } from './limiter'
import { TgContext } from './telegraf/types'
import { PlatformWorker } from './worker'
const ctx = new MeasureMetricsContext(
'telegram-bot-service',
{},
{},
newMetrics(),
new SplitLogger('telegram-bot-service', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
const ctx = initStatisticsContext('telegram-bot', {
factory: () =>
new MeasureMetricsContext(
'telegram-bot-service',
{},
{},
newMetrics(),
new SplitLogger('telegram-bot-service', {
root: join(process.cwd(), 'logs'),
enableConsole: (process.env.ENABLE_CONSOLE ?? 'true') === 'true'
})
)
})
configureAnalytics(config.SentryDSN, config)
Analytics.setTag('application', 'telegram-bot-service')

View File

@ -3,10 +3,9 @@ import { PlatformWorker } from './platform'
import { createServer, Handler, listen } from './server'
import { telegram } from './telegram'
import { MeasureMetricsContext, newMetrics } from '@hcengineering/core'
import { setMetadata } from '@hcengineering/platform'
import serverClient from '@hcengineering/server-client'
import { type StorageConfiguration } from '@hcengineering/server-core'
import { initStatisticsContext, type StorageConfiguration } from '@hcengineering/server-core'
import { buildStorageFromConfig, storageConfigFromEnv } from '@hcengineering/server-storage'
import serverToken, { decodeToken, type Token } from '@hcengineering/server-token'
import config from './config'
@ -20,7 +19,7 @@ const extractToken = (header: IncomingHttpHeaders): Token | undefined => {
}
export const main = async (): Promise<void> => {
const ctx = new MeasureMetricsContext('telegram', {}, {}, newMetrics())
const ctx = initStatisticsContext('telegram', {})
setMetadata(serverClient.metadata.Endpoint, config.AccountsURL)
setMetadata(serverClient.metadata.UserAgent, config.ServiceID)