From ea438bb5733bfc6d1473a494ccea0ee4861e1d82 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 2 Feb 2026 02:58:47 +0800 Subject: [PATCH 1/4] Rebrand and remove Pigsty-specific references from documentation, configuration defaults, and build scripts. --- Makefile | 54 +++++++++++------------------------ README.md | 31 ++++++-------------- roles/infra/defaults/main.yml | 12 ++++---- 3 files changed, 30 insertions(+), 67 deletions(-) diff --git a/Makefile b/Makefile index 5778038..361066e 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,13 @@ #==============================================================# # File : Makefile -# Desc : pigsty shortcuts +# Desc : observability shortcuts # Ctime : 2019-04-13 -# Mtime : 2026-01-27 +# Mtime : 2026-02-01 # Path : Makefile -# License : Apache-2.0 @ https://pigsty.io/docs/about/license/ +# License : Apache-2.0 @ https://svc.plus/docs/about/license/ # Copyright : 2018-2026 Ruohang Feng / Vonng (rh@vonng.com) #==============================================================# -# pigsty version string +# version string VERSION?=v4.0.0 # detect architecture @@ -68,53 +68,31 @@ doc: docs/serve #-------------------------------------------------------------# -# (1). BOOTSTRAP pigsty pkg & util preparedness +# Prepare +# (1). make deps (once) Install MacOS deps with homebrew +# (2). make dns (once) Write static DNS +# (3). make start (once) Pull-up vm nodes and setup ssh access +# (4). make demo Boot meta node same as Quick-Start +#=============================================================# + +#-------------------------------------------------------------# +# (1). BOOTSTRAP util preparedness boot: bootstrap bootstrap: ./bootstrap -# (2). CONFIGURE pigsty in interactive mode +# (2). CONFIGURE interactive mode conf: configure configure: ./configure -# (3). DEPLOY pigsty on current node +# (3). DEPLOY install on current node deploy: ./deploy.yml ############################################################### -############################################################### -# OUTLINE # -############################################################### -# (1). Quick-Start : shortcuts for launching pigsty (above) -# (2). Download : shortcuts for downloading resources -# (3). Configure : shortcuts for configure pigsty -# (4). Install : shortcuts for running playbooks -# (5). Sandbox : shortcuts for manage sandbox vm nodes -# (6). Testing : shortcuts for testing features -# (7). Develop : shortcuts for dev purpose -# (8). Release : shortcuts for release and publish -# (9). Misc : shortcuts for miscellaneous tasks -############################################################### - - - -############################################################### -# 2. Download # -############################################################### -# There are two things that need to be downloaded: -# pigsty.tgz : source code -# pkg.tgz : offline rpm packages (optional) -# -# get latest stable version to ~/pigsty -src: - curl -SL https://github.com/pgsty/pigsty/releases/download/${VERSION}/${SRC_PKG} -o ~/pigsty.tgz -############################################################### - - - ############################################################### # 3. Configure # ############################################################### @@ -507,7 +485,7 @@ release-dba: gd: get-dba get-dba: - curl -fsSL "https://repo.pigsty.cc/dba/$(DBA_PKG)" | tar -xzf - + curl -fsSL "https://repo.svc.plus/dba/$(DBA_PKG)" | tar -xzf - @echo "DBA package extracted to current directory" ud: upload-dba diff --git a/README.md b/README.md index 3b34158..698f6d5 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # Observability.svc.plus [![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-green.svg)](LICENSE) -[![Base: Pigsty v4.0](https://img.shields.io/badge/Base-Pigsty_v4.0-blue)](https://github.com/pgsty/pigsty) +[![Status: Stable](https://img.shields.io/badge/Status-Stable-blue)](https://svc.plus) -**Observability.svc.plus** is an advanced observability platform based on [**Pigsty v4.0**](https://github.com/pgsty/pigsty), strictly following the **Apache 2.0** license. +**Observability.svc.plus** is an advanced observability platform strictly following the **Apache 2.0** license. > **Focus**: Monitoring & Observability (监控/可观测). Integrating **OpenTelemetry (OTel)**, with future plans to incorporate **DeepFlow Agent** and other open-source **NPM** (Network Performance Monitoring) probes. @@ -33,7 +33,7 @@ curl -fsSL https://raw.githubusercontent.com/cloud-neutral-toolkit/observability - **Observability First**: SOTA monitoring for **PG** / **Infra** / **Node** based on **VictoriaMetrics**, **Grafana**, and **OpenTelemetry**. - **OTel Integration**: Native support for **OpenTelemetry**, facilitating unified trace, metric, and log ingestion. - **Future Ready**: Planned integration for **DeepFlow Agent** and other open-source **NPM** probes for deep network and application observability. -- **Reliable Base**: Inherits **Pigsty**'s robust self-healing **HA** clusters, **PITR**, and secure infrastructure. +- **Reliable Base**: Robust self-healing **HA** clusters, **PITR**, and secure infrastructure. - **Maintainable**: **One-Cmd Deploy**, **IaC** support, and easy customization. - **Controllable**: Self-sufficient Cloud Neutral FOSS. Run on **bare Linux**. @@ -67,7 +67,7 @@ And gather the synergistic superpowers of all [**444+ PostgreSQL Extensions**](h [![Ubuntu Support: 22/24](https://img.shields.io/badge/Ubuntu-22/24-%23E95420?style=flat&logo=ubuntu&logoColor=%23E95420)](https://svc.plus/docs/ref/linux#ubuntu) [![Docker Image](https://img.shields.io/badge/Docker-v4.0.0-%232496ED?style=flat&logo=docker&logoColor=white)](https://svc.plus/docs/setup/docker) -[**Prepare**](https://svc.plus/docs/deploy/prepare) a fresh `x86_64` / `aarch64` node runs any [**compatible**](https://svc.plus/docs/ref/linux) **Linux** OS Distros, then [**Install**](https://svc.plus/docs/setup/install#install) **Pigsty** with: +[**Prepare**](https://svc.plus/docs/deploy/prepare) a fresh `x86_64` / `aarch64` node runs any [**compatible**](https://svc.plus/docs/ref/linux) **Linux** OS Distros, then [**Install**](https://svc.plus/docs/setup/install#install) the platform with: ```bash curl -fsSL https://raw.githubusercontent.com/cloud-neutral-toolkit/observability.svc.plus/main/scripts/server-install.sh | bash @@ -80,9 +80,9 @@ Then [**configure**](https://svc.plus/docs/concept/iac/configure) and run the [* ./deploy.yml # deploy everything on current node ``` -Finally, you will get a pigsty [**singleton node ready**](https://svc.plus/docs/setup/install), with [**WebUI**](https://svc.plus/docs/setup/webui) on port `80/443` and [**Postgres**](https://svc.plus/docs/setup/pgsql) on port `5432`. +Finally, you will get a [**singleton node ready**](https://svc.plus/docs/setup/install), with [**WebUI**](https://svc.plus/docs/setup/webui) on port `80/443` and [**Postgres**](https://svc.plus/docs/setup/pgsql) on port `5432`. -For dev/testing purposes, you can also run Pigsty inside [**Docker**](https://svc.plus/docs/setup/docker) containers: `cd docker; make launch` +For dev/testing purposes, you can also run it inside [**Docker**](https://svc.plus/docs/setup/docker) containers: `cd docker; make launch` -------- @@ -148,28 +148,13 @@ curl -fsSL https://raw.githubusercontent.com/cloud-neutral-toolkit/observability -## Architecture - -Pigsty uses a [**modular**](https://svc.plus/docs/concept/arch) design. you can [**use one or all**](https://svc.plus/docs/deploy/planning), Best of breed products. Integrated as a platform. - -[![board](https://pigsty.io/img/pigsty/motherboard.gif)](https://svc.plus/docs/concept/arch) - -[![PGSQL](https://img.shields.io/badge/PGSQL-%233E668F?style=flat&logo=postgresql&labelColor=3E668F&logoColor=white)](https://svc.plus/docs/pgsql) Self-healing PostgreSQL HA cluster powered by Patroni, Pgbouncer, PgBackrest & HAProxy +Integrated as a platform. [![INFRA](https://img.shields.io/badge/INFRA-%23009639?style=flat&logo=nginx&labelColor=009639&logoColor=white)](https://svc.plus/docs/infra) Nginx, Local Repo, DNSMasq, and the entire Victoria & Grafana observability stack. -[![NODE](https://img.shields.io/badge/NODE-%23FCC624?style=flat&logo=linux&labelColor=FCC624&logoColor=black)](https://svc.plus/docs/node) Init node name, repo, pkg, NTP, ssh, admin, tune, expose services, collect logs & metrics. - -[![ETCD](https://img.shields.io/badge/ETCD-%23419EDA?style=flat&logo=etcd&labelColor=419EDA&logoColor=white)](https://svc.plus/docs/etcd) Etcd cluster is used as a reliable distributive configuration store by PostgreSQL HA Agents. - -You can compose them freely in a declarative manner. `INFRA` & `NODE` will suffice for host monitoring. -`ETCD` and `PGSQL` are used for HA PG clusters; Installing them on multiple nodes automatically forms HA clusters. - -The default [`deploy.yml`](https://github.com/pgsty/pigsty/blob/main/deploy.yml) playbook will deploy `INFRA`, `NODE`, `ETCD` & `PGSQL` on the current node. +The default [`deploy.yml`](deploy.yml) playbook will deploy `INFRA`, `NODE`, `ETCD` & `PGSQL` on the current node. Which gives you an out-of-the-box PostgreSQL singleton instance (`admin_ip:5432`) with everything ready. -[![pigsty-arch](https://pigsty.io/img/pigsty/arch.png)](https://svc.plus/docs/concept/arch) - The node can be used as an admin controller to deploy & monitor more nodes & clusters. For example, you can install these **6** **OPTIONAL** [extra modules](https://svc.plus/docs/ref/module#extra-modules) for advanced use cases: [![MinIO](https://img.shields.io/badge/MINIO-%23C72E49?style=flat&logo=minio&logoColor=white)](https://svc.plus/docs/minio) S3-compatible object storage service; used as an optional central backup server for `PGSQL`. diff --git a/roles/infra/defaults/main.yml b/roles/infra/defaults/main.yml index 8870fb0..e6ba8ef 100644 --- a/roles/infra/defaults/main.yml +++ b/roles/infra/defaults/main.yml @@ -2,18 +2,18 @@ #----------------------------------------------------------------- # INFRA_META #----------------------------------------------------------------- -version: v4.0.0 # pigsty version string +version: v4.0.0 # version string admin_ip: 10.10.10.10 # admin node ip address, overwritten by configure region: default # upstream mirror region: default,china,europe language: en # default language, en by default, could be zh -proxy_env: { no_proxy: "localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,*.pigsty,*.aliyun.com,mirrors.*,*.myqcloud.com,*.tsinghua.edu.cn" } +proxy_env: { no_proxy: "localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,*.aliyun.com,mirrors.*,*.myqcloud.com,*.tsinghua.edu.cn" } #----------------------------------------------------------------- # INFRA_IDENTITY #----------------------------------------------------------------- #infra_seq: 1 # infra node identity, explicitly required infra_portal: # infra services exposed via portal - home : { domain: i.pigsty } # default home server definition + home : { domain: i.observability } # default home server definition infra_domain: observability.svc.plus infra_data: /data/infra # default data path for infrastructure data infra_services: # home page navigation entries @@ -23,8 +23,8 @@ infra_services: # home page navigation entries - { name: Monitor Targets ,url: '/vmetrics/targets' ,desc: 'Prometheus Scrape Targets' ,icon: 'target' ,name_cn: '监控目标' ,desc_cn: 'VictoriaMetrics 监控对象列表' } - { name: Alert Rules ,url: '/vmalert/vmalert/groups' ,desc: 'VMAlert alert/record Rules' ,icon: 'alert' ,name_cn: '告警规则' ,desc_cn: 'VMAlert 告警规则管理' } - { name: Alert Manager ,url: '/alertmgr/#/alerts' ,desc: 'Alert Manage & Silence' ,icon: 'alertmgr' ,name_cn: '告警管理' ,desc_cn: 'AlertManager 告警管理与屏蔽' } - - { name: CA Certificate ,url: '/ca.crt' ,desc: 'Self-Signed CA Certificate' ,icon: 'lock' ,name_cn: 'CA 证书' ,desc_cn: 'Pigsty 自签CA根证书' } - - { name: Software Repo ,url: '/pigsty' ,desc: 'Local YUM/APT Repository' ,icon: 'package' ,name_cn: '软件仓库' ,desc_cn: '本地 YUM/APT 软件源' } + - { name: CA Certificate ,url: '/ca.crt' ,desc: 'Self-Signed CA Certificate' ,icon: 'lock' ,name_cn: 'CA 证书' ,desc_cn: '自签CA根证书' } + - { name: Software Repo ,url: '/repo' ,desc: 'Local YUM/APT Repository' ,icon: 'package' ,name_cn: '软件仓库' ,desc_cn: '本地 YUM/APT 软件源' } - { name: Explain Visualizer ,url: '/pev' ,desc: 'Postgres EXPLAIN Visualizer' ,icon: 'search' ,name_cn: '执行计划' ,desc_cn: 'PG 执行计划可视化工具' } infra_extra_services: [] # extra services to be added on infra home page @@ -107,7 +107,7 @@ grafana_enabled: true # enable grafana on this infra node? grafana_port: 3000 # default listen port for grafana grafana_clean: false # clean grafana data during init? grafana_admin_username: admin # grafana admin username, `admin` by default -grafana_admin_password: pigsty # grafana admin password, `pigsty` by default +grafana_admin_password: observability # grafana admin password, `observability` by default grafana_auth_proxy: false # enable grafana auth proxy? grafana_pgurl: '' # external postgres database url for grafana if given grafana_view_password: DBUser.Viewer # password for grafana meta pg datasource From 5381caef48c416f1c3000e84b2de24941e97abce Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 2 Feb 2026 03:22:58 +0800 Subject: [PATCH 2/4] feat: introduce the new Insight Workbench application for observability. --- roles/infra/templates/nginx/home.conf | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/roles/infra/templates/nginx/home.conf b/roles/infra/templates/nginx/home.conf index 668c5c0..e1e4b15 100644 --- a/roles/infra/templates/nginx/home.conf +++ b/roles/infra/templates/nginx/home.conf @@ -135,8 +135,7 @@ server { # chinese homepage location = /zh { - alias {{ upstream.path|default(nginx_home|default('/www')) }}/zh.html; - default_type text/html; + return 301 https://console.svc.plus/insight; } # postgres explain visualizer @@ -154,8 +153,7 @@ server { # home server location = / { - root {{ upstream.path|default(nginx_home|default('/www')) }}; - default_type text/html; + return 301 https://console.svc.plus/insight; } # home server From 06d7270c9764259cc25f6e482ce4e36edd40cbe5 Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Mon, 2 Feb 2026 03:28:48 +0800 Subject: [PATCH 3/4] feat: Add new observability workbench application with AI assistant, SLOs, and various data visualization components, and update Nginx configuration. --- roles/infra/templates/nginx/home.conf | 14 +- workbench/.env.local | 10 + workbench/next.config.mjs | 124 ++ workbench/package.json | 107 + workbench/postcss.config.mjs | 14 + workbench/src/app/globals.css | 75 + workbench/src/app/insight-temp-page.tsx | 12 + workbench/src/app/insight/page.tsx | 7 + workbench/src/app/layout.tsx | 37 + workbench/src/app/page.tsx | 5 + .../components/insight/InsightWorkbench.tsx | 375 ++++ .../src/components/insight/ai/Assistant.tsx | 225 ++ .../insight/explore/ExploreBuilder.tsx | 341 +++ .../insight/layout/BreadcrumbBar.tsx | 90 + .../insight/layout/InsightSidebarContent.tsx | 243 +++ .../src/components/insight/layout/Sidebar.tsx | 31 + .../insight/layout/TimeRangePicker.tsx | 33 + .../insight/layout/WorkspaceGrid.tsx | 88 + .../insight/layout/WorkspaceHeader.tsx | 44 + .../insight/services/adapters/logs.ts | 38 + .../insight/services/adapters/openobserve.ts | 24 + .../insight/services/adapters/prometheus.ts | 49 + .../insight/services/adapters/traces.ts | 62 + .../components/insight/services/correlator.ts | 19 + .../src/components/insight/services/gitops.ts | 28 + .../components/insight/slo/AlertWizard.tsx | 120 ++ .../src/components/insight/slo/SLOPanel.tsx | 127 ++ .../insight/snippets/SnippetLibrary.tsx | 96 + .../src/components/insight/store/urlState.ts | 209 ++ .../insight/store/useInsightState.ts | 154 ++ .../insight/topology/NetworkTopologyPanel.tsx | 92 + .../insight/topology/TopologyCanvas.tsx | 189 ++ .../src/components/insight/topology/types.ts | 16 + .../src/components/insight/viz/LogsTable.tsx | 49 + .../components/insight/viz/LogsTopStats.tsx | 59 + .../src/components/insight/viz/LogsViewer.tsx | 36 + .../components/insight/viz/MetricsChart.tsx | 48 + .../components/insight/viz/MetricsTable.tsx | 66 + .../insight/viz/MetricsTopStats.tsx | 45 + .../components/insight/viz/TracesTable.tsx | 48 + .../components/insight/viz/TracesTopStats.tsx | 42 + .../insight/viz/TracesWaterfall.tsx | 41 + .../src/components/insight/viz/VizArea.tsx | 134 ++ .../src/components/layout/SidebarRoot.tsx | 47 + workbench/src/i18n/LanguageProvider.tsx | 87 + workbench/src/i18n/translations.ts | 1918 +++++++++++++++++ workbench/src/lib/moltbotStore.ts | 35 + workbench/src/lib/userStore.ts | 248 +++ workbench/src/lib/utils.ts | 6 + workbench/tailwind.config.js | 97 + workbench/tsconfig.json | 47 + 51 files changed, 6149 insertions(+), 2 deletions(-) create mode 100644 workbench/.env.local create mode 100644 workbench/next.config.mjs create mode 100644 workbench/package.json create mode 100644 workbench/postcss.config.mjs create mode 100644 workbench/src/app/globals.css create mode 100644 workbench/src/app/insight-temp-page.tsx create mode 100644 workbench/src/app/insight/page.tsx create mode 100644 workbench/src/app/layout.tsx create mode 100644 workbench/src/app/page.tsx create mode 100644 workbench/src/components/insight/InsightWorkbench.tsx create mode 100644 workbench/src/components/insight/ai/Assistant.tsx create mode 100644 workbench/src/components/insight/explore/ExploreBuilder.tsx create mode 100644 workbench/src/components/insight/layout/BreadcrumbBar.tsx create mode 100644 workbench/src/components/insight/layout/InsightSidebarContent.tsx create mode 100644 workbench/src/components/insight/layout/Sidebar.tsx create mode 100644 workbench/src/components/insight/layout/TimeRangePicker.tsx create mode 100644 workbench/src/components/insight/layout/WorkspaceGrid.tsx create mode 100644 workbench/src/components/insight/layout/WorkspaceHeader.tsx create mode 100644 workbench/src/components/insight/services/adapters/logs.ts create mode 100644 workbench/src/components/insight/services/adapters/openobserve.ts create mode 100644 workbench/src/components/insight/services/adapters/prometheus.ts create mode 100644 workbench/src/components/insight/services/adapters/traces.ts create mode 100644 workbench/src/components/insight/services/correlator.ts create mode 100644 workbench/src/components/insight/services/gitops.ts create mode 100644 workbench/src/components/insight/slo/AlertWizard.tsx create mode 100644 workbench/src/components/insight/slo/SLOPanel.tsx create mode 100644 workbench/src/components/insight/snippets/SnippetLibrary.tsx create mode 100644 workbench/src/components/insight/store/urlState.ts create mode 100644 workbench/src/components/insight/store/useInsightState.ts create mode 100644 workbench/src/components/insight/topology/NetworkTopologyPanel.tsx create mode 100644 workbench/src/components/insight/topology/TopologyCanvas.tsx create mode 100644 workbench/src/components/insight/topology/types.ts create mode 100644 workbench/src/components/insight/viz/LogsTable.tsx create mode 100644 workbench/src/components/insight/viz/LogsTopStats.tsx create mode 100644 workbench/src/components/insight/viz/LogsViewer.tsx create mode 100644 workbench/src/components/insight/viz/MetricsChart.tsx create mode 100644 workbench/src/components/insight/viz/MetricsTable.tsx create mode 100644 workbench/src/components/insight/viz/MetricsTopStats.tsx create mode 100644 workbench/src/components/insight/viz/TracesTable.tsx create mode 100644 workbench/src/components/insight/viz/TracesTopStats.tsx create mode 100644 workbench/src/components/insight/viz/TracesWaterfall.tsx create mode 100644 workbench/src/components/insight/viz/VizArea.tsx create mode 100644 workbench/src/components/layout/SidebarRoot.tsx create mode 100644 workbench/src/i18n/LanguageProvider.tsx create mode 100644 workbench/src/i18n/translations.ts create mode 100644 workbench/src/lib/moltbotStore.ts create mode 100644 workbench/src/lib/userStore.ts create mode 100644 workbench/src/lib/utils.ts create mode 100644 workbench/tailwind.config.js create mode 100644 workbench/tsconfig.json diff --git a/roles/infra/templates/nginx/home.conf b/roles/infra/templates/nginx/home.conf index e1e4b15..6368b29 100644 --- a/roles/infra/templates/nginx/home.conf +++ b/roles/infra/templates/nginx/home.conf @@ -28,6 +28,16 @@ server { proxy_buffering off; proxy_request_buffering off; + # insight workbench + location /insight/ { + auth_basic off; + proxy_pass http://127.0.0.1:{{ workbench_port|default('8080') }}; + proxy_set_header Host $http_host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + # grafana location /ui/ { auth_basic off; @@ -135,7 +145,7 @@ server { # chinese homepage location = /zh { - return 301 https://console.svc.plus/insight; + return 301 /insight; } # postgres explain visualizer @@ -153,7 +163,7 @@ server { # home server location = / { - return 301 https://console.svc.plus/insight; + return 301 /insight; } # home server diff --git a/workbench/.env.local b/workbench/.env.local new file mode 100644 index 0000000..9da11da --- /dev/null +++ b/workbench/.env.local @@ -0,0 +1,10 @@ +# Moltbot Service URL +# Defaults to https://moltbot.svc.plus if not set +MOLTBOT_SERVICE_URL=https://moltbot.svc.plus + +# Giscus Configuration (GitHub Discussions Integration) +# See https://giscus.app to generate these values +NEXT_PUBLIC_GISCUS_REPO=cloud-neutral-toolkit/console.svc.plus +NEXT_PUBLIC_GISCUS_REPO_ID=R_kgDOQoiZ_g +NEXT_PUBLIC_GISCUS_CATEGORY=General +NEXT_PUBLIC_GISCUS_CATEGORY_ID=DIC_kwDOQoiZ_s4Clj_q diff --git a/workbench/next.config.mjs b/workbench/next.config.mjs new file mode 100644 index 0000000..df1417a --- /dev/null +++ b/workbench/next.config.mjs @@ -0,0 +1,124 @@ +import path from "path"; +import { fileURLToPath } from "url"; +import { withContentlayer } from "next-contentlayer"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +const nextConfig = { + // =============================== + // 🚀 生产优化 —— 最关键的三行 + // =============================== + output: "standalone", // 让 Next.js 生成可独立运行的最小产物(大幅减小 Docker 镜像) + compress: true, // Gzip 压缩输出(确保小体积网络传输) + + // 配置允许的外部图片域名 + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'dl.svc.plus', + }, + { + protocol: 'https', + hostname: 'www.svc.plus', + }, + { + protocol: 'https', + hostname: 'images.unsplash.com', + }, + ], + }, + + webpack: (config) => { + // 添加 YAML 文件支持 + config.module.rules.push({ + test: /\.ya?ml$/i, + type: 'asset/source', + }); + + // 显式 alias,保证 Turbopack 也能解析 + config.resolve.alias = { + ...(config.resolve.alias ?? {}), + "@components": path.join(__dirname, "src", "components"), + "@i18n": path.join(__dirname, "src", "i18n"), + "@lib": path.join(__dirname, "src", "lib"), + "@types": path.join(__dirname, "types"), + "@server": path.join(__dirname, "src", "server"), + "@modules": path.join(__dirname, "src", "modules"), + "@extensions": path.join(__dirname, "src", "modules", "extensions"), + "@theme": path.join(__dirname, "src", "components", "theme"), + "@templates": path.join(__dirname, "src", "modules", "templates"), + "@src": path.join(__dirname, "src"), + "@": path.join(__dirname, "src"), + }; + + // 添加模块搜索路径 + config.resolve.modules = [ + ...(config.resolve.modules || []), + __dirname, + path.join(__dirname, "src"), + ]; + + return config; + }, + async headers() { + return [ + { + source: "/api/:path*", + headers: [ + { key: "Access-Control-Allow-Credentials", value: "true" }, + { key: "Access-Control-Allow-Origin", value: process.env.CORS_ALLOWED_ORIGINS || "https://console.svc.plus,http://localhost:3000" }, + { key: "Access-Control-Allow-Methods", value: "GET,POST,PUT,PATCH,DELETE,OPTIONS" }, + { key: "Access-Control-Allow-Headers", value: "Content-Type, Authorization, X-Requested-With, X-Account-Session" }, + ], + }, + ]; + }, + + reactStrictMode: true, + typedRoutes: false, + turbopack: { + root: path.resolve(__dirname), + }, +}; + +export async function redirects() { + return [ + { + source: '/XStream', + destination: '/xstream', + permanent: true, + }, + { + source: '/Xstream', + destination: '/xstream', + permanent: true, + }, + { + source: '/XScopeHub', + destination: '/xscopehub', + permanent: true, + }, + { + source: '/XCloudFlow', + destination: '/xcloudflow', + permanent: true, + }, + ]; +} + +export async function rewrites() { + return [ + { + source: '/editor', + destination: 'http://localhost:4000', + }, + { + source: '/editor/:path*', + destination: 'http://localhost:4000/:path*', + }, + ]; +} + +export default withContentlayer(nextConfig); diff --git a/workbench/package.json b/workbench/package.json new file mode 100644 index 0000000..079559a --- /dev/null +++ b/workbench/package.json @@ -0,0 +1,107 @@ +{ + "name": "dashboard", + "version": "1.0.0", + "private": true, + "engines": { + "node": ">=18.17 <25" + }, + "scripts": { + "dev": "bash scripts/Dev-MCP-Server.sh && next dev --turbo", + "prebuild": "bash scripts/prebuild.sh", + "build": "next build", + "build:static": "npm run prebuild && next build", + "start": "node ./scripts/start.js", + "lint": "next lint", + "typecheck": "tsc --noEmit", + "format": "prettier --write .", + "preview": "next build && next start", + "test": "vitest run --config tests/unit/vitest.config.ts", + "test:unit": "vitest run --config tests/unit/vitest.config.ts", + "test:e2e": "playwright test --config tests/e2e/playwright.config.ts" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.956.0", + "@floating-ui/dom": "^1.6.0", + "@giscus/react": "^3.1.0", + "@radix-ui/react-alert-dialog": "^1.1.15", + "@radix-ui/react-dialog": "^1.1.15", + "@radix-ui/react-dropdown-menu": "^2.1.2", + "@radix-ui/react-label": "^2.1.8", + "@radix-ui/react-scroll-area": "^1.2.10", + "@radix-ui/react-select": "^2.2.6", + "@radix-ui/react-separator": "^1.1.8", + "@radix-ui/react-slot": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.13", + "@radix-ui/react-toast": "^1.2.15", + "@radix-ui/react-toggle": "^1.1.0", + "@radix-ui/react-tooltip": "^1.2.8", + "@tiptap/core": "^3.13.0", + "@tiptap/pm": "^3.13.0", + "@tiptap/react": "^3.13.0", + "@tiptap/starter-kit": "^3.13.0", + "@vercel/analytics": "^1.6.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "contentlayer": "^0.3.4", + "dompurify": "^3.2.6", + "gray-matter": "^4.0.3", + "html2canvas": "^1.4.1", + "js-yaml": "^4.1.0", + "jszip": "^3.10.1", + "katex": "^0.16.27", + "lucide-react": "^0.319.0", + "marked": "^16.1.2", + "mermaid": "^11.12.2", + "next": "^16.0.9", + "next-contentlayer": "^0.3.4", + "next-mdx-remote": "^5.0.0", + "next-themes": "^0.4.6", + "pdfjs-dist": "^4.2.67", + "prismjs": "^1.30.0", + "prop-types": "^15.8.1", + "qr.js": "0.0.0", + "qrcode": "^1.5.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-grid-layout": "^1.4.4", + "react-pdf": "^9.1.0", + "react-resizable": "^3.0.4", + "sanitize-html": "^2.13.0", + "swr": "^2.3.0", + "tailwind-merge": "^3.4.0", + "zustand": "^4.5.4" + }, + "devDependencies": { + "@playwright/test": "^1.49.1", + "@tailwindcss/typography": "^0.5.19", + "@testing-library/dom": "^9.3.1", + "@testing-library/jest-dom": "^6.4.6", + "@testing-library/react": "^14.3.1", + "@testing-library/user-event": "^14.6.1", + "@types/js-yaml": "^4.0.9", + "@types/mdx": "^2.0.13", + "@types/node": "24.0.3", + "@types/prismjs": "^1.26.3", + "@types/react": "^18.3.26", + "@types/react-dom": "^18.2.0", + "@types/react-grid-layout": "^1.3.5", + "@types/sanitize-html": "^2.16.0", + "autoprefixer": "^10.4.16", + "baseline-browser-mapping": "^2.8.32", + "eslint": "8.57.0", + "eslint-config-next": "^15.5.3", + "jsdom": "^24.0.0", + "postcss": "^8.4.32", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.3", + "tsconfig-paths-webpack-plugin": "^4.2.0", + "tsx": "^4.7.1", + "typescript": "^5.4.2", + "vitest": "^4.0.7" + }, + "resolutions": { + "glob": "10.5.0", + "@opentelemetry/api": "1.4.1" + }, + "packageManager": "yarn@4.12.0" +} diff --git a/workbench/postcss.config.mjs b/workbench/postcss.config.mjs new file mode 100644 index 0000000..42f2eb1 --- /dev/null +++ b/workbench/postcss.config.mjs @@ -0,0 +1,14 @@ +/** + * PostCSS 配置文件 + * 使用 ES Module 格式 - 统一现代标准 + * + * 参考: https://postcss.org/ + */ + +export default { + // 插件列表 + plugins: { + autoprefixer: {}, + tailwindcss: {}, + }, +} diff --git a/workbench/src/app/globals.css b/workbench/src/app/globals.css new file mode 100644 index 0000000..5267f2a --- /dev/null +++ b/workbench/src/app/globals.css @@ -0,0 +1,75 @@ +@import 'react-grid-layout/css/styles.css'; +@import 'react-resizable/css/styles.css'; + +@tailwind base; +@tailwind components; +@tailwind utilities; + +:root { + --font-geist-sans: 'Geist', sans-serif; + --font-geist-mono: 'Geist Mono', monospace; + --app-shell-nav-offset: 5.5rem; + + /* Light theme defaults */ + --color-background: #f4f6fb; + --color-background-muted: #e7ecf6; + --color-surface: #ffffff; + --color-surface-elevated: rgba(255, 255, 255, 0.96); + --color-surface-translucent: rgba(255, 255, 255, 0.88); + --color-surface-muted: #f1f4fb; + --color-surface-hover: #f0f4ff; + --color-surface-border: #d6e0ff; + --color-surface-border-strong: #b4c5ff; + --color-text: #1e2e55; + --color-heading: #2e3a59; + --color-text-muted: #4a5672; + --color-text-subtle: #61708c; + --color-text-inverse: #f8fbff; + --color-primary: #3366ff; + --color-primary-hover: #4d7aff; + --color-primary-muted: #f0f4ff; + --color-primary-border: #d6e0ff; + --color-primary-foreground: #ffffff; + --color-accent: #254edb; + --color-accent-muted: #e3e9ff; + --color-accent-foreground: #162a6b; + --color-success: #16a34a; + --color-success-muted: #dcfce7; + --color-success-foreground: #166534; + --color-warning: #f59e0b; + --color-warning-muted: #fef3c7; + --color-warning-foreground: #92400e; + --color-danger: #ef4444; + --color-danger-muted: #fee2e2; + --color-danger-foreground: #7f1d1d; + --color-info: #3366ff; + --color-info-muted: #f0f4ff; + --color-info-foreground: #254edb; + --color-overlay: rgba(30, 46, 85, 0.45); + --color-ring: #d6e0ff; + --color-focus: rgba(51, 102, 255, 0.35); + --color-divider: rgba(15, 23, 42, 0.08); + --color-badge-surface: #e5e7eb; + --color-badge-muted: #f3f4f6; + --color-badge-foreground: #1f2937; + + --gradient-app-from: #f5f8ff; + --gradient-app-via: #eef3ff; + --gradient-app-to: #f4f9ff; + --gradient-primary-from: #3366ff; + --gradient-primary-to: #254edb; + + --shadow-sm: 0 1px 3px rgba(30, 46, 85, 0.08), 0 1px 2px rgba(30, 46, 85, 0.04); + --shadow-md: 0 12px 32px rgba(30, 46, 85, 0.12); + + --radius-lg: 1rem; + --radius-xl: 1.5rem; + --radius-pill: 999px; +} + +body { + font-family: var(--font-geist-sans); + background-color: var(--color-background); + color: var(--color-text); + transition: background-color 150ms ease, color 150ms ease; +} diff --git a/workbench/src/app/insight-temp-page.tsx b/workbench/src/app/insight-temp-page.tsx new file mode 100644 index 0000000..e375a97 --- /dev/null +++ b/workbench/src/app/insight-temp-page.tsx @@ -0,0 +1,12 @@ +import { notFound } from 'next/navigation' + +import InsightWorkbench from './InsightWorkbench' +import { isFeatureEnabled } from '@lib/featureToggles' + +export default function InsightPage() { + if (!isFeatureEnabled('appModules', '/insight')) { + notFound() + } + + return +} diff --git a/workbench/src/app/insight/page.tsx b/workbench/src/app/insight/page.tsx new file mode 100644 index 0000000..67e90fd --- /dev/null +++ b/workbench/src/app/insight/page.tsx @@ -0,0 +1,7 @@ +"use client"; + +import InsightWorkbench from "@/components/insight/InsightWorkbench"; + +export default function InsightPage() { + return ; +} diff --git a/workbench/src/app/layout.tsx b/workbench/src/app/layout.tsx new file mode 100644 index 0000000..00bf756 --- /dev/null +++ b/workbench/src/app/layout.tsx @@ -0,0 +1,37 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; +import { LanguageProvider } from "../i18n/LanguageProvider"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "Observability Workbench", + description: "Insight & Monitoring Dashboard", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + + ); +} diff --git a/workbench/src/app/page.tsx b/workbench/src/app/page.tsx new file mode 100644 index 0000000..06f046e --- /dev/null +++ b/workbench/src/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Home() { + redirect("/insight"); +} diff --git a/workbench/src/components/insight/InsightWorkbench.tsx b/workbench/src/components/insight/InsightWorkbench.tsx new file mode 100644 index 0000000..8a03627 --- /dev/null +++ b/workbench/src/components/insight/InsightWorkbench.tsx @@ -0,0 +1,375 @@ +'use client' + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import type { Layout } from 'react-grid-layout' +import { ChevronLeft, ChevronRight, PanelLeftOpen } from 'lucide-react' +import { Sidebar } from './layout/Sidebar' +import { WorkspaceHeader } from './layout/WorkspaceHeader' +import { BreadcrumbBar } from './layout/BreadcrumbBar' +import { WorkspaceGrid } from './layout/WorkspaceGrid' +import { NetworkTopologyPanel } from './topology/NetworkTopologyPanel' +import { ExploreBuilder, languageMeta } from './explore/ExploreBuilder' +import { VizArea } from './viz/VizArea' +import { SLOPanel } from './slo/SLOPanel' +import { AIAssistant } from './ai/Assistant' +import { useInsightStore } from './store/useInsightState' +import { DataSource, QueryLanguage } from './store/urlState' + +const LAYOUT_STORAGE_KEY = 'insight-workspace-layout-v1' + +const DEFAULT_LAYOUT: Layout[] = [ + { i: 'network', x: 0, y: 0, w: 6, h: 8, minW: 4, minH: 6 }, + { i: 'promql', x: 6, y: 0, w: 6, h: 8, minW: 4, minH: 6 }, + { i: 'logql', x: 0, y: 8, w: 6, h: 8, minW: 4, minH: 6 }, + { i: 'traceql', x: 6, y: 8, w: 6, h: 8, minW: 4, minH: 6 } +] + +export default function InsightWorkbench() { + const state = useInsightStore((store) => store.state) + const updateState = useInsightStore((store) => store.updateInsight) + const shareableLink = useInsightStore((store) => store.shareableLink) + const [activeSection, setActiveSection] = useState('topology') + const [history, setHistory] = useState>({ + promql: [], + logql: [], + traceql: [] + }) + const [resultData, setResultData] = useState>({ + promql: [], + logql: [], + traceql: [] + }) + const [sidebarCollapsed, setSidebarCollapsed] = useState(false) + const [sidebarHidden, setSidebarHidden] = useState(false) + const [panelLayout, setPanelLayout] = useState(() => DEFAULT_LAYOUT.map(item => ({ ...item }))) + const [layoutDirty, setLayoutDirty] = useState(false) + const [layoutStatus, setLayoutStatus] = useState(null) + const [detailsCollapsed, setDetailsCollapsed] = useState(false) + const statusTimeout = useRef(null) + + const handleSelectSection = useCallback((section: string) => { + setActiveSection(section) + const el = document.getElementById(section) + if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' }) + }, []) + + const updateHistory = useCallback( + (language: QueryLanguage, items: string[]) => { + setHistory(prev => ({ ...prev, [language]: items })) + }, + [] + ) + + const updateResults = useCallback((language: QueryLanguage, data: any) => { + setResultData(prev => ({ ...prev, [language]: data })) + }, []) + + const toggleLanguage = useCallback( + (language: QueryLanguage) => { + const exists = state.activeLanguages.includes(language) + let nextActive = exists + ? state.activeLanguages.filter(item => item !== language) + : [...state.activeLanguages, language] + if (nextActive.length === 0) { + nextActive = [language] + } + const primary = nextActive[0] + const nextSource: DataSource = primary === 'promql' ? 'metrics' : primary === 'logql' ? 'logs' : 'traces' + updateState({ + activeLanguages: nextActive, + queryLanguage: primary, + dataSource: nextSource + }) + }, + [state.activeLanguages, updateState] + ) + + const handleLayoutChange = useCallback((next: Layout[]) => { + setPanelLayout(next) + setLayoutDirty(true) + }, []) + + const resetStatusMessage = useCallback(() => { + if (statusTimeout.current) { + window.clearTimeout(statusTimeout.current) + statusTimeout.current = null + } + }, []) + + const handleSaveLayout = useCallback(() => { + if (typeof window === 'undefined') return + window.localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(panelLayout)) + setLayoutDirty(false) + setLayoutStatus('Layout saved locally') + resetStatusMessage() + statusTimeout.current = window.setTimeout(() => setLayoutStatus(null), 2200) + }, [panelLayout, resetStatusMessage]) + + const handleResetLayout = useCallback(() => { + setPanelLayout(DEFAULT_LAYOUT.map(item => ({ ...item }))) + setLayoutDirty(false) + if (typeof window !== 'undefined') { + window.localStorage.removeItem(LAYOUT_STORAGE_KEY) + } + setLayoutStatus('Layout reset to default') + resetStatusMessage() + statusTimeout.current = window.setTimeout(() => setLayoutStatus(null), 2200) + }, [resetStatusMessage]) + + useEffect(() => { + if (typeof window === 'undefined') return + const stored = window.localStorage.getItem(LAYOUT_STORAGE_KEY) + if (!stored) return + try { + const parsed = JSON.parse(stored) as Layout[] + if (!Array.isArray(parsed)) return + const merged = DEFAULT_LAYOUT.map(item => { + const match = parsed.find(entry => entry.i === item.i) + return match ? { ...item, ...match } : { ...item } + }) + setPanelLayout(merged) + setLayoutDirty(false) + } catch (error) { + console.error('Failed to restore insight layout', error) + } + }, []) + + useEffect(() => { + return () => { + if (statusTimeout.current) { + window.clearTimeout(statusTimeout.current) + } + } + }, []) + + const keyMetrics = useMemo( + () => [ + { + label: 'Availability', + value: state.topologyMode === 'network' ? '99.96%' : '99.90%', + trend: '+0.3% vs last 7d', + tone: 'positive' as const + }, + { + label: 'P95 latency', + value: state.topologyMode === 'network' ? '82 ms' : '248 ms', + trend: `${state.timeRange} window`, + tone: 'neutral' as const + }, + { + label: 'Error rate', + value: state.topologyMode === 'application' ? '0.7%' : '0.4%', + trend: 'Target < 1%', + tone: 'warning' as const + } + ], + [state.timeRange, state.topologyMode] + ) + + const explorerPanels = (language: QueryLanguage, domId?: string) => { + const enabled = state.activeLanguages.includes(language) + return { + id: language, + domId, + minW: 4, + minH: 6, + content: enabled ? ( + + ) : ( + toggleLanguage(language)} /> + ) + } + } + + const panels = [ + { + id: 'network', + domId: 'topology', + minW: 4, + minH: 6, + content: + }, + explorerPanels('promql', 'explore'), + explorerPanels('logql'), + explorerPanels('traceql') + ] + + const insightAsideWidth = detailsCollapsed ? 'lg:w-60 xl:w-64' : 'lg:w-80 xl:w-96' + + return ( +
+ {sidebarHidden && ( + + )} +
+
+ +
+ + +
+
+
+
+ {!sidebarHidden && ( +
+ updateState({ topologyMode: mode })} + onToggleLanguage={toggleLanguage} + onToggleCollapse={() => setSidebarCollapsed(prev => !prev)} + onHide={() => setSidebarHidden(true)} + collapsed={sidebarCollapsed} + /> +
+ )} +
+
+
+
+ +
+ +
+
+ +
+
+ +
+
+
+
+
+ ) +} + +interface DisabledExplorerCardProps { + language: QueryLanguage + onEnable: () => void +} + +function DisabledExplorerCard({ language, onEnable }: DisabledExplorerCardProps) { + const meta = languageMeta[language] + return ( +
+
+

{meta.label}

+

Enable this explorer from the navigation to build queries.

+
+
+

Capture metrics, logs or traces by toggling the language on the left-hand menu.

+ +
+
+ ) +} diff --git a/workbench/src/components/insight/ai/Assistant.tsx b/workbench/src/components/insight/ai/Assistant.tsx new file mode 100644 index 0000000..f85525d --- /dev/null +++ b/workbench/src/components/insight/ai/Assistant.tsx @@ -0,0 +1,225 @@ +'use client' + +import { useMemo, useState } from 'react' +import { InsightState } from '../../insight/store/urlState' + +const quickActions = [ + { id: 'explain', label: 'Explain anomaly', prompt: 'Explain the recent anomaly in my metrics.' }, + { id: 'logs', label: 'Fetch related logs', prompt: 'Show me logs correlated with the current filters.' }, + { id: 'rca', label: 'Root cause analysis', prompt: 'Run an RCA for the checkout service using metrics, logs and traces.' }, + { id: 'alert', label: 'Draft alert', prompt: 'Generate an alert rule for p95 latency over 300ms.' }, + { id: 'report', label: 'Incident report', prompt: 'Create a short incident report for the last hour.' } +] + +interface AssistantProps { + state: InsightState +} + +export function AIAssistant({ state }: AssistantProps) { + const [isOpen, setIsOpen] = useState(false) + const [isMinimized, setIsMinimized] = useState(false) + const [isMaximized, setIsMaximized] = useState(false) + const [message, setMessage] = useState('') + const [history, setHistory] = useState<{ question: string; timestamp: number }[]>([]) + const [conversation, setConversation] = useState< + { author: 'user' | 'ai'; text: string; timestamp: number }[] + >([]) + + const contextSummary = useMemo( + () => + `Org=${state.org}, Project=${state.project}, Env=${state.env}, Region=${state.region}, Topology=${state.topologyMode}, Service=${state.service || 'all'}, Time=${state.timeRange}`, + [state] + ) + + function openPanel(prompt?: string) { + setIsOpen(true) + setIsMinimized(false) + if (prompt) { + appendMessage(prompt) + } + } + + async function appendMessage(prompt: string) { + const timestamp = Date.now() + const updatedHistory = [{ question: prompt, timestamp }, ...history].slice(0, 10) + setHistory(updatedHistory) + + // Optimistic update + setConversation(prev => [ + ...prev, + { author: 'user', text: prompt, timestamp }, + ]) + + try { + // Include context in the prompt or as a separate field if the API supports it + // For now, we prepend it to the prompt to ensure the bot knows the current view context + const contextAwarePrompt = `Context: [${contextSummary}]\n\nQuestion: ${prompt}` + + const response = await fetch('/api/moltbot/chat', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ message: contextAwarePrompt }) + }) + + if (!response.ok) { + throw new Error(`Error: ${response.statusText}`) + } + + const data = await response.json() + const reply = data.reply || data.message || JSON.stringify(data) + + setConversation(prev => [ + ...prev, + { author: 'ai', text: reply, timestamp: Date.now() } + ]) + } catch (error: any) { + setConversation(prev => [ + ...prev, + { author: 'ai', text: `Sorry, I encountered an error: ${error.message}`, timestamp: Date.now() } + ]) + } + } + + function handleSend() { + if (!message.trim()) return + appendMessage(message.trim()) + setMessage('') + } + + function toggleMinimize() { + setIsMinimized(prev => !prev) + } + + function toggleMaximize() { + setIsMaximized(prev => !prev) + setIsMinimized(false) + } + + function closePanel() { + setIsOpen(false) + setIsMinimized(false) + setIsMaximized(false) + } + + return ( +
+
+

AI Assistant

+

Bring AskAI insights into your observability workflow.

+
+
+ {quickActions.map(action => ( + + ))} +
+
+

Current context

+

{contextSummary}

+
+
+

Recent questions

+ {history.length === 0 ? ( +

Run a quick action or open the assistant to get started.

+ ) : ( +
    + {history.map(item => ( +
  • + {item.question} + {new Date(item.timestamp).toLocaleTimeString()} +
  • + ))} +
+ )} +
+ + + {isOpen && ( +
+
+
+

AI copilot

+ {!isMinimized && ( +

Context aware responses for the current workspace.

+ )} +
+
+ + + +
+
+ + {!isMinimized && ( +
+ {conversation.length === 0 ? ( +

+ Ask a question to start a conversation. The assistant replies with enriched placeholders until wired to + your backend. +

+ ) : ( +
    + {conversation.map(entry => ( +
  • +

    + {entry.author === 'user' ? 'You' : 'Assistant'} · {new Date(entry.timestamp).toLocaleTimeString()} +

    +

    {entry.text}

    +
  • + ))} +
+ )} +
+ )} + + {!isMinimized && ( +
+