Compare commits

...
8 Commits
13 changed files with 196 additions and 147 deletions
+5 -3
View File
@@ -11,6 +11,8 @@
<p>Supports <b>monitoring</b> system status, HTTP (SSL certificate change, upcoming expiration, expired), TCP, Ping and supports <b>push alerts</b>, run scheduled tasks and <b>web terminal</b>.</p> <p>Supports <b>monitoring</b> system status, HTTP (SSL certificate change, upcoming expiration, expired), TCP, Ping and supports <b>push alerts</b>, run scheduled tasks and <b>web terminal</b>.</p>
</div> </div>
\>> Telegram Channel: [哪吒监控(中文通知频道)](https://t.me/nezhanews)
\>> Telegram Group: [Nezha Monitoring Global (English Only)](https://t.me/nezhamonitoring_global), [哪吒监控(中文群组)](https://t.me/nezhamonitoring) \>> Telegram Group: [Nezha Monitoring Global (English Only)](https://t.me/nezhamonitoring_global), [哪吒监控(中文群组)](https://t.me/nezhamonitoring)
\>> [Use Cases | 我们的用户](https://www.google.com/search?q=%22powered+by+Nezha+Monitoring%22+OR+%22powered+by+%E5%93%AA%E5%90%92%E7%9B%91%E6%8E%A7%22) (Google) \>> [Use Cases | 我们的用户](https://www.google.com/search?q=%22powered+by+Nezha+Monitoring%22+OR+%22powered+by+%E5%93%AA%E5%90%92%E7%9B%91%E6%8E%A7%22) (Google)
@@ -42,13 +44,13 @@ You can change the dashboard language in the settings page (`/setting`) after th
<a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a> <a href="https://github.com/AkkiaS7" title="Akkia"><img src="https://avatars.githubusercontent.com/u/68485070?v=4" width="50;" alt="Akkia"/></a>
<a href="https://github.com/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?v=4" width="50;" alt="卖女孩的小火柴"/></a> <a href="https://github.com/Erope" title="卖女孩的小火柴"><img src="https://avatars.githubusercontent.com/u/44471469?v=4" width="50;" alt="卖女孩的小火柴"/></a>
<a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?v=4" width="50;" alt="黑歌"/></a> <a href="https://github.com/dysf888" title="黑歌"><img src="https://avatars.githubusercontent.com/u/47450409?v=4" width="50;" alt="黑歌"/></a>
<a href="https://github.com/xykt" title="xykt"><img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/></a>
<a href="https://github.com/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a> <a href="https://github.com/uubulb" title="UUBulb"><img src="https://avatars.githubusercontent.com/u/35923940?v=4" width="50;" alt="UUBulb"/></a>
<a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></a> <a href="https://github.com/xykt" title="xykt"><img src="https://avatars.githubusercontent.com/u/152045469?v=4" width="50;" alt="xykt"/></a>
<a href="https://github.com/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></a> <a href="https://github.com/nap0o" title="nap0o"><img src="https://avatars.githubusercontent.com/u/144927971?v=4" width="50;" alt="nap0o"/></a>
<a href="https://github.com/MikoyChinese" title="MikoyChinese"><img src="https://avatars.githubusercontent.com/u/22676744?v=4" width="50;" alt="MikoyChinese"/></a>
<a href="https://github.com/JackieSung4ev" title="JackieSung4ev"><img src="https://avatars.githubusercontent.com/u/24974735?v=4" width="50;" alt="JackieSung4ev"/></a> <a href="https://github.com/JackieSung4ev" title="JackieSung4ev"><img src="https://avatars.githubusercontent.com/u/24974735?v=4" width="50;" alt="JackieSung4ev"/></a>
<a href="https://github.com/lemoeo" title="Lemoe"><img src="https://avatars.githubusercontent.com/u/18618627?v=4" width="50;" alt="Lemoe"/></a>
<a href="https://github.com/cantoblanco" title="Kris"><img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/></a> <a href="https://github.com/cantoblanco" title="Kris"><img src="https://avatars.githubusercontent.com/u/116849421?v=4" width="50;" alt="Kris"/></a>
<a href="https://github.com/lemoeo" title="Lemoe"><img src="https://avatars.githubusercontent.com/u/18618627?v=4" width="50;" alt="Lemoe"/></a>
<a href="https://github.com/spiritLHLS" title="spiritlhl"><img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/></a> <a href="https://github.com/spiritLHLS" title="spiritlhl"><img src="https://avatars.githubusercontent.com/u/103393591?v=4" width="50;" alt="spiritlhl"/></a>
<a href="https://github.com/liuyanxi975" title="刘颜溪"><img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/></a> <a href="https://github.com/liuyanxi975" title="刘颜溪"><img src="https://avatars.githubusercontent.com/u/24417037?v=4" width="50;" alt="刘颜溪"/></a>
<a href="https://github.com/CosmosZ-code" title="CosmosZ-code"><img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/></a> <a href="https://github.com/CosmosZ-code" title="CosmosZ-code"><img src="https://avatars.githubusercontent.com/u/81398224?v=4" width="50;" alt="CosmosZ-code"/></a>
+28 -3
View File
@@ -42,6 +42,19 @@ func percentage(used, total uint64) float64 {
return float64(used) * 100 / float64(total) return float64(used) * 100 / float64(total)
} }
func maxSliceValue(slice []float64) float64 {
if len(slice) != 0 {
max := slice[0]
for _, val := range slice {
if max < val {
max = val
}
}
return max
}
return 0
}
// Snapshot 未通过规则返回 struct{}{}, 通过返回 nil // Snapshot 未通过规则返回 struct{}{}, 通过返回 nil
func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} { func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server, db *gorm.DB) interface{} {
// 监控全部但是排除了此服务器 // 监控全部但是排除了此服务器
@@ -63,6 +76,8 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
switch u.Type { switch u.Type {
case "cpu": case "cpu":
src = float64(server.State.CPU) src = float64(server.State.CPU)
case "gpu":
src = float64(server.State.GPU)
case "memory": case "memory":
src = percentage(server.State.MemUsed, server.Host.MemTotal) src = percentage(server.State.MemUsed, server.Host.MemTotal)
case "swap": case "swap":
@@ -91,21 +106,21 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
src = float64(server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn)) src = float64(server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
if u.CycleInterval != 0 { if u.CycleInterval != 0 {
var res NResult var res NResult
db.Model(&Transfer{}).Select("SUM(`in`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res) db.Model(&Transfer{}).Select("SUM(`in`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
src += float64(res.N) src += float64(res.N)
} }
case "transfer_out_cycle": case "transfer_out_cycle":
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut)) src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut))
if u.CycleInterval != 0 { if u.CycleInterval != 0 {
var res NResult var res NResult
db.Model(&Transfer{}).Select("SUM(`out`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res) db.Model(&Transfer{}).Select("SUM(`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
src += float64(res.N) src += float64(res.N)
} }
case "transfer_all_cycle": case "transfer_all_cycle":
src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut) + server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn)) src = float64(server.State.NetOutTransfer - uint64(server.PrevHourlyTransferOut) + server.State.NetInTransfer - uint64(server.PrevHourlyTransferIn))
if u.CycleInterval != 0 { if u.CycleInterval != 0 {
var res NResult var res NResult
db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("created_at > ? AND server_id = ?", u.GetTransferDurationStart(), server.ID).Scan(&res) db.Model(&Transfer{}).Select("SUM(`in`+`out`) AS n").Where("datetime(`created_at`) >= datetime(?) AND server_id = ?", u.GetTransferDurationStart().UTC(), server.ID).Scan(&res)
src += float64(res.N) src += float64(res.N)
} }
case "load1": case "load1":
@@ -120,6 +135,16 @@ func (u *Rule) Snapshot(cycleTransferStats *CycleTransferStats, server *Server,
src = float64(server.State.UdpConnCount) src = float64(server.State.UdpConnCount)
case "process_count": case "process_count":
src = float64(server.State.ProcessCount) src = float64(server.State.ProcessCount)
case "temperature_max":
var temp []float64
if server.State.Temperatures != nil {
for _, tempStat := range server.State.Temperatures {
if tempStat.Temperature != 0 {
temp = append(temp, tempStat.Temperature)
}
}
src = maxSliceValue(temp)
}
} }
// 循环区间流量检测 · 更新下次需要检测时间 // 循环区间流量检测 · 更新下次需要检测时间
+7 -7
View File
@@ -13,7 +13,7 @@ type ProviderCloudflare struct {
Secret string Secret string
} }
func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool { func (provider *ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil { if domainConfig == nil {
return false return false
} }
@@ -42,7 +42,7 @@ func (provider ProviderCloudflare) UpdateDomain(domainConfig *DomainConfig) bool
return resultV4 && resultV6 return resultV4 && resultV6
} }
func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool { func (provider *ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4) record, err := provider.findDNSRecord(zoneID, domainConfig.FullDomain, isIpv4)
if err != nil { if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err) log.Printf("查找 DNS 记录时出错: %s\n", err)
@@ -58,7 +58,7 @@ func (provider ProviderCloudflare) addDomainRecord(zoneID string, domainConfig *
} }
} }
func (provider ProviderCloudflare) getZoneID(domain string) (string, error) { func (provider *ProviderCloudflare) getZoneID(domain string) (string, error) {
_, realDomain := SplitDomain(domain) _, realDomain := SplitDomain(domain)
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain) url := fmt.Sprintf("https://api.cloudflare.com/client/v4/zones?name=%s", realDomain)
body, err := provider.sendRequest("GET", url, nil) body, err := provider.sendRequest("GET", url, nil)
@@ -81,7 +81,7 @@ func (provider ProviderCloudflare) getZoneID(domain string) (string, error) {
return "", fmt.Errorf("找不到 Zone ID") return "", fmt.Errorf("找不到 Zone ID")
} }
func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) { func (provider *ProviderCloudflare) findDNSRecord(zoneID string, domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A" var ipType = "A"
if !isIPv4 { if !isIPv4 {
ipType = "AAAA" ipType = "AAAA"
@@ -106,7 +106,7 @@ func (provider ProviderCloudflare) findDNSRecord(zoneID string, domain string, i
return nil, nil // 没有找到 DNS 记录 return nil, nil // 没有找到 DNS 记录
} }
func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool { func (provider *ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A" var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 { if !isIPv4 {
@@ -126,7 +126,7 @@ func (provider ProviderCloudflare) createDNSRecord(zoneID string, domainConfig *
return err == nil return err == nil
} }
func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool { func (provider *ProviderCloudflare) updateDNSRecord(zoneID string, recordID string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A" var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 { if !isIPv4 {
@@ -147,7 +147,7 @@ func (provider ProviderCloudflare) updateDNSRecord(zoneID string, recordID strin
} }
// 以下为辅助方法,如发送 HTTP 请求等 // 以下为辅助方法,如发送 HTTP 请求等
func (provider ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) { func (provider *ProviderCloudflare) sendRequest(method string, url string, data []byte) ([]byte, error) {
client := &http.Client{} client := &http.Client{}
req, err := http.NewRequest(method, url, bytes.NewBuffer(data)) req, err := http.NewRequest(method, url, bytes.NewBuffer(data))
if err != nil { if err != nil {
+1 -1
View File
@@ -2,6 +2,6 @@ package ddns
type ProviderDummy struct{} type ProviderDummy struct{}
func (provider ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool { func (provider *ProviderDummy) UpdateDomain(domainConfig *DomainConfig) bool {
return false return false
} }
+10 -10
View File
@@ -23,7 +23,7 @@ type ProviderTencentCloud struct {
SecretKey string SecretKey string
} }
func (provider ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool { func (provider *ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil { if domainConfig == nil {
return false return false
} }
@@ -46,7 +46,7 @@ func (provider ProviderTencentCloud) UpdateDomain(domainConfig *DomainConfig) bo
return resultV4 && resultV6 return resultV4 && resultV6
} }
func (provider ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool { func (provider *ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig, isIpv4 bool) bool {
record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4) record, err := provider.findDNSRecord(domainConfig.FullDomain, isIpv4)
if err != nil { if err != nil {
log.Printf("查找 DNS 记录时出错: %s\n", err) log.Printf("查找 DNS 记录时出错: %s\n", err)
@@ -66,7 +66,7 @@ func (provider ProviderTencentCloud) addDomainRecord(domainConfig *DomainConfig,
return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4) return provider.updateDNSRecord(domainConfig.FullDomain, record["RecordList"].([]interface{})[0].(map[string]interface{})["RecordId"].(float64), domainConfig, isIpv4)
} }
func (provider ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) { func (provider *ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (map[string]interface{}, error) {
var ipType = "A" var ipType = "A"
if !isIPv4 { if !isIPv4 {
ipType = "AAAA" ipType = "AAAA"
@@ -95,7 +95,7 @@ func (provider ProviderTencentCloud) findDNSRecord(domain string, isIPv4 bool) (
return result, nil return result, nil
} }
func (provider ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool { func (provider *ProviderTencentCloud) createDNSRecord(domain string, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A" var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 { if !isIPv4 {
@@ -117,7 +117,7 @@ func (provider ProviderTencentCloud) createDNSRecord(domain string, domainConfig
return err == nil return err == nil
} }
func (provider ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool { func (provider *ProviderTencentCloud) updateDNSRecord(domain string, recordID float64, domainConfig *DomainConfig, isIPv4 bool) bool {
var ipType = "A" var ipType = "A"
var ipAddr = domainConfig.Ipv4Addr var ipAddr = domainConfig.Ipv4Addr
if !isIPv4 { if !isIPv4 {
@@ -141,7 +141,7 @@ func (provider ProviderTencentCloud) updateDNSRecord(domain string, recordID flo
} }
// 以下为辅助方法,如发送 HTTP 请求等 // 以下为辅助方法,如发送 HTTP 请求等
func (provider ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) { func (provider *ProviderTencentCloud) sendRequest(action string, data []byte) ([]byte, error) {
client := &http.Client{} client := &http.Client{}
req, err := http.NewRequest("POST", url, bytes.NewBuffer(data)) req, err := http.NewRequest("POST", url, bytes.NewBuffer(data))
if err != nil { if err != nil {
@@ -173,18 +173,18 @@ func (provider ProviderTencentCloud) sendRequest(action string, data []byte) ([]
// https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go // https://github.com/jeessy2/ddns-go/blob/master/util/tencent_cloud_signer.go
func (provider ProviderTencentCloud) sha256hex(s string) string { func (provider *ProviderTencentCloud) sha256hex(s string) string {
b := sha256.Sum256([]byte(s)) b := sha256.Sum256([]byte(s))
return hex.EncodeToString(b[:]) return hex.EncodeToString(b[:])
} }
func (provider ProviderTencentCloud) hmacsha256(s, key string) string { func (provider *ProviderTencentCloud) hmacsha256(s, key string) string {
hashed := hmac.New(sha256.New, []byte(key)) hashed := hmac.New(sha256.New, []byte(key))
hashed.Write([]byte(s)) hashed.Write([]byte(s))
return string(hashed.Sum(nil)) return string(hashed.Sum(nil))
} }
func (provider ProviderTencentCloud) WriteString(strs ...string) string { func (provider *ProviderTencentCloud) WriteString(strs ...string) string {
var b strings.Builder var b strings.Builder
for _, str := range strs { for _, str := range strs {
b.WriteString(str) b.WriteString(str)
@@ -193,7 +193,7 @@ func (provider ProviderTencentCloud) WriteString(strs ...string) string {
return b.String() return b.String()
} }
func (provider ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) { func (provider *ProviderTencentCloud) signRequest(secretId string, secretKey string, r *http.Request, action string, payload string) {
algorithm := "TC3-HMAC-SHA256" algorithm := "TC3-HMAC-SHA256"
service := "dnspod" service := "dnspod"
host := provider.WriteString(service, ".tencentcloudapi.com") host := provider.WriteString(service, ".tencentcloudapi.com")
+1 -1
View File
@@ -14,7 +14,7 @@ type ProviderWebHook struct {
RequestHeader string RequestHeader string
} }
func (provider ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool { func (provider *ProviderWebHook) UpdateDomain(domainConfig *DomainConfig) bool {
if domainConfig == nil { if domainConfig == nil {
return false return false
} }
+3
View File
@@ -27,6 +27,9 @@ const mixinsVue = {
toggleView() { toggleView() {
this.showGroup = !this.showGroup; this.showGroup = !this.showGroup;
localStorage.setItem("showGroup", JSON.stringify(this.showGroup)); localStorage.setItem("showGroup", JSON.stringify(this.showGroup));
if(this.$root.page == 'service') {
this.$root.initTooltip();
}
return this.showGroup; return this.showGroup;
}, },
storedShowGroup() { storedShowGroup() {
+1 -1
View File
@@ -21,7 +21,7 @@
<script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/vue.min.js"></script>
<script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script> <script src="https://unpkg.com/[email protected]/dist/echarts.min.js"></script>
<script src="https://unpkg.com/[email protected]/map/js/world.js"></script> <script src="https://unpkg.com/[email protected]/map/js/world.js"></script>
<script src="/static/theme-server-status/js/mixin.js?v20240302"></script> <script src="/static/theme-server-status/js/mixin.js?v20240630"></script>
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="/static/theme-server-status/js/html5shiv.js"></script> <script src="/static/theme-server-status/js/html5shiv.js"></script>
+116 -97
View File
@@ -4,74 +4,74 @@
{{template "theme-server-status/content-nav" .}} {{template "theme-server-status/content-nav" .}}
<!-- showGroup true --> <!-- showGroup true -->
<template v-if="showGroup"> <template v-if="showGroup">
<section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;" v-for="group in servicesTag"> <section v-if="servicesTag.length === 0" class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
</section>
<section v-else class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;" v-for="group in servicesTag">
{{template "theme-server-status/service-group-true" .}} {{template "theme-server-status/service-group-true" .}}
</section> </section>
</template> </template>
<!-- showGroup false --> <!-- showGroup false -->
<template v-else> <template v-else>
<section class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;"> <section v-if="servicesNoTag.length === 0" class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
<p>No Valid Service Monitor Configuration Entries Found. Please Verify in the <a href="/monitor">Admin Panel</a>.</p>
</section>
<section v-else class="container content" style="max-width: 95vw; min-height: .01%;overflow-x: auto;">
{{template "theme-server-status/service-group-false" .}} {{template "theme-server-status/service-group-false" .}}
</section> </section>
</template> </template>
<section class="container content table-responsive" style="max-width: 95vw"> {{if .CycleTransferStats}}
{{if .CycleTransferStats}} <section class="container content table-responsive" style="max-width: 95vw">
<table class="table table-striped table-condensed table-hover"> <table class="table table-striped table-condensed table-hover">
<thead> <thead>
<tr class="node-group-tag"> <tr class="node-group-tag">
<th colspan="16" style="border:none;"> <th colspan="16" style="border:none;">
{{tr "CycleTransferStats"}} {{tr "CycleTransferStats"}}
</th> </th>
</tr> </tr>
<tr class="node-group-cell"> <tr class="node-group-cell">
<th class="node-cell center">ID</th> <th class="node-cell center">ID</th>
<th class="node-cell center">{{tr "Rules"}}</th> <th class="node-cell center">{{tr "Rules"}}</th>
<th class="node-cell center">{{tr "Server"}}</th> <th class="node-cell center">{{tr "Server"}}</th>
<th class="node-cell center">{{tr "From"}}</th> <th class="node-cell center">{{tr "From"}}</th>
<th class="node-cell center">{{tr "To"}}</th> <th class="node-cell center">{{tr "To"}}</th>
<th class="node-cell center">MAX</th> <th class="node-cell center">MAX</th>
<th class="node-cell center">MIN</th> <th class="node-cell center">MIN</th>
<th class="node-cell center">{{tr "NextCheck"}}</th> <th class="node-cell center">{{tr "NextCheck"}}</th>
<th class="node-cell center">{{tr "CurrentUsage"}}</th> <th class="node-cell center">{{tr "CurrentUsage"}}</th>
<th class="node-cell center">{{tr "Transleft"}}</th> <th class="node-cell center">{{tr "Transleft"}}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{range $id, $stats := .CycleTransferStats}} {{range $id, $stats := .CycleTransferStats}}
{{range $innerId, $transfer := $stats.Transfer}} {{range $innerId, $transfer := $stats.Transfer}}
{{$TransLeftPercent := TransLeftPercent (UintToFloat $transfer) (UintToFloat $stats.Max)}} {{$TransLeftPercent := TransLeftPercent (UintToFloat $transfer) (UintToFloat $stats.Max)}}
<tr> <tr>
<td class="node-cell center">{{$id}}</td> <td class="node-cell center">{{$id}}</td>
<td class="node-cell center">{{$stats.Name}}</td> <td class="node-cell center">{{$stats.Name}}</td>
<td class="node-cell center">{{index $stats.ServerName $innerId}}</td> <td class="node-cell center">{{index $stats.ServerName $innerId}}</td>
<td class="node-cell center">{{$stats.From|tf}}</td> <td class="node-cell center">{{$stats.From|tf}}</td>
<td class="node-cell center">{{$stats.To|tf}}</td> <td class="node-cell center">{{$stats.To|tf}}</td>
<td class="node-cell center">{{$stats.Max|bf}}</td> <td class="node-cell center">{{$stats.Max|bf}}</td>
<td class="node-cell center">{{$stats.Min|bf}}</td> <td class="node-cell center">{{$stats.Min|bf}}</td>
<td class="node-cell center">{{(index $stats.NextUpdate $innerId)|sft}}</td> <td class="node-cell center">{{(index $stats.NextUpdate $innerId)|sft}}</td>
<td class="node-cell center">{{$transfer|bf}}</td> <td class="node-cell center">{{$transfer|bf}}</td>
<td class="node-cell center"> <td class="node-cell center">
<div class="progress"> <div class="progress">
<div style="width: {{$TransLeftPercent}}%" :class="'progress-bar progress-bar-' + toSSBar('{{TransClassName $TransLeftPercent}}')"> <div style="width: {{$TransLeftPercent}}%" :class="'progress-bar progress-bar-' + toSSBar('{{TransClassName $TransLeftPercent}}')">
<small style="display: inline-block;width: max-content;">{{TransLeft $stats.Max $transfer}} / {{$TransLeftPercent}} %</small> <small style="display: inline-block;width: max-content;">{{TransLeft $stats.Max $transfer}} / {{$TransLeftPercent}} %</small>
</div> </div>
</div> </div>
</td> </td>
</tr> </tr>
{{end}} {{end}}
{{end}} {{end}}
</tbody> </tbody>
</table> </table>
{{end}} </section>
</section> {{end}}
{{template "theme-server-status/content-footer" .}} {{template "theme-server-status/content-footer" .}}
</div> </div>
<script>
// 初始化 Tooltip
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
</script>
<script> <script>
new Vue({ new Vue({
el: '#app', el: '#app',
@@ -80,13 +80,16 @@
page: 'service', page: 'service',
defaultTemplate: {{.Conf.Site.Theme}}, defaultTemplate: {{.Conf.Site.Theme}},
templates: {{.Themes}}, templates: {{.Themes}},
services: {{.Services}},
servicesTag: [], servicesTag: [],
servicesNoTag: [], servicesNoTag: []
}, },
created() { created() {
this.initData(); this.servicesTag = this.groupingData(this.initData(this.services),"type");
this.servicesNoTag = this.initData(this.services);
}, },
mounted() { mounted() {
this.initTooltip();
}, },
mixins: [mixinsVue], mixins: [mixinsVue],
methods: { methods: {
@@ -103,32 +106,36 @@
} }
return n return n
}, },
initData() { initData(services) {
// @formatter:off let nodes = [];
const services = [] for (let key in services) {
{{range $service := .Services}} const service = services[key];
services.push({ let node = {
type: '{{$service.Monitor.Type}}', type: service.Monitor.Type,
name: '{{$service.Monitor.Name}}', name: service.Monitor.Name,
currentUp: parseInt('{{$service.CurrentUp}}'), currentUp: parseInt(service.CurrentUp),
currentDown: parseInt('{{$service.CurrentDown}}'), currentDown: parseInt(service.CurrentDown),
totalUp: parseInt('{{$service.TotalUp}}'), totalUp: parseInt(service.TotalUp),
totalDown: parseInt('{{$service.TotalDown}}'), totalDown: parseInt(service.TotalDown),
delay: '{{$service.Delay}}'.replaceAll("[","").replaceAll("]","").split(" "), up: service.Up,
up: '{{$service.Up}}'.replaceAll("[","").replaceAll("]","").split(" "), down: service.Down,
down: '{{$service.Down}}'.replaceAll("[","").replaceAll("]","").split(" "), delay: service.Delay,
}) avgDelay: parseInt(this.getAvgDelay(service.Delay)) + "ms",
{{end}} health: this.getStateInfo(this.getPercent(service.CurrentUp, service.CurrentDown)),
// @formatter:on totalUpTime: this.getProgressInfo(this.getPercent(service.TotalUp, service.TotalDown))
for (let i = 0; i < services.length; i++) { };
const service = services[i]; nodes.push(node);
service.avgDelay = parseInt(service.delay[service.delay.length - 1]) + "ms" };
service.health = this.getStateInfo(this.getPercent(service.currentUp, service.currentDown)) for (let i = 0; i < nodes.length; i++) {
service.dayDetail = this.getDayTails(service) const node = nodes[i];
service.totalUpTime = this.getProgressInfo(this.getPercent(service.totalUp, service.totalDown)) node.dayDetail = this.getDayTails(node)
} }
this.servicesTag = this.groupingData(services,"type"); return nodes
this.servicesNoTag = services; },
initTooltip() {
$(document).ready(function(){
$('[data-toggle="tooltip"]').tooltip();
});
}, },
getPercent(up, down) { getPercent(up, down) {
if (!up) { if (!up) {
@@ -151,24 +158,24 @@
return this.toFixed2(currentUp / total * 100) return this.toFixed2(currentUp / total * 100)
}, },
getDayTails(service) { getDayTails(service) {
const result = [] const result = [];
for (let i = 0; i < service.up.length; i++) { for (let i = 0; i < service.up.length; i++) {
const up = service.up[i] const up = service.up[i];
const down = service.down[i] const down = service.down[i];
const delay = service.delay[i] let delay = Number(service.delay[i]);
let percent = this.getPercent(up, down) let percent = this.getPercent(up, down);
if (percent <= 0) { if (percent <= 0) {
percent = 0; percent = 0;
} }
let className = this.getStateInfo(percent).className let className = this.getStateInfo(percent).className;
let available = '{{tr "Availability"}}' let available = '{{tr "Availability"}}';
let averageLatency = '{{tr "AverageLatency"}}' let averageLatency = '{{tr "AverageLatency"}}';
const text = `${this.beforeDay(service.up.length - i - 1)}${available}${percent}%${averageLatency}${delay}ms` const text = `${this.beforeDay(service.up.length - i - 1)}${available}${Number(percent).toFixed(3)}%${averageLatency}${Number(delay).toFixed(3)}ms`;
result.push({ result.push({
text, className text, className
}) });
} }
return result return result;
}, },
beforeDay(days) { beforeDay(days) {
const today = new Date(); const today = new Date();
@@ -203,8 +210,9 @@
return result; return result;
}, },
getProgressInfo(percent) { getProgressInfo(percent) {
const result = this.getStateInfo(percent) const result = this.getStateInfo(percent);
result.style = `width: ${parseInt(percent)}%`; result.style = `width: ${parseInt(percent)}%`;
result.percent = Number(percent).toFixed(2);
const className = result.className; const className = result.className;
if (className === "good") { if (className === "good") {
result.className = 'progress-bar progress-bar-success' result.className = 'progress-bar progress-bar-success'
@@ -218,6 +226,17 @@
} }
return result return result
}, },
getAvgDelay(array) {
const { sum, count } = array.reduce((acc, value) => {
if (value !== 0) {
acc.sum += value;
acc.count += 1;
}
return acc;
}, { sum: 0, count: 0 });
return count > 0 ? sum / count : 0;
}
} }
}) })
</script> </script>
+2 -2
View File
@@ -24,8 +24,8 @@ os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1' [ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
sudo() { sudo() {
EUID=$(id -ru) myEUID=$(id -ru)
if [ "$EUID" -ne 0 ]; then if [ "$myEUID" -ne 0 ]; then
if command -v sudo > /dev/null 2>&1; then if command -v sudo > /dev/null 2>&1; then
command sudo "$@" command sudo "$@"
else else
+2 -2
View File
@@ -24,8 +24,8 @@ os_arch=""
[ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1' [ -e /etc/os-release ] && grep -i "PRETTY_NAME" /etc/os-release | grep -qi "alpine" && os_alpine='1'
sudo() { sudo() {
EUID=$(id -ru) myEUID=$(id -ru)
if [ "$EUID" -ne 0 ]; then if [ "$myEUID" -ne 0 ]; then
if command -v sudo > /dev/null 2>&1; then if command -v sudo > /dev/null 2>&1; then
command sudo "$@" command sudo "$@"
else else
+14 -14
View File
@@ -1,10 +1,10 @@
package singleton package singleton
import ( import (
"errors"
"fmt" "fmt"
ddns2 "github.com/naiba/nezha/pkg/ddns"
"log" "log"
ddns2 "github.com/naiba/nezha/pkg/ddns"
) )
func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool { func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig, maxRetries int) bool {
@@ -25,54 +25,54 @@ func RetryableUpdateDomain(provider ddns2.Provider, config *ddns2.DomainConfig,
func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) { func GetDDNSProviderFromString(provider string) (ddns2.Provider, error) {
switch provider { switch provider {
case "webhook": case "webhook":
return ddns2.ProviderWebHook{ return &ddns2.ProviderWebHook{
URL: Conf.DDNS.WebhookURL, URL: Conf.DDNS.WebhookURL,
RequestMethod: Conf.DDNS.WebhookMethod, RequestMethod: Conf.DDNS.WebhookMethod,
RequestBody: Conf.DDNS.WebhookRequestBody, RequestBody: Conf.DDNS.WebhookRequestBody,
RequestHeader: Conf.DDNS.WebhookHeaders, RequestHeader: Conf.DDNS.WebhookHeaders,
}, nil }, nil
case "dummy": case "dummy":
return ddns2.ProviderDummy{}, nil return &ddns2.ProviderDummy{}, nil
case "cloudflare": case "cloudflare":
return ddns2.ProviderCloudflare{ return &ddns2.ProviderCloudflare{
Secret: Conf.DDNS.AccessSecret, Secret: Conf.DDNS.AccessSecret,
}, nil }, nil
case "tencentcloud": case "tencentcloud":
return ddns2.ProviderTencentCloud{ return &ddns2.ProviderTencentCloud{
SecretID: Conf.DDNS.AccessID, SecretID: Conf.DDNS.AccessID,
SecretKey: Conf.DDNS.AccessSecret, SecretKey: Conf.DDNS.AccessSecret,
}, nil }, nil
} }
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider)) return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", Conf.DDNS.Provider)
} }
func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) { func GetDDNSProviderFromProfile(profileName string) (ddns2.Provider, error) {
profile, ok := Conf.DDNS.Profiles[profileName] profile, ok := Conf.DDNS.Profiles[profileName]
if !ok { if !ok {
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("未找到配置项 %s", profileName)) return &ddns2.ProviderDummy{}, fmt.Errorf("未找到配置项 %s", profileName)
} }
switch profile.Provider { switch profile.Provider {
case "webhook": case "webhook":
return ddns2.ProviderWebHook{ return &ddns2.ProviderWebHook{
URL: profile.WebhookURL, URL: profile.WebhookURL,
RequestMethod: profile.WebhookMethod, RequestMethod: profile.WebhookMethod,
RequestBody: profile.WebhookRequestBody, RequestBody: profile.WebhookRequestBody,
RequestHeader: profile.WebhookHeaders, RequestHeader: profile.WebhookHeaders,
}, nil }, nil
case "dummy": case "dummy":
return ddns2.ProviderDummy{}, nil return &ddns2.ProviderDummy{}, nil
case "cloudflare": case "cloudflare":
return ddns2.ProviderCloudflare{ return &ddns2.ProviderCloudflare{
Secret: profile.AccessSecret, Secret: profile.AccessSecret,
}, nil }, nil
case "tencentcloud": case "tencentcloud":
return ddns2.ProviderTencentCloud{ return &ddns2.ProviderTencentCloud{
SecretID: profile.AccessID, SecretID: profile.AccessID,
SecretKey: profile.AccessSecret, SecretKey: profile.AccessSecret,
}, nil }, nil
} }
return ddns2.ProviderDummy{}, errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", profile.Provider)) return &ddns2.ProviderDummy{}, fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
} }
func ValidateDDNSProvidersFromProfiles() error { func ValidateDDNSProvidersFromProfiles() error {
@@ -80,7 +80,7 @@ func ValidateDDNSProvidersFromProfiles() error {
providers := make(map[string]string) providers := make(map[string]string)
for profileName, profile := range Conf.DDNS.Profiles { for profileName, profile := range Conf.DDNS.Profiles {
if _, ok := validProviders[profile.Provider]; !ok { if _, ok := validProviders[profile.Provider]; !ok {
return errors.New(fmt.Sprintf("无法找到配置的DDNS提供者%s", profile.Provider)) return fmt.Errorf("无法找到配置的DDNS提供者%s", profile.Provider)
} }
providers[profileName] = profile.Provider providers[profileName] = profile.Provider
} }
+4 -4
View File
@@ -93,7 +93,7 @@ func RecordTransferHourlyUsage() {
ServerLock.Lock() ServerLock.Lock()
defer ServerLock.Unlock() defer ServerLock.Unlock()
now := time.Now() now := time.Now()
nowTrimSeconds := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, Loc) nowTrimSeconds := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, now.Location())
var txs []model.Transfer var txs []model.Transfer
for id, server := range ServerList { for id, server := range ServerList {
tx := model.Transfer{ tx := model.Transfer{
@@ -136,7 +136,7 @@ func CleanMonitorHistory() {
if !rule.IsTransferDurationRule() { if !rule.IsTransferDurationRule() {
continue continue
} }
dataCouldRemoveBefore := rule.GetTransferDurationStart() dataCouldRemoveBefore := rule.GetTransferDurationStart().UTC()
// 判断规则影响的机器范围 // 判断规则影响的机器范围
if rule.Cover == model.RuleCoverAll { if rule.Cover == model.RuleCoverAll {
// 更新全局可以清理的数据点 // 更新全局可以清理的数据点
@@ -155,12 +155,12 @@ func CleanMonitorHistory() {
} }
} }
for id, couldRemove := range specialServerKeep { for id, couldRemove := range specialServerKeep {
DB.Unscoped().Delete(&model.Transfer{}, "server_id = ? AND created_at < ?", id, couldRemove) DB.Unscoped().Delete(&model.Transfer{}, "server_id = ? AND datetime(`created_at`) < datetime(?)", id, couldRemove)
} }
if allServerKeep.IsZero() { if allServerKeep.IsZero() {
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?)", specialServerIDs) DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?)", specialServerIDs)
} else { } else {
DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?) AND created_at < ?", specialServerIDs, allServerKeep) DB.Unscoped().Delete(&model.Transfer{}, "server_id NOT IN (?) AND datetime(`created_at`) < datetime(?)", specialServerIDs, allServerKeep)
} }
} }