mirror of
https://github.com/tnb-labs/panel.git
synced 2026-08-30 17:05:19 +08:00
feat: 添加prometheus和grafana支持 (#1508)
* feat: 阶段性提交 * feat: 完成开发 * feat: 阶段性提交 * feat: 阶段性提交
This commit is contained in:
+5
-1
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
@@ -161,6 +163,7 @@ func initAce() (*app.Ace, error) {
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
giteaApp := gitea.NewApp()
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
@@ -171,12 +174,13 @@ func initAce() (*app.Ace, error) {
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
prometheusApp := prometheus.NewApp(locale, config, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, grafanaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
http := route.NewHttp(config, userService, userPasskeyService, userTokenService, homeService, taskService, websiteService, projectService, databaseService, databaseServerService, databaseRedisService, databaseUserService, backupService, backupStorageService, certService, certDNSService, certAccountService, appService, environmentService, environmentGoService, environmentJavaService, environmentNodejsService, environmentPHPService, environmentPythonService, environmentDotnetService, cronService, processService, safeService, firewallService, firewallScanService, sshService, containerService, containerComposeService, containerNetworkService, containerImageService, containerVolumeService, fileService, logService, monitorService, settingService, systemctlService, toolboxNetworkService, toolboxSystemService, toolboxBenchmarkService, toolboxSSHService, toolboxDiskService, toolboxLogService, toolboxMigrationService, webHookService, templateService, websiteStatService, loader)
|
||||
wsService := service.NewWsService(locale, config, logger, sshRepo, settingRepo)
|
||||
ws := route.NewWs(wsService, toolboxMigrationService)
|
||||
|
||||
+5
-1
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
@@ -80,6 +82,7 @@ func initCli() (*app.Cli, error) {
|
||||
fail2banApp := fail2ban.NewApp(locale, websiteRepo)
|
||||
frpApp := frp.NewApp()
|
||||
giteaApp := gitea.NewApp()
|
||||
grafanaApp := grafana.NewApp(locale)
|
||||
mariadbApp := mariadb.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
memcachedApp := memcached.NewApp(locale)
|
||||
minioApp := minio.NewApp()
|
||||
@@ -90,12 +93,13 @@ func initCli() (*app.Cli, error) {
|
||||
phpmyadminApp := phpmyadmin.NewApp(locale)
|
||||
podmanApp := podman.NewApp()
|
||||
postgresqlApp := postgresql.NewApp(locale, settingRepo, databaseServerRepo)
|
||||
prometheusApp := prometheus.NewApp(locale, config, taskRepo)
|
||||
pureftpdApp := pureftpd.NewApp(locale)
|
||||
redisApp := redis.NewApp(locale, databaseServerRepo)
|
||||
rsyncApp := rsync.NewApp(locale)
|
||||
s3fsApp := s3fs.NewApp(locale)
|
||||
supervisorApp := supervisor.NewApp(locale)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
loader := bootstrap.NewLoader(apacheApp, codeserverApp, dockerApp, fail2banApp, frpApp, giteaApp, grafanaApp, mariadbApp, memcachedApp, minioApp, mysqlApp, nginxApp, openrestyApp, perconaApp, phpmyadminApp, podmanApp, postgresqlApp, prometheusApp, pureftpdApp, redisApp, rsyncApp, s3fsApp, supervisorApp)
|
||||
appCli := app.NewCli(command, gormigrate, loader)
|
||||
return appCli, nil
|
||||
}
|
||||
|
||||
@@ -152,6 +152,8 @@ github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OI
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/subcommands v1.2.0 h1:vWQspBTo2nEqTUFita5/KeEWlUL8kQObDFbub/EN9oE=
|
||||
github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
|
||||
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/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
@@ -324,6 +326,8 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
|
||||
github.com/pkg/sftp v1.13.10 h1:+5FbKNTe5Z9aspU88DPIKJ9z2KZoaGCu6Sr6kKR/5mU=
|
||||
github.com/pkg/sftp v1.13.10/go.mod h1:bJ1a7uDhrX/4OII+agvy28lzRvQrmIQuaHrcI1HbeGA=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
@@ -33,6 +35,7 @@ var ProviderSet = wire.NewSet(
|
||||
fail2ban.NewApp,
|
||||
frp.NewApp,
|
||||
gitea.NewApp,
|
||||
grafana.NewApp,
|
||||
mariadb.NewApp,
|
||||
memcached.NewApp,
|
||||
minio.NewApp,
|
||||
@@ -43,6 +46,7 @@ var ProviderSet = wire.NewSet(
|
||||
phpmyadmin.NewApp,
|
||||
podman.NewApp,
|
||||
postgresql.NewApp,
|
||||
prometheus.NewApp,
|
||||
pureftpd.NewApp,
|
||||
redis.NewApp,
|
||||
rsync.NewApp,
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
package grafana
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale) *App {
|
||||
return &App{t: t}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("grafana")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get grafana status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
// 从配置中获取端口
|
||||
confPath := fmt.Sprintf("%s/server/grafana/conf/grafana.ini", app.Root)
|
||||
config, _ := io.Read(confPath)
|
||||
port := s.getINIValue(config, "server", "http_port")
|
||||
if port == "" {
|
||||
port = "3000"
|
||||
}
|
||||
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get(fmt.Sprintf("http://127.0.0.1:%s/api/health", port))
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var health struct {
|
||||
Commit string `json:"commit"`
|
||||
Database string `json:"database"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &health); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Version"), Value: health.Version},
|
||||
{Name: "Commit", Value: health.Commit},
|
||||
{Name: s.t.Get("Database"), Value: health.Database},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
confPath := fmt.Sprintf("%s/server/grafana/conf/grafana.ini", app.Root)
|
||||
config, _ := io.Read(confPath)
|
||||
|
||||
service.Success(w, config)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/grafana/conf/grafana.ini", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("grafana"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Grafana 配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
config, _ := io.Read(fmt.Sprintf("%s/server/grafana/conf/grafana.ini", app.Root))
|
||||
|
||||
tune := ConfigTune{
|
||||
// [server]
|
||||
HTTPPort: s.getINIValue(config, "server", "http_port"),
|
||||
Domain: s.getINIValue(config, "server", "domain"),
|
||||
RootURL: s.getINIValue(config, "server", "root_url"),
|
||||
Protocol: s.getINIValue(config, "server", "protocol"),
|
||||
// [database]
|
||||
DBType: s.getINIValue(config, "database", "type"),
|
||||
DBHost: s.getINIValue(config, "database", "host"),
|
||||
DBName: s.getINIValue(config, "database", "name"),
|
||||
DBUser: s.getINIValue(config, "database", "user"),
|
||||
DBPassword: s.getINIValue(config, "database", "password"),
|
||||
// [security]
|
||||
AdminUser: s.getINIValue(config, "security", "admin_user"),
|
||||
AdminPassword: s.getINIValue(config, "security", "admin_password"),
|
||||
// [users]
|
||||
AllowSignUp: s.getINIValue(config, "users", "allow_sign_up"),
|
||||
AutoAssignOrgRole: s.getINIValue(config, "users", "auto_assign_org_role"),
|
||||
// [smtp]
|
||||
SMTPEnabled: s.getINIValue(config, "smtp", "enabled"),
|
||||
SMTPHost: s.getINIValue(config, "smtp", "host"),
|
||||
SMTPUser: s.getINIValue(config, "smtp", "user"),
|
||||
SMTPPassword: s.getINIValue(config, "smtp", "password"),
|
||||
SMTPFromAddress: s.getINIValue(config, "smtp", "from_address"),
|
||||
// [log]
|
||||
LogMode: s.getINIValue(config, "log", "mode"),
|
||||
LogLevel: s.getINIValue(config, "log", "level"),
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Grafana 配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/grafana/conf/grafana.ini", app.Root)
|
||||
config, _ := io.Read(confPath)
|
||||
|
||||
// [server]
|
||||
config = s.setINIValue(config, "server", "http_port", req.HTTPPort)
|
||||
config = s.setINIValue(config, "server", "domain", req.Domain)
|
||||
config = s.setINIValue(config, "server", "root_url", req.RootURL)
|
||||
config = s.setINIValue(config, "server", "protocol", req.Protocol)
|
||||
// [database]
|
||||
config = s.setINIValue(config, "database", "type", req.DBType)
|
||||
config = s.setINIValue(config, "database", "host", req.DBHost)
|
||||
config = s.setINIValue(config, "database", "name", req.DBName)
|
||||
config = s.setINIValue(config, "database", "user", req.DBUser)
|
||||
config = s.setINIValue(config, "database", "password", req.DBPassword)
|
||||
// [security]
|
||||
config = s.setINIValue(config, "security", "admin_user", req.AdminUser)
|
||||
config = s.setINIValue(config, "security", "admin_password", req.AdminPassword)
|
||||
// [users]
|
||||
config = s.setINIValue(config, "users", "allow_sign_up", req.AllowSignUp)
|
||||
config = s.setINIValue(config, "users", "auto_assign_org_role", req.AutoAssignOrgRole)
|
||||
// [smtp]
|
||||
config = s.setINIValue(config, "smtp", "enabled", req.SMTPEnabled)
|
||||
config = s.setINIValue(config, "smtp", "host", req.SMTPHost)
|
||||
config = s.setINIValue(config, "smtp", "user", req.SMTPUser)
|
||||
config = s.setINIValue(config, "smtp", "password", req.SMTPPassword)
|
||||
config = s.setINIValue(config, "smtp", "from_address", req.SMTPFromAddress)
|
||||
// [log]
|
||||
config = s.setINIValue(config, "log", "mode", req.LogMode)
|
||||
config = s.setINIValue(config, "log", "level", req.LogLevel)
|
||||
|
||||
if err = io.Write(confPath, config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("grafana"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getINIValue 从 INI 配置中获取指定 section 下的 key 值
|
||||
func (s *App) getINIValue(content string, section string, key string) string {
|
||||
currentSection := ""
|
||||
for line := range strings.SplitSeq(content, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, ";") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
currentSection = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
continue
|
||||
}
|
||||
if currentSection != section {
|
||||
continue
|
||||
}
|
||||
parts := strings.SplitN(trimmed, "=", 2)
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
|
||||
return strings.TrimSpace(parts[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// setINIValue 在 INI 配置中设置指定 section 下的 key 值
|
||||
func (s *App) setINIValue(content string, section string, key string, value string) string {
|
||||
lines := strings.Split(content, "\n")
|
||||
result := make([]string, 0, len(lines))
|
||||
currentSection := ""
|
||||
found := false
|
||||
lastSectionLine := -1 // 目标 section 的最后一行索引
|
||||
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
|
||||
// 检测 section 头
|
||||
if strings.HasPrefix(trimmed, "[") && strings.HasSuffix(trimmed, "]") {
|
||||
// 如果离开目标 section 且未找到 key,在 section 末尾插入
|
||||
if currentSection == section && !found && lastSectionLine >= 0 {
|
||||
found = true
|
||||
if value != "" {
|
||||
// 在 section 末尾插入新行
|
||||
insertIdx := lastSectionLine + 1
|
||||
newLine := key + " = " + value
|
||||
result = append(result[:insertIdx+1], append([]string{newLine}, result[insertIdx+1:]...)...)
|
||||
}
|
||||
}
|
||||
currentSection = strings.TrimSpace(trimmed[1 : len(trimmed)-1])
|
||||
}
|
||||
|
||||
if currentSection == section {
|
||||
lastSectionLine = len(result)
|
||||
}
|
||||
|
||||
// 在目标 section 内匹配 key
|
||||
if currentSection == section && !found {
|
||||
checkLine := trimmed
|
||||
commented := false
|
||||
if strings.HasPrefix(checkLine, ";") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
commented = true
|
||||
} else if strings.HasPrefix(checkLine, "#") {
|
||||
checkLine = strings.TrimSpace(checkLine[1:])
|
||||
commented = true
|
||||
}
|
||||
parts := strings.SplitN(checkLine, "=", 2)
|
||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) == key {
|
||||
found = true
|
||||
if value == "" {
|
||||
// 值为空时注释掉
|
||||
if !commented {
|
||||
result = append(result, ";"+line)
|
||||
} else {
|
||||
result = append(result, line)
|
||||
}
|
||||
} else {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
_ = i
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
result = append(result, line)
|
||||
}
|
||||
|
||||
// 如果在最后一个 section 中未找到 key
|
||||
if currentSection == section && !found {
|
||||
found = true
|
||||
if value != "" {
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
}
|
||||
|
||||
// section 不存在,在文件末尾追加
|
||||
if !found && value != "" {
|
||||
result = append(result, "")
|
||||
result = append(result, "["+section+"]")
|
||||
result = append(result, key+" = "+value)
|
||||
}
|
||||
|
||||
return strings.Join(result, "\n")
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package grafana
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Grafana 配置调整
|
||||
type ConfigTune struct {
|
||||
// [server]
|
||||
HTTPPort string `form:"http_port" json:"http_port"`
|
||||
Domain string `form:"domain" json:"domain"`
|
||||
RootURL string `form:"root_url" json:"root_url"`
|
||||
Protocol string `form:"protocol" json:"protocol"`
|
||||
// [database]
|
||||
DBType string `form:"db_type" json:"db_type"`
|
||||
DBHost string `form:"db_host" json:"db_host"`
|
||||
DBName string `form:"db_name" json:"db_name"`
|
||||
DBUser string `form:"db_user" json:"db_user"`
|
||||
DBPassword string `form:"db_password" json:"db_password"`
|
||||
// [security]
|
||||
AdminUser string `form:"admin_user" json:"admin_user"`
|
||||
AdminPassword string `form:"admin_password" json:"admin_password"`
|
||||
// [users]
|
||||
AllowSignUp string `form:"allow_sign_up" json:"allow_sign_up"`
|
||||
AutoAssignOrgRole string `form:"auto_assign_org_role" json:"auto_assign_org_role"`
|
||||
// [smtp]
|
||||
SMTPEnabled string `form:"smtp_enabled" json:"smtp_enabled"`
|
||||
SMTPHost string `form:"smtp_host" json:"smtp_host"`
|
||||
SMTPUser string `form:"smtp_user" json:"smtp_user"`
|
||||
SMTPPassword string `form:"smtp_password" json:"smtp_password"`
|
||||
SMTPFromAddress string `form:"smtp_from_address" json:"smtp_from_address"`
|
||||
// [log]
|
||||
LogMode string `form:"log_mode" json:"log_mode"`
|
||||
LogLevel string `form:"log_level" json:"log_level"`
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
package prometheus
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/spf13/cast"
|
||||
"go.yaml.in/yaml/v4"
|
||||
"resty.dev/v3"
|
||||
|
||||
"github.com/acepanel/panel/v3/internal/app"
|
||||
"github.com/acepanel/panel/v3/internal/biz"
|
||||
"github.com/acepanel/panel/v3/internal/service"
|
||||
"github.com/acepanel/panel/v3/pkg/config"
|
||||
"github.com/acepanel/panel/v3/pkg/io"
|
||||
"github.com/acepanel/panel/v3/pkg/systemctl"
|
||||
"github.com/acepanel/panel/v3/pkg/types"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
t *gotext.Locale
|
||||
conf *config.Config
|
||||
taskRepo biz.TaskRepo
|
||||
}
|
||||
|
||||
func NewApp(t *gotext.Locale, conf *config.Config, taskRepo biz.TaskRepo) *App {
|
||||
return &App{t: t, conf: conf, taskRepo: taskRepo}
|
||||
}
|
||||
|
||||
func (s *App) Route(r chi.Router) {
|
||||
r.Get("/load", s.Load)
|
||||
r.Get("/config", s.GetConfig)
|
||||
r.Post("/config", s.UpdateConfig)
|
||||
r.Get("/config_tune", s.GetConfigTune)
|
||||
r.Post("/config_tune", s.UpdateConfigTune)
|
||||
// Exporters 管理
|
||||
r.Get("/exporters", s.ExporterList)
|
||||
r.Post("/exporters", s.InstallExporter)
|
||||
r.Delete("/exporters", s.UninstallExporter)
|
||||
r.Post("/exporters/{slug}/start", s.StartExporter)
|
||||
r.Post("/exporters/{slug}/stop", s.StopExporter)
|
||||
r.Post("/exporters/{slug}/restart", s.RestartExporter)
|
||||
r.Get("/exporters/{slug}/config", s.GetExporterConfig)
|
||||
r.Post("/exporters/{slug}/config", s.UpdateExporterConfig)
|
||||
}
|
||||
|
||||
func (s *App) Load(w http.ResponseWriter, r *http.Request) {
|
||||
status, err := systemctl.Status("prometheus")
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, s.t.Get("failed to get prometheus status: %v", err))
|
||||
return
|
||||
}
|
||||
if !status {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
client := resty.New().SetTimeout(10 * time.Second)
|
||||
defer func(client *resty.Client) { _ = client.Close() }(client)
|
||||
resp, err := client.R().Get("http://127.0.0.1:9090/api/v1/status/runtimeinfo")
|
||||
if err != nil || !resp.IsSuccess() {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Data struct {
|
||||
StartTime string `json:"startTime"`
|
||||
GoroutineCount int `json:"goroutineCount"`
|
||||
GOMAXPROCS int `json:"GOMAXPROCS"`
|
||||
StorageRetention string `json:"storageRetention"`
|
||||
ReloadConfigSuccess bool `json:"reloadConfigSuccess"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err = json.Unmarshal(resp.Bytes(), &result); err != nil {
|
||||
service.Success(w, []types.NV{})
|
||||
return
|
||||
}
|
||||
|
||||
data := []types.NV{
|
||||
{Name: s.t.Get("Start Time"), Value: result.Data.StartTime},
|
||||
{Name: s.t.Get("Storage Retention"), Value: result.Data.StorageRetention},
|
||||
{Name: s.t.Get("Goroutine Count"), Value: cast.ToString(result.Data.GoroutineCount)},
|
||||
{Name: "GOMAXPROCS", Value: cast.ToString(result.Data.GOMAXPROCS)},
|
||||
{Name: s.t.Get("Config Reload Success"), Value: cast.ToString(result.Data.ReloadConfigSuccess)},
|
||||
}
|
||||
|
||||
service.Success(w, data)
|
||||
}
|
||||
|
||||
func (s *App) GetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root))
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
func (s *App) UpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[UpdateConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root), req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetConfigTune 获取 Prometheus 全局配置调整参数
|
||||
func (s *App) GetConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
conf, _ := io.Read(fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root))
|
||||
|
||||
var cfg struct {
|
||||
Global struct {
|
||||
ScrapeInterval string `yaml:"scrape_interval"`
|
||||
EvaluationInterval string `yaml:"evaluation_interval"`
|
||||
ScrapeTimeout string `yaml:"scrape_timeout"`
|
||||
} `yaml:"global"`
|
||||
}
|
||||
_ = yaml.Unmarshal([]byte(conf), &cfg)
|
||||
|
||||
tune := ConfigTune{
|
||||
ScrapeInterval: cfg.Global.ScrapeInterval,
|
||||
EvaluationInterval: cfg.Global.EvaluationInterval,
|
||||
ScrapeTimeout: cfg.Global.ScrapeTimeout,
|
||||
}
|
||||
|
||||
service.Success(w, tune)
|
||||
}
|
||||
|
||||
// UpdateConfigTune 更新 Prometheus 全局配置调整参数
|
||||
func (s *App) UpdateConfigTune(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ConfigTune](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
confPath := fmt.Sprintf("%s/server/prometheus/prometheus.yml", app.Root)
|
||||
raw, _ := io.Read(confPath)
|
||||
|
||||
var cfg map[string]any
|
||||
if err = yaml.Unmarshal([]byte(raw), &cfg); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
global, ok := cfg["global"].(map[string]any)
|
||||
if !ok {
|
||||
global = make(map[string]any)
|
||||
}
|
||||
|
||||
if req.ScrapeInterval != "" {
|
||||
global["scrape_interval"] = req.ScrapeInterval
|
||||
}
|
||||
if req.EvaluationInterval != "" {
|
||||
global["evaluation_interval"] = req.EvaluationInterval
|
||||
}
|
||||
if req.ScrapeTimeout != "" {
|
||||
global["scrape_timeout"] = req.ScrapeTimeout
|
||||
}
|
||||
cfg["global"] = global
|
||||
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, string(data), 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus"); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// ExporterList 获取 Exporter 列表及状态
|
||||
func (s *App) ExporterList(w http.ResponseWriter, r *http.Request) {
|
||||
exporters := s.getExporters()
|
||||
for i := range exporters {
|
||||
exporters[i].Installed = io.Exists(fmt.Sprintf("%s/server/prometheus/exporters/%s", app.Root, exporters[i].Slug))
|
||||
if exporters[i].Installed {
|
||||
running, _ := systemctl.Status("prometheus-" + exporters[i].Slug)
|
||||
exporters[i].Running = running
|
||||
}
|
||||
}
|
||||
|
||||
service.Success(w, exporters)
|
||||
}
|
||||
|
||||
// InstallExporter 安装 Exporter(异步任务)
|
||||
func (s *App) InstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExporterSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExporter(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/prometheus/exporters/exporter.sh' | bash -s -- 'install' '%s'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = s.t.Get("Install Prometheus exporter %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// UninstallExporter 卸载 Exporter(异步任务)
|
||||
func (s *App) UninstallExporter(w http.ResponseWriter, r *http.Request) {
|
||||
req, err := service.Bind[ExporterSlug](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if !s.checkExporter(req.Slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", req.Slug))
|
||||
return
|
||||
}
|
||||
|
||||
cmd := fmt.Sprintf(`curl -sSLm 10 --retry 3 'https://%s/prometheus/exporters/exporter.sh' | bash -s -- 'uninstall' '%s'`, s.conf.App.DownloadEndpoint, url.PathEscape(req.Slug))
|
||||
|
||||
task := new(biz.Task)
|
||||
task.Name = s.t.Get("Uninstall Prometheus exporter %s", req.Slug)
|
||||
task.Status = biz.TaskStatusWaiting
|
||||
task.Shell = cmd
|
||||
if err = s.taskRepo.Push(task); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// StartExporter 启动 Exporter
|
||||
func (s *App) StartExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Start("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// StopExporter 停止 Exporter
|
||||
func (s *App) StopExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Stop("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// RestartExporter 重启 Exporter
|
||||
func (s *App) RestartExporter(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
if err := systemctl.Restart("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// GetExporterConfig 获取 Exporter 配置
|
||||
func (s *App) GetExporterConfig(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
confPath := s.getExporterConfigPath(slug)
|
||||
if confPath == "" {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s has no configuration file", slug))
|
||||
return
|
||||
}
|
||||
|
||||
conf, _ := io.Read(confPath)
|
||||
|
||||
service.Success(w, conf)
|
||||
}
|
||||
|
||||
// UpdateExporterConfig 更新 Exporter 配置
|
||||
func (s *App) UpdateExporterConfig(w http.ResponseWriter, r *http.Request) {
|
||||
slug := chi.URLParam(r, "slug")
|
||||
if !s.checkExporter(slug) {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s does not exist", slug))
|
||||
return
|
||||
}
|
||||
|
||||
confPath := s.getExporterConfigPath(slug)
|
||||
if confPath == "" {
|
||||
service.Error(w, http.StatusUnprocessableEntity, s.t.Get("exporter %s has no configuration file", slug))
|
||||
return
|
||||
}
|
||||
|
||||
req, err := service.Bind[ExporterConfig](r)
|
||||
if err != nil {
|
||||
service.Error(w, http.StatusUnprocessableEntity, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = io.Write(confPath, req.Config, 0644); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = systemctl.Restart("prometheus-" + slug); err != nil {
|
||||
service.Error(w, http.StatusInternalServerError, "%v", err)
|
||||
return
|
||||
}
|
||||
|
||||
service.Success(w, nil)
|
||||
}
|
||||
|
||||
// getExporters 返回所有 exporter 定义
|
||||
func (s *App) getExporters() []Exporter {
|
||||
return []Exporter{
|
||||
{Name: "Node Exporter", Slug: "node_exporter", Description: s.t.Get("Hardware and OS metrics")},
|
||||
{Name: "MySQL Exporter", Slug: "mysqld_exporter", Description: s.t.Get("MySQL database metrics"), HasConfig: true},
|
||||
{Name: "PostgreSQL Exporter", Slug: "postgres_exporter", Description: s.t.Get("PostgreSQL database metrics"), HasConfig: true},
|
||||
{Name: "Redis Exporter", Slug: "redis_exporter", Description: s.t.Get("Redis metrics"), HasConfig: true},
|
||||
{Name: "Memcached Exporter", Slug: "memcached_exporter", Description: s.t.Get("Memcached metrics")},
|
||||
{Name: "Nginx Exporter", Slug: "nginx_exporter", Description: s.t.Get("Nginx metrics")},
|
||||
}
|
||||
}
|
||||
|
||||
// getExporterConfigPath 获取 exporter 配置文件路径
|
||||
func (s *App) getExporterConfigPath(slug string) string {
|
||||
base := fmt.Sprintf("%s/server/prometheus/exporters/%s", app.Root, slug)
|
||||
switch slug {
|
||||
case "redis_exporter", "postgres_exporter":
|
||||
return base + "/env"
|
||||
case "mysqld_exporter":
|
||||
return base + "/.my.cnf"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// checkExporter 检查 slug 是否有效
|
||||
func (s *App) checkExporter(slug string) bool {
|
||||
for _, e := range s.getExporters() {
|
||||
if e.Slug == slug {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package prometheus
|
||||
|
||||
type UpdateConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
|
||||
// ConfigTune Prometheus 全局配置调整
|
||||
type ConfigTune struct {
|
||||
ScrapeInterval string `form:"scrape_interval" json:"scrape_interval"`
|
||||
EvaluationInterval string `form:"evaluation_interval" json:"evaluation_interval"`
|
||||
ScrapeTimeout string `form:"scrape_timeout" json:"scrape_timeout"`
|
||||
}
|
||||
|
||||
// Exporter Prometheus Exporter 信息
|
||||
type Exporter struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Installed bool `json:"installed"`
|
||||
Running bool `json:"running"`
|
||||
HasConfig bool `json:"has_config"`
|
||||
}
|
||||
|
||||
// ExporterSlug Exporter 操作请求
|
||||
type ExporterSlug struct {
|
||||
Slug string `form:"slug" json:"slug" validate:"required"`
|
||||
}
|
||||
|
||||
// ExporterConfig Exporter 配置更新请求
|
||||
type ExporterConfig struct {
|
||||
Config string `form:"config" json:"config" validate:"required"`
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/fail2ban"
|
||||
"github.com/acepanel/panel/v3/internal/apps/frp"
|
||||
"github.com/acepanel/panel/v3/internal/apps/gitea"
|
||||
"github.com/acepanel/panel/v3/internal/apps/grafana"
|
||||
"github.com/acepanel/panel/v3/internal/apps/mariadb"
|
||||
"github.com/acepanel/panel/v3/internal/apps/memcached"
|
||||
"github.com/acepanel/panel/v3/internal/apps/minio"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"github.com/acepanel/panel/v3/internal/apps/phpmyadmin"
|
||||
"github.com/acepanel/panel/v3/internal/apps/podman"
|
||||
"github.com/acepanel/panel/v3/internal/apps/postgresql"
|
||||
"github.com/acepanel/panel/v3/internal/apps/prometheus"
|
||||
"github.com/acepanel/panel/v3/internal/apps/pureftpd"
|
||||
"github.com/acepanel/panel/v3/internal/apps/redis"
|
||||
"github.com/acepanel/panel/v3/internal/apps/rsync"
|
||||
@@ -32,6 +34,7 @@ func NewLoader(
|
||||
fail2ban *fail2ban.App,
|
||||
frp *frp.App,
|
||||
gitea *gitea.App,
|
||||
grafana *grafana.App,
|
||||
mariadb *mariadb.App,
|
||||
memcached *memcached.App,
|
||||
minio *minio.App,
|
||||
@@ -42,6 +45,7 @@ func NewLoader(
|
||||
phpmyadmin *phpmyadmin.App,
|
||||
podman *podman.App,
|
||||
postgresql *postgresql.App,
|
||||
prometheus *prometheus.App,
|
||||
pureftpd *pureftpd.App,
|
||||
redis *redis.App,
|
||||
rsync *rsync.App,
|
||||
@@ -49,6 +53,6 @@ func NewLoader(
|
||||
supervisor *supervisor.App,
|
||||
) *apploader.Loader {
|
||||
loader := new(apploader.Loader)
|
||||
loader.Add(apache, codeserver, docker, fail2ban, frp, gitea, mariadb, memcached, minio, mysql, nginx, openresty, percona, phpmyadmin, podman, postgresql, pureftpd, redis, rsync, s3fs, supervisor)
|
||||
loader.Add(apache, codeserver, docker, fail2ban, frp, gitea, grafana, mariadb, memcached, minio, mysql, nginx, openresty, percona, phpmyadmin, podman, postgresql, prometheus, pureftpd, redis, rsync, s3fs, supervisor)
|
||||
return loader
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 负载状态
|
||||
load: (): any => http.Get('/apps/grafana/load'),
|
||||
// 获取配置
|
||||
config: (): any => http.Get('/apps/grafana/config'),
|
||||
// 保存配置
|
||||
saveConfig: (config: string): any => http.Post('/apps/grafana/config', { config }),
|
||||
// 获取配置调整参数
|
||||
configTune: (): any => http.Get('/apps/grafana/config_tune'),
|
||||
// 保存配置调整参数
|
||||
saveConfigTune: (data: any): any => http.Post('/apps/grafana/config_tune', data)
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { http } from '@/utils'
|
||||
|
||||
export default {
|
||||
// 负载状态
|
||||
load: (): any => http.Get('/apps/prometheus/load'),
|
||||
// 获取配置
|
||||
config: (): any => http.Get('/apps/prometheus/config'),
|
||||
// 保存配置
|
||||
saveConfig: (config: string): any => http.Post('/apps/prometheus/config', { config }),
|
||||
// 获取配置调整参数
|
||||
configTune: (): any => http.Get('/apps/prometheus/config_tune'),
|
||||
// 保存配置调整参数
|
||||
saveConfigTune: (data: any): any => http.Post('/apps/prometheus/config_tune', data),
|
||||
// Exporters 管理
|
||||
exporters: (): any => http.Get('/apps/prometheus/exporters'),
|
||||
installExporter: (slug: string): any => http.Post('/apps/prometheus/exporters', { slug }),
|
||||
uninstallExporter: (slug: string): any => http.Delete('/apps/prometheus/exporters', { slug }),
|
||||
startExporter: (slug: string): any => http.Post(`/apps/prometheus/exporters/${slug}/start`),
|
||||
stopExporter: (slug: string): any => http.Post(`/apps/prometheus/exporters/${slug}/stop`),
|
||||
restartExporter: (slug: string): any => http.Post(`/apps/prometheus/exporters/${slug}/restart`),
|
||||
exporterConfig: (slug: string): any => http.Get(`/apps/prometheus/exporters/${slug}/config`),
|
||||
saveExporterConfig: (slug: string, config: string): any =>
|
||||
http.Post(`/apps/prometheus/exporters/${slug}/config`, { config })
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'grafana-config-tune'
|
||||
})
|
||||
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import grafana from '@/api/apps/grafana'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('server')
|
||||
|
||||
// [server]
|
||||
const httpPort = ref<number | null>(null)
|
||||
const domain = ref('')
|
||||
const rootUrl = ref('')
|
||||
const protocol = ref('')
|
||||
|
||||
// [database]
|
||||
const dbType = ref('')
|
||||
const dbHost = ref('')
|
||||
const dbName = ref('')
|
||||
const dbUser = ref('')
|
||||
const dbPassword = ref('')
|
||||
|
||||
// [security]
|
||||
const adminUser = ref('')
|
||||
const adminPassword = ref('')
|
||||
|
||||
// [users]
|
||||
const allowSignUp = ref('')
|
||||
const autoAssignOrgRole = ref('')
|
||||
|
||||
// [smtp]
|
||||
const smtpEnabled = ref('')
|
||||
const smtpHost = ref('')
|
||||
const smtpUser = ref('')
|
||||
const smtpPassword = ref('')
|
||||
const smtpFromAddress = ref('')
|
||||
|
||||
// [log]
|
||||
const logMode = ref('')
|
||||
const logLevel = ref('')
|
||||
|
||||
const saveLoading = ref(false)
|
||||
|
||||
const protocolOptions = [
|
||||
{ label: 'http', value: 'http' },
|
||||
{ label: 'https', value: 'https' }
|
||||
]
|
||||
|
||||
const dbTypeOptions = [
|
||||
{ label: 'sqlite3', value: 'sqlite3' },
|
||||
{ label: 'mysql', value: 'mysql' },
|
||||
{ label: 'postgres', value: 'postgres' }
|
||||
]
|
||||
|
||||
const boolOptions = [
|
||||
{ label: 'true', value: 'true' },
|
||||
{ label: 'false', value: 'false' }
|
||||
]
|
||||
|
||||
const orgRoleOptions = [
|
||||
{ label: 'Viewer', value: 'Viewer' },
|
||||
{ label: 'Editor', value: 'Editor' },
|
||||
{ label: 'Admin', value: 'Admin' }
|
||||
]
|
||||
|
||||
const logModeOptions = [
|
||||
{ label: 'console', value: 'console' },
|
||||
{ label: 'file', value: 'file' },
|
||||
{ label: 'console file', value: 'console file' }
|
||||
]
|
||||
|
||||
const logLevelOptions = [
|
||||
{ label: 'debug', value: 'debug' },
|
||||
{ label: 'info', value: 'info' },
|
||||
{ label: 'warn', value: 'warn' },
|
||||
{ label: 'error', value: 'error' },
|
||||
{ label: 'critical', value: 'critical' }
|
||||
]
|
||||
|
||||
useRequest(grafana.configTune()).onSuccess(({ data }: any) => {
|
||||
// [server]
|
||||
httpPort.value = Number(data.http_port) || null
|
||||
domain.value = data.domain ?? ''
|
||||
rootUrl.value = data.root_url ?? ''
|
||||
protocol.value = data.protocol || null
|
||||
// [database]
|
||||
dbType.value = data.db_type || null
|
||||
dbHost.value = data.db_host ?? ''
|
||||
dbName.value = data.db_name ?? ''
|
||||
dbUser.value = data.db_user ?? ''
|
||||
dbPassword.value = data.db_password ?? ''
|
||||
// [security]
|
||||
adminUser.value = data.admin_user ?? ''
|
||||
adminPassword.value = data.admin_password ?? ''
|
||||
// [users]
|
||||
allowSignUp.value = data.allow_sign_up || null
|
||||
autoAssignOrgRole.value = data.auto_assign_org_role || null
|
||||
// [smtp]
|
||||
smtpEnabled.value = data.smtp_enabled || null
|
||||
smtpHost.value = data.smtp_host ?? ''
|
||||
smtpUser.value = data.smtp_user ?? ''
|
||||
smtpPassword.value = data.smtp_password ?? ''
|
||||
smtpFromAddress.value = data.smtp_from_address ?? ''
|
||||
// [log]
|
||||
logMode.value = data.log_mode || null
|
||||
logLevel.value = data.log_level || null
|
||||
})
|
||||
|
||||
const getConfigData = () => ({
|
||||
http_port: String(httpPort.value ?? ''),
|
||||
domain: domain.value,
|
||||
root_url: rootUrl.value,
|
||||
protocol: protocol.value ?? '',
|
||||
db_type: dbType.value ?? '',
|
||||
db_host: dbHost.value,
|
||||
db_name: dbName.value,
|
||||
db_user: dbUser.value,
|
||||
db_password: dbPassword.value,
|
||||
admin_user: adminUser.value,
|
||||
admin_password: adminPassword.value,
|
||||
allow_sign_up: allowSignUp.value ?? '',
|
||||
auto_assign_org_role: autoAssignOrgRole.value ?? '',
|
||||
smtp_enabled: smtpEnabled.value ?? '',
|
||||
smtp_host: smtpHost.value,
|
||||
smtp_user: smtpUser.value,
|
||||
smtp_password: smtpPassword.value,
|
||||
smtp_from_address: smtpFromAddress.value,
|
||||
log_mode: logMode.value ?? '',
|
||||
log_level: logLevel.value ?? ''
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
saveLoading.value = true
|
||||
useRequest(grafana.saveConfigTune(getConfigData()))
|
||||
.onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
.onComplete(() => {
|
||||
saveLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-tabs v-model:value="currentTab" type="line" placement="left" animated>
|
||||
<n-tab-pane name="server" :tab="$gettext('Server')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Grafana server settings.') }}
|
||||
</n-alert>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('HTTP Port (http_port)')">
|
||||
<n-input-number class="w-full" v-model:value="httpPort" :placeholder="$gettext('e.g. 3000')" :min="1" :max="65535" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Domain (domain)')">
|
||||
<n-input v-model:value="domain" :placeholder="$gettext('e.g. localhost')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Root URL (root_url)')">
|
||||
<n-input v-model:value="rootUrl" :placeholder="$gettext('e.g. %(protocol)s://%(domain)s:%(http_port)s/')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Protocol (protocol)')">
|
||||
<n-select v-model:value="protocol" :options="protocolOptions" clearable />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveLoading" :disabled="saveLoading" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="database" :tab="$gettext('Database')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Grafana database settings.') }}
|
||||
</n-alert>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Type (type)')">
|
||||
<n-select v-model:value="dbType" :options="dbTypeOptions" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Host (host)')">
|
||||
<n-input v-model:value="dbHost" :placeholder="$gettext('e.g. 127.0.0.1:3306')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Name (name)')">
|
||||
<n-input v-model:value="dbName" :placeholder="$gettext('e.g. grafana')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('User (user)')">
|
||||
<n-input v-model:value="dbUser" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Password (password)')">
|
||||
<n-input v-model:value="dbPassword" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveLoading" :disabled="saveLoading" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="security" :tab="$gettext('Security')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Grafana security settings.') }}
|
||||
</n-alert>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Admin User (admin_user)')">
|
||||
<n-input v-model:value="adminUser" :placeholder="$gettext('e.g. admin')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Admin Password (admin_password)')">
|
||||
<n-input v-model:value="adminPassword" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveLoading" :disabled="saveLoading" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="users" :tab="$gettext('Users')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Grafana user management settings.') }}
|
||||
</n-alert>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Allow Sign Up (allow_sign_up)')">
|
||||
<n-select v-model:value="allowSignUp" :options="boolOptions" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Auto Assign Org Role (auto_assign_org_role)')">
|
||||
<n-select v-model:value="autoAssignOrgRole" :options="orgRoleOptions" clearable />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveLoading" :disabled="saveLoading" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="smtp" tab="SMTP">
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Grafana SMTP email settings.') }}
|
||||
</n-alert>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Enabled (enabled)')">
|
||||
<n-select v-model:value="smtpEnabled" :options="boolOptions" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Host (host)')">
|
||||
<n-input v-model:value="smtpHost" :placeholder="$gettext('e.g. smtp.example.com:587')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('User (user)')">
|
||||
<n-input v-model:value="smtpUser" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Password (password)')">
|
||||
<n-input v-model:value="smtpPassword" type="password" show-password-on="click" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('From Address (from_address)')">
|
||||
<n-input v-model:value="smtpFromAddress" :placeholder="$gettext('e.g. admin@example.com')" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveLoading" :disabled="saveLoading" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="log" :tab="$gettext('Log')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Grafana log settings.') }}
|
||||
</n-alert>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Mode (mode)')">
|
||||
<n-select v-model:value="logMode" :options="logModeOptions" clearable />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Level (level)')">
|
||||
<n-select v-model:value="logLevel" :options="logLevelOptions" clearable />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveLoading" :disabled="saveLoading" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</template>
|
||||
@@ -0,0 +1,99 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'apps-grafana-index'
|
||||
})
|
||||
|
||||
import { NButton, NDataTable } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import grafana from '@/api/apps/grafana'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
import GrafanaConfigTuneView from './GrafanaConfigTuneView.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
const saveConfigLoading = ref(false)
|
||||
|
||||
const { data: config, send: refreshConfig } = useRequest(grafana.config, {
|
||||
initialData: ''
|
||||
})
|
||||
|
||||
watch(currentTab, (val) => {
|
||||
if (val === 'config') {
|
||||
refreshConfig()
|
||||
}
|
||||
})
|
||||
const { data: load } = useRequest(grafana.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
saveConfigLoading.value = true
|
||||
useRequest(grafana.saveConfig(config.value))
|
||||
.onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
.onComplete(() => {
|
||||
saveConfigLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<service-status service="grafana" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Main Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the Grafana main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveConfigLoading" :disabled="saveConfigLoading" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config-tune" :tab="$gettext('Parameter Tuning')">
|
||||
<grafana-config-tune-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<realtime-log service="grafana" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { RouteType } from '~/types/router'
|
||||
|
||||
const Layout = () => import('@/layout/IndexView.vue')
|
||||
|
||||
export default {
|
||||
name: 'grafana',
|
||||
path: '/apps/grafana',
|
||||
component: Layout,
|
||||
isHidden: true,
|
||||
children: [
|
||||
{
|
||||
name: 'apps-grafana-index',
|
||||
path: '',
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: 'Grafana',
|
||||
role: ['admin'],
|
||||
requireAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} as RouteType
|
||||
@@ -0,0 +1,103 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'apps-prometheus-index'
|
||||
})
|
||||
|
||||
import { NButton, NDataTable } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import prometheus from '@/api/apps/prometheus'
|
||||
import ServiceStatus from '@/components/common/ServiceStatus.vue'
|
||||
import PrometheusConfigTuneView from './PrometheusConfigTuneView.vue'
|
||||
import PrometheusExportersView from './PrometheusExportersView.vue'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
const currentTab = ref('status')
|
||||
const saveConfigLoading = ref(false)
|
||||
|
||||
const { data: config, send: refreshConfig } = useRequest(prometheus.config, {
|
||||
initialData: ''
|
||||
})
|
||||
|
||||
watch(currentTab, (val) => {
|
||||
if (val === 'config') {
|
||||
refreshConfig()
|
||||
}
|
||||
})
|
||||
const { data: load } = useRequest(prometheus.load, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const loadColumns: any = [
|
||||
{
|
||||
title: $gettext('Property'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
resizable: true,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Current Value'),
|
||||
key: 'value',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
}
|
||||
]
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
saveConfigLoading.value = true
|
||||
useRequest(prometheus.saveConfig(config.value))
|
||||
.onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
.onComplete(() => {
|
||||
saveConfigLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<common-page show-footer>
|
||||
<n-tabs v-model:value="currentTab" type="line" animated>
|
||||
<n-tab-pane name="status" :tab="$gettext('Running Status')">
|
||||
<service-status service="prometheus" />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config" :tab="$gettext('Main Configuration')">
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{
|
||||
$gettext(
|
||||
'This modifies the Prometheus main configuration file. If you do not understand the meaning of each parameter, please do not modify it randomly!'
|
||||
)
|
||||
}}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="config" height="60vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveConfigLoading" :disabled="saveConfigLoading" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="config-tune" :tab="$gettext('Parameter Tuning')">
|
||||
<prometheus-config-tune-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="exporters" :tab="$gettext('Exporters')">
|
||||
<prometheus-exporters-view />
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="load" :tab="$gettext('Load Status')">
|
||||
<n-data-table
|
||||
striped
|
||||
remote
|
||||
:scroll-x="1000"
|
||||
:loading="false"
|
||||
:columns="loadColumns"
|
||||
:data="load"
|
||||
/>
|
||||
</n-tab-pane>
|
||||
<n-tab-pane name="run-log" :tab="$gettext('Runtime Logs')">
|
||||
<realtime-log service="prometheus" />
|
||||
</n-tab-pane>
|
||||
</n-tabs>
|
||||
</common-page>
|
||||
</template>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'prometheus-config-tune'
|
||||
})
|
||||
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import prometheus from '@/api/apps/prometheus'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const scrapeInterval = ref('')
|
||||
const evaluationInterval = ref('')
|
||||
const scrapeTimeout = ref('')
|
||||
|
||||
const saveLoading = ref(false)
|
||||
|
||||
useRequest(prometheus.configTune()).onSuccess(({ data }: any) => {
|
||||
scrapeInterval.value = data.scrape_interval ?? ''
|
||||
evaluationInterval.value = data.evaluation_interval ?? ''
|
||||
scrapeTimeout.value = data.scrape_timeout ?? ''
|
||||
})
|
||||
|
||||
const getConfigData = () => ({
|
||||
scrape_interval: scrapeInterval.value,
|
||||
evaluation_interval: evaluationInterval.value,
|
||||
scrape_timeout: scrapeTimeout.value
|
||||
})
|
||||
|
||||
const handleSave = () => {
|
||||
saveLoading.value = true
|
||||
useRequest(prometheus.saveConfigTune(getConfigData()))
|
||||
.onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
})
|
||||
.onComplete(() => {
|
||||
saveLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Prometheus global scrape and evaluation settings.') }}
|
||||
</n-alert>
|
||||
<n-form>
|
||||
<n-form-item :label="$gettext('Scrape Interval (scrape_interval)')">
|
||||
<n-input v-model:value="scrapeInterval" :placeholder="$gettext('e.g. 15s')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Evaluation Interval (evaluation_interval)')">
|
||||
<n-input v-model:value="evaluationInterval" :placeholder="$gettext('e.g. 15s')" />
|
||||
</n-form-item>
|
||||
<n-form-item :label="$gettext('Scrape Timeout (scrape_timeout)')">
|
||||
<n-input v-model:value="scrapeTimeout" :placeholder="$gettext('e.g. 10s')" />
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveLoading" :disabled="saveLoading" @click="handleSave">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
@@ -0,0 +1,182 @@
|
||||
<script setup lang="ts">
|
||||
defineOptions({
|
||||
name: 'prometheus-exporters'
|
||||
})
|
||||
|
||||
import { NButton, NDataTable, NPopconfirm, NSpace, NTag, NModal } from 'naive-ui'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
|
||||
import prometheus from '@/api/apps/prometheus'
|
||||
|
||||
const { $gettext } = useGettext()
|
||||
|
||||
const showConfigModal = ref(false)
|
||||
const configSlug = ref('')
|
||||
const configContent = ref('')
|
||||
const saveConfigLoading = ref(false)
|
||||
|
||||
const { data: exporters, send: refreshExporters } = useRequest(prometheus.exporters, {
|
||||
initialData: []
|
||||
})
|
||||
|
||||
const columns: any = [
|
||||
{
|
||||
title: $gettext('Name'),
|
||||
key: 'name',
|
||||
minWidth: 200,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Description'),
|
||||
key: 'description',
|
||||
minWidth: 250,
|
||||
ellipsis: { tooltip: true }
|
||||
},
|
||||
{
|
||||
title: $gettext('Status'),
|
||||
key: 'status',
|
||||
width: 150,
|
||||
render(row: any) {
|
||||
if (!row.installed) {
|
||||
return h(NTag, { type: 'default', size: 'small' }, { default: () => $gettext('Not Installed') })
|
||||
}
|
||||
return row.running
|
||||
? h(NTag, { type: 'success', size: 'small' }, { default: () => $gettext('Running') })
|
||||
: h(NTag, { type: 'warning', size: 'small' }, { default: () => $gettext('Stopped') })
|
||||
}
|
||||
},
|
||||
{
|
||||
title: $gettext('Actions'),
|
||||
key: 'actions',
|
||||
width: 360,
|
||||
render(row: any) {
|
||||
const buttons: any[] = []
|
||||
|
||||
if (!row.installed) {
|
||||
buttons.push(
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => handleInstall(row.slug) },
|
||||
{
|
||||
default: () => $gettext('Are you sure you want to install %{ name }?', { name: row.name }),
|
||||
trigger: () => h(NButton, { size: 'small', type: 'info' }, { default: () => $gettext('Install') })
|
||||
}
|
||||
)
|
||||
)
|
||||
} else {
|
||||
if (!row.running) {
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', type: 'success', onClick: () => handleStart(row.slug) }, { default: () => $gettext('Start') })
|
||||
)
|
||||
} else {
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', type: 'warning', onClick: () => handleStop(row.slug) }, { default: () => $gettext('Stop') })
|
||||
)
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', onClick: () => handleRestart(row.slug) }, { default: () => $gettext('Restart') })
|
||||
)
|
||||
}
|
||||
if (row.has_config) {
|
||||
buttons.push(
|
||||
h(NButton, { size: 'small', onClick: () => handleOpenConfig(row.slug) }, { default: () => $gettext('Config') })
|
||||
)
|
||||
}
|
||||
buttons.push(
|
||||
h(
|
||||
NPopconfirm,
|
||||
{ onPositiveClick: () => handleUninstall(row.slug) },
|
||||
{
|
||||
default: () => $gettext('Are you sure you want to uninstall %{ name }?', { name: row.name }),
|
||||
trigger: () => h(NButton, { size: 'small', type: 'error' }, { default: () => $gettext('Delete') })
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return h(NSpace, { size: 'small' }, { default: () => buttons })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
const handleInstall = (slug: string) => {
|
||||
useRequest(prometheus.installExporter(slug)).onSuccess(() => {
|
||||
window.$message.success($gettext('Task submitted, please check progress in background tasks'))
|
||||
refreshExporters()
|
||||
})
|
||||
}
|
||||
|
||||
const handleUninstall = (slug: string) => {
|
||||
useRequest(prometheus.uninstallExporter(slug)).onSuccess(() => {
|
||||
window.$message.success($gettext('Task submitted, please check progress in background tasks'))
|
||||
refreshExporters()
|
||||
})
|
||||
}
|
||||
|
||||
const handleStart = (slug: string) => {
|
||||
useRequest(prometheus.startExporter(slug)).onSuccess(() => {
|
||||
window.$message.success($gettext('Started successfully'))
|
||||
refreshExporters()
|
||||
})
|
||||
}
|
||||
|
||||
const handleStop = (slug: string) => {
|
||||
useRequest(prometheus.stopExporter(slug)).onSuccess(() => {
|
||||
window.$message.success($gettext('Stopped successfully'))
|
||||
refreshExporters()
|
||||
})
|
||||
}
|
||||
|
||||
const handleRestart = (slug: string) => {
|
||||
useRequest(prometheus.restartExporter(slug)).onSuccess(() => {
|
||||
window.$message.success($gettext('Restarted successfully'))
|
||||
refreshExporters()
|
||||
})
|
||||
}
|
||||
|
||||
const handleOpenConfig = (slug: string) => {
|
||||
configSlug.value = slug
|
||||
useRequest(prometheus.exporterConfig(slug)).onSuccess(({ data }: any) => {
|
||||
configContent.value = data
|
||||
showConfigModal.value = true
|
||||
})
|
||||
}
|
||||
|
||||
const handleSaveConfig = () => {
|
||||
saveConfigLoading.value = true
|
||||
useRequest(prometheus.saveExporterConfig(configSlug.value, configContent.value))
|
||||
.onSuccess(() => {
|
||||
window.$message.success($gettext('Saved successfully'))
|
||||
showConfigModal.value = false
|
||||
})
|
||||
.onComplete(() => {
|
||||
saveConfigLoading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n-flex vertical>
|
||||
<n-alert type="info">
|
||||
{{ $gettext('Manage Prometheus exporters. Exporters collect metrics from various services.') }}
|
||||
</n-alert>
|
||||
<n-data-table striped :columns="columns" :data="exporters" :scroll-x="960" />
|
||||
<n-modal
|
||||
v-model:show="showConfigModal"
|
||||
preset="card"
|
||||
:title="$gettext('Exporter Configuration') + ' - ' + configSlug"
|
||||
style="width: 800px"
|
||||
>
|
||||
<n-flex vertical>
|
||||
<n-alert type="warning">
|
||||
{{ $gettext('Modifying the exporter configuration will restart the exporter service.') }}
|
||||
</n-alert>
|
||||
<common-editor v-model:value="configContent" height="40vh" />
|
||||
<n-flex>
|
||||
<n-button type="primary" :loading="saveConfigLoading" :disabled="saveConfigLoading" @click="handleSaveConfig">
|
||||
{{ $gettext('Save') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-modal>
|
||||
</n-flex>
|
||||
</template>
|
||||
@@ -0,0 +1,22 @@
|
||||
import type { RouteType } from '~/types/router'
|
||||
|
||||
const Layout = () => import('@/layout/IndexView.vue')
|
||||
|
||||
export default {
|
||||
name: 'prometheus',
|
||||
path: '/apps/prometheus',
|
||||
component: Layout,
|
||||
isHidden: true,
|
||||
children: [
|
||||
{
|
||||
name: 'apps-prometheus-index',
|
||||
path: '',
|
||||
component: () => import('./IndexView.vue'),
|
||||
meta: {
|
||||
title: 'Prometheus',
|
||||
role: ['admin'],
|
||||
requireAuth: true
|
||||
}
|
||||
}
|
||||
]
|
||||
} as RouteType
|
||||
Reference in New Issue
Block a user