feat: 添加prometheus和grafana支持 (#1508)

* feat: 阶段性提交

* feat: 完成开发

* feat: 阶段性提交

* feat: 阶段性提交
This commit is contained in:
耗子
2026-03-24 22:45:22 +08:00
committed by GitHub
parent 0870b100cc
commit 0ae7327105
18 changed files with 1608 additions and 3 deletions
+5 -1
View File
@@ -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
View File
@@ -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
}
+4
View File
@@ -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=
+4
View File
@@ -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,
+298
View File
@@ -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")
}
+35
View File
@@ -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"`
}
+396
View File
@@ -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
}
+32
View File
@@ -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"`
}
+5 -1
View File
@@ -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
}
+14
View File
@@ -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)
}
+24
View File
@@ -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>
+99
View File
@@ -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>
+22
View File
@@ -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
+103
View File
@@ -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>
+22
View File
@@ -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