mirror of
https://github.com/trafficlunar/statsys.git
synced 2026-03-28 11:13:17 +00:00
feat: optimization and metadata
This commit is contained in:
parent
5a07e249ac
commit
53079f5971
8 changed files with 123 additions and 20 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +1,2 @@
|
||||||
|
*.db
|
||||||
data/
|
data/
|
||||||
1
TODO.md
1
TODO.md
|
|
@ -2,4 +2,3 @@
|
||||||
- [ ] View incidents
|
- [ ] View incidents
|
||||||
- [ ] Latency graphs?
|
- [ ] Latency graphs?
|
||||||
- [ ] README
|
- [ ] README
|
||||||
- [ ] Site metadata
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ title = "trafficlunar"
|
||||||
link_text = "go back"
|
link_text = "go back"
|
||||||
link_url = "https://trafficlunar.net"
|
link_url = "https://trafficlunar.net"
|
||||||
|
|
||||||
# Default timeline shown on the index page
|
# Default timeline shown
|
||||||
# Users can still switch views manually
|
# Users can still switch views manually
|
||||||
#
|
#
|
||||||
# Available options:
|
# Available options:
|
||||||
|
|
@ -14,7 +14,7 @@ link_url = "https://trafficlunar.net"
|
||||||
# "days" - past 30 days
|
# "days" - past 30 days
|
||||||
default_view = "hours"
|
default_view = "hours"
|
||||||
|
|
||||||
# Default theme for the status page
|
# Default theme
|
||||||
# Available themes: https://github.com/trafficlunar/statsys/tree/main/www/themes
|
# Available themes: https://github.com/trafficlunar/statsys/tree/main/www/themes
|
||||||
# Defaults to "monochrome"
|
# Defaults to "monochrome"
|
||||||
default_theme = "color"
|
default_theme = "color"
|
||||||
|
|
@ -22,6 +22,10 @@ default_theme = "color"
|
||||||
# Defaults to true
|
# Defaults to true
|
||||||
enable_theme_switcher = true
|
enable_theme_switcher = true
|
||||||
|
|
||||||
|
# Set to true to show a small statsys watermark
|
||||||
|
# Defaults to true
|
||||||
|
enable_watermark = true
|
||||||
|
|
||||||
# List of services to monitor
|
# List of services to monitor
|
||||||
[[services]]
|
[[services]]
|
||||||
name = "website"
|
name = "website"
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ type Config struct {
|
||||||
DefaultView string `toml:"default_view"`
|
DefaultView string `toml:"default_view"`
|
||||||
DefaultTheme string `toml:"default_theme"`
|
DefaultTheme string `toml:"default_theme"`
|
||||||
EnableThemeSwitcher bool `toml:"enable_theme_switcher"`
|
EnableThemeSwitcher bool `toml:"enable_theme_switcher"`
|
||||||
|
EnableWatermark bool `toml:"enable_watermark"`
|
||||||
Services []ServiceConfig `toml:"services"`
|
Services []ServiceConfig `toml:"services"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -39,6 +40,7 @@ func LoadConfig() {
|
||||||
templateData.LinkText = config.LinkText
|
templateData.LinkText = config.LinkText
|
||||||
templateData.LinkUrl = config.LinkUrl
|
templateData.LinkUrl = config.LinkUrl
|
||||||
templateData.EnableThemeSwitcher = config.EnableThemeSwitcher
|
templateData.EnableThemeSwitcher = config.EnableThemeSwitcher
|
||||||
|
templateData.EnableWatermark = config.EnableWatermark
|
||||||
templateData.Services = make([]Service, len(config.Services))
|
templateData.Services = make([]Service, len(config.Services))
|
||||||
for index, ser := range config.Services {
|
for index, ser := range config.Services {
|
||||||
// default to 1000ms
|
// default to 1000ms
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@ func InitDatabase() {
|
||||||
log.Fatalf("failed to create table: %v", err)
|
log.Fatalf("failed to create table: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templateDataMu.Lock()
|
||||||
|
|
||||||
// load template data from database
|
// load template data from database
|
||||||
for index, service := range templateData.Services {
|
for index, service := range templateData.Services {
|
||||||
// timelines
|
// timelines
|
||||||
|
|
@ -97,6 +99,7 @@ func InitDatabase() {
|
||||||
templateData.Services[index] = service
|
templateData.Services[index] = service
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templateDataMu.Unlock()
|
||||||
log.Printf("database initalized at '%s'", *DatabasePath)
|
log.Printf("database initalized at '%s'", *DatabasePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -332,8 +335,10 @@ func AddToTimeline(serviceIndex int, status string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templateDataMu.Lock()
|
||||||
templateData.Services[serviceIndex] = service
|
templateData.Services[serviceIndex] = service
|
||||||
calculateUptimePercentages(serviceIndex)
|
calculateUptimePercentages(serviceIndex)
|
||||||
|
templateDataMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddIncident(serviceIndex int, status string, startTime time.Time) {
|
func AddIncident(serviceIndex int, status string, startTime time.Time) {
|
||||||
|
|
@ -348,7 +353,9 @@ func AddIncident(serviceIndex int, status string, startTime time.Time) {
|
||||||
Status: status,
|
Status: status,
|
||||||
StartTime: startTime,
|
StartTime: startTime,
|
||||||
})
|
})
|
||||||
|
templateDataMu.Lock()
|
||||||
templateData.Services[serviceIndex] = service
|
templateData.Services[serviceIndex] = service
|
||||||
|
templateDataMu.Unlock()
|
||||||
log.Println("(" + service.Name + ") incident started")
|
log.Println("(" + service.Name + ") incident started")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -364,6 +371,8 @@ func ResolveIncident(serviceIndex int, serviceName string, endTime time.Time) {
|
||||||
}
|
}
|
||||||
|
|
||||||
service.Incidents[len(service.Incidents)-1].EndTime = &endTime
|
service.Incidents[len(service.Incidents)-1].EndTime = &endTime
|
||||||
|
templateDataMu.Lock()
|
||||||
templateData.Services[serviceIndex] = service
|
templateData.Services[serviceIndex] = service
|
||||||
|
templateDataMu.Unlock()
|
||||||
log.Println("(" + service.Name + ") incident resolved")
|
log.Println("(" + service.Name + ") incident resolved")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/klauspost/compress/gzhttp"
|
"github.com/klauspost/compress/gzhttp"
|
||||||
|
|
@ -45,6 +46,7 @@ type TemplateData struct {
|
||||||
LinkText string
|
LinkText string
|
||||||
LinkUrl string
|
LinkUrl string
|
||||||
EnableThemeSwitcher bool
|
EnableThemeSwitcher bool
|
||||||
|
EnableWatermark bool
|
||||||
LastUpdated int64
|
LastUpdated int64
|
||||||
IsOperational bool
|
IsOperational bool
|
||||||
Services []Service
|
Services []Service
|
||||||
|
|
@ -61,8 +63,10 @@ func ToUpper(s string) string {
|
||||||
|
|
||||||
var templateData = TemplateData{
|
var templateData = TemplateData{
|
||||||
EnableThemeSwitcher: true,
|
EnableThemeSwitcher: true,
|
||||||
|
EnableWatermark: true,
|
||||||
LastUpdated: time.Now().UTC().UnixMilli(),
|
LastUpdated: time.Now().UTC().UnixMilli(),
|
||||||
}
|
}
|
||||||
|
var templateDataMu sync.Mutex
|
||||||
var tmpl *template.Template
|
var tmpl *template.Template
|
||||||
var errorTmpl *template.Template
|
var errorTmpl *template.Template
|
||||||
|
|
||||||
|
|
@ -79,16 +83,16 @@ func renderError(w http.ResponseWriter, statusCode int, message string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func index(w http.ResponseWriter, req *http.Request) {
|
func index(w http.ResponseWriter, r *http.Request) {
|
||||||
// handle 404
|
// handle 404
|
||||||
if req.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
renderError(w, http.StatusNotFound, "Page not found")
|
renderError(w, http.StatusNotFound, "Page not found")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data := templateData
|
data := templateData
|
||||||
data.View = req.URL.Query().Get("view")
|
data.View = r.URL.Query().Get("view")
|
||||||
data.Theme = req.URL.Query().Get("theme")
|
data.Theme = r.URL.Query().Get("theme")
|
||||||
|
|
||||||
switch data.View {
|
switch data.View {
|
||||||
case "hours", "minutes", "days":
|
case "hours", "minutes", "days":
|
||||||
|
|
@ -111,16 +115,19 @@ func index(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func styles(w http.ResponseWriter, req *http.Request) {
|
func styles(w http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(w, req, "www/styles.css")
|
w.Header().Add("Cache-Control", "public, max-age=31536000")
|
||||||
|
http.ServeFile(w, r, "www/styles.css")
|
||||||
}
|
}
|
||||||
|
|
||||||
func favicon(w http.ResponseWriter, req *http.Request) {
|
func favicon(w http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(w, req, "www/favicon.ico")
|
w.Header().Add("Cache-Control", "public, max-age=31536000")
|
||||||
|
http.ServeFile(w, r, "www/favicon.ico")
|
||||||
}
|
}
|
||||||
|
|
||||||
func robots(w http.ResponseWriter, req *http.Request) {
|
func robots(w http.ResponseWriter, r *http.Request) {
|
||||||
http.ServeFile(w, req, "www/robots.txt")
|
w.Header().Add("Cache-Control", "public, max-age=31536000")
|
||||||
|
http.ServeFile(w, r, "www/robots.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartHttpServer() {
|
func StartHttpServer() {
|
||||||
|
|
@ -147,8 +154,14 @@ func StartHttpServer() {
|
||||||
mux.HandleFunc("/styles.css", styles)
|
mux.HandleFunc("/styles.css", styles)
|
||||||
mux.HandleFunc("/favicon.ico", favicon)
|
mux.HandleFunc("/favicon.ico", favicon)
|
||||||
mux.HandleFunc("/robots.txt", robots)
|
mux.HandleFunc("/robots.txt", robots)
|
||||||
mux.Handle("/themes/", gzhttp.GzipHandler(http.StripPrefix("/themes/", http.FileServer(http.Dir("www/themes/")))))
|
mux.Handle("/themes/", gzhttp.GzipHandler(http.StripPrefix("/themes/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
mux.Handle("/fonts/", http.StripPrefix("/fonts/", http.FileServer(http.Dir("www/fonts/"))))
|
w.Header().Add("Cache-Control", "public, max-age=31536000")
|
||||||
|
http.FileServer(http.Dir("www/themes/")).ServeHTTP(w, r)
|
||||||
|
}))))
|
||||||
|
mux.Handle("/fonts/", http.StripPrefix("/fonts/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Add("Cache-Control", "public, max-age=31536000")
|
||||||
|
http.FileServer(http.Dir("www/fonts/")).ServeHTTP(w, r)
|
||||||
|
})))
|
||||||
|
|
||||||
// wrap with gzip middleware
|
// wrap with gzip middleware
|
||||||
handler := gzhttp.GzipHandler(mux)
|
handler := gzhttp.GzipHandler(mux)
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,15 @@ func checkStatuses() {
|
||||||
startTime := time.Now().UTC()
|
startTime := time.Now().UTC()
|
||||||
res, err := client.Get(service.Url)
|
res, err := client.Get(service.Url)
|
||||||
latency := time.Since(startTime).Milliseconds()
|
latency := time.Since(startTime).Milliseconds()
|
||||||
|
if res != nil {
|
||||||
|
defer res.Body.Close()
|
||||||
|
}
|
||||||
|
|
||||||
status := "Online"
|
status := "Online"
|
||||||
|
|
||||||
if err != nil || res.StatusCode != 200 {
|
if err != nil {
|
||||||
|
status = "Offline"
|
||||||
|
} else if res.StatusCode != 200 {
|
||||||
status = "Offline"
|
status = "Offline"
|
||||||
} else if latency >= service.LatencyThreshold {
|
} else if latency >= service.LatencyThreshold {
|
||||||
status = "Degraded"
|
status = "Degraded"
|
||||||
|
|
@ -49,7 +54,9 @@ func checkStatuses() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templateDataMu.Lock()
|
||||||
templateData.Services[index].Status = status
|
templateData.Services[index].Status = status
|
||||||
|
templateDataMu.Unlock()
|
||||||
}(index, service)
|
}(index, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -65,17 +72,22 @@ func checkStatuses() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
templateDataMu.Lock()
|
||||||
templateData.IsOperational = isOperational
|
templateData.IsOperational = isOperational
|
||||||
templateData.LastUpdated = time.Now().UTC().UnixMilli()
|
templateData.LastUpdated = time.Now().UTC().UnixMilli()
|
||||||
|
templateDataMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartCheckingStatuses() {
|
func StartCheckingStatuses() {
|
||||||
log.Println("started checking statuses...")
|
log.Println("started checking statuses...")
|
||||||
|
|
||||||
go func() {
|
|
||||||
checkStatuses()
|
checkStatuses()
|
||||||
|
|
||||||
for range time.Tick(1 * time.Minute) {
|
go func() {
|
||||||
|
ticker := time.NewTicker(1 * time.Minute)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for range ticker.C {
|
||||||
checkStatuses()
|
checkStatuses()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,27 @@
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>status - {{ .Title }}</title>
|
<meta
|
||||||
|
http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; img-src 'self'; font-src 'self'; connect-src 'self'; base-uri 'self'; form-action 'self';"
|
||||||
|
/>
|
||||||
|
<title>status — {{ .Title }}</title>
|
||||||
|
<meta name="title" content="{{ .Title }} Status" />
|
||||||
|
<meta name="description" content="Real-time status monitoring for {{ .Title }}" />
|
||||||
|
<meta name="keywords" content="status page, uptime, system status, service status, {{ .Title }}, monitoring, statsys" />
|
||||||
|
<meta name="author" content="{{ .Title }}" />
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content="{{ .Title }} Status" />
|
||||||
|
<meta property="og:description" content="Real-time status monitoring for {{ .Title }}" />
|
||||||
|
<meta property="og:site_name" content="{{ .Title }} Status" />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary" />
|
||||||
|
<meta name="twitter:title" content="{{ .Title }} Status" />
|
||||||
|
<meta name="twitter:description" content="Real-time status monitoring for {{ .Title }}" />
|
||||||
|
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
|
||||||
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon" />
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
|
@ -450,6 +470,23 @@
|
||||||
color: var(--accent-secondary);
|
color: var(--accent-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#watermark {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--accent-faded);
|
||||||
|
word-break: break-word;
|
||||||
|
width: 15%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
#watermark:hover {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 39.625rem) {
|
@media (min-width: 39.625rem) {
|
||||||
body::before {
|
body::before {
|
||||||
content: "";
|
content: "";
|
||||||
|
|
@ -475,6 +512,20 @@
|
||||||
.bars-gradient {
|
.bars-gradient {
|
||||||
display: block !important;
|
display: block !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#watermark {
|
||||||
|
top: 2rem;
|
||||||
|
left: 2rem;
|
||||||
|
right: unset;
|
||||||
|
width: 20%;
|
||||||
|
text-align: left;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
#watermark span:nth-child(2) {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 30rem) {
|
@media (max-width: 30rem) {
|
||||||
|
|
@ -500,9 +551,14 @@
|
||||||
.uptime-percentage {
|
.uptime-percentage {
|
||||||
transform: translateX(0%);
|
transform: translateX(0%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#watermark {
|
||||||
|
left: 1.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<link rel="preload" as="style" href="/themes/{{ .Theme }}.css" />
|
||||||
<link rel="stylesheet" href="/themes/{{ .Theme }}.css" />
|
<link rel="stylesheet" href="/themes/{{ .Theme }}.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -674,6 +730,13 @@
|
||||||
</footer>
|
</footer>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{{ if .EnableWatermark }}
|
||||||
|
<a id="watermark" href="https://github.com/trafficlunar/statsys">
|
||||||
|
<span>powered by</span>
|
||||||
|
<span style="font-family: 'JetBrains Mono', monospace; font-weight: 700; color: var(--accent-primary)">statsys</span>
|
||||||
|
</a>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// responsiveness
|
// responsiveness
|
||||||
const timelineLengthTexts = document.querySelectorAll(".timeline-length");
|
const timelineLengthTexts = document.querySelectorAll(".timeline-length");
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue