@allbluecn/admin-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +86 -0
- package/src/client.tsx +31 -0
- package/src/components/admin/admin-user-menu.tsx +90 -0
- package/src/components/admin/developer-log.tsx +356 -0
- package/src/components/ai-provider/model-list-editor.tsx +147 -0
- package/src/components/ai-provider/provider-form.tsx +434 -0
- package/src/components/ai-provider/provider-icon.tsx +43 -0
- package/src/components/ai-provider/provider-logo.tsx +87 -0
- package/src/components/ai-provider/provider-table.tsx +258 -0
- package/src/components/auth/admin-login-form.tsx +214 -0
- package/src/components/auth/auth-brand-panel.tsx +60 -0
- package/src/components/auth/auth-header.tsx +38 -0
- package/src/components/auth/auth-layout.tsx +66 -0
- package/src/components/auth/auth-providers.tsx +34 -0
- package/src/components/auth/icon-input.tsx +46 -0
- package/src/components/auth/oauth-section.tsx +51 -0
- package/src/components/auth/password-input.tsx +67 -0
- package/src/components/common/page-header.tsx +42 -0
- package/src/components/common/placeholder-page.tsx +42 -0
- package/src/components/component/component-form-dialog.tsx +348 -0
- package/src/components/component/component-import-dialog.tsx +235 -0
- package/src/components/component/component-table.tsx +153 -0
- package/src/components/data-table/data-table-pagination.tsx +114 -0
- package/src/components/data-table/data-table.tsx +218 -0
- package/src/components/data-table/table-detail-layout.tsx +99 -0
- package/src/components/dependency/changelog-dialog.tsx +108 -0
- package/src/components/dependency/columns.tsx +232 -0
- package/src/components/dependency/dependency-table.tsx +368 -0
- package/src/components/dependency/edit-dialog.tsx +200 -0
- package/src/components/knowledge-public/bookmark-form-dialog.tsx +215 -0
- package/src/components/knowledge-public/compliance-form-dialog.tsx +308 -0
- package/src/components/knowledge-public/legal-form-dialog.tsx +315 -0
- package/src/components/layout/sidebar-02/app-sidebar.tsx +160 -0
- package/src/components/layout/sidebar-02/index.tsx +33 -0
- package/src/components/layout/sidebar-02/logo.tsx +40 -0
- package/src/components/layout/sidebar-02/nav-main.tsx +153 -0
- package/src/components/layout/sidebar-02/nav-notifications.tsx +78 -0
- package/src/components/layout/sidebar-02/team-switcher.tsx +106 -0
- package/src/components/magicui/animated-theme-toggler.tsx +272 -0
- package/src/components/magicui/rainbow-button.tsx +78 -0
- package/src/components/market/columns.tsx +151 -0
- package/src/components/market/market-app-form.tsx +237 -0
- package/src/components/market/market-app-list.tsx +156 -0
- package/src/components/monitoring/error-list.tsx +143 -0
- package/src/components/monitoring/health-cards.tsx +152 -0
- package/src/components/monitoring/metrics-stats.tsx +184 -0
- package/src/components/monitoring/request-log-table.tsx +179 -0
- package/src/components/providers/theme-toggle.tsx +59 -0
- package/src/components/settings/avatar/avatar-config.ts +46 -0
- package/src/components/ui/alert-dialog.tsx +216 -0
- package/src/components/ui/alert.tsx +76 -0
- package/src/components/ui/avatar.tsx +129 -0
- package/src/components/ui/badge.tsx +66 -0
- package/src/components/ui/button.tsx +84 -0
- package/src/components/ui/card.tsx +120 -0
- package/src/components/ui/checkbox.tsx +50 -0
- package/src/components/ui/collapsible.tsx +50 -0
- package/src/components/ui/dialog.tsx +185 -0
- package/src/components/ui/dropdown-menu.tsx +286 -0
- package/src/components/ui/field.tsx +255 -0
- package/src/components/ui/input-group.tsx +171 -0
- package/src/components/ui/input.tsx +36 -0
- package/src/components/ui/label.tsx +41 -0
- package/src/components/ui/pagination.tsx +146 -0
- package/src/components/ui/select.tsx +209 -0
- package/src/components/ui/separator.tsx +45 -0
- package/src/components/ui/sheet.tsx +164 -0
- package/src/components/ui/sidebar.tsx +719 -0
- package/src/components/ui/skeleton.tsx +30 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +48 -0
- package/src/components/ui/table.tsx +133 -0
- package/src/components/ui/textarea.tsx +35 -0
- package/src/components/ui/tooltip.tsx +74 -0
- package/src/components/user/columns.tsx +153 -0
- package/src/components/user/settings-activity-log.tsx +358 -0
- package/src/components/user/settings-profile/constants.ts +42 -0
- package/src/components/user/settings-profile/types.ts +47 -0
- package/src/components/user/settings-profile.tsx +664 -0
- package/src/components/user/user-create-dialog.tsx +137 -0
- package/src/components/user/user-filters.tsx +117 -0
- package/src/hooks/use-mobile.ts +36 -0
- package/src/instrument.client.ts +34 -0
- package/src/lib/anyhub/capabilities.ts +59 -0
- package/src/lib/anyhub/types.ts +43 -0
- package/src/lib/auth-client.ts +83 -0
- package/src/lib/auth-utils.ts +24 -0
- package/src/lib/auth.config.ts +264 -0
- package/src/lib/avatar.ts +41 -0
- package/src/lib/i18n/client.ts +32 -0
- package/src/lib/i18n/config.ts +40 -0
- package/src/lib/i18n/detect.ts +43 -0
- package/src/lib/i18n/locales/en/admin-ai.json +128 -0
- package/src/lib/i18n/locales/en/admin-billing.json +56 -0
- package/src/lib/i18n/locales/en/admin-users.json +88 -0
- package/src/lib/i18n/locales/en/common.json +877 -0
- package/src/lib/i18n/locales/en/enterprise.json +208 -0
- package/src/lib/i18n/locales/zh/admin-ai.json +128 -0
- package/src/lib/i18n/locales/zh/admin-billing.json +56 -0
- package/src/lib/i18n/locales/zh/admin-users.json +88 -0
- package/src/lib/i18n/locales/zh/common.json +877 -0
- package/src/lib/i18n/locales/zh/enterprise.json +208 -0
- package/src/lib/i18n/server.ts +18 -0
- package/src/lib/i18n/types.ts +5 -0
- package/src/lib/monitoring-benchmarks.ts +85 -0
- package/src/lib/monitoring-descriptions.ts +78 -0
- package/src/lib/prisma.ts +18 -0
- package/src/lib/router-context.ts +38 -0
- package/src/lib/session.ts +38 -0
- package/src/lib/utils.ts +23 -0
- package/src/providers/index.tsx +45 -0
- package/src/routeTree.gen.ts +1199 -0
- package/src/router.tsx +51 -0
- package/src/routes/__root.tsx +170 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.lazy.tsx +74 -0
- package/src/routes/_nav/ai-providers/$id/edit/route.tsx +35 -0
- package/src/routes/_nav/ai-providers/index/route.lazy.tsx +33 -0
- package/src/routes/_nav/ai-providers/new/route.lazy.tsx +31 -0
- package/src/routes/_nav/ai-providers/new/route.tsx +20 -0
- package/src/routes/_nav/ai-providers/route.lazy.tsx +26 -0
- package/src/routes/_nav/ai-providers/route.tsx +31 -0
- package/src/routes/_nav/apps/route.lazy.tsx +30 -0
- package/src/routes/_nav/apps/route.tsx +20 -0
- package/src/routes/_nav/audit/route.lazy.tsx +100 -0
- package/src/routes/_nav/audit/route.tsx +25 -0
- package/src/routes/_nav/billing/invoices/route.lazy.tsx +86 -0
- package/src/routes/_nav/billing/invoices/route.tsx +25 -0
- package/src/routes/_nav/billing/route.lazy.tsx +48 -0
- package/src/routes/_nav/billing/route.tsx +27 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.lazy.tsx +160 -0
- package/src/routes/_nav/billing/subscriptions/$subscriptionId/route.tsx +25 -0
- package/src/routes/_nav/billing/subscriptions/route.lazy.tsx +113 -0
- package/src/routes/_nav/billing/subscriptions/route.tsx +28 -0
- package/src/routes/_nav/compliance/route.lazy.tsx +184 -0
- package/src/routes/_nav/compliance/route.tsx +25 -0
- package/src/routes/_nav/components/route.lazy.tsx +184 -0
- package/src/routes/_nav/components/route.tsx +40 -0
- package/src/routes/_nav/dashboard/route.lazy.tsx +33 -0
- package/src/routes/_nav/dashboard/route.tsx +20 -0
- package/src/routes/_nav/dependencies/route.lazy.tsx +66 -0
- package/src/routes/_nav/dependencies/route.tsx +32 -0
- package/src/routes/_nav/developer-logs/route.lazy.tsx +6 -0
- package/src/routes/_nav/developer-logs/route.tsx +14 -0
- package/src/routes/_nav/docs/route.lazy.tsx +30 -0
- package/src/routes/_nav/docs/route.tsx +20 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.lazy.tsx +355 -0
- package/src/routes/_nav/knowledge-public/bookmarks/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/compliance/route.lazy.tsx +245 -0
- package/src/routes/_nav/knowledge-public/compliance/route.tsx +40 -0
- package/src/routes/_nav/knowledge-public/legal/route.lazy.tsx +242 -0
- package/src/routes/_nav/knowledge-public/legal/route.tsx +40 -0
- package/src/routes/_nav/licenses/$licenseId/route.lazy.tsx +235 -0
- package/src/routes/_nav/licenses/$licenseId/route.tsx +25 -0
- package/src/routes/_nav/licenses/new/route.lazy.tsx +237 -0
- package/src/routes/_nav/licenses/route.lazy.tsx +143 -0
- package/src/routes/_nav/licenses/route.tsx +25 -0
- package/src/routes/_nav/market/route.lazy.tsx +31 -0
- package/src/routes/_nav/market/route.tsx +20 -0
- package/src/routes/_nav/monitoring/ai-usage/route.lazy.tsx +305 -0
- package/src/routes/_nav/monitoring/ai-usage/route.tsx +3 -0
- package/src/routes/_nav/monitoring/errors/route.lazy.tsx +111 -0
- package/src/routes/_nav/monitoring/health/route.lazy.tsx +260 -0
- package/src/routes/_nav/monitoring/logs/route.lazy.tsx +165 -0
- package/src/routes/_nav/monitoring/metrics/route.lazy.tsx +240 -0
- package/src/routes/_nav/monitoring/overview/route.lazy.tsx +138 -0
- package/src/routes/_nav/monitoring/route.lazy.tsx +68 -0
- package/src/routes/_nav/monitoring/route.tsx +20 -0
- package/src/routes/_nav/monitoring/sentry/route.lazy.tsx +399 -0
- package/src/routes/_nav/monitoring/sentry/route.tsx +20 -0
- package/src/routes/_nav/permissions/route.lazy.tsx +504 -0
- package/src/routes/_nav/permissions/route.tsx +26 -0
- package/src/routes/_nav/route.lazy.tsx +31 -0
- package/src/routes/_nav/route.tsx +34 -0
- package/src/routes/_nav/settings/route.lazy.tsx +30 -0
- package/src/routes/_nav/settings/route.tsx +20 -0
- package/src/routes/_nav/sso/route.lazy.tsx +145 -0
- package/src/routes/_nav/sso/route.tsx +25 -0
- package/src/routes/_nav/users/$userId/route.lazy.tsx +432 -0
- package/src/routes/_nav/users/$userId/route.tsx +27 -0
- package/src/routes/_nav/users/-components/users-table.tsx +38 -0
- package/src/routes/_nav/users/route.lazy.tsx +74 -0
- package/src/routes/_nav/users/route.tsx +43 -0
- package/src/routes/api/compliance-download/$jobId.ts +35 -0
- package/src/routes/api/observability/errors.ts +41 -0
- package/src/routes/api/observability/health.ts +30 -0
- package/src/routes/api.auth.$.ts +31 -0
- package/src/routes/api.sentry-test.ts +33 -0
- package/src/routes/index/route.lazy.tsx +22 -0
- package/src/routes/index/route.tsx +24 -0
- package/src/routes/login/route.lazy.tsx +46 -0
- package/src/routes/login/route.tsx +28 -0
- package/src/routes/sentry-tunnel.ts +61 -0
- package/src/routes/setup/route.lazy.tsx +94 -0
- package/src/routes/setup/route.tsx +28 -0
- package/src/server/__tests__/admin-ai-alerts.test.ts +113 -0
- package/src/server/__tests__/licenses-issue.test.ts +98 -0
- package/src/server/admin-ai-alerts.ts +177 -0
- package/src/server/admin-ai-usage.ts +134 -0
- package/src/server/auth-helpers.ts +51 -0
- package/src/server/entitlements.ts +24 -0
- package/src/server/knowledge-public/canonical-id.ts +55 -0
- package/src/server/knowledge-public/github-sync.ts +86 -0
- package/src/server/license-issue.ts +78 -0
- package/src/server/middlewares/request.ts +26 -0
- package/src/server/observability/debug-middleware.ts +39 -0
- package/src/server/observability/error-reporter.ts +99 -0
- package/src/server/observability/health-check.ts +140 -0
- package/src/server/observability/index.ts +33 -0
- package/src/server/observability/logger.ts +49 -0
- package/src/server/observability/metrics.ts +105 -0
- package/src/server/observability/request-middleware.ts +125 -0
- package/src/server/observability/timing.ts +42 -0
- package/src/server/pro-services.ts +32 -0
- package/src/server/serverFns/admin-billing.ts +94 -0
- package/src/server/serverFns/ai-provider/create.ts +92 -0
- package/src/server/serverFns/ai-provider/delete.ts +42 -0
- package/src/server/serverFns/ai-provider/get.ts +35 -0
- package/src/server/serverFns/ai-provider/list.ts +92 -0
- package/src/server/serverFns/ai-provider/update.ts +73 -0
- package/src/server/serverFns/anyhub/market.ts +64 -0
- package/src/server/serverFns/audit.ts +57 -0
- package/src/server/serverFns/auth/session.ts +26 -0
- package/src/server/serverFns/auth/setup.ts +73 -0
- package/src/server/serverFns/compliance.ts +85 -0
- package/src/server/serverFns/components/import.ts +197 -0
- package/src/server/serverFns/components/list.ts +51 -0
- package/src/server/serverFns/components/mutations.ts +84 -0
- package/src/server/serverFns/dependency/helpers.ts +245 -0
- package/src/server/serverFns/dependency/list.ts +127 -0
- package/src/server/serverFns/dependency/sync.ts +123 -0
- package/src/server/serverFns/dependency/update.ts +51 -0
- package/src/server/serverFns/developer-log-auto-generate.ts +176 -0
- package/src/server/serverFns/developer-logs.ts +132 -0
- package/src/server/serverFns/knowledge-public/bookmarks/create.ts +53 -0
- package/src/server/serverFns/knowledge-public/bookmarks/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/bookmarks/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/bookmarks/list.ts +66 -0
- package/src/server/serverFns/knowledge-public/bookmarks/sync-all-github.ts +56 -0
- package/src/server/serverFns/knowledge-public/bookmarks/update.ts +50 -0
- package/src/server/serverFns/knowledge-public/compliance/create.ts +58 -0
- package/src/server/serverFns/knowledge-public/compliance/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/compliance/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/compliance/list.ts +59 -0
- package/src/server/serverFns/knowledge-public/compliance/update.ts +53 -0
- package/src/server/serverFns/knowledge-public/legal/create.ts +62 -0
- package/src/server/serverFns/knowledge-public/legal/delete.ts +31 -0
- package/src/server/serverFns/knowledge-public/legal/detail.ts +32 -0
- package/src/server/serverFns/knowledge-public/legal/list.ts +60 -0
- package/src/server/serverFns/knowledge-public/legal/update.ts +55 -0
- package/src/server/serverFns/licenses.ts +92 -0
- package/src/server/serverFns/locale.ts +10 -0
- package/src/server/serverFns/monitoring/check-ai-alerts.ts +13 -0
- package/src/server/serverFns/monitoring/get-ai-usage-overview.ts +25 -0
- package/src/server/serverFns/monitoring/get-errors.ts +43 -0
- package/src/server/serverFns/monitoring/get-health-settings.ts +35 -0
- package/src/server/serverFns/monitoring/get-health.ts +55 -0
- package/src/server/serverFns/monitoring/get-metrics.ts +46 -0
- package/src/server/serverFns/monitoring/get-request-logs.ts +66 -0
- package/src/server/serverFns/monitoring/report-error.ts +55 -0
- package/src/server/serverFns/monitoring/reset.ts +70 -0
- package/src/server/serverFns/permissions.ts +214 -0
- package/src/server/serverFns/sentry/config.ts +88 -0
- package/src/server/serverFns/sso-config.ts +83 -0
- package/src/server/serverFns/user/create.ts +40 -0
- package/src/server/serverFns/user/delete.ts +29 -0
- package/src/server/serverFns/user/detail.ts +45 -0
- package/src/server/serverFns/user/list.ts +56 -0
- package/src/server/serverFns/user/reset-password.ts +28 -0
- package/src/server/serverFns/user/update-role.ts +26 -0
- package/src/server/serverFns/user/update-status.ts +26 -0
- package/src/server.ts +27 -0
- package/src/start.ts +85 -0
- package/src/styles/_keyframe-animations.scss +91 -0
- package/src/styles/_variables.scss +296 -0
- package/src/styles/globals.css +200 -0
- package/src/types/start-authjs.d.ts +27 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { prisma } from '@allbluecn/database'
|
|
19
|
+
|
|
20
|
+
export interface GithubData {
|
|
21
|
+
stars: number
|
|
22
|
+
repoUpdatedAt: Date | null
|
|
23
|
+
latestVersion: string | null
|
|
24
|
+
versionDate: Date | null
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const API = 'https://api.github.com'
|
|
28
|
+
const HEADERS: HeadersInit = {
|
|
29
|
+
Accept: 'application/vnd.github+json',
|
|
30
|
+
'User-Agent': 'AllBlue-KnowledgeBase',
|
|
31
|
+
...(process.env.GITHUB_TOKEN
|
|
32
|
+
? { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` }
|
|
33
|
+
: {}),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function fetchGithubData(
|
|
37
|
+
owner: string,
|
|
38
|
+
repo: string,
|
|
39
|
+
): Promise<GithubData | null> {
|
|
40
|
+
const repoRes = await fetch(`${API}/repos/${owner}/${repo}`, { headers: HEADERS })
|
|
41
|
+
if (!repoRes.ok) return null
|
|
42
|
+
const repoJson: any = await repoRes.json()
|
|
43
|
+
|
|
44
|
+
let latestVersion: string | null = null
|
|
45
|
+
let versionDate: Date | null = null
|
|
46
|
+
try {
|
|
47
|
+
const relRes = await fetch(`${API}/repos/${owner}/${repo}/releases/latest`, { headers: HEADERS })
|
|
48
|
+
if (relRes.ok) {
|
|
49
|
+
const relJson: any = await relRes.json()
|
|
50
|
+
latestVersion = relJson.tag_name ?? null
|
|
51
|
+
versionDate = relJson.published_at ? new Date(relJson.published_at) : null
|
|
52
|
+
}
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
stars: repoJson.stargazers_count ?? 0,
|
|
59
|
+
repoUpdatedAt: repoJson.updated_at ? new Date(repoJson.updated_at) : null,
|
|
60
|
+
latestVersion,
|
|
61
|
+
versionDate,
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function syncBookmarkGithub(bookmarkId: string): Promise<boolean> {
|
|
66
|
+
const bm = await prisma.websiteBookmark.findUnique({
|
|
67
|
+
where: { id: bookmarkId },
|
|
68
|
+
select: { id: true, isGithub: true, githubOwner: true, githubRepo: true },
|
|
69
|
+
})
|
|
70
|
+
if (!bm?.isGithub || !bm.githubOwner || !bm.githubRepo) return false
|
|
71
|
+
|
|
72
|
+
const data = await fetchGithubData(bm.githubOwner, bm.githubRepo)
|
|
73
|
+
await prisma.websiteBookmark.update({
|
|
74
|
+
where: { id: bookmarkId },
|
|
75
|
+
data: data
|
|
76
|
+
? {
|
|
77
|
+
githubStars: data.stars,
|
|
78
|
+
githubRepoUpdatedAt: data.repoUpdatedAt,
|
|
79
|
+
githubLatestVersion: data.latestVersion,
|
|
80
|
+
githubVersionDate: data.versionDate,
|
|
81
|
+
githubSyncedAt: new Date(),
|
|
82
|
+
}
|
|
83
|
+
: { githubSyncedAt: new Date() },
|
|
84
|
+
})
|
|
85
|
+
return data !== null
|
|
86
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { prisma } from '@allbluecn/database'
|
|
19
|
+
import { getProService } from '@/server/pro-services'
|
|
20
|
+
import { FeatureRegistry } from '@allbluecn/kernel'
|
|
21
|
+
import { requireAdmin } from '@/server/auth-helpers'
|
|
22
|
+
|
|
23
|
+
export const issueLicense = async (data: {
|
|
24
|
+
customerName: string
|
|
25
|
+
customerEmail: string
|
|
26
|
+
customerId: string
|
|
27
|
+
plan: string
|
|
28
|
+
seats: number
|
|
29
|
+
features: Record<string, boolean>
|
|
30
|
+
expiresAt: string | null
|
|
31
|
+
}) => {
|
|
32
|
+
const session = await requireAdmin()
|
|
33
|
+
|
|
34
|
+
const unknownKeys = Object.keys(data.features).filter(
|
|
35
|
+
(k) => !(k in FeatureRegistry),
|
|
36
|
+
)
|
|
37
|
+
if (unknownKeys.length > 0) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`未注册的功能 key:${unknownKeys.join(', ')}(须先在 FeatureRegistry 注册)`,
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const license = getProService('license')
|
|
44
|
+
if (!license?.issue) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'当前部署不支持 license 签发(需 Pro 签发环境 + LICENSE_SIGNING_PRIVATE_KEY)',
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const expiresAt = data.expiresAt ? new Date(data.expiresAt) : null
|
|
51
|
+
const result = await license.issue({
|
|
52
|
+
customerName: data.customerName,
|
|
53
|
+
customerEmail: data.customerEmail,
|
|
54
|
+
customerId: data.customerId,
|
|
55
|
+
plan: data.plan,
|
|
56
|
+
seats: data.seats,
|
|
57
|
+
features: data.features,
|
|
58
|
+
expiresAt,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await prisma.licenseKey.create({
|
|
62
|
+
data: {
|
|
63
|
+
key: result.key,
|
|
64
|
+
signature: result.signature,
|
|
65
|
+
payload: result.payload as never,
|
|
66
|
+
customerId: data.customerId,
|
|
67
|
+
customerName: data.customerName,
|
|
68
|
+
customerEmail: data.customerEmail,
|
|
69
|
+
plan: result.payload.plan,
|
|
70
|
+
seats: data.seats,
|
|
71
|
+
features: data.features as never,
|
|
72
|
+
expiresAt,
|
|
73
|
+
issuedById: session.user!.id,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return { key: result.key }
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
19
|
+
|
|
20
|
+
export interface RequestContext {
|
|
21
|
+
request: Request
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const requestContextMiddleware = createMiddleware().server<RequestContext>(async ({ request, next }) => {
|
|
25
|
+
return next({ context: { request } })
|
|
26
|
+
})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { createMiddleware } from '@tanstack/react-start'
|
|
19
|
+
|
|
20
|
+
export const debugMiddleware = createMiddleware().server(async ({ next }: any) => {
|
|
21
|
+
const result = await next() as any
|
|
22
|
+
|
|
23
|
+
if (process.env.NODE_ENV === 'development') {
|
|
24
|
+
const response = result.response || result
|
|
25
|
+
if (response instanceof Response) {
|
|
26
|
+
const headers = new Headers(response.headers)
|
|
27
|
+
headers.set('X-Debug-Timestamp', new Date().toISOString())
|
|
28
|
+
headers.set('X-Debug-Node-Version', process.version)
|
|
29
|
+
headers.set('X-Debug-Uptime', process.uptime().toString())
|
|
30
|
+
return new Response(response.body, {
|
|
31
|
+
status: response.status,
|
|
32
|
+
statusText: response.statusText,
|
|
33
|
+
headers,
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result
|
|
39
|
+
})
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { Prisma, prisma } from '@allbluecn/database'
|
|
19
|
+
|
|
20
|
+
interface ErrorRecord {
|
|
21
|
+
name: string
|
|
22
|
+
message: string
|
|
23
|
+
stack?: string
|
|
24
|
+
context?: Record<string, unknown>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface ErrorEntry {
|
|
28
|
+
count: number
|
|
29
|
+
lastSeen: Date
|
|
30
|
+
error: ErrorRecord
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const errorStore = new Map<string, ErrorEntry>()
|
|
34
|
+
let initialized = false
|
|
35
|
+
|
|
36
|
+
export function reportError(error: Error, context?: Record<string, unknown>): void {
|
|
37
|
+
const key = `${error.name}:${error.message}`
|
|
38
|
+
const existing = errorStore.get(key)
|
|
39
|
+
|
|
40
|
+
if (existing) {
|
|
41
|
+
existing.count++
|
|
42
|
+
existing.lastSeen = new Date()
|
|
43
|
+
} else {
|
|
44
|
+
errorStore.set(key, {
|
|
45
|
+
count: 1,
|
|
46
|
+
lastSeen: new Date(),
|
|
47
|
+
error: { name: error.name, message: error.message, stack: error.stack, context },
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const entry = errorStore.get(key)!
|
|
52
|
+
saveErrorReportAsync(key, entry).catch((e) => console.error('[ErrorReportSave]', e))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function saveErrorReportAsync(key: string, entry: ErrorEntry): Promise<void> {
|
|
56
|
+
await prisma.monitoringErrorReport.upsert({
|
|
57
|
+
where: { id: key },
|
|
58
|
+
create: {
|
|
59
|
+
id: key,
|
|
60
|
+
errorName: entry.error.name,
|
|
61
|
+
errorMessage: entry.error.message,
|
|
62
|
+
stackTrace: entry.error.stack,
|
|
63
|
+
count: entry.count,
|
|
64
|
+
firstSeen: entry.lastSeen,
|
|
65
|
+
lastSeen: entry.lastSeen,
|
|
66
|
+
context: (entry.error.context as Prisma.InputJsonValue) ?? null,
|
|
67
|
+
},
|
|
68
|
+
update: {
|
|
69
|
+
count: entry.count,
|
|
70
|
+
lastSeen: entry.lastSeen,
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function getErrorStore(): Array<{ key: string; entry: ErrorEntry }> {
|
|
76
|
+
return Array.from(errorStore.entries()).map(([key, entry]) => ({ key, entry }))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function resetErrors(): void {
|
|
80
|
+
errorStore.clear()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function initErrorCollector(): void {
|
|
84
|
+
if (initialized) return
|
|
85
|
+
initialized = true
|
|
86
|
+
|
|
87
|
+
process.on('unhandledRejection', (reason: unknown) => {
|
|
88
|
+
const error = reason instanceof Error
|
|
89
|
+
? reason
|
|
90
|
+
: new Error(`Unhandled rejection: ${String(reason)}`)
|
|
91
|
+
reportError(error, { source: 'unhandledRejection' })
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
process.on('uncaughtException', (error, origin) => {
|
|
95
|
+
reportError(error, { source: 'uncaughtException', origin })
|
|
96
|
+
console.error('[FATAL]', error.message || 'Uncaught exception')
|
|
97
|
+
process.exit(1)
|
|
98
|
+
})
|
|
99
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import os from 'node:os'
|
|
19
|
+
import { prisma } from '@allbluecn/database'
|
|
20
|
+
|
|
21
|
+
interface HealthResult {
|
|
22
|
+
status: 'healthy' | 'unhealthy'
|
|
23
|
+
checks: {
|
|
24
|
+
uptime: number
|
|
25
|
+
memory: NodeJS.MemoryUsage
|
|
26
|
+
database: { status: 'connected' | 'error'; latency?: number; error?: string }
|
|
27
|
+
cpu: { usage: number; cores: number; model: string }
|
|
28
|
+
disk: { total: number; free: number; usedPct: number }
|
|
29
|
+
load: { one: number; five: number; fifteen: number }
|
|
30
|
+
system: { platform: string; type: string; release: string; nodeVersion: string }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let healthTimer: ReturnType<typeof setInterval> | null = null
|
|
35
|
+
let healthEnabled = true
|
|
36
|
+
|
|
37
|
+
export async function performHealthCheck(): Promise<HealthResult> {
|
|
38
|
+
let dbResult: { status: 'connected' | 'error'; latency?: number; error?: string }
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const dbStart = Date.now()
|
|
42
|
+
await prisma.$queryRaw`SELECT 1`
|
|
43
|
+
const dbLatency = Date.now() - dbStart
|
|
44
|
+
dbResult = { status: 'connected', latency: dbLatency }
|
|
45
|
+
} catch (e) {
|
|
46
|
+
dbResult = { status: 'error', error: (e as Error).message }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const status: HealthResult['status'] = dbResult.status === 'error' ? 'unhealthy' : 'healthy'
|
|
50
|
+
const memory = process.memoryUsage()
|
|
51
|
+
const cpus = os.cpus()
|
|
52
|
+
const cpuCount = cpus.length
|
|
53
|
+
const cpuModel = cpus[0]?.model || 'unknown'
|
|
54
|
+
const loadavg = os.loadavg()
|
|
55
|
+
const totalMem = os.totalmem()
|
|
56
|
+
const freeMem = os.freemem()
|
|
57
|
+
const usedPct = ((totalMem - freeMem) / totalMem) * 100
|
|
58
|
+
|
|
59
|
+
const snapshot = {
|
|
60
|
+
status,
|
|
61
|
+
uptimeSeconds: process.uptime(),
|
|
62
|
+
memoryUsage: {
|
|
63
|
+
rss: memory.rss,
|
|
64
|
+
heapTotal: memory.heapTotal,
|
|
65
|
+
heapUsed: memory.heapUsed,
|
|
66
|
+
external: memory.external,
|
|
67
|
+
arrayBuffers: memory.arrayBuffers,
|
|
68
|
+
},
|
|
69
|
+
dbStatus: dbResult.status,
|
|
70
|
+
dbLatencyMs: dbResult.latency || null,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await prisma.monitoringHealthSnapshot.create({ data: snapshot })
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
status,
|
|
77
|
+
checks: {
|
|
78
|
+
uptime: process.uptime(),
|
|
79
|
+
memory,
|
|
80
|
+
database: dbResult,
|
|
81
|
+
cpu: { usage: usedPct, cores: cpuCount, model: cpuModel },
|
|
82
|
+
disk: { total: totalMem, free: freeMem, usedPct },
|
|
83
|
+
load: { one: loadavg[0], five: loadavg[1], fifteen: loadavg[2] },
|
|
84
|
+
system: {
|
|
85
|
+
platform: os.platform(),
|
|
86
|
+
type: os.type(),
|
|
87
|
+
release: os.release(),
|
|
88
|
+
nodeVersion: process.version,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function startHealthCheckTimer(intervalMs = 30_000): void {
|
|
95
|
+
if (healthTimer) clearInterval(healthTimer)
|
|
96
|
+
performHealthCheck().catch((e) => console.error('[HealthCheck]', e))
|
|
97
|
+
healthTimer = setInterval(() => {
|
|
98
|
+
performHealthCheck().catch((e) => console.error('[HealthCheck]', e))
|
|
99
|
+
}, intervalMs)
|
|
100
|
+
healthEnabled = true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function stopHealthCheckTimer(): void {
|
|
104
|
+
if (healthTimer) {
|
|
105
|
+
clearInterval(healthTimer)
|
|
106
|
+
healthTimer = null
|
|
107
|
+
}
|
|
108
|
+
healthEnabled = false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function isHealthCheckEnabled(): boolean {
|
|
112
|
+
return healthEnabled
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function setHealthCheckEnabled(enabled: boolean): Promise<void> {
|
|
116
|
+
if (enabled) {
|
|
117
|
+
startHealthCheckTimer()
|
|
118
|
+
} else {
|
|
119
|
+
stopHealthCheckTimer()
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
await prisma.systemSetting.upsert({
|
|
123
|
+
where: { key: 'monitoring_health_check_enabled' },
|
|
124
|
+
create: {
|
|
125
|
+
key: 'monitoring_health_check_enabled',
|
|
126
|
+
value: enabled ? 'true' : 'false',
|
|
127
|
+
category: 'monitoring',
|
|
128
|
+
desc: '监控健康检查自动定时开关',
|
|
129
|
+
},
|
|
130
|
+
update: { value: enabled ? 'true' : 'false' },
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function getHealthCheckEnabled(): Promise<boolean> {
|
|
135
|
+
const setting = await prisma.systemSetting.findUnique({
|
|
136
|
+
where: { key: 'monitoring_health_check_enabled' },
|
|
137
|
+
})
|
|
138
|
+
if (!setting) return true
|
|
139
|
+
return setting.value === 'true'
|
|
140
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
export { logger } from './logger'
|
|
19
|
+
export { metrics } from './metrics'
|
|
20
|
+
export { reportError, getErrorStore, resetErrors, initErrorCollector } from './error-reporter'
|
|
21
|
+
export {
|
|
22
|
+
performHealthCheck,
|
|
23
|
+
startHealthCheckTimer,
|
|
24
|
+
stopHealthCheckTimer,
|
|
25
|
+
isHealthCheckEnabled,
|
|
26
|
+
setHealthCheckEnabled,
|
|
27
|
+
getHealthCheckEnabled,
|
|
28
|
+
} from './health-check'
|
|
29
|
+
export { observabilityMiddleware, requestLogStore } from './request-middleware'
|
|
30
|
+
export { debugMiddleware } from './debug-middleware'
|
|
31
|
+
export { timed } from './timing'
|
|
32
|
+
export type { LogLevel } from './logger'
|
|
33
|
+
export type { TimingResult } from './timing'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { createIsomorphicFn } from '@tanstack/react-start'
|
|
19
|
+
|
|
20
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
21
|
+
|
|
22
|
+
type LogData = Record<string, unknown>
|
|
23
|
+
|
|
24
|
+
const logger = createIsomorphicFn()
|
|
25
|
+
.server((level: LogLevel, message: string, data?: LogData) => {
|
|
26
|
+
const timestamp = new Date().toISOString()
|
|
27
|
+
|
|
28
|
+
if (process.env.NODE_ENV === 'development') {
|
|
29
|
+
(console as any)[level](`[${timestamp}] [${level.toUpperCase()}]`, message, data)
|
|
30
|
+
} else {
|
|
31
|
+
console.log(
|
|
32
|
+
JSON.stringify({
|
|
33
|
+
timestamp,
|
|
34
|
+
level,
|
|
35
|
+
message,
|
|
36
|
+
data,
|
|
37
|
+
service: 'allblue-admin',
|
|
38
|
+
environment: process.env.NODE_ENV,
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
.client((level: LogLevel, message: string, data?: LogData) => {
|
|
44
|
+
if (process.env.NODE_ENV === 'development') {
|
|
45
|
+
(console as any)[level](`[CLIENT] [${level.toUpperCase()}]`, message, data)
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
export { logger }
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
//
|
|
5
|
+
// This program is free software: you can redistribute it and/or modify
|
|
6
|
+
// it under the terms of the GNU Affero General Public License as published by
|
|
7
|
+
// the Free Software Foundation, either version 3 of the License, or
|
|
8
|
+
// (at your option) any later version.
|
|
9
|
+
//
|
|
10
|
+
// This program is distributed in the hope that it will be useful,
|
|
11
|
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
// GNU Affero General Public License for more details.
|
|
14
|
+
//
|
|
15
|
+
// You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
|
|
18
|
+
import { prisma } from '@allbluecn/database'
|
|
19
|
+
|
|
20
|
+
interface MetricStats {
|
|
21
|
+
count: number
|
|
22
|
+
avg: number
|
|
23
|
+
p50: number
|
|
24
|
+
p95: number
|
|
25
|
+
p99: number
|
|
26
|
+
min: number
|
|
27
|
+
max: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class MetricsCollector {
|
|
31
|
+
private metrics = new Map<string, number[]>()
|
|
32
|
+
private readonly MAX_SAMPLES = 1000
|
|
33
|
+
private flushTimer?: ReturnType<typeof setInterval>
|
|
34
|
+
|
|
35
|
+
constructor() {
|
|
36
|
+
this.flushTimer = setInterval(() => {
|
|
37
|
+
this.flushToDb().catch((e) => console.error('[MetricsFlush]', e))
|
|
38
|
+
}, 60_000)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
recordTiming(name: string, duration: number): void {
|
|
42
|
+
if (!this.metrics.has(name)) {
|
|
43
|
+
this.metrics.set(name, [])
|
|
44
|
+
}
|
|
45
|
+
const samples = this.metrics.get(name)!
|
|
46
|
+
samples.push(duration)
|
|
47
|
+
if (samples.length > this.MAX_SAMPLES) {
|
|
48
|
+
samples.shift()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getStats(name: string): MetricStats | null {
|
|
53
|
+
const timings = this.metrics.get(name)
|
|
54
|
+
if (!timings || timings.length === 0) return null
|
|
55
|
+
|
|
56
|
+
const sorted = [...timings].sort((a, b) => a - b)
|
|
57
|
+
const sum = sorted.reduce((a, b) => a + b, 0)
|
|
58
|
+
return {
|
|
59
|
+
count: sorted.length,
|
|
60
|
+
avg: Math.round(sum / sorted.length * 100) / 100,
|
|
61
|
+
p50: sorted[Math.floor(sorted.length * 0.5)],
|
|
62
|
+
p95: sorted[Math.floor(sorted.length * 0.95)],
|
|
63
|
+
p99: sorted[Math.floor(sorted.length * 0.99)],
|
|
64
|
+
min: sorted[0],
|
|
65
|
+
max: sorted[sorted.length - 1],
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getAllStats(): Record<string, MetricStats> {
|
|
70
|
+
const stats: Record<string, MetricStats> = {}
|
|
71
|
+
for (const [name, timings] of this.metrics) {
|
|
72
|
+
const s = this.getStats(name)
|
|
73
|
+
if (s) stats[name] = s
|
|
74
|
+
}
|
|
75
|
+
return stats
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async flushToDb(): Promise<void> {
|
|
79
|
+
const allStats = this.getAllStats()
|
|
80
|
+
for (const [name, stats] of Object.entries(allStats)) {
|
|
81
|
+
await prisma.monitoringMetricsSnapshot.create({
|
|
82
|
+
data: {
|
|
83
|
+
metricName: name,
|
|
84
|
+
count: stats.count,
|
|
85
|
+
avg: stats.avg,
|
|
86
|
+
p50: stats.p50,
|
|
87
|
+
p95: stats.p95,
|
|
88
|
+
p99: stats.p99,
|
|
89
|
+
min: stats.min,
|
|
90
|
+
max: stats.max,
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
reset(): void {
|
|
97
|
+
this.metrics.clear()
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
destroy(): void {
|
|
101
|
+
if (this.flushTimer) clearInterval(this.flushTimer)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const metrics = new MetricsCollector()
|