chore: sync local updates

This commit is contained in:
2026-05-16 19:03:34 +08:00
parent 9c086c3301
commit 80cf54a201
89 changed files with 10622 additions and 2443 deletions

View File

@@ -0,0 +1,9 @@
data/
.git
.gitignore
*.md
.env
.env.*
*.exe
mengyamonitor-backend-server
__debug_bin

View File

@@ -0,0 +1,21 @@
# 中心服务HTTP/WebSocket + gRPC纯 Go / modernc SQLiteCGO_ENABLED=0
# 镜像内进程为明文 HTTPHTTPS 请在宿主机 Nginx 等反代层配置,内网直连本端口即可。
FROM golang:1.22-alpine AS build
WORKDIR /src
RUN apk add --no-cache git
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /central .
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
# 与 config 默认一致; compose 中可覆盖
ENV HOST=0.0.0.0
ENV PORT=9393
ENV GRPC_PORT=9394
ENV DB_PATH=/app/data/servers.db
COPY --from=build /central /app/central
EXPOSE 9393 9394
ENTRYPOINT ["/app/central"]

View File

@@ -0,0 +1,13 @@
# 使用本机/CI 已编译好的 Linux amd64 二进制打包镜像(不在 Docker 内 go build
# 先执行: build-linux-amd64.bat 或 release 目录放置同名文件
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
ENV HOST=0.0.0.0
ENV PORT=9393
ENV GRPC_PORT=9394
ENV DB_PATH=/app/data/servers.db
COPY release/mengyamonitor-central-linux-amd64 /app/central
RUN chmod +x /app/central
EXPOSE 9393 9394
ENTRYPOINT ["/app/central"]

View File

@@ -0,0 +1,28 @@
@echo off
setlocal
cd /d "%~dp0"
where go >nul 2>&1
if errorlevel 1 (
echo [错误] 未找到 go请先安装 Go 并加入 PATH
exit /b 1
)
if not exist release mkdir release
set GOOS=linux
set GOARCH=amd64
set CGO_ENABLED=0
echo 正在编译 Linux amd64 release\mengyamonitor-central-linux-amd64 ...
go build -trimpath -ldflags="-s -w" -o release\mengyamonitor-central-linux-amd64 .
if errorlevel 1 (
echo [错误] go build 失败
exit /b 1
)
echo 完成: %cd%\release\mengyamonitor-central-linux-amd64
echo.
echo 可选:用预编译镜像启动(不再在 Docker 里编译)
echo docker compose -f docker-compose.binary.yml up -d --build
exit /b 0

View File

@@ -0,0 +1,20 @@
# 使用 release/mengyamonitor-central-linux-amd64 打包镜像(先在 Windows 运行 build-linux-amd64.bat
# 容器内仍是 HTTP:9393 / 9394HTTPS 仅公网 Nginx 反代层
services:
central:
build:
context: .
dockerfile: Dockerfile.binary
image: mengyamonitor-central:latest
restart: unless-stopped
ports:
- "9393:9393"
- "9394:9394"
environment:
HOST: "0.0.0.0"
PORT: "9393"
GRPC_PORT: "9394"
ADMIN_TOKEN: ${ADMIN_TOKEN:-shumengya520}
DB_PATH: /app/data/servers.db
volumes:
- ./data:/app/data

View File

@@ -0,0 +1,23 @@
# 中心服务在容器内仅为 HTTP9393 Web/WS、9394 gRPCTLS 由公网 Nginx 做,与内网无关。
#
# 本文件:在 Docker 内完整执行 go build适合无本机 Go 或 CI
# 若已在 Windows 编好 Linux 二进制,避免镜像内再编译:先运行 build-linux-amd64.bat
# docker compose -f docker-compose.binary.yml up -d --build
services:
central:
build: .
image: mengyamonitor-central:latest
restart: unless-stopped
ports:
# 宿主:容器 —— 可按需改为 "127.0.0.1:9393:9393" 仅本机反代
- "9393:9393"
- "9394:9394"
environment:
HOST: "0.0.0.0"
PORT: "9393"
GRPC_PORT: "9394"
# 务必在宿主设置强口令export ADMIN_TOKEN=... 或下方 env_file
ADMIN_TOKEN: ${ADMIN_TOKEN:-shumengya520}
DB_PATH: /app/data/servers.db
volumes:
- ./data:/app/data

View File

@@ -0,0 +1,47 @@
module mengyamonitor-backend-server
go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
google.golang.org/grpc v1.70.0
google.golang.org/protobuf v1.35.2
modernc.org/sqlite v1.34.5
)
require (
github.com/bytedance/sonic v1.11.6 // indirect
github.com/bytedance/sonic/loader v0.1.1 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.30.0 // indirect
golang.org/x/net v0.32.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
)

View File

@@ -0,0 +1,149 @@
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U=
go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg=
go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M=
go.opentelemetry.io/otel/metric v1.32.0/go.mod h1:jH7CIbbK6SH2V2wE16W05BHCtIDzauciCRLoc/SyMv8=
go.opentelemetry.io/otel/sdk v1.32.0 h1:RNxepc9vK59A8XsgZQouW8ue8Gkb4jpWtJm9ge5lEG4=
go.opentelemetry.io/otel/sdk v1.32.0/go.mod h1:LqgegDBjKMmb2GC6/PrTnteJG39I8/vJCAP9LlJXEjU=
go.opentelemetry.io/otel/sdk/metric v1.32.0 h1:rZvFnvmvawYb0alrYkjraqJq0Z4ZUJAiyYCU9snn1CU=
go.opentelemetry.io/otel/sdk/metric v1.32.0/go.mod h1:PWeZlq0zt9YkYAp3gjKZ0eicRYvOh1Gd+X99x6GHpCQ=
go.opentelemetry.io/otel/trace v1.32.0 h1:WIC9mYrXf8TmY/EXuULKc8hR17vE+Hjv2cssQDe03fM=
go.opentelemetry.io/otel/trace v1.32.0/go.mod h1:+i4rkvCraA+tG6AzwloGaCtkx53Fa+L+V8e9a7YvhT8=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a h1:hgh8P4EuoxpsuKMXX/To36nOFD7vixReXgn8lPGnt+o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241202173237-19429a94021a/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU=
google.golang.org/grpc v1.70.0 h1:pWFv03aZoHzlRKHWicjsZytKAiYCtNS0dHbXnIdq7jQ=
google.golang.org/grpc v1.70.0/go.mod h1:ofIJqVKDXx/JiXrwr2IG4/zwdH9txy3IlF40RmcJSQw=
google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -0,0 +1,30 @@
package config
import (
"os"
)
type Config struct {
Host string
Port string
GRPCPort string
AdminToken string
DBPath string
}
func Load() *Config {
return &Config{
Host: getenv("HOST", "0.0.0.0"),
Port: getenv("PORT", "9393"),
GRPCPort: getenv("GRPC_PORT", "9394"),
AdminToken: getenv("ADMIN_TOKEN", "shumengya520"),
DBPath: getenv("DB_PATH", "./data/servers.db"),
}
}
func getenv(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}

View File

@@ -0,0 +1,492 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.5
// protoc v5.28.3
// source: mengya/v1/agent.proto
package mengyav1
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type AgentMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Payload:
//
// *AgentMessage_Hello
// *AgentMessage_Heartbeat
// *AgentMessage_Metrics
Payload isAgentMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *AgentMessage) Reset() {
*x = AgentMessage{}
mi := &file_mengya_v1_agent_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *AgentMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AgentMessage) ProtoMessage() {}
func (x *AgentMessage) ProtoReflect() protoreflect.Message {
mi := &file_mengya_v1_agent_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AgentMessage.ProtoReflect.Descriptor instead.
func (*AgentMessage) Descriptor() ([]byte, []int) {
return file_mengya_v1_agent_proto_rawDescGZIP(), []int{0}
}
func (x *AgentMessage) GetPayload() isAgentMessage_Payload {
if x != nil {
return x.Payload
}
return nil
}
func (x *AgentMessage) GetHello() *Hello {
if x != nil {
if x, ok := x.Payload.(*AgentMessage_Hello); ok {
return x.Hello
}
}
return nil
}
func (x *AgentMessage) GetHeartbeat() *Heartbeat {
if x != nil {
if x, ok := x.Payload.(*AgentMessage_Heartbeat); ok {
return x.Heartbeat
}
}
return nil
}
func (x *AgentMessage) GetMetrics() *MetricsReport {
if x != nil {
if x, ok := x.Payload.(*AgentMessage_Metrics); ok {
return x.Metrics
}
}
return nil
}
type isAgentMessage_Payload interface {
isAgentMessage_Payload()
}
type AgentMessage_Hello struct {
Hello *Hello `protobuf:"bytes,1,opt,name=hello,proto3,oneof"`
}
type AgentMessage_Heartbeat struct {
Heartbeat *Heartbeat `protobuf:"bytes,2,opt,name=heartbeat,proto3,oneof"`
}
type AgentMessage_Metrics struct {
Metrics *MetricsReport `protobuf:"bytes,3,opt,name=metrics,proto3,oneof"`
}
func (*AgentMessage_Hello) isAgentMessage_Payload() {}
func (*AgentMessage_Heartbeat) isAgentMessage_Payload() {}
func (*AgentMessage_Metrics) isAgentMessage_Payload() {}
type Hello struct {
state protoimpl.MessageState `protogen:"open.v1"`
ServerId string `protobuf:"bytes,1,opt,name=server_id,json=serverId,proto3" json:"server_id,omitempty"`
AgentKey string `protobuf:"bytes,2,opt,name=agent_key,json=agentKey,proto3" json:"agent_key,omitempty"`
Hostname string `protobuf:"bytes,3,opt,name=hostname,proto3" json:"hostname,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Hello) Reset() {
*x = Hello{}
mi := &file_mengya_v1_agent_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Hello) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Hello) ProtoMessage() {}
func (x *Hello) ProtoReflect() protoreflect.Message {
mi := &file_mengya_v1_agent_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Hello.ProtoReflect.Descriptor instead.
func (*Hello) Descriptor() ([]byte, []int) {
return file_mengya_v1_agent_proto_rawDescGZIP(), []int{1}
}
func (x *Hello) GetServerId() string {
if x != nil {
return x.ServerId
}
return ""
}
func (x *Hello) GetAgentKey() string {
if x != nil {
return x.AgentKey
}
return ""
}
func (x *Hello) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
type Heartbeat struct {
state protoimpl.MessageState `protogen:"open.v1"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Heartbeat) Reset() {
*x = Heartbeat{}
mi := &file_mengya_v1_agent_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Heartbeat) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Heartbeat) ProtoMessage() {}
func (x *Heartbeat) ProtoReflect() protoreflect.Message {
mi := &file_mengya_v1_agent_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Heartbeat.ProtoReflect.Descriptor instead.
func (*Heartbeat) Descriptor() ([]byte, []int) {
return file_mengya_v1_agent_proto_rawDescGZIP(), []int{2}
}
type MetricsReport struct {
state protoimpl.MessageState `protogen:"open.v1"`
PayloadJson string `protobuf:"bytes,1,opt,name=payload_json,json=payloadJson,proto3" json:"payload_json,omitempty"`
CollectedAtUnixMs int64 `protobuf:"varint,2,opt,name=collected_at_unix_ms,json=collectedAtUnixMs,proto3" json:"collected_at_unix_ms,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *MetricsReport) Reset() {
*x = MetricsReport{}
mi := &file_mengya_v1_agent_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *MetricsReport) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MetricsReport) ProtoMessage() {}
func (x *MetricsReport) ProtoReflect() protoreflect.Message {
mi := &file_mengya_v1_agent_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MetricsReport.ProtoReflect.Descriptor instead.
func (*MetricsReport) Descriptor() ([]byte, []int) {
return file_mengya_v1_agent_proto_rawDescGZIP(), []int{3}
}
func (x *MetricsReport) GetPayloadJson() string {
if x != nil {
return x.PayloadJson
}
return ""
}
func (x *MetricsReport) GetCollectedAtUnixMs() int64 {
if x != nil {
return x.CollectedAtUnixMs
}
return 0
}
type ControlMessage struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Payload:
//
// *ControlMessage_Ack
Payload isControlMessage_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *ControlMessage) Reset() {
*x = ControlMessage{}
mi := &file_mengya_v1_agent_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *ControlMessage) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*ControlMessage) ProtoMessage() {}
func (x *ControlMessage) ProtoReflect() protoreflect.Message {
mi := &file_mengya_v1_agent_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use ControlMessage.ProtoReflect.Descriptor instead.
func (*ControlMessage) Descriptor() ([]byte, []int) {
return file_mengya_v1_agent_proto_rawDescGZIP(), []int{4}
}
func (x *ControlMessage) GetPayload() isControlMessage_Payload {
if x != nil {
return x.Payload
}
return nil
}
func (x *ControlMessage) GetAck() *Ack {
if x != nil {
if x, ok := x.Payload.(*ControlMessage_Ack); ok {
return x.Ack
}
}
return nil
}
type isControlMessage_Payload interface {
isControlMessage_Payload()
}
type ControlMessage_Ack struct {
Ack *Ack `protobuf:"bytes,1,opt,name=ack,proto3,oneof"`
}
func (*ControlMessage_Ack) isControlMessage_Payload() {}
type Ack struct {
state protoimpl.MessageState `protogen:"open.v1"`
Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Ack) Reset() {
*x = Ack{}
mi := &file_mengya_v1_agent_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Ack) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Ack) ProtoMessage() {}
func (x *Ack) ProtoReflect() protoreflect.Message {
mi := &file_mengya_v1_agent_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Ack.ProtoReflect.Descriptor instead.
func (*Ack) Descriptor() ([]byte, []int) {
return file_mengya_v1_agent_proto_rawDescGZIP(), []int{5}
}
func (x *Ack) GetMessage() string {
if x != nil {
return x.Message
}
return ""
}
var File_mengya_v1_agent_proto protoreflect.FileDescriptor
var file_mengya_v1_agent_proto_rawDesc = string([]byte{
0x0a, 0x15, 0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x67, 0x65, 0x6e,
0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x2e,
0x76, 0x31, 0x22, 0xaf, 0x01, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73,
0x61, 0x67, 0x65, 0x12, 0x28, 0x0a, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x18, 0x01, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x10, 0x2e, 0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x48,
0x65, 0x6c, 0x6c, 0x6f, 0x48, 0x00, 0x52, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x34, 0x0a,
0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b,
0x32, 0x14, 0x2e, 0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x48, 0x65, 0x61,
0x72, 0x74, 0x62, 0x65, 0x61, 0x74, 0x48, 0x00, 0x52, 0x09, 0x68, 0x65, 0x61, 0x72, 0x74, 0x62,
0x65, 0x61, 0x74, 0x12, 0x34, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x03,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x2e, 0x76, 0x31,
0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x48, 0x00,
0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x70, 0x61, 0x79,
0x6c, 0x6f, 0x61, 0x64, 0x22, 0x5d, 0x0a, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x12, 0x1b, 0x0a,
0x09, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
0x52, 0x08, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x61, 0x67,
0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61,
0x67, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e,
0x61, 0x6d, 0x65, 0x22, 0x0b, 0x0a, 0x09, 0x48, 0x65, 0x61, 0x72, 0x74, 0x62, 0x65, 0x61, 0x74,
0x22, 0x63, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x52, 0x65, 0x70, 0x6f, 0x72,
0x74, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x5f, 0x6a, 0x73, 0x6f,
0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64,
0x4a, 0x73, 0x6f, 0x6e, 0x12, 0x2f, 0x0a, 0x14, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65,
0x64, 0x5f, 0x61, 0x74, 0x5f, 0x75, 0x6e, 0x69, 0x78, 0x5f, 0x6d, 0x73, 0x18, 0x02, 0x20, 0x01,
0x28, 0x03, 0x52, 0x11, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x65, 0x64, 0x41, 0x74, 0x55,
0x6e, 0x69, 0x78, 0x4d, 0x73, 0x22, 0x3f, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c,
0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x22, 0x0a, 0x03, 0x61, 0x63, 0x6b, 0x18, 0x01,
0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x2e, 0x76, 0x31,
0x2e, 0x41, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x03, 0x61, 0x63, 0x6b, 0x42, 0x09, 0x0a, 0x07, 0x70,
0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x1f, 0x0a, 0x03, 0x41, 0x63, 0x6b, 0x12, 0x18, 0x0a,
0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x50, 0x0a, 0x0b, 0x41, 0x67, 0x65, 0x6e, 0x74,
0x49, 0x6e, 0x67, 0x65, 0x73, 0x74, 0x12, 0x41, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63,
0x74, 0x12, 0x17, 0x2e, 0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x67,
0x65, 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x19, 0x2e, 0x6d, 0x65, 0x6e,
0x67, 0x79, 0x61, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x4d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x3d, 0x5a, 0x3b, 0x6d, 0x65, 0x6e,
0x67, 0x79, 0x61, 0x6d, 0x6f, 0x6e, 0x69, 0x74, 0x6f, 0x72, 0x2d, 0x62, 0x61, 0x63, 0x6b, 0x65,
0x6e, 0x64, 0x2d, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x76, 0x31, 0x3b,
0x6d, 0x65, 0x6e, 0x67, 0x79, 0x61, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
})
var (
file_mengya_v1_agent_proto_rawDescOnce sync.Once
file_mengya_v1_agent_proto_rawDescData []byte
)
func file_mengya_v1_agent_proto_rawDescGZIP() []byte {
file_mengya_v1_agent_proto_rawDescOnce.Do(func() {
file_mengya_v1_agent_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_mengya_v1_agent_proto_rawDesc), len(file_mengya_v1_agent_proto_rawDesc)))
})
return file_mengya_v1_agent_proto_rawDescData
}
var file_mengya_v1_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 6)
var file_mengya_v1_agent_proto_goTypes = []any{
(*AgentMessage)(nil), // 0: mengya.v1.AgentMessage
(*Hello)(nil), // 1: mengya.v1.Hello
(*Heartbeat)(nil), // 2: mengya.v1.Heartbeat
(*MetricsReport)(nil), // 3: mengya.v1.MetricsReport
(*ControlMessage)(nil), // 4: mengya.v1.ControlMessage
(*Ack)(nil), // 5: mengya.v1.Ack
}
var file_mengya_v1_agent_proto_depIdxs = []int32{
1, // 0: mengya.v1.AgentMessage.hello:type_name -> mengya.v1.Hello
2, // 1: mengya.v1.AgentMessage.heartbeat:type_name -> mengya.v1.Heartbeat
3, // 2: mengya.v1.AgentMessage.metrics:type_name -> mengya.v1.MetricsReport
5, // 3: mengya.v1.ControlMessage.ack:type_name -> mengya.v1.Ack
0, // 4: mengya.v1.AgentIngest.Connect:input_type -> mengya.v1.AgentMessage
4, // 5: mengya.v1.AgentIngest.Connect:output_type -> mengya.v1.ControlMessage
5, // [5:6] is the sub-list for method output_type
4, // [4:5] is the sub-list for method input_type
4, // [4:4] is the sub-list for extension type_name
4, // [4:4] is the sub-list for extension extendee
0, // [0:4] is the sub-list for field type_name
}
func init() { file_mengya_v1_agent_proto_init() }
func file_mengya_v1_agent_proto_init() {
if File_mengya_v1_agent_proto != nil {
return
}
file_mengya_v1_agent_proto_msgTypes[0].OneofWrappers = []any{
(*AgentMessage_Hello)(nil),
(*AgentMessage_Heartbeat)(nil),
(*AgentMessage_Metrics)(nil),
}
file_mengya_v1_agent_proto_msgTypes[4].OneofWrappers = []any{
(*ControlMessage_Ack)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_mengya_v1_agent_proto_rawDesc), len(file_mengya_v1_agent_proto_rawDesc)),
NumEnums: 0,
NumMessages: 6,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_mengya_v1_agent_proto_goTypes,
DependencyIndexes: file_mengya_v1_agent_proto_depIdxs,
MessageInfos: file_mengya_v1_agent_proto_msgTypes,
}.Build()
File_mengya_v1_agent_proto = out.File
file_mengya_v1_agent_proto_goTypes = nil
file_mengya_v1_agent_proto_depIdxs = nil
}

View File

@@ -0,0 +1,119 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v5.28.3
// source: mengya/v1/agent.proto
package mengyav1
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
AgentIngest_Connect_FullMethodName = "/mengya.v1.AgentIngest/Connect"
)
// AgentIngestClient is the client API for AgentIngest service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// AgentIngest: 采集端出站连接,双向流上报指标。
type AgentIngestClient interface {
Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[AgentMessage, ControlMessage], error)
}
type agentIngestClient struct {
cc grpc.ClientConnInterface
}
func NewAgentIngestClient(cc grpc.ClientConnInterface) AgentIngestClient {
return &agentIngestClient{cc}
}
func (c *agentIngestClient) Connect(ctx context.Context, opts ...grpc.CallOption) (grpc.BidiStreamingClient[AgentMessage, ControlMessage], error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
stream, err := c.cc.NewStream(ctx, &AgentIngest_ServiceDesc.Streams[0], AgentIngest_Connect_FullMethodName, cOpts...)
if err != nil {
return nil, err
}
x := &grpc.GenericClientStream[AgentMessage, ControlMessage]{ClientStream: stream}
return x, nil
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type AgentIngest_ConnectClient = grpc.BidiStreamingClient[AgentMessage, ControlMessage]
// AgentIngestServer is the server API for AgentIngest service.
// All implementations must embed UnimplementedAgentIngestServer
// for forward compatibility.
//
// AgentIngest: 采集端出站连接,双向流上报指标。
type AgentIngestServer interface {
Connect(grpc.BidiStreamingServer[AgentMessage, ControlMessage]) error
mustEmbedUnimplementedAgentIngestServer()
}
// UnimplementedAgentIngestServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedAgentIngestServer struct{}
func (UnimplementedAgentIngestServer) Connect(grpc.BidiStreamingServer[AgentMessage, ControlMessage]) error {
return status.Errorf(codes.Unimplemented, "method Connect not implemented")
}
func (UnimplementedAgentIngestServer) mustEmbedUnimplementedAgentIngestServer() {}
func (UnimplementedAgentIngestServer) testEmbeddedByValue() {}
// UnsafeAgentIngestServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to AgentIngestServer will
// result in compilation errors.
type UnsafeAgentIngestServer interface {
mustEmbedUnimplementedAgentIngestServer()
}
func RegisterAgentIngestServer(s grpc.ServiceRegistrar, srv AgentIngestServer) {
// If the following call pancis, it indicates UnimplementedAgentIngestServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&AgentIngest_ServiceDesc, srv)
}
func _AgentIngest_Connect_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(AgentIngestServer).Connect(&grpc.GenericServerStream[AgentMessage, ControlMessage]{ServerStream: stream})
}
// This type alias is provided for backwards compatibility with existing code that references the prior non-generic stream type by name.
type AgentIngest_ConnectServer = grpc.BidiStreamingServer[AgentMessage, ControlMessage]
// AgentIngest_ServiceDesc is the grpc.ServiceDesc for AgentIngest service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var AgentIngest_ServiceDesc = grpc.ServiceDesc{
ServiceName: "mengya.v1.AgentIngest",
HandlerType: (*AgentIngestServer)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "Connect",
Handler: _AgentIngest_Connect_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "mengya/v1/agent.proto",
}

View File

@@ -0,0 +1,22 @@
package grpcrun
import (
"fmt"
"net"
"mengyamonitor-backend-server/internal/gen/mengyav1"
"mengyamonitor-backend-server/internal/ingest"
"google.golang.org/grpc"
)
// Serve starts a blocking gRPC server for agent ingest.
func Serve(listenAddr string, srv *ingest.Server) error {
lis, err := net.Listen("tcp", listenAddr)
if err != nil {
return fmt.Errorf("listen: %w", err)
}
g := grpc.NewServer()
mengyav1.RegisterAgentIngestServer(g, srv)
return g.Serve(lis)
}

View File

@@ -0,0 +1,100 @@
package handler
import (
"crypto/subtle"
"database/sql"
"errors"
"net/http"
"mengyamonitor-backend-server/internal/config"
"mengyamonitor-backend-server/internal/model"
"mengyamonitor-backend-server/internal/store"
"github.com/gin-gonic/gin"
)
type Admin struct {
Store *store.Store
Cfg *config.Config
}
func (h *Admin) Auth(c *gin.Context) {
var in model.AuthInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
if subtle.ConstantTimeCompare([]byte(in.Token), []byte(h.Cfg.AdminToken)) != 1 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *Admin) ListServers(c *gin.Context) {
list, err := h.Store.ListAll(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": list})
}
func (h *Admin) CreateServer(c *gin.Context) {
var in model.CreateServerInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
s, err := h.Store.Create(c.Request.Context(), in)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": s})
}
func (h *Admin) UpdateServer(c *gin.Context) {
id := c.Param("id")
var in model.UpdateServerInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
s, err := h.Store.Update(c.Request.Context(), id, in)
if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": s})
}
func (h *Admin) DeleteServer(c *gin.Context) {
id := c.Param("id")
if err := h.Store.Delete(c.Request.Context(), id); err != nil {
if errors.Is(err, sql.ErrNoRows) {
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}
func (h *Admin) Reorder(c *gin.Context) {
var in model.ReorderInput
if err := c.ShouldBindJSON(&in); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid body"})
return
}
if err := h.Store.Reorder(c.Request.Context(), in.IDs); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"ok": true})
}

View File

@@ -0,0 +1,70 @@
package handler
import (
"net/http"
"time"
"mengyamonitor-backend-server/internal/model"
"mengyamonitor-backend-server/internal/store"
"github.com/gin-gonic/gin"
)
type Public struct {
Store *store.Store
}
// publicServer hides agent_key from unauthenticated API consumers.
type publicServer struct {
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Enabled bool `json:"enabled"`
SortOrder int `json:"sortOrder"`
CreatedAt time.Time `json:"createdAt,omitempty"`
}
func toPublicServers(in []model.MonitoredServer) []publicServer {
out := make([]publicServer, 0, len(in))
for _, s := range in {
out = append(out, publicServer{
ID: s.ID, Name: s.Name, URL: s.URL, Enabled: s.Enabled,
SortOrder: s.SortOrder, CreatedAt: s.CreatedAt,
})
}
return out
}
func (h *Public) Health(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
func (h *Public) ListServers(c *gin.Context) {
list, err := h.Store.ListEnabled(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": toPublicServers(list)})
}
func (h *Public) Root(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"service": "萌芽监控 — 中心服务",
"version": "1.0.0",
"public": []string{
"GET /api/health",
"GET /api/servers",
"GET /api/ws/monitor — WebSocket 指标快照",
"gRPC AgentIngest — 见环境变量 GRPC_PORT",
},
"admin": []string{
"POST /api/admin/auth",
"GET /api/admin/servers",
"POST /api/admin/servers",
"PUT /api/admin/servers/:id",
"DELETE /api/admin/servers/:id",
"PUT /api/admin/servers/reorder",
},
})
}

View File

@@ -0,0 +1,50 @@
package handler
import (
"log"
"net/http"
"time"
"mengyamonitor-backend-server/internal/hub"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
var wsUpgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
// MonitorWebSocket upgrades to WebSocket and streams hub snapshots.
func MonitorWebSocket(h *hub.Hub) gin.HandlerFunc {
return func(c *gin.Context) {
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
log.Printf("ws: upgrade: %v", err)
return
}
defer h.Unregister(conn)
snap, err := h.BuildSnapshotMessage()
if err != nil {
_ = conn.Close()
return
}
_ = conn.SetWriteDeadline(time.Now().Add(15 * time.Second))
if err := conn.WriteMessage(websocket.TextMessage, snap); err != nil {
_ = conn.Close()
return
}
h.AddSubscriber(conn)
for {
_ = conn.SetReadDeadline(time.Now().Add(120 * time.Second))
_, _, err := conn.ReadMessage()
if err != nil {
break
}
}
}
}

View File

@@ -0,0 +1,108 @@
package hub
import (
"encoding/json"
"sync"
"time"
"github.com/gorilla/websocket"
)
// WSMessage is sent to browsers (matches frontend ServerStatus shape).
type WSMessage struct {
Type string `json:"type"`
Data map[string]json.RawMessage `json:"data"`
}
type Hub struct {
mu sync.RWMutex
// serverId -> latest metrics JSON object (not wrapped)
metrics map[string]json.RawMessage
subsMu sync.Mutex
// subscribers for broadcast
conns map[*websocket.Conn]struct{}
}
func New() *Hub {
return &Hub{
metrics: make(map[string]json.RawMessage),
conns: make(map[*websocket.Conn]struct{}),
}
}
func (h *Hub) SetMetrics(serverID string, metricsJSON []byte) {
h.mu.Lock()
if len(metricsJSON) == 0 {
delete(h.metrics, serverID)
} else {
h.metrics[serverID] = json.RawMessage(append([]byte(nil), metricsJSON...))
}
h.mu.Unlock()
}
func (h *Hub) snapshotData() map[string]json.RawMessage {
h.mu.RLock()
defer h.mu.RUnlock()
out := make(map[string]json.RawMessage, len(h.metrics))
now := time.Now().UnixMilli()
for id, raw := range h.metrics {
// Wrap as ServerStatus for frontend compatibility
wrap := map[string]any{
"serverId": id,
"online": true,
"metrics": json.RawMessage(raw),
"lastUpdate": now,
}
b, err := json.Marshal(wrap)
if err != nil {
continue
}
out[id] = b
}
return out
}
func (h *Hub) BuildSnapshotMessage() ([]byte, error) {
msg := WSMessage{
Type: "snapshot",
Data: h.snapshotData(),
}
return json.Marshal(msg)
}
// AddSubscriber registers for broadcast fan-out (call after sending the initial snapshot).
func (h *Hub) AddSubscriber(c *websocket.Conn) {
h.subsMu.Lock()
h.conns[c] = struct{}{}
h.subsMu.Unlock()
}
func (h *Hub) Unregister(c *websocket.Conn) {
h.subsMu.Lock()
delete(h.conns, c)
h.subsMu.Unlock()
}
// BroadcastSnapshot pushes full snapshot to all subscribers.
func (h *Hub) BroadcastSnapshot() {
body, err := h.BuildSnapshotMessage()
if err != nil {
return
}
h.subsMu.Lock()
defer h.subsMu.Unlock()
for c := range h.conns {
_ = c.SetWriteDeadline(time.Now().Add(10 * time.Second))
if err := c.WriteMessage(websocket.TextMessage, body); err != nil {
_ = c.Close()
delete(h.conns, c)
}
}
}
// NotifyMetricsUpdate should be called after SetMetrics to push to browsers.
func (h *Hub) NotifyMetricsUpdate(serverID string, metricsJSON []byte) {
h.SetMetrics(serverID, metricsJSON)
h.BroadcastSnapshot()
}

View File

@@ -0,0 +1,82 @@
package ingest
import (
"database/sql"
"errors"
"io"
"log"
"mengyamonitor-backend-server/internal/gen/mengyav1"
"mengyamonitor-backend-server/internal/hub"
"mengyamonitor-backend-server/internal/store"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type Server struct {
mengyav1.UnimplementedAgentIngestServer
Store *store.Store
Hub *hub.Hub
}
func (s *Server) Connect(stream grpc.BidiStreamingServer[mengyav1.AgentMessage, mengyav1.ControlMessage]) error {
var authedID string
for {
msg, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
ctx := stream.Context()
switch {
case msg.GetHello() != nil:
h := msg.GetHello()
srv, err := s.Store.VerifyAgent(ctx, h.GetServerId(), h.GetAgentKey())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
log.Printf("ingest: auth failed server_id=%q: no such id (SERVER_ID must be the UUID from admin table, not the display name)", h.GetServerId())
} else {
log.Printf("ingest: auth failed server_id=%s: %v", h.GetServerId(), err)
}
return status.Errorf(codes.Unauthenticated, "invalid credentials")
}
authedID = srv.ID
_ = stream.Send(&mengyav1.ControlMessage{
Payload: &mengyav1.ControlMessage_Ack{
Ack: &mengyav1.Ack{Message: "hello_ok"},
},
})
case msg.GetHeartbeat() != nil:
if authedID == "" {
continue
}
_ = stream.Send(&mengyav1.ControlMessage{
Payload: &mengyav1.ControlMessage_Ack{
Ack: &mengyav1.Ack{Message: "pong"},
},
})
case msg.GetMetrics() != nil:
if authedID == "" {
return status.Errorf(codes.FailedPrecondition, "send hello first")
}
m := msg.GetMetrics()
if len(m.GetPayloadJson()) == 0 {
continue
}
s.Hub.NotifyMetricsUpdate(authedID, []byte(m.GetPayloadJson()))
_ = stream.Send(&mengyav1.ControlMessage{
Payload: &mengyav1.ControlMessage_Ack{
Ack: &mengyav1.Ack{Message: "metrics_ok"},
},
})
}
}
}

View File

@@ -0,0 +1,23 @@
package middleware
import (
"crypto/subtle"
"net/http"
"mengyamonitor-backend-server/internal/config"
"github.com/gin-gonic/gin"
)
// AdminToken rejects requests without a valid X-Admin-Token header.
func AdminToken(cfg *config.Config) gin.HandlerFunc {
tok := []byte(cfg.AdminToken)
return func(c *gin.Context) {
given := []byte(c.GetHeader("X-Admin-Token"))
if subtle.ConstantTimeCompare(given, tok) != 1 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
return
}
c.Next()
}
}

View File

@@ -0,0 +1,35 @@
package model
import "time"
// MonitoredServer is persisted and exposed to the frontend display.
type MonitoredServer struct {
ID string `json:"id"`
Name string `json:"name"`
URL string `json:"url"`
Enabled bool `json:"enabled"`
SortOrder int `json:"sortOrder"`
AgentKey string `json:"agentKey,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
}
type CreateServerInput struct {
Name string `json:"name" binding:"required"`
URL string `json:"url"`
Enabled *bool `json:"enabled"`
}
type UpdateServerInput struct {
Name string `json:"name"`
URL *string `json:"url"`
Enabled *bool `json:"enabled"`
AgentKey *string `json:"agentKey"`
}
type ReorderInput struct {
IDs []string `json:"ids" binding:"required"`
}
type AuthInput struct {
Token string `json:"token" binding:"required"`
}

View File

@@ -0,0 +1,71 @@
package router
import (
"net/http"
"mengyamonitor-backend-server/internal/config"
"mengyamonitor-backend-server/internal/handler"
"mengyamonitor-backend-server/internal/hub"
"mengyamonitor-backend-server/internal/middleware"
"mengyamonitor-backend-server/internal/store"
"github.com/gin-gonic/gin"
)
func New(cfg *config.Config, st *store.Store, h *hub.Hub) *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
// 公网宽松 CORS任意 Origin + 透传浏览器请求的 Header预检
r.Use(func(c *gin.Context) {
h := c.Writer.Header()
origin := c.GetHeader("Origin")
if origin != "" {
h.Set("Access-Control-Allow-Origin", origin)
h.Set("Access-Control-Allow-Credentials", "true")
} else {
h.Set("Access-Control-Allow-Origin", "*")
}
h.Set("Access-Control-Allow-Methods", "GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS")
if reqH := c.GetHeader("Access-Control-Request-Headers"); reqH != "" {
h.Set("Access-Control-Allow-Headers", reqH)
} else {
h.Set("Access-Control-Allow-Headers", "Content-Type, X-Admin-Token, Authorization, Accept, Origin, X-Requested-With")
}
h.Set("Access-Control-Max-Age", "86400")
if c.Request.Method == http.MethodOptions {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
})
pub := &handler.Public{Store: st}
r.GET("/", pub.Root)
r.GET("/api/health", pub.Health)
r.GET("/api/servers", pub.ListServers)
r.GET("/api/ws/monitor", handler.MonitorWebSocket(h))
adm := &handler.Admin{Store: st, Cfg: cfg}
r.POST("/api/admin/auth", adm.Auth)
adminG := r.Group("/api/admin")
adminG.Use(middleware.AdminToken(cfg))
{
adminG.GET("/servers", adm.ListServers)
adminG.POST("/servers", adm.CreateServer)
adminG.PUT("/servers/reorder", adm.Reorder)
adminG.PUT("/servers/:id", adm.UpdateServer)
adminG.DELETE("/servers/:id", adm.DeleteServer)
}
r.NoRoute(func(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{
"error": "not_found",
"path": c.Request.URL.Path,
"method": c.Request.Method,
})
})
return r
}

View File

@@ -0,0 +1,307 @@
package store
import (
"context"
"crypto/subtle"
"database/sql"
"errors"
"os"
"path/filepath"
"strings"
"time"
"mengyamonitor-backend-server/internal/model"
"github.com/google/uuid"
_ "modernc.org/sqlite"
)
type Store struct {
db *sql.DB
}
func Open(dbPath string) (*Store, error) {
dir := filepath.Dir(dbPath)
if dir != "." && dir != "" {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, err
}
}
db, err := sql.Open("sqlite", dbPath+"?_pragma=foreign_keys(1)")
if err != nil {
return nil, err
}
if err := db.Ping(); err != nil {
_ = db.Close()
return nil, err
}
s := &Store{db: db}
if err := s.migrate(); err != nil {
_ = db.Close()
return nil, err
}
return s, nil
}
func (s *Store) Close() error {
return s.db.Close()
}
func (s *Store) migrate() error {
const ddl = `
CREATE TABLE IF NOT EXISTS servers (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
sort_order INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_servers_sort ON servers(sort_order);
`
if _, err := s.db.Exec(ddl); err != nil {
return err
}
return s.ensureAgentKeyColumn()
}
func (s *Store) ensureAgentKeyColumn() error {
rows, err := s.db.Query(`PRAGMA table_info(servers)`)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var cid int
var name, typ string
var notnull, pk int
var dflt sql.NullString
if err := rows.Scan(&cid, &name, &typ, &notnull, &dflt, &pk); err != nil {
return err
}
if name == "agent_key" {
return nil
}
}
if err := rows.Err(); err != nil {
return err
}
if _, err = s.db.Exec(`ALTER TABLE servers ADD COLUMN agent_key TEXT`); err != nil {
return err
}
// Backfill keys for existing rows so gRPC can be enabled without manual SQL.
bRows, err := s.db.Query(`SELECT id FROM servers WHERE agent_key IS NULL OR agent_key = ''`)
if err != nil {
return err
}
var ids []string
for bRows.Next() {
var id string
if err := bRows.Scan(&id); err != nil {
_ = bRows.Close()
return err
}
ids = append(ids, id)
}
_ = bRows.Close()
for _, id := range ids {
if _, err := s.db.Exec(`UPDATE servers SET agent_key = ? WHERE id = ?`, uuid.NewString(), id); err != nil {
return err
}
}
return nil
}
func (s *Store) ListEnabled(ctx context.Context) ([]model.MonitoredServer, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, name, url, enabled, sort_order, agent_key, created_at
FROM servers WHERE enabled = 1 ORDER BY sort_order ASC, created_at ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
return scanServers(rows)
}
func (s *Store) ListAll(ctx context.Context) ([]model.MonitoredServer, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT id, name, url, enabled, sort_order, agent_key, created_at
FROM servers ORDER BY sort_order ASC, created_at ASC`)
if err != nil {
return nil, err
}
defer rows.Close()
return scanServers(rows)
}
func scanServers(rows *sql.Rows) ([]model.MonitoredServer, error) {
out := make([]model.MonitoredServer, 0)
for rows.Next() {
var m model.MonitoredServer
var enabled int
var created, agKey sql.NullString
if err := rows.Scan(&m.ID, &m.Name, &m.URL, &enabled, &m.SortOrder, &agKey, &created); err != nil {
return nil, err
}
m.Enabled = enabled != 0
if agKey.Valid {
m.AgentKey = agKey.String
}
if t, err := time.Parse(time.RFC3339Nano, created.String); err == nil {
m.CreatedAt = t
}
out = append(out, m)
}
return out, rows.Err()
}
func (s *Store) Create(ctx context.Context, in model.CreateServerInput) (*model.MonitoredServer, error) {
name := strings.TrimSpace(in.Name)
urlStr := normalizeURL(strings.TrimSpace(in.URL))
if name == "" {
return nil, errors.New("invalid name")
}
enabled := true
if in.Enabled != nil {
enabled = *in.Enabled
}
var maxOrder int
_ = s.db.QueryRowContext(ctx, `SELECT COALESCE(MAX(sort_order), -1) FROM servers`).Scan(&maxOrder)
id := uuid.NewString()
agentKey := uuid.NewString()
now := time.Now().UTC()
_, err := s.db.ExecContext(ctx, `
INSERT INTO servers (id, name, url, enabled, sort_order, agent_key, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`,
id, name, urlStr, boolToInt(enabled), maxOrder+1, agentKey, now.Format(time.RFC3339Nano))
if err != nil {
return nil, err
}
return &model.MonitoredServer{
ID: id, Name: name, URL: urlStr, Enabled: enabled, SortOrder: maxOrder + 1, AgentKey: agentKey, CreatedAt: now,
}, nil
}
func (s *Store) Update(ctx context.Context, id string, in model.UpdateServerInput) (*model.MonitoredServer, error) {
cur, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
name := cur.Name
urlStr := cur.URL
enabled := cur.Enabled
if in.Name != "" {
name = strings.TrimSpace(in.Name)
}
if in.URL != nil {
urlStr = normalizeURL(strings.TrimSpace(*in.URL))
}
if in.Enabled != nil {
enabled = *in.Enabled
}
if name == "" {
return nil, errors.New("invalid name")
}
agentKey := cur.AgentKey
if in.AgentKey != nil {
agentKey = *in.AgentKey
}
_, err = s.db.ExecContext(ctx, `UPDATE servers SET name = ?, url = ?, enabled = ?, agent_key = ? WHERE id = ?`,
name, urlStr, boolToInt(enabled), nullIfEmpty(agentKey), id)
if err != nil {
return nil, err
}
cur.Name, cur.URL, cur.Enabled, cur.AgentKey = name, urlStr, enabled, agentKey
return cur, nil
}
func (s *Store) Get(ctx context.Context, id string) (*model.MonitoredServer, error) {
var m model.MonitoredServer
var enabled int
var created, agKey sql.NullString
err := s.db.QueryRowContext(ctx, `
SELECT id, name, url, enabled, sort_order, agent_key, created_at FROM servers WHERE id = ?`, id).
Scan(&m.ID, &m.Name, &m.URL, &enabled, &m.SortOrder, &agKey, &created)
if errors.Is(err, sql.ErrNoRows) {
return nil, err
}
if err != nil {
return nil, err
}
m.Enabled = enabled != 0
if agKey.Valid {
m.AgentKey = agKey.String
}
if t, err := time.Parse(time.RFC3339Nano, created.String); err == nil {
m.CreatedAt = t
}
return &m, nil
}
// VerifyAgent checks server_id + agent_key for gRPC ingest.
func (s *Store) VerifyAgent(ctx context.Context, id, key string) (*model.MonitoredServer, error) {
srv, err := s.Get(ctx, id)
if err != nil {
return nil, err
}
if !srv.Enabled {
return nil, errors.New("disabled")
}
if srv.AgentKey == "" {
return nil, errors.New("agent_key not set")
}
if subtle.ConstantTimeCompare([]byte(srv.AgentKey), []byte(key)) != 1 {
return nil, errors.New("invalid agent_key")
}
return srv, nil
}
func nullIfEmpty(s string) any {
if s == "" {
return nil
}
return s
}
func (s *Store) Delete(ctx context.Context, id string) error {
res, err := s.db.ExecContext(ctx, `DELETE FROM servers WHERE id = ?`, id)
if err != nil {
return err
}
n, _ := res.RowsAffected()
if n == 0 {
return sql.ErrNoRows
}
return nil
}
func (s *Store) Reorder(ctx context.Context, ids []string) error {
tx, err := s.db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer func() { _ = tx.Rollback() }()
for i, id := range ids {
if _, err := tx.ExecContext(ctx, `UPDATE servers SET sort_order = ? WHERE id = ?`, i, id); err != nil {
return err
}
}
return tx.Commit()
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
func normalizeURL(u string) string {
if u == "" {
return ""
}
if !strings.HasPrefix(strings.ToLower(u), "http://") && !strings.HasPrefix(strings.ToLower(u), "https://") {
return "http://" + u
}
return u
}

View File

@@ -0,0 +1,39 @@
package main
import (
"fmt"
"log"
"mengyamonitor-backend-server/internal/config"
"mengyamonitor-backend-server/internal/grpcrun"
"mengyamonitor-backend-server/internal/hub"
"mengyamonitor-backend-server/internal/ingest"
"mengyamonitor-backend-server/internal/router"
"mengyamonitor-backend-server/internal/store"
)
func main() {
cfg := config.Load()
st, err := store.Open(cfg.DBPath)
if err != nil {
log.Fatalf("store: %v", err)
}
defer st.Close()
metricsHub := hub.New()
ingestSrv := &ingest.Server{Store: st, Hub: metricsHub}
grpcAddr := fmt.Sprintf("%s:%s", cfg.Host, cfg.GRPCPort)
go func() {
log.Printf("gRPC AgentIngest listening on %s", grpcAddr)
if err := grpcrun.Serve(grpcAddr, ingestSrv); err != nil {
log.Fatalf("grpc: %v", err)
}
}()
httpAddr := fmt.Sprintf("%s:%s", cfg.Host, cfg.Port)
log.Printf("central HTTP/WS on http://%s", httpAddr)
if err := router.New(cfg, st, metricsHub).Run(httpAddr); err != nil {
log.Fatalf("http: %v", err)
}
}

View File

@@ -0,0 +1,41 @@
syntax = "proto3";
package mengya.v1;
option go_package = "mengyamonitor-backend-server/internal/gen/mengyav1;mengyav1";
// AgentIngest: 采集端出站连接,双向流上报指标。
service AgentIngest {
rpc Connect(stream AgentMessage) returns (stream ControlMessage);
}
message AgentMessage {
oneof payload {
Hello hello = 1;
Heartbeat heartbeat = 2;
MetricsReport metrics = 3;
}
}
message Hello {
string server_id = 1;
string agent_key = 2;
string hostname = 3;
}
message Heartbeat {}
message MetricsReport {
string payload_json = 1;
int64 collected_at_unix_ms = 2;
}
message ControlMessage {
oneof payload {
Ack ack = 1;
}
}
message Ack {
string message = 1;
}