feat: crash log reporting
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
ice.plezy.app {
|
||||
reverse_proxy relay:8080
|
||||
}
|
||||
|
||||
bugs.plezy.app {
|
||||
reverse_proxy bugsink:8000 {
|
||||
header_up X-Real-IP {remote_host}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM golang:1.22-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /relay .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /relay /relay
|
||||
VOLUME /data/logs
|
||||
EXPOSE 8080
|
||||
ENV GOMEMLIMIT=64MiB
|
||||
ENTRYPOINT ["/relay"]
|
||||
@@ -0,0 +1,36 @@
|
||||
services:
|
||||
relay:
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- logs:/data/logs
|
||||
expose:
|
||||
- "8080"
|
||||
|
||||
caddy:
|
||||
image: caddy:2-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./Caddyfile:/etc/caddy/Caddyfile:ro
|
||||
- caddy_data:/data
|
||||
- caddy_config:/config
|
||||
|
||||
bugsink:
|
||||
image: bugsink/bugsink
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
SECRET_KEY: ${BUGSINK_SECRET_KEY}
|
||||
CREATE_SUPERUSER: ${BUGSINK_SUPERUSER}
|
||||
PORT: 8000
|
||||
BEHIND_HTTPS_PROXY: "True"
|
||||
expose:
|
||||
- "8000"
|
||||
|
||||
volumes:
|
||||
logs:
|
||||
caddy_data:
|
||||
caddy_config:
|
||||
+1
-9
@@ -2,12 +2,4 @@ module github.com/edde746/plezy-relay
|
||||
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
golang.org/x/crypto v0.31.0
|
||||
)
|
||||
|
||||
require (
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
)
|
||||
require github.com/gorilla/websocket v1.5.3
|
||||
|
||||
@@ -1,8 +1,2 @@
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
|
||||
+57
-38
@@ -9,12 +9,14 @@ import (
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"golang.org/x/crypto/acme/autocert"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -29,13 +31,16 @@ const (
|
||||
pingInterval = 30 * time.Second
|
||||
maxMessageSize = 64 * 1024
|
||||
maxLogSize = 1 * 1024 * 1024 // 1MB
|
||||
logMaxAge = 7 * 24 * time.Hour
|
||||
logMaxAge = 3 * 24 * time.Hour
|
||||
logIDLength = 5
|
||||
logRateInterval = 1 * time.Minute
|
||||
maxLogEntries = 500
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
// --- Rate limiter (token bucket) ---
|
||||
@@ -150,21 +155,36 @@ func (r *Room) sendTo(targetID string, msg serverMsg) bool {
|
||||
// --- Log store ---
|
||||
|
||||
type logEntry struct {
|
||||
Text string
|
||||
Size int
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type logStore struct {
|
||||
entries map[string]logEntry
|
||||
rateLimit map[string]time.Time // IP -> last upload time
|
||||
dir string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newLogStore() *logStore {
|
||||
return &logStore{
|
||||
func newLogStore(dir string) *logStore {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
log.Fatalf("failed to create log dir %s: %v", dir, err)
|
||||
}
|
||||
ls := &logStore{
|
||||
entries: make(map[string]logEntry),
|
||||
rateLimit: make(map[string]time.Time),
|
||||
dir: dir,
|
||||
}
|
||||
// Clean orphaned files from prior runs
|
||||
files, _ := os.ReadDir(dir)
|
||||
for _, f := range files {
|
||||
os.Remove(filepath.Join(dir, f.Name()))
|
||||
}
|
||||
return ls
|
||||
}
|
||||
|
||||
func (ls *logStore) filePath(id string) string {
|
||||
return filepath.Join(ls.dir, id+".log")
|
||||
}
|
||||
|
||||
const logIDChars = "abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
@@ -184,6 +204,7 @@ func (ls *logStore) cleanup() {
|
||||
now := time.Now()
|
||||
for id, entry := range ls.entries {
|
||||
if now.After(entry.ExpiresAt) {
|
||||
os.Remove(ls.filePath(id))
|
||||
delete(ls.entries, id)
|
||||
}
|
||||
}
|
||||
@@ -202,8 +223,8 @@ type Server struct {
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func newServer() *Server {
|
||||
s := &Server{rooms: make(map[string]*Room), logs: newLogStore()}
|
||||
func newServer(logDir string) *Server {
|
||||
s := &Server{rooms: make(map[string]*Room), logs: newLogStore(logDir)}
|
||||
go s.cleanupLoop()
|
||||
return s
|
||||
}
|
||||
@@ -271,10 +292,24 @@ func (s *Server) handlePostLogs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
s.logs.mu.Lock()
|
||||
if len(s.logs.entries) >= maxLogEntries {
|
||||
s.logs.mu.Unlock()
|
||||
http.Error(w, "Log store full", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
s.logs.mu.Unlock()
|
||||
|
||||
id := generateLogID()
|
||||
if err := os.WriteFile(s.logs.filePath(id), body, 0644); err != nil {
|
||||
log.Printf("logs: failed to write %s: %v", id, err)
|
||||
http.Error(w, "Failed to store log", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
s.logs.mu.Lock()
|
||||
s.logs.entries[id] = logEntry{
|
||||
Text: string(body),
|
||||
Size: len(body),
|
||||
ExpiresAt: time.Now().Add(logMaxAge),
|
||||
}
|
||||
s.logs.mu.Unlock()
|
||||
@@ -306,8 +341,15 @@ func (s *Server) handleGetLogs(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(s.logs.filePath(id))
|
||||
if err != nil {
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write([]byte(entry.Text))
|
||||
w.Header().Set("Content-Length", strconv.Itoa(entry.Size))
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) sendError(conn *websocket.Conn, code, message string) {
|
||||
@@ -485,11 +527,11 @@ func (s *Server) handleWS(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
dev := flag.Bool("dev", false, "Run in development mode (plain HTTP on :8080)")
|
||||
host := flag.String("host", "ice.plezy.app", "Hostname for TLS autocert")
|
||||
addr := flag.String("addr", ":8080", "Listen address")
|
||||
logDir := flag.String("log-dir", "/data/logs", "Directory for log file storage")
|
||||
flag.Parse()
|
||||
|
||||
srv := newServer()
|
||||
srv := newServer(*logDir)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/relay", srv.handleWS)
|
||||
@@ -500,29 +542,6 @@ func main() {
|
||||
mux.HandleFunc("/logs", srv.handlePostLogs)
|
||||
mux.HandleFunc("/logs/", srv.handleGetLogs)
|
||||
|
||||
if *dev {
|
||||
log.Println("Starting dev server on :8080")
|
||||
log.Fatal(http.ListenAndServe(":8080", mux))
|
||||
} else {
|
||||
certManager := autocert.Manager{
|
||||
Prompt: autocert.AcceptTOS,
|
||||
HostPolicy: autocert.HostWhitelist(*host),
|
||||
Cache: autocert.DirCache("/var/lib/plezy-relay/certs"),
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: ":443",
|
||||
Handler: mux,
|
||||
TLSConfig: certManager.TLSConfig(),
|
||||
}
|
||||
|
||||
// HTTP challenge server for Let's Encrypt
|
||||
go func() {
|
||||
log.Println("Starting HTTP challenge server on :80")
|
||||
log.Fatal(http.ListenAndServe(":80", certManager.HTTPHandler(nil)))
|
||||
}()
|
||||
|
||||
log.Printf("Starting relay server on :443 (host=%s)", *host)
|
||||
log.Fatal(server.ListenAndServeTLS("", ""))
|
||||
}
|
||||
log.Printf("Starting relay server on %s", *addr)
|
||||
log.Fatal(http.ListenAndServe(*addr, mux))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user