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/
|
||||
1
TODO.md
1
TODO.md
|
|
@ -2,4 +2,3 @@
|
|||
- [ ] View incidents
|
||||
- [ ] Latency graphs?
|
||||
- [ ] README
|
||||
- [ ] Site metadata
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ title = "trafficlunar"
|
|||
link_text = "go back"
|
||||
link_url = "https://trafficlunar.net"
|
||||
|
||||
# Default timeline shown on the index page
|
||||
# Default timeline shown
|
||||
# Users can still switch views manually
|
||||
#
|
||||
# Available options:
|
||||
|
|
@ -14,7 +14,7 @@ link_url = "https://trafficlunar.net"
|
|||
# "days" - past 30 days
|
||||
default_view = "hours"
|
||||
|
||||
# Default theme for the status page
|
||||
# Default theme
|
||||
# Available themes: https://github.com/trafficlunar/statsys/tree/main/www/themes
|
||||
# Defaults to "monochrome"
|
||||
default_theme = "color"
|
||||
|
|
@ -22,6 +22,10 @@ default_theme = "color"
|
|||
# Defaults to 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
|
||||
[[services]]
|
||||
name = "website"
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type Config struct {
|
|||
DefaultView string `toml:"default_view"`
|
||||
DefaultTheme string `toml:"default_theme"`
|
||||
EnableThemeSwitcher bool `toml:"enable_theme_switcher"`
|
||||
EnableWatermark bool `toml:"enable_watermark"`
|
||||
Services []ServiceConfig `toml:"services"`
|
||||
}
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ func LoadConfig() {
|
|||
templateData.LinkText = config.LinkText
|
||||
templateData.LinkUrl = config.LinkUrl
|
||||
templateData.EnableThemeSwitcher = config.EnableThemeSwitcher
|
||||
templateData.EnableWatermark = config.EnableWatermark
|
||||
templateData.Services = make([]Service, len(config.Services))
|
||||
for index, ser := range config.Services {
|
||||
// default to 1000ms
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ func InitDatabase() {
|
|||
log.Fatalf("failed to create table: %v", err)
|
||||
}
|
||||
|
||||
templateDataMu.Lock()
|
||||
|
||||
// load template data from database
|
||||
for index, service := range templateData.Services {
|
||||
// timelines
|
||||
|
|
@ -97,6 +99,7 @@ func InitDatabase() {
|
|||
templateData.Services[index] = service
|
||||
}
|
||||
|
||||
templateDataMu.Unlock()
|
||||
log.Printf("database initalized at '%s'", *DatabasePath)
|
||||
}
|
||||
|
||||
|
|
@ -332,8 +335,10 @@ func AddToTimeline(serviceIndex int, status string) {
|
|||
}
|
||||
}
|
||||
|
||||
templateDataMu.Lock()
|
||||
templateData.Services[serviceIndex] = service
|
||||
calculateUptimePercentages(serviceIndex)
|
||||
templateDataMu.Unlock()
|
||||
}
|
||||
|
||||
func AddIncident(serviceIndex int, status string, startTime time.Time) {
|
||||
|
|
@ -348,7 +353,9 @@ func AddIncident(serviceIndex int, status string, startTime time.Time) {
|
|||
Status: status,
|
||||
StartTime: startTime,
|
||||
})
|
||||
templateDataMu.Lock()
|
||||
templateData.Services[serviceIndex] = service
|
||||
templateDataMu.Unlock()
|
||||
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
|
||||
templateDataMu.Lock()
|
||||
templateData.Services[serviceIndex] = service
|
||||
templateDataMu.Unlock()
|
||||
log.Println("(" + service.Name + ") incident resolved")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/klauspost/compress/gzhttp"
|
||||
|
|
@ -45,6 +46,7 @@ type TemplateData struct {
|
|||
LinkText string
|
||||
LinkUrl string
|
||||
EnableThemeSwitcher bool
|
||||
EnableWatermark bool
|
||||
LastUpdated int64
|
||||
IsOperational bool
|
||||
Services []Service
|
||||
|
|
@ -61,8 +63,10 @@ func ToUpper(s string) string {
|
|||
|
||||
var templateData = TemplateData{
|
||||
EnableThemeSwitcher: true,
|
||||
EnableWatermark: true,
|
||||
LastUpdated: time.Now().UTC().UnixMilli(),
|
||||
}
|
||||
var templateDataMu sync.Mutex
|
||||
var tmpl *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
|
||||
if req.URL.Path != "/" {
|
||||
if r.URL.Path != "/" {
|
||||
renderError(w, http.StatusNotFound, "Page not found")
|
||||
return
|
||||
}
|
||||
|
||||
data := templateData
|
||||
data.View = req.URL.Query().Get("view")
|
||||
data.Theme = req.URL.Query().Get("theme")
|
||||
data.View = r.URL.Query().Get("view")
|
||||
data.Theme = r.URL.Query().Get("theme")
|
||||
|
||||
switch data.View {
|
||||
case "hours", "minutes", "days":
|
||||
|
|
@ -111,16 +115,19 @@ func index(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func styles(w http.ResponseWriter, req *http.Request) {
|
||||
http.ServeFile(w, req, "www/styles.css")
|
||||
func styles(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Cache-Control", "public, max-age=31536000")
|
||||
http.ServeFile(w, r, "www/styles.css")
|
||||
}
|
||||
|
||||
func favicon(w http.ResponseWriter, req *http.Request) {
|
||||
http.ServeFile(w, req, "www/favicon.ico")
|
||||
func favicon(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Cache-Control", "public, max-age=31536000")
|
||||
http.ServeFile(w, r, "www/favicon.ico")
|
||||
}
|
||||
|
||||
func robots(w http.ResponseWriter, req *http.Request) {
|
||||
http.ServeFile(w, req, "www/robots.txt")
|
||||
func robots(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Cache-Control", "public, max-age=31536000")
|
||||
http.ServeFile(w, r, "www/robots.txt")
|
||||
}
|
||||
|
||||
func StartHttpServer() {
|
||||
|
|
@ -147,8 +154,14 @@ func StartHttpServer() {
|
|||
mux.HandleFunc("/styles.css", styles)
|
||||
mux.HandleFunc("/favicon.ico", favicon)
|
||||
mux.HandleFunc("/robots.txt", robots)
|
||||
mux.Handle("/themes/", gzhttp.GzipHandler(http.StripPrefix("/themes/", http.FileServer(http.Dir("www/themes/")))))
|
||||
mux.Handle("/fonts/", http.StripPrefix("/fonts/", http.FileServer(http.Dir("www/fonts/"))))
|
||||
mux.Handle("/themes/", gzhttp.GzipHandler(http.StripPrefix("/themes/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
handler := gzhttp.GzipHandler(mux)
|
||||
|
|
|
|||
|
|
@ -25,10 +25,15 @@ func checkStatuses() {
|
|||
startTime := time.Now().UTC()
|
||||
res, err := client.Get(service.Url)
|
||||
latency := time.Since(startTime).Milliseconds()
|
||||
if res != nil {
|
||||
defer res.Body.Close()
|
||||
}
|
||||
|
||||
status := "Online"
|
||||
|
||||
if err != nil || res.StatusCode != 200 {
|
||||
if err != nil {
|
||||
status = "Offline"
|
||||
} else if res.StatusCode != 200 {
|
||||
status = "Offline"
|
||||
} else if latency >= service.LatencyThreshold {
|
||||
status = "Degraded"
|
||||
|
|
@ -49,7 +54,9 @@ func checkStatuses() {
|
|||
}
|
||||
}
|
||||
|
||||
templateDataMu.Lock()
|
||||
templateData.Services[index].Status = status
|
||||
templateDataMu.Unlock()
|
||||
}(index, service)
|
||||
}
|
||||
|
||||
|
|
@ -65,17 +72,22 @@ func checkStatuses() {
|
|||
}
|
||||
}
|
||||
|
||||
templateDataMu.Lock()
|
||||
templateData.IsOperational = isOperational
|
||||
templateData.LastUpdated = time.Now().UTC().UnixMilli()
|
||||
templateDataMu.Unlock()
|
||||
}
|
||||
|
||||
func StartCheckingStatuses() {
|
||||
log.Println("started checking statuses...")
|
||||
|
||||
go func() {
|
||||
checkStatuses()
|
||||
|
||||
for range time.Tick(1 * time.Minute) {
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
checkStatuses()
|
||||
}
|
||||
}()
|
||||
|
|
|
|||
|
|
@ -3,7 +3,27 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<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="stylesheet" href="/styles.css" />
|
||||
|
|
@ -450,6 +470,23 @@
|
|||
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) {
|
||||
body::before {
|
||||
content: "";
|
||||
|
|
@ -475,6 +512,20 @@
|
|||
.bars-gradient {
|
||||
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) {
|
||||
|
|
@ -500,9 +551,14 @@
|
|||
.uptime-percentage {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
#watermark {
|
||||
left: 1.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<link rel="preload" as="style" href="/themes/{{ .Theme }}.css" />
|
||||
<link rel="stylesheet" href="/themes/{{ .Theme }}.css" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
@ -674,6 +730,13 @@
|
|||
</footer>
|
||||
</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>
|
||||
// responsiveness
|
||||
const timelineLengthTexts = document.querySelectorAll(".timeline-length");
|
||||
|
|
|
|||
Loading…
Reference in a new issue